Inverser le tri

Un programmeur indien montre clairement Zig-Zag, Zig-Zig et Zig utilisés dans l'algorithme SplaySort:


Cette saison, nous explorons une variété de tas et comment ils peuvent être utilisés pour le tri . Cependant, cette fois, nous nous éloignerons du thème principal. La structure d'aujourd'hui - l'arbre splay - n'est pas un groupe. Mais nous en avons besoin pour préparer mentalement l'étude de la prochaine pile - la semaine prochaine, il y aura une conférence sur le tri par arbre cartésien.
Logiciel EDISON - développement web
Cet article a été préparé avec le soutien d'EDISON. L'une des directions de notre activité est l' automatisation des mesures et des systèmes experts , grâce à laquelle nous réalisons des projets de haute technologie selon une approche scientifique stricte. Nous aimons l'informatique! ;-)






Tri d'arbre de recherche binaire


L'arbre Splay est un arbre de recherche binaire amélioré. Tout d'abord, rappelons-nous comment trier en utilisant l'arbre binaire «non amélioré» habituel.

Comme vous le savez bien, dans un arbre binaire, tout enfant gauche est inférieur au parent, tout enfant droit n'est pas moins (c'est-à-dire supérieur ou égal) que le parent.


Le tri est généralement simple:

  • Étape 1. En fonction du tableau, créez un arbre de recherche binaire. Nous plaçons le premier élément du tableau à la racine, comparons d'abord les éléments restants avec la racine, puis, selon la comparaison, descendons les branches gauche ou droite (et en cours de route, nous comparons l'élément du tableau avec les nœuds d'arbre existants). En fin de compte, l'élément suivant atteint la fin d'une branche et devient un nœud lui-même.
  • 2. . , ( , ) . .

La construction d'un arbre est tout à fait tolérable en termes de complexité algorithmique - en moyenne, l'insertion d'un nouveau nœud coûte O (log n ) , donc la complexité temporelle de la première étape est O ( n log n ) .

Mais dans la deuxième étape, tout n'est pas si rose. Une promenade dans les arbres récursive peut facilement devenir un long voyage à travers un labyrinthe extrêmement sinueux, et la complexité temporelle se dégrade souvent en O ( n 2 ) .

Arbre Splay


Pour résoudre ce problème, il y a seulement 35 à 37 ans, deux scientifiques, Robert Tarjan et Daniel Slitor, ont développé cette structure arborescente. Ils ont suggéré que pour toutes les opérations (insérer, rechercher, supprimer) avec n'importe quel nœud d'arbre, rééquilibrer immédiatement l'arbre, faisant du nœud la racine de la structure entière.


Sur la photo de gauche, Robert Tarjan (première rangée, deuxième à droite) en compagnie de l'architecte Matrix et inventeur de Pascal. Sur la photo de droite, Daniel Slitor.
Cliquer sur l'image ouvrira une version plein format.


En russe, le nom infructueux "arbre en expansion" est resté, moins souvent - "arbre oblique". Bien que s'il était simplement traduit littéralement, alors «l'arbre développé» (ou, mieux encore, «tourné») sonne bien et reflète plus précisément l'essence de cette structure algorithmique. Mais ça l'est.

Afin de pousser le nœud à la racine, des opérations simples spéciales sont utilisées, les soi-disant tours:

Zig Turn


Si l'élément que vous souhaitez créer est situé au deuxième niveau d'imbrication, tout est extrêmement simple.

On note cet élément comme le X , et son parent (qui est aussi la racine de l'arbre) - comme P .
A , B , C sont des sous-arbres. Combien y at - il dans ces nœuds sous - arbres n'a pas d' importance, seule intéressés par les racines de ces sous - arbres qui ont des relations « -parents et enfants » avec X et P . Si l'un des sous-arbres est manquant (c'est-à-dire si X et P n'ont pas de descendants), cela n'affecte pas l'ordre des actions.


Pour faire de X la racine de l'arbre, vous devez inverser la relation parent-enfant entre X et P , et l'emporter également sur le sous - arbre B - c'était le descendant droit de X , est devenu le descendant gauche de P ( vous n'avez rien à faire avec A et C ). Et c'est tout! À la suite de ces manipulations simples, la structure, comme c'était un arbre de recherche binaire, est restée ainsi - le principe «l'enfant gauche est inférieur au parent, l'enfant droit est supérieur ou égal au parent» ne sera pas violé.

L'image précédente X est un descendant gauche de P . Si X était un bon descendant, il suffit de refléter la situation:


Si X n'a pas le deuxième niveau de difficulté, mais le troisième, alors tout est plus compliqué, mais pas beaucoup.

Si X a un parent P , qui à son tour a un parent G , alors nous n'avons que deux situations différentes:
1) si X est un descendant de P du même côté que P pour G ;
2) si X et P sont des descendants polyvalents pour leurs parents.

Considérons d'abord le premier cas.

ZigZig Turn


Soit X le descendant gauche pour P , et P le descendant gauche pour G (l'option lorsque X et P sont simultanément descendants droits est résolue de la même manière).


Comme vous pouvez le voir, il est nécessaire de changer la relation entre le X , P et G , ainsi que compenser correctement les sous - arbres B et C . Et notre arbre de recherche binaire n'en souffrira pas.

ZigZag Turn


Si X est le descendant droit, et P est gauche (ou X est gauche, et P est droit, pas l'essence), alors j'espère que vous avez déjà tout compris de cette image:


Si X dans l'arbre est toujours à un niveau d'imbrication inférieur, alors pour le relever, vous devez appliquer ZigZig ou ZigZag (si nécessaire, faites-le plusieurs fois) à ce parent dans la branche, qui est au troisième niveau d'imbrication.

Tri inversé :: Tri Splay


En fait, ici, nous effectuons les mêmes points que dans le tri des arbres - nous construisons d'abord un arbre de tri binaire, puis nous le contournons. Mais chaque fois que nous insérons un nouveau nœud dans l'arbre - en utilisant des crêtes et des zags, nous en faisons la racine.

À la première étape de la victoire, cela ne donne pas, l'insertion d'un nœud (en tenant compte du fait que vous devez zigzaguer pour en faire une racine) coûte en moyenne O (log n ) - et donc la complexité de cette étape est O ( n log n ) .

Cependant, en conséquence, des transformations étonnantes se produisent avec l'arbre - il se redresse et est maintenu dans un tel état redressé tout le temps. Et cela accélère de manière décisive la deuxième étape (traversée de l'arbre), car l'échelle finale résultante est traitée avec une complexité O ( n ).

Ainsi, la complexité totale de l'algorithme (pire, moyen, meilleur) dans le temps est O ( n log n ) .

En pratique, ce tri est plus lent que MergeSort, QuickSort et autres HeapSort, mais il montre clairement comment vous pouvez accélérer les opérations avec l'arborescence de recherche binaire.

La morale de cette fable est la suivante: si vous devez traiter avec un arbre de recherche binaire - si possible, faites-le auto-équilibré. Comme option possible - travaillez avec elle comme avec un arbre splay, c'est-à-dire dans n'importe quelle action avec n'importe quel nœud de l'arborescence, faites de ce nœud la racine. Bien sûr, il existe d'autres arbres de recherche auto-équilibrés alternatifs (arbre rouge-noir, arbre AVL, etc.), qui peuvent être plus préférables.

Code C


ne pas avoir l'air nerveux
/*
 *------------------------------------------------------------
 *
 *      File..........: $RCSfile: splaysort.c,v $
 *      Revision......: $Revision: 1.2 $
 *      Author........: Gary Eddy (gary@cs.mu.OZ.AU)
 *			Alistair Moffat (alistair@cs.mu.OZ.AU)
 *      Date..........: $Date: 1995/09/07 06:19:17 $
 *
 *	Description:
 *		Sorts by repeated insertion into a splay tree.
 *		Insertions are done top down
 *
 *------------------------------------------------------------
 */

#include	<stdio.h>
#include	<stdlib.h>
#include	<malloc.h>
#include	<alloca.h>

char	*sort_name = "Splaysort";
char	*sort_version = "$Revision: 1.2 $";

/*
** Define DATAPTR for the 12 byte per record version of the program.
** Otherwise only 8 extra bytes per record are used and data
** references are done by indexing the data array.
** Different compiler/architecture combinations can cause wild
** variation in the ratio of speeds between these variations.
*/

#define DATAPTR

#ifdef DATAPTR
#define DATA(p) ((p)->data)
#else
#define DATA(p) (A+size*(p-T))
#endif

/*
** With the fast copy option enabled a more intelligent copy is used
** depending on the size of the items being copied.
** This approach adopted from the nqsort program of Bentley and McIlroy,
** see Software Practice and Experience, v23, n11, November 1993, 1249-65.
*/

#define FASTCOPY

#ifdef FASTCOPY
#define COPYCODE(TYPE, parmi, parmj, n) { \
        long i = (n) / sizeof (TYPE); \
        register TYPE *pi = (TYPE *) (parmi); \
        register TYPE *pj = (TYPE *) (parmj); \
        do { \
                *pi++ = *pj++;                    \
        } while (--i > 0);      \
}

void
copyfunc(char *d, char *s, int size, int copytype)
{
        if(copytype <= 1)
                COPYCODE(long, d, s, size)
        else
                COPYCODE(char, d, s, size)
	return;
}

#define COPY(d,s,size) \
        if (copytype == 0) { \
                *(long *)(d) = *(long *)(s); \
        } else \
                copyfunc(d, s, size, copytype)
#else
#define COPY(d,s,size)	memcpy(d,s,size)
#endif

typedef struct  node_rec node;

struct	node_rec {
	node	*left, *rght;
#ifdef DATAPTR
	char	*data;
#endif
};

/*
 *	sort():
 *		The entire sort code 
 *
 *	Accesses outside variables:	none
 *
 *	Return value...:		none
 */

void
sort(void *A, size_t n, size_t size, int (*cmp)(const void *, const void *))
{
	register node *next, *curr, *prnt;
	char 	*item;
	node	*l, *r, *ch;
	node	root, *T, *new, *end, *move;
#ifndef DATAPTR
	char	*p;
#endif

/*
** Determine which copying method will be used.
*/
#ifdef FASTCOPY
	int	copytype=((char *)A - (char *)0) % sizeof(long) ||
		size % sizeof(long) ? 2 : size == sizeof(long) ? 0 : 1;
#endif


	if(n < 2)
		return;
	if((T = new = (node *) malloc(sizeof(*T)*n)) == NULL) {
		fprintf(stderr, "Couldn't allocate space for structure\n");
	}
	/* 
	** Do the first insertion manually to avoid the empty tree
	** case in the main loop.
	*/
	curr = new++;
	item = A;
	curr->left = curr->rght = NULL;
#ifdef DATAPTR
	curr->data = item;
#endif
	item += size;
	end = T+n;
	/*
	** For each item move down the tree dividing it into
	** two subtrees, one containing items less than the new
	** element and the other those which are greater.
	** The pointers l and r refer to the greatest element in the
	** smaller subtree and the smallest element in the large
	** subtree respectively. During the splitting of the tree
	** l and r retain children that may be incorrect in the final
	** tree but these false links are cut at the end of the
	** insertion.
	*/

	for( ; new<end; ) {
		l = r = &root;
		while(curr != NULL) {
			if(cmp(item, DATA(curr)) < 0) {
				/* Left root case */
				if((ch = curr->left) == NULL) {
					r = r->left = curr;
					break;
				}
				/* Left zig-zig */
				else if(cmp(item, DATA(ch)) < 0) {
					curr->left = ch->rght;
					ch->rght = curr;
					r = r->left = ch;
					curr = ch->left;
				}
				/* Left zig-zag */
				else {
					r = r->left = curr;
					l = l->rght = ch;
					curr = ch->rght;
				}
			}
			else {
				/* Right root case */
				if((ch = curr->rght) == NULL) {
					l = l->rght = curr;
					break;
				}
				/* Right zig-zag */
				else if (cmp(item, DATA(ch)) < 0) {
					l = l->rght = curr;
					r = r->left = ch;
					curr = ch->left;
				}
				/* Right zig-zig */
				else {
					curr->rght = ch->left;
					ch->left = curr;
					l = l->rght = ch;
					curr = ch->rght;
				}
			}
		}
		/* Cut false links */
		r->left = l->rght = NULL;
		new->rght = root.left;
		new->left = root.rght;
#ifdef DATAPTR
		new->data = item;
#endif
		curr = new++;
		item += size;
	}
	/* Now copy all of the data back into the input array.
	** Uses an iterative destructive inorder traversal.
	** Last item inserted is the current root.
	*/
	prnt = NULL;
	move = T;
	while (1) {
		if ((next = curr->left) != NULL) {
			/* left subtree exists */
			curr->left = prnt;
			prnt = curr;
			curr = next;
		}
		else {
			next = curr->rght;
			curr->rght = move++;
			if (next != NULL) {
				/* and arrange for a visit */
				if((curr = next->left) != NULL) {
					next->left = prnt;
					prnt = next;
					continue;
				}
				else {
					curr = next;
					continue;
				}
			}
			/* no right subtree either, turn around*/
			if (prnt != NULL) {
				curr = prnt;
				prnt = prnt->left;
				curr->left = NULL;
				continue;
			}
			/* nothing left to be done */
			break;
		}
	}
#ifndef DATAPTR
	/*
	** Change the goes-to array in rght to a comes_from in left.
	** Note the kludge on pointers, where left points into the 
	** character array containing the elements.
	*/
	for(next = T, p = A; next < end; p += size, next++)
		next->rght->left = (node *)p;
	/* and use the `comes from' array to unscramble the permutation */
	item = (char *)malloc(size);
        for (next=T; next<end; next++) {
                char *datacurr, *dataleftcurr, *final;
                if (next->left == NULL)
                        continue;
                curr = next;
                final = datacurr = DATA(curr);
                COPY(item, datacurr, size);
                while ((char *)(curr->left) != final) {
                        dataleftcurr = (char *)(curr->left);
                        COPY(datacurr, dataleftcurr, size);
                        prnt = curr;
                        curr = T + (((char *)(curr->left)-A)/size);
                        prnt->left = NULL;
                        datacurr = dataleftcurr;
                }
                COPY(datacurr, item, size);
                curr->left = NULL;
        }
#else
	/* Change the goes-to array in rght to a comes_from in left */
	for(next = T; next < end; next++)
		next->rght->left = next;
	/* and use the `comes from' array to unscramble the permutation */
	/*
	** This 12 byte version uses the presence of the data pointer
	** by making it the flag for already placed items. This means
	** that left pointers can point to nodes, eliminating the
	** calculation to find the next node.
	*/
	item = (char *)malloc(size);
	for (next=T; next<end; next++) {
		char *datacurr, *dataleftcurr, *final;
		/* second condition guarantees at least one iteration
		** of while loop.
		*/
		if (DATA(next) == (char *) NULL || next->left == next)
			continue;
		final = datacurr = DATA(next);
		curr = next->left;
		COPY(item, datacurr, size);
		while ((dataleftcurr = DATA(curr)) != final) {
			COPY(datacurr, dataleftcurr, size);
			prnt = curr;
			curr = curr->left;
			DATA(prnt) = (char *) NULL;
			datacurr = dataleftcurr;
		}
		COPY(datacurr, item, size);
		DATA(curr) = (char *) NULL;
	}
#endif
	free(item);
	free(T);
} /* sort() */

Remorque de la série suivante


Et nous retournons aux tas. La prochaine petite chose sera plus intéressante que celle que nous avons examinée aujourd'hui. Cet hybride est une structure de données arborescente qui est à la fois un tas et un arbre de recherche binaire.



Références


Arbre de recherche binaire , Arbre

Splay Arbres Splay

Articles de série:



Tri ajouté à AlgoLab. Donc, si vous mettez à jour le fichier Excel avec des macros, vous pouvez personnellement perturber l'arborescence de recherche binaire.

All Articles