Estructuras de datos dinámicas/Árboles
Árboles
editarÁrbol: estructura no lineal y dinámica de datos. Dinámica: puede cambiar durante la ejecución de un programa. No lineal: a cada elemento del árbol pueden seguirle varios elementos. Están formados por un conjunto de nodos y un conjunto de aristas que conectan pares de nodos.
Definición no recursiva
editarConjunto de nodos y conjunto de aristas que conectan pares de nodos con las siguientes características:
Definición recursiva
editarUn árbol es o bien vacío o consiste en una raíz y cero o más subárboles no vacíos , ,…, , cada una de cuyas raíces está conectada por medio de una arista con la raíz.
Definiciones
editarOperaciones básicas
editarImplementación primer hijo - siguiente hermano
editarConsiste en mantener los hijos de cada nodo en una lista enlazada. Cada nodo tiene dos referencias: una a su hijo más a la izquierda y otra a su hermano de la derecha.
Árboles binarios
editarUn árbol binario es o bien vacío o consta de una raíz, un hijo árbol binario izquierdo y otro derecho. Los árboles binarios de búsqueda permiten inserciones y acceso a los elementos en tiempo logarítmico. Los árboles binarios llamados colas con prioridad soportan acceso y eliminación del mínimo de una colección de elementos.
Árboles binarios de búsqueda
editarPara todo nodo A del árbol:
Operación “buscar”
editarbuscar(Object o) { if (o.equals(valor)) return true; else if (o.compareTo(valor)<0) return buscar(getIzq(),o); else return buscar(getDer(),o); }
Operación “insertar”
editarpublic NodoBinario insertar(Comparable o){ if (o.compareTo(valor)<0) setIzq(insertar(getIzq(),o)); else setDer(insertar(getDer(),o)); return this; }
Dentro de la clase NodoBinarioVacio:
public NodoBinario insertar(Comparable o) { return new NodoBinario(o); }
Operación “recorrer”
editarLos recorridos pueden ser en preorden, postorden o inorden (orden simétrico). Todos son O(N).
public void preOrder(SList aList) { aList.addElement(value); left.preOrder(aList); right.preOrder(aList); }
public void inOrder(SList aList) { left.inOrder(aList); aList.addElement(value); right.inOrder(aList); }
public void posOrder(SList aList) { left.posOrder(aList); right.posOrder(aList); aList.addElement(value); }
Los recorridos no necesitan obligatoriamente recursividad, se puede emplear una pila para realizarlos iterativamente.
Operación “borrado”
editarEl nodo a borrar debe ser reemplazado por el nodo más a la derecha en el subárbol izquierdo o el nodo más a la izquierda en el subárbol derecho (el nodo más a la derecha del subárbol izquierdo será mayor o igual que cualquier otro nodo de ese subárbol y menor que todos los del subárbol derecho, y el nodo más a la izquierda del subárbol derecho será menor que todos los demás nodos de ese subárbol y mayor que todos los del subárbol izquierdo). Para el caso en el que el nodo elegido tengo un subárbol, hay por lo menos tres soluciones posibles:
Árboles binarios perfectamente equilibrados
editarLa eficiencia de las operaciones depende exclusivamente de la altura del árbol. Para un árbol de N nodos perfectamente equilibrado el coste de acceso es de orden logarítmico: O(log N). Sin embargo, se dice que si el árbol crece o decrece descontroladamente, el rendimiento puede disminuir considerablemente, siendo para el caso más desfavorable (insertar un conjunto de claves ordenadas en forma ascendente o descendente) el coste de acceso: O(N). En un árbol binario perfectamente equilibrado, el número de nodos en el subárbol izquierdo y el número de nodos en el subárbol derecho, difieren como mucho en una unidad, y los subárboles son también equilibrados
Árboles equilibrados
editarUn procedimiento de inserción que siempre restaure la estructura del árbol a un equilibrio perfecto es poco eficiente. Se usa una formulación menos estricta de “equilibrio”: Un árbol está equilibrado si para cada uno de sus nodos ocurre que las alturas de sus dos subárboles difieren como mucho en 1(Árboles AVL).
Factor de equilibrio (FE) de un nodo
editarEl FE es la altura del subárbol izquierdo menos la altura del subárbol derecho. Los valores que puede tomar son -1, 0, 1. Si llegara a tomar los valores -2 o 2 debe reestructurarse el árbol. Todos los árboles perfectamente equilibrados son AVL. La longitud de camino media es prácticamente idéntica a la de un árbol perfectamente equilibrado. En un AVL se puede realizar con complejidad del O(log N) las siguientes operaciones:
- Una inserción en el subárbol izquierdo del hijo izquierdo de X.
- Una inserción en el subárbol derecho del hijo izquierdo de X.
- Una inserción en el subárbol izquierdo del hijo derecho de X.
- Una inserción en el subárbol derecho del hijo derecho de X.
Inserciones en los “márgenes”
editar1 y 4: inserciones en los “márgenes”: rotación simple: intercambia los papeles de los padres y de los hijos, manteniendo la ordenación.
// Rotación izquierda - izquierda private static NodoAVL rotarConHijoIzq(NodoAVL A) { NodoAVL B = (NodoAVL) A.getIzq(); //Asigna nombre A.setIzq(B.getDer()); B.setDer(A); return B; }
Inserciones por “dentro”
editar2 y 3: inserciones por “dentro”: rotación doble. Notar que una rotación simple no resuelve el problema, ya que la rama que provocó el desequilibrio se descuelga del nodo promovido y se cuelga al nodo que desciende un nivel, de manera que se mantiene con la misma profundidad, que es la que provocó el desequilibrio. Por lo tanto, antes de rotar, debe desplazarse el desequilibrio a la rama correspondiente, es decir, transformamos el caso de una inserción por dentro, a un caso de inserción en el margen, utilizando una rotación.
// Rotación izquierda - derecha private static NodoAVL rotarDobleConHijoIzq(NodoAVL A) { NodoAVL B = (NodoAVL) A.getIzq(); //Asigna nombre A.setIzq(rotarConHijoDer((NodoAVL) B)); return rotarConHijoIzq(A); }
Como se ve, el problema se convirtió en un problema igual al del primer caso. Los pasos necesarios para suprimir un nodo en un árbol AVL son:
Árboles rojinegros
editarÁrbol binario de búsqueda, donde cada nodo está coloreado con los colores rojo o negro, y se verifican las siguientes propiedades:
1. La raíz es negra.
2. Si un nodo es rojo, sus hijos deben ser negros.
3. Todos los caminos desde un nodo a un nodo vacío deben contener el mismo número de nodos negros.
Las condiciones (2) y (3) aseguran que el árbol nunca esté demasiado desbalanceado. (2) asegura que no puedan haber demasiados nodos rojos, y (3) dice que, despreciando el número de nodos rojos, que es limitado, el árbol es perfectamente balanceado. La condición (1) es trivial: si la raíz es roja, simplemente se colorea negra, ya que esto no violará ninguna regla. En los ARN la operación eliminar se complica. Cuando se necesitan árboles equilibrados y se requieren muchas eliminaciones se emplean los AA-Árboles que añaden una condición adicional a las impuestas por los ARN:
4. Los hijos izquierdos no pueden ser rojos.
Altura de un ARN
editarEn un ARN con n nodos, la altura h será:
Demostración
editarLa condición (3) nos permite asegurar que, despreciando el número de nodos rojos, el árbol es perfectamente balanceado, y, en virtud de esa característica, su altura . La condición (2) evita que haya nodos rojos consecutivos, como máximo, la mitad de los nodos de un camino -que constituirán una altura- serán rojos.
Operaciones
editarSe pueden realizar operaciones de búsqueda con complejidad O(log N), por lo expuesto anteriormente. Al realizar una inserción, la complejidad de búsqueda será O(log N), pero aparece un problema: el resultado será un árbol de búsqueda binario, pero no necesariamente un ARN. Si coloreamos el nuevo nodo rojo, el balance de negros quedará intacto, pero se puede incurrir en una violación rojo-rojo. Si lo coloreamos negro, no se incurrirá en una violación rojo-rojo, pero en este caso, siempre alteraremos el balance de negros. Al eliminar, si el nodo a eliminar es negro, ambas violaciones pueden aparecer.
Reparación del balance del árbol
editarUna vez detectada una violación, se deben tomar medidas que reparen el balance del árbol. Estas medidas utilizan dos herramientas: rotaciones y cambios de color.
Inserción ascendente
editarSea X la nueva hoja añadida, P su padre, S el hermano de P (si existe) y G el abuelo.
1. Los nuevos nodos se insertan en el árbol como hojas de color rojo.
2. Si el padre es negro, hemos acabado.
3. Si el padre es rojo violamos la regla 2, entonces debemos modificar el árbol de forma que se cumpla la regla (2) sin introducir violaciones de la propiedad (3).
4. Si el padre P es la raíz, se colorea negro. La altura negra aumenta en 1, pero el balance se preserva. Hemos acabado.
Reparación del balance
editarAsumiendo que P no es la raíz
5. Si S es rojo, se puede aplicar un cambio de color: Se elimina así la violación, y el balance de negros se mantiene. ¿Qué pasa si el padre de G es también rojo? Solución: propagar este procedimiento hacia arriba hasta conseguir que no haya dos nodos rojos consecutivos o alcanzar la raíz. Esta propagación es análoga a la que se hace en los árboles AVL.
6. Si S es negro, tenemos dos casos, con sus simétricos asociados: violación en el margen, y violación por dentro.
7. Si es una violación es por dentro, la convertimos a una violación en el margen haciendo una rotación: Como X y P son rojos, no añaden nada en el balance de negros a los caminos que pasan por g, de manera que el balance se preserva.
8. Teniendo el caso de violación por dentro, se efectúa una rotación simple. La rama izquierda de desbalancea, le falta un nodo negro para todos sus caminos, y aún tenemos la violación rojo-rojo. Solucionamos ambos problemas haciendo un cambio de color.
Inserción descendente
editarObjetivo: garantizar que en el momento de la inserción S no sea rojo, de manera que sólo haya que añadir una hoja roja y, si fuere necesario, realizar una rotación (simple o doble). En el camino descendente, si un nodo X tiene dos hijos rojos, el color de X cambia a rojo y el de sus dos hijos a negro. El número de nodos negros en los caminos por debajo de X permanece inalterable. Si X es la raíz, la convertiríamos en roja, hay que volver a negro (esto no puede violar ninguna de las reglas). Si el padre de X es rojo, hay que aplicar rotación simple o doble. ¿Qué pasa si el hermano del padre de X es también rojo? Esta situación NO puede darse, gracias al proceso efectuado en el camino descendiente.
Árboles B
editarMientras que la altura de un árbol binario completo es, aproximadamente, , la altura de un árbol M-ario completo es, más o menos, . Un B-árbol de orden M es un árbol M-ario que verifica:
Relación entre los árboles B y los árboles rojinegros
editarSi juntamos un nodo negro con sus hijos rojos, si los hubiere, en un mismo nodo, se obtiene un árbol no binario con altura igual a la altura negra, con un máximo de 3 elementos y 4 hijos, y un mínimo de un elemento; en definitiva, es un árbol B de orden 4.
Búsqueda
editarDebe tenerse en memoria principal la página sobre la cual vamos a buscar. Considérese el elemento a buscar x. Si la búsqueda es infructuosa dentro de la página se estará en una de las siguientes situaciones:
Inserción
editarSiempre se inserta en los nodos hojas. Primero se comprueba que la clave no se encuentre en el árbol. Si la cantidad de elementos es menor que 2n:
Borrado
editarSi la clave a ser borrada no está en una hoja, su predecesor inmediato tiene que estar en una hoja (esto se debe a que todas las hojas tienen el mismo nivel, de manera que si existe un valor menor en un nodo más abajo, también tiene que haber uno mayor), y se puede promover, y así borrar el espacio que estaba en la hoja. Si la hoja queda con menos de n elementos, se comprueba si se puede promover un elemento de un hermano adyacente a su padre, y bajar el del padre a la hoja. Si el hermano tiene sólo n elementos, las dos hojas y la clave del medio se unen, y la clave del medio se elimina del nodo padre.
Árboles B+
editarLas diferencias con los árboles B son que:
Búsqueda por clave
editarBuscar en la raíz el valor más pequeño mayor que la clave x. La búsqueda sigue por el puntero hasta que llegue a un nodo hoja, que será donde esté el puntero al bloque o cubeta (cuando un elemento se encuentre en una página raíz o interior la búsqueda continuará por la rama derecha de dicha clave, hasta llegar a una hoja).
Inserción
editarSe busca el nodo hoja correspondiente y se inserta la clave si no está allí. Si tiene lugar una partición, se inserta una clave en el nodo padre, que será duplicada si la partición ocurre en una hoja.
- include<conio.h>
- include<stdio.h>
- include<iostream.h>
- include<stdlib.h>
- include<dos.h>
struct nodo {
int valor; struct nodo*sig;
}*cab; void insertar(int num); void mostrar(void); void BorrarTodo(void); void Eliminar(int num); void insertord(int num);
void main (void)
{
int op, num;
cab=NULL;
while(1) { clrscr(); printf("1.-Insertar un nodo.\n2.-Eliminar un nodo.\n3.-Mostrar\n4.-Insettar nodos en orden\n0.-Salir\n"); printf("\nElige una opcion: "); scanf("%d",&op); switch(op) { case 1: printf("Valor del nodo que deseas insertar: "); scanf("%d",&num); insertar(num); break; case 2: printf("Dame el valor del nodo a eliminar: "); scanf("%d",&num); Eliminar(num); break; case 3: mostrar(); break; case 4: printf("Valor del nodo que deseas insertar: "); scanf("%d",&num); insertord(num); break; case 0: BorrarTodo(); exit(0);//para usar exit debes declarar stdlib.h break; default: printf("ERROR Opcion incorrecta"); getch(); } getch(); }
} void insertar(int num) {
nodo *aux,*nuevo; aux=cab; nuevo=new nodo; nuevo->valor=num; nuevo->sig=NULL; if(cab==NULL) cab=nuevo; else { while(aux->sig!=NULL) aux=aux->sig; aux->sig=nuevo; }
}
void mostrar() { nodo *aux; //Aux es un apuntador aux=cab; //En esta line aux toma la direccion de cab while(aux!=NULL) { printf("%d, ",aux->valor); aux=aux->sig; } }
void BorrarTodo(void)
{ nodo *aux; aux=cab; while(aux!=NULL) { cab=aux->sig; delete aux; aux=cab; } }
void Eliminar(int num) {
nodo *aux,*aux2, *prev; int c=0; aux=cab; while(c!=1) { if(aux->valor==num) { c=1; } else{ prev=aux; aux=aux->sig; } } if(c==1) { if(prev->sig==NULL) { cab=cab->sig; delete aux; } else { aux2 = aux->sig; prev->sig = aux2; delete aux; } }
}
void insertord(int num) {
nodo *aux,*nuevo,*aux2,*prev; int c=0; aux2=cab; nuevo=new nodo; // nuevo->valor=num; while(aux2!=NULL) { if(aux2->valor>=num) { prev->sig=nuevo; nuevo->valor=num; nuevo->sig=aux2; cab=prev; aux2=NULL; c=1; } prev=aux2; aux2=aux2->sig; } if(aux2==NULL) if(c==0) { insertar(num); } else { delete aux2; }
}
Eliminación
editarSe busca el nodo hoja correspondiente y se elimina la clave. Si al eliminar una clave, n queda menor a (M/2 -1), entonces debe realizarse una redistribución de claves, tanto en el índice como en las páginas hojas.
- include<conio.h>
- include<stdio.h>
- include<iostream.h>
- include<stdlib.h>
- include<dos.h>
struct nodo {
int valor; struct nodo*sig;
}*cab; void insertar(int num); void mostrar(void); void BorrarTodo(void); void Eliminar(int num); void insertord(int num);
void main (void)
{
int op, num;
cab=NULL;
while(1) { clrscr(); printf("1.-Insertar un nodo.\n2.-Eliminar un nodo.\n3.-Mostrar\n4.-Insettar nodos en orden\n0.-Salir\n"); printf("\nElige una opcion: "); scanf("%d",&op); switch(op) { case 1: printf("Valor del nodo que deseas insertar: "); scanf("%d",&num); insertar(num); break; case 2: printf("Dame el valor del nodo a eliminar: "); scanf("%d",&num); Eliminar(num); break; case 3: mostrar(); break; case 4: printf("Valor del nodo que deseas insertar: "); scanf("%d",&num); insertord(num); break; case 0: BorrarTodo(); exit(0);//para usar exit debes declarar stdlib.h break; default: printf("ERROR Opcion incorrecta"); getch(); } getch(); }
} void insertar(int num) {
nodo *aux,*nuevo; aux=cab; nuevo=new nodo; nuevo->valor=num; nuevo->sig=NULL; if(cab==NULL) cab=nuevo; else { while(aux->sig!=NULL) aux=aux->sig; aux->sig=nuevo; }
}
void mostrar() { nodo *aux; //Aux es un apuntador aux=cab; //En esta line aux toma la direccion de cab while(aux!=NULL) { printf("%d, ",aux->valor); aux=aux->sig; } }
void BorrarTodo(void)
{ nodo *aux; aux=cab; while(aux!=NULL) { cab=aux->sig; delete aux; aux=cab; } }
void Eliminar(int num) {
nodo *aux,*aux2, *prev; int c=0; aux=cab; while(c!=1) { if(aux->valor==num) { c=1; } else{ prev=aux; aux=aux->sig; } } if(c==1) { if(prev->sig==NULL) { cab=cab->sig; delete aux; } else { aux2 = aux->sig; prev->sig = aux2; delete aux; } }
}
void insertord(int num) {
nodo *aux,*nuevo,*aux2,*prev; int c=0; aux2=cab; nuevo=new nodo; // nuevo->valor=num; while(aux2!=NULL) { if(aux2->valor>=num) { prev->sig=nuevo; nuevo->valor=num; nuevo->sig=aux2; cab=prev; aux2=NULL; c=1; } prev=aux2; aux2=aux2->sig; } if(aux2==NULL) if(c==0) { insertar(num); } else { delete aux2; }
}