Ordenar invertido

Un programador de la India muestra claramente Zig-Zag, Zig-Zig y Zig utilizados en el algoritmo SplaySort:


Esta temporada estamos explorando una variedad de montones y cómo se pueden usar para la clasificación . Sin embargo, esta vez nos alejaremos del tema principal. La estructura actual, el árbol de separación, no es un montón. Pero lo necesitamos para prepararnos mentalmente para el estudio de la próxima pila: la próxima semana habrá una conferencia sobre la clasificación por un árbol cartesiano.
Software EDISON - desarrollo web
Este artículo fue preparado con el apoyo de EDISON. Una de las direcciones de nuestra actividad es la automatización de mediciones y sistemas expertos , por lo que llevamos a cabo proyectos de alta tecnología utilizando un enfoque científico estricto. ¡Amamos la informática! ;-)






Árbol de búsqueda binaria


Splay Tree es un árbol de búsqueda binario mejorado. Primero, recordemos cómo ordenar usando el árbol binario "no mejorado" habitual.

Como bien sabe, en un árbol binario, cualquier hijo izquierdo es menor que el padre, cualquier hijo derecho no es menor (es decir, mayor o igual) que el padre.


La clasificación es generalmente sencilla:

  • Etapa 1. Basado en la matriz, construya un árbol de búsqueda binario. Ponemos el primer elemento de la matriz en la raíz, primero comparamos los elementos restantes con la raíz, luego, según la comparación, nos movemos hacia abajo de las ramas izquierda o derecha (y en el camino comparamos el elemento de la matriz con los nodos de árbol existentes). Al final, el siguiente elemento llega al final de una rama y se convierte en un nodo en sí mismo.
  • 2. . , ( , ) . .

La construcción de un árbol es bastante tolerable en términos de complejidad algorítmica: en promedio, insertar un nuevo nodo cuesta O (log n ) , por lo que la complejidad temporal de la primera etapa es O ( n log n ) .

Pero en la segunda etapa, no todo es tan color de rosa. Un paseo recursivo por el árbol puede convertirse fácilmente en un largo viaje a través de un laberinto extremadamente sinuoso, y la complejidad del tiempo a menudo se degrada a O ( n 2 ) .

Árbol de separación


Para resolver este problema, solo hace unos 35-37 años, dos científicos científicos Robert Tarjan y Daniel Slitor desarrollaron esta estructura de árbol. Sugirieron que para cualquier operación (insertar, buscar, eliminar) con cualquier nodo de árbol, reequilibre inmediatamente el árbol, haciendo que el nodo sea la raíz de toda la estructura.


En la foto de la izquierda está Robert Tarjan (primera fila, segunda a la derecha) en compañía del Matrix Architect e inventor de Pascal. En la foto de la derecha está Daniel Slitor.
Al hacer clic en la imagen, se abrirá una versión de formato completo.


En ruso, el nombre fallido "árbol en expansión" se pegó, con menos frecuencia, "árbol oblicuo". Aunque si simplemente se tradujera literalmente, entonces el "árbol expandido" (o, mejor aún, "convertido") suena bien y refleja con mayor precisión la esencia de esta estructura algorítmica. Pero eso es.

Para empujar el nodo a la raíz, se utilizan operaciones simples especiales, los llamados giros:

Giro en zig


Si el elemento que desea hacer una raíz está en el segundo nivel de anidamiento, entonces todo es extremadamente simple.

Denotamos este elemento como el X , y su padre (que es también la raíz del árbol) - como el P .
A , B , C son subárboles. ¿Cuánto hay en estos subárboles nodos no importa, sólo está interesado en las raíces de estos subárboles que tienen relaciones "padre-hijo" con X y P . Si falta alguno de los subárboles (es decir, si X y P no tienen descendientes), esto no afecta el orden de las acciones.


Para hacer de X la raíz del árbol, debe revertir la relación padre-hijo entre X y P , y también superar al subárbol B : fue el descendiente derecho de X , se convirtió en el descendiente izquierdo de P ( no necesita hacer nada con A y C ). Y es todo! Como resultado de estas manipulaciones simples, la estructura, como era un árbol de búsqueda binario, permaneció igual: el principio "el niño izquierdo es menor que el padre, el niño derecho es mayor o igual que el padre" no se violará en ninguna parte.

La imagen anterior X es un descendiente izquierda de P . Si X era un descendiente correcto, solo necesitas reflejar la situación:


Si X no tiene el segundo nivel de dificultad, sino el tercero, entonces todo es más complicado, pero no mucho.

Si X tiene un padre P , que a su vez tiene un padre G , entonces tenemos solo dos situaciones diferentes:
1) si X es un descendiente de P en el mismo lado que P para G ;
2) si X y P son descendientes versátiles para sus padres.

Primero, considere el primer caso.

ZigZig Turn


Deje que X sea ​​el descendiente izquierdo para P , y P el descendiente izquierdo para G (la opción cuando X y P son simultáneamente descendientes derechos se resuelve de manera similar).


Como se puede ver, es necesario cambiar la relación entre la X , P y G , así como compensar adecuadamente subárboles B y el C . Y nuestro árbol de búsqueda binario no sufrirá esto.

Giro en zigzag


Si X es el descendiente correcto, y P está a la izquierda (o X está a la izquierda, y P está a la derecha, no la esencia), entonces espero que ya entiendas todo de esta imagen:


Si X en el árbol todavía está en un nivel inferior de anidación, entonces para elevarlo, debe aplicar ZigZig o ZigZag (si es necesario, hacer esto varias veces) a ese padre en la rama, que está en el tercer nivel de anidación.

Ordenar Invertir :: Ordenar Splay


En realidad, aquí realizamos los mismos puntos que en la clasificación de árboles: primero construimos un árbol de clasificación binario, luego lo rodeamos. Pero cada vez que insertamos un nuevo nudo en el árbol, usando crestas y zags, lo convertimos en la raíz.

En la primera etapa de ganar, esto no da, la inserción de un nodo (teniendo en cuenta que tiene que zigzaguear para convertirlo en raíz) cuesta en promedio O (log n ) , y por lo tanto la complejidad de esta etapa es O ( n log n ) .

Sin embargo, como resultado, se producen transformaciones sorprendentes con el árbol: se endereza y se mantiene en un estado tan enderezado todo el tiempo. Y esto acelera decisivamente la segunda etapa (transversal del árbol), ya que la escalera final resultante se procesa con complejidad O ( n ).

Por lo tanto, la complejidad total del algoritmo (peor, medio, mejor) en el tiempo es O ( n log n ) .

En la práctica, esta clasificación es más lenta que MergeSort, QuickSort y otros HeapSort, pero demuestra claramente cómo puede acelerar las operaciones con el árbol de búsqueda binario.

La moraleja de esta fábula es la siguiente: si tienes que lidiar con un árbol de búsqueda binario, si es posible, haz que se equilibre. Como una opción posible, trabaje con él como con un árbol de despliegue, es decir En cualquier acción con cualquier nodo en el árbol, haga que este nodo sea la raíz. Por supuesto, hay otros árboles de búsqueda alternativos de equilibrio automático (árbol rojo-negro, árbol AVL, etc.), que pueden ser más preferibles.

Código C


no parecer nervioso
/*
 *------------------------------------------------------------
 *
 *      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() */

Trailer de la próxima serie


Y volvemos a los montones. La próxima cosita será más interesante que la que examinamos hoy. Este híbrido es una estructura de datos en forma de árbol que es tanto un árbol de búsqueda dinámico como binario.



Referencias


Árbol de búsqueda binaria , árbol

Splay Árboles Splay

Artículos de la serie:



Ordenar agregado a AlgoLab. Entonces, si actualiza el archivo de Excel con macros, puede alterar personalmente el árbol de búsqueda binario.

All Articles