Download Código Fuente 1
Document related concepts
no text concepts found
Transcript
Capítulo 15 Árboles B EJEMPLO 15.1 . EJEMPLO 15.2 EJEMPLO 15.3 . Los archivos que se van a crear son dos: HUECOS.DAT que, almacenará la raiz y posiciones libres intermedias del fichero de datos DATOSAB.DAT para contener los datos. Suponga que los ficheros se encuentran inicialmente vacíos y se introducen las claves 2, 4, 0, 5. El contenido del primer registro del archivo de datos al terminar de introducir dicha seríe de números y su posición relativa al comienzo del archivo es 1 2 Estructura de datos. Problemas en Pascal Imagine que a continuación se añaden las claves 9 y 3. La adición de la clave 9 da origen a la creación de una nueva página y a la formación de una nueva raíz (segunda nueva página). Las nuevas páginas se sitúan al final del archivo de datos y la raíz ya no coincide con el primer registro de dicho archivo. Al terminar el trabajo y cerrar el fichero será necesario almacenar la posición de la raíz en HUECOS.DAT, pues sin ésta información cuando se abra el fichero de nuevo no se podrá acceder al resto de la estructura. La situación se podrá representar de forma análoga a como se efectúa con los árboles B almacenados en memoria interna. Si se abren nuevamente los ficheros, se añaden las claves 6, 7 y 8, se borra la 0 y la 2 y se vuelven a guardar, el resultado final será: y el contenido de los ficheros: las claves del árbol. Los árboles B+ ocupan algo más de espacio que los árboles B, debido a la mencionada duplicidad en EJEMPLO 15.4 Apéndice B 3 Problema 15.1. Efectivamente, la clave 26 no se encuentra en el árbol y deberá ser insertada. Como no cabe en la página, se crea una nueva página y se distribuye la información entre ambas. La clave con valor intermedio o clave a subir es 32 y la NuevaRamaDer queda como se indica, tras trasladar las claves y ramas necesarias a la nueva página. Como la clave a subir tampoco cabe en la página donde debe ser insertada, se crea una nueva página y se distribuye la información entre ambas. 4 Estructura de datos. Problemas en Pascal La clave con valor intermedio o clave a subir es 49 y la NuevaRamaDer queda como se indica, tras terminar de trasladar todas las ramas necesarias a la nueva página Las sucesivas divisiones se han propagado hasta la raíz. y se necesita crear un nuevo nodo donde se colocan la antigua Raíz, la ClaveASubir y NuevaRamaDer. El puntero a este nodo se convierte en la nueva Raíz. Problema 15.2. En primer lugar se baja buscando en el árbol hasta que se encuentra la clave o se determina que no está. En este caso la clave está en una hoja. . A continuación se quita la clave y se retrocede por el camino de búsqueda. Apéndice B 5 Como A->Rama[p] ha quedado con un número menor de claves que el mínimo permitido, tiene hermano izquierdo y éste no posee suficientes claves como para cederle una y quedarse con el mínimo aceptable, se efectúa la fusión o combinación de ambos nodos y se continúa retrocediendo por el camino de búsqueda. Como A->Rama[0] ha quedado con un número menor de claves que el mínimo permitido, sólo tiene hermano derecho, y éste no posee suficientes claves como para cederle una y quedarse con el mínimo aceptable, se efectúa la fusión o combinación entre A->Rama[0] y A->Rama[1] Como se ha bajado la última clave de la raíz del árbol, ésta debe ser sustituida por Raiz->Rama[0]. 6 Estructura de datos. Problemas en Pascal Por último, se libera el antiguo nodo raiz (Auxi) . Como puede observarse el árbol ha decrecido en altura. Problema 15.3 Problema 15.4 .Tras eliminar la clave 91: Tras eliminar la clave 48: Como se puede observar hay reducción en el número de nodos Problema 15.5. procedimiento insertar(E/S ArbolB: Raíz; E <tipo_clave>: ClaveAInsertar) var Apéndice B 7 lógico: SubirClave <tipo_clave>: ClaveASubir ArbolB: NuevaRamaDer, Nueva inicio BuscarClavePropagarDivisión(Raíz, ClaveAInsertar, SubirClave, ClaveASubir, NuevaRamaDer) si SubirClave entonces /* Indica que el árbol estaba vacío o que la raíz ha necesitado dividirse y es necesario crear una nueva raíz y subir a ella la clave intermedia de la anterior */ reservar(Nueva) Nueva^.Cont 1 Nueva^.Clave[1] ClaveASubir Nueva^.Rama[1] NuevaRamaDer Nueva^.Rama[0] Raíz Raíz Nueva Fin_si Fin_procedimiento procedimiento BuscarClavePropagarDivisión(E ArbolB: A; E <tipo_clave>: Cl; S lógico: SubirClave; S <tipo_clave>: ClaveASubir; S ArbolB: NuevaRamaDer) var entero: P /*número de rama por la que se continúa la búsqueda*/ lógico: Encontrada /*si ya existe no se inserta*/ inicio si A = nulo entonces /*termina el proceso recursivo y retorna para realizar la inserción*/ SubirClave verdad ClaveASubir Cl NuevaRamaDer nulo si_no <Se recorre la página apuntada por raíz comparando sus claves con la que se desea insertar (Cl), de esta forma, o se localiza la clave o se obtiene la posición (P) la de rama por donde debiera continuar el proceso de búsqueda> si Encontrada entonces /*la clave ya existe y no se puede insertar*/ SubirClave falso si_no BuscarClavePropagarDivisión(A^.Rama[p], Cl, SubirClave, ClaveASubir, NuevaRamaDer) /*Las llamadas recursivas devuelven el control a este punto, por tanto siempre se ejecuta la sentencia siguiente*/ si SubirClave entonces si A^.Cont < m entonces InsertarEnPágina(A, ClaveASubir, NuevaRamaDer, P) SubirClave falso si_no /*Como no hay espacio llama a DividirPágina que recibe a través de ClaveASubir y NuevaRamaDer la clave y rama a insertar. DividirPágina crea una nueva página y copia en ella las claves y ramas adecuadas. Además, el procedimiento selecciona entre ambas páginas la apropiada e inserta la clave y rama recibidas en la posición que les corresponde dentro de la página. Por último, envía la clave más a la derecha de la página izquierda y el puntero a la página creada, mediante ClaveASubir y NuevaRamaDer, hacia arriba para una inserción posterior en otra página*/ DividirPágina(A, ClaveASubir, NuevaRamaDer, P, ClaveASubir, NuevaRamaDer) fin_si fin_si fin_si fin_si fin_procedimiento Problema 15.6 procedimiento borrar(E/S ArbolB: Raíz; E <tipo_clave>: ClaveABorrar) var 8 Estructura de datos. Problemas en Pascal lógico: Encontrada ArbolB: Auxi inicio BuscarClavePropagarFusion(Raíz, ClaveABorrar, Encontrada); si no Encontrada entonces /*La clave no está*/ si_no si (Raiz^.Cont=0) entonces /*La raíz se ha quedado vacía*/ <Se sustituye por Raiz^.Rama[0] y se libera el nodo apuntado anteriormente por ella> fin_si fin_si fin_procedimiento procedimiento BuscarClavePropagarFusion(E ArbolB: A; E <tipo_clave>: Cl; S lógico: Encontrada) var entero: P <tipo_clave>: sucesora inicio si Arbolvacio(A) entonces Encontrada falso si_no <Se recorre la página apuntada por Raíz comparando sus claves con la buscada, de esta forma, o se localiza la clave o se obtiene el número de la rama, P, por donde debiera continuar el proceso de búsqueda> si <se encuentra la clave> entonces si <está en una hoja> entonces <se quita> si_no <se busca la clave menor en la rama derecha de la actual, es decir la que sigue en valor a la actual. Se copia en la clave a borrar dicha clave menor y se manda eliminar la duplicada en su nodo, el procedimiento se llama a sí mismo para eliminar la clave en la hoja> fin_si si_no <el procedimiento se llama a sí mismo para intentar eliminar la clave en la rama obtenida> fin_si /*Las llamadas recursivas devuelven el control a este punto del procedimiento*/ si <el nodo hijo no es una hoja y tiene un número de claves menor que el mínimo necesario> entonces /*la página de la rama está desocupada y habrá que arreglarla modificando la clave en el padre y añadiendo elementos a la desocupada de la página de la izquierda, de la de la derecha o bien fusionar dos páginas. El proceso de fusión puede propagarse hacia arriba y llegar a disminuir la altura del árbol en una unidad*/ Arreglar(A, P) fin_si fin_si fin_procedimiento Problema 15.7. void BuscarEnArbol (Pagina* Raiz, TipoClave ClaveBuscada, int* Encontrada, Pagina** Pag, int* Posic) { *Pag = Raiz; *Encontrada = False; while (*Pag != NULL && !(*Encontrada)) { if (ClaveBuscada < (*Pag)->Clave[1]) { *Encontrada = False; *Posic = 0; } else { *Posic = (*Pag)->Cont; while ((*Posic > 1) && (ClaveBuscada < (*Pag)->Clave[*Posic])) *Posic = *Posic - 1; *Encontrada = (ClaveBuscada == (*Pag)->Clave[*Posic]); } Apéndice B if (!*Encontrada) *Pag = (*Pag)->Rama[*Posic]; } } Problema 15.8. void Preorden(Pagina* Raiz) { int j; if (Raiz != NULL) { for (j = 1; j <= Raiz->Cont; j++) { printf("%d ",Raiz->Clave[j]); Preorden(Raiz->Rama[j-1]); } Preorden(Raiz->Rama[Raiz->Cont]); } } Problema 15.9 void Postorden(Pagina* Raiz) { int j; if (Raiz != NULL) { Postorden(Raiz->Rama[0]); for (j = 1; j <= Raiz->Cont; j++) { Postorden(Raiz->Rama[j]); printf("%d ",Raiz->Clave[j]); } } } Problema 15.10. /* Declaraciones del TAD Pila */ /* Implementación del recorrido en inorden de manera iteativa */ void InordenIt (Pagina* Raiz) { int I; Pagina* P; Pila* Pl; VaciaP(&Pl); P = Raiz; do { I = 0; while (! Arbolvacio(P)) { AnadeP(&Pl, P, I); P = P->Rama[I]; } if (! EsVaciaP(Pl)) { PrimeroP(Pl, &P, &I); BorrarP(&Pl); I = I + 1; if (I <= P->Cont) { printf("%d ",P->Clave[I]); if (I < P->Cont) AnadeP(&Pl, P, I); 9 10 Estructura de datos. Problemas en Pascal P = P->Rama[I]; } } }while (!EsVaciaP(Pl) || !Arbolvacio(P)); } Problema 15.11 void Escribearbol(Pagina* A, int H) { int I, J; if (A != NULL) { Escribearbol(A->Rama[A->Cont], H+1); for (I = A->Cont; I >= 1; I--) { for (J = 1; J<= H; J++) printf(" "); //desplazamiento que se estima para el valor relativo H printf("%4d\n",A->Clave[I]); Escribearbol(A->Rama[I-1], H+1); } } } Ejecución Problema 15.12. #include <stdio.h> #include <stdlib.h> #define mr 5 //Orden del arbol /* En un árbol B de orden mr, el número de ramas de una página es uno más que el de sus claves */ #define m 4 //Numero maximo de claves en un nodo #define True 1 #define False 0 typedef int TipoClave; typedef struct pagina { int Cont; TipoClave Clave[mr]; struct pagina* Rama[mr]; /* Las claves que se consideran son de la 1 a la m Apéndice B y las ramas de la 0 a la m */ }Pagina; //Procedimiento auxiliar del programa principal void Menu(int* Opcion); //Primitivas para el manejo del árbol void Inicializar(Pagina** Raiz); int Arbolvacio(Pagina* Raiz); void BuscarEnArbol (Pagina* Raiz, TipoClave ClaveBuscada, int* Encontrada, Pagina** PunteroAPaginaConClave, int* Posicion); void BuscarEnPagina (Pagina* A, TipoClave ClaveBuscada, int * Encontrada, int* Posicion); void Insertar(Pagina** Raiz, TipoClave ClaveAInsertar); void BuscarClavePropagarDivision(Pagina* A, TipoClave Cl, int* SubirClave, TipoClave* ClaveASubir, Pagina** NuevaRamaDer); void InsertarEnPagina(Pagina* A, TipoClave ClaveAInsertar, Pagina* RamaDer, int Posicion); void DividirPagina(Pagina* A, TipoClave Clave, Pagina* RamaD, int Posicion, TipoClave* ClaveASubir, Pagina** NuevaRamaDer); void Borrar (Pagina** Raiz, TipoClave ClaveABorrar); void BuscarClavePropagarFusion(Pagina* A, TipoClave Cl, int* Encontrada); TipoClave Menor(Pagina* A); void Arreglar(Pagina* A, int P); void Quitar(Pagina* A, int Posicion); void Combina(Pagina* A, int P); void MoverADrcha(Pagina* A, int P); void MoverAIzqda(Pagina* A, int P); void Inorden(Pagina* Raiz); // Programa principal int main () { Pagina* Raiz, * A; TipoClave Cl; int Opcion, P, Esta; char ch; Inicializar(&Raiz); do { Menu(&Opcion); switch (Opcion) { case 1: printf("\nIndique la clave a insertar: "); scanf("%d", &Cl); Esta = False; BuscarEnArbol(Raiz, Cl, &Esta, &A, &P ); if (!Esta) Insertar(&Raiz, Cl); else printf("La clave ya está"); break; case 2: printf("\nIndique la clave a eliminar: "); scanf("%d", &Cl); Borrar(&Raiz, Cl); break; case 3: Inorden(Raiz); break; } printf("\nPulse una tecla para continuar\n"); ch = getchar(); putchar(ch); }while (Opcion != 4); printf("FIN"); 11 12 Estructura de datos. Problemas en Pascal ch = getchar(); return 0; } void Menu(int* Opcion) { printf("\n1. Insertar una clave\n"); printf("2. Eliminar una clave\n"); printf("3. Listar\n"); printf("4. Fin\n\n"); do { printf("Opción ?: "); scanf("%d", Opcion); }while ((*Opcion < 1) || *Opcion > 4); } //Implementación de las primitivas para manejo del árbol B void Inicializar(Pagina** Raiz) { *Raiz = NULL; } int Arbolvacio(Pagina* Raiz) { return Raiz == NULL; } /* Procedimiento que busca una clave en el árbol y, si la encuentra, informa de ello y devuelve un puntero a la página donde ha aparecido y la posición que la mencionada clave ocupa en la misma */ void BuscarEnArbol (Pagina* Raiz, TipoClave ClaveBuscada, int* Encontrada, Pagina** PunteroAPaginaConClave, int* Posicion) { if (Arbolvacio(Raiz)) *Encontrada = False; else { BuscarEnPagina(Raiz, ClaveBuscada, Encontrada, Posicion); if (*Encontrada) *PunteroAPaginaConClave = Raiz; else BuscarEnArbol(Raiz->Rama[*Posicion], ClaveBuscada, &*Encontrada, &*PunteroAPaginaConClave, &*Posicion); } } /*Busca una clave en un determinado nodo y, si la encuentra, devuelve la posición que ocupa, cuando no la encuentra devuelve la posición de la rama por donde, para intentar localizarla, se debiera continuar la búsqueda */ void BuscarEnPagina (Pagina* A, TipoClave ClaveBuscada, int * Encontrada, int* Posicion) { if (ClaveBuscada < A->Clave[1]) { *Encontrada = False; *Posicion = 0; } else { *Posicion = A->Cont; //como mínimo Posición valdrá 1 pues ClaveBuscada >= A->Clave[1] while (ClaveBuscada < A->Clave[*Posicion]) *Posicion = *Posicion - 1; *Encontrada = (ClaveBuscada == A->Clave[*Posicion]); } } /* El procedimiento Insertar se invoca para añadir una nueva clave al árbol, Apéndice B pero pasa el control a BuscarClavePropagarDivision () que es quien generalmente se encarga de realizar el proceso de inserción. En realidad Insertar sólo se ocupa de crear nuevos nodos raíz y hacer crecer al árbol en altura y esto sólo ocurre cuando se inserta la primera clave en un árbol anteriormente vacío o la nueva inserción provoca un desbordamiento del nodo raiz */ void Insertar(Pagina** Raiz, TipoClave ClaveAInsertar) { int SubirClave; TipoClave ClaveASubir; Pagina* NuevaRamaDer; Pagina* Nueva; BuscarClavePropagarDivision(*Raiz, ClaveAInsertar, &SubirClave, &ClaveASubir, &NuevaRamaDer); if (SubirClave) { Nueva = (Pagina*) malloc(sizeof(Pagina)); Nueva->Cont = 1; Nueva->Clave[1] = ClaveASubir; Nueva->Rama[1] = NuevaRamaDer; Nueva->Rama[0] = *Raiz; *Raiz = Nueva; } } /* BuscarClavePropagarDivision es el procedimiento recursivo que generalmente lleva a cabo las inserciones. En primer lugar baja buscando la clave y cuando no la encuentra obtiene el lugar de inserción. Después, al subir por el camino de búsqueda, si en la correspondiente página hay sitio, inserta la clave y el proceso termina, pero si ésta página se encuentra llena llama al procedimiento DividirPagina () que crea un nuevo nodo, copia en el las claves y ramas adecuadas y devuelve la clave más a la derecha de la página izquierda y el puntero a la nueva página creada para su inserción posterior en una página de nivel superior. La simulación de bajar y subir por el camino de búsqueda se implementa mediante las llamadas recursivas, de tal forma que cuando se retorna de las llamadas se regresa a los nodos por los que se pasó antes, es decir se sube por el camino de búsqueda. */ void BuscarClavePropagarDivision(Pagina* A, TipoClave Cl, int* SubirClave, TipoClave* ClaveASubir, Pagina** NuevaRamaDer) { int P; int Encontrada; //Detecta que la clave ya está en el nodo if (Arbolvacio(A)) { *SubirClave = True; *ClaveASubir = Cl; *NuevaRamaDer = NULL; } else { BuscarEnPagina(A, Cl, &Encontrada, &P); if (Encontrada) { printf("%d está repetida\n", Cl); *SubirClave = False; } else { BuscarClavePropagarDivision(A->Rama[P], Cl, SubirClave, ClaveASubir, NuevaRamaDer); if (*SubirClave) if (A->Cont < m) //No está lleno el nodo { *SubirClave = False; //Proceso termina InsertarEnPagina(A, *ClaveASubir, *NuevaRamaDer, P); } else //Nodo lleno DividirPagina(A, *ClaveASubir,*NuevaRamaDer, P, ClaveASubir, NuevaRamaDer); 13 14 Estructura de datos. Problemas en Pascal } } } void InsertarEnPagina(Pagina* A, TipoClave ClaveAInsertar, Pagina* RamaDer, int Posicion) { int I; for (I = A->Cont; I >= Posicion + 1; I--) { //Son desplazadas claves y ramas A->Clave[I+1] = A->Clave[I]; A->Rama[I+1] = A->Rama[I]; } A->Clave[Posicion + 1] = ClaveAInsertar; A->Rama[Posicion + 1] = RamaDer; A->Cont = A->Cont + 1; } void DividirPagina(Pagina* A, TipoClave Clave, Pagina* RamaD, int Posicion, TipoClave* ClaveASubir, Pagina** NuevaRamaDer) { int I, Medio; // se determina cuantas claves va a ser necesario copiar en el nuevo nodo if (Posicion <= m / 2) /* La clave se sitúa en la pagina izquierda y por tanto al nuevo nodo deben pasar inicialmente M / 2 claves y ramas */ Medio = m / 2; else Medio = m / 2 + 1; *NuevaRamaDer = (Pagina*) malloc(sizeof(Pagina)); // Nuevo nodo for (I = Medio + 1; I <= m; I++) { (*NuevaRamaDer)->Clave[I-Medio] = A->Clave[I]; (*NuevaRamaDer)->Rama[I-Medio] = A->Rama[I]; } //Claves en el nuevo nodo (*NuevaRamaDer)->Cont = m - Medio; //Claves que quedan en el nodo izquierdo A->Cont = Medio; //Inserción de Clave y su RamaD if (Posicion <= m / 2) //inserta en nodo izquierdo InsertarEnPagina(A, Clave, RamaD, Posicion); else InsertarEnPagina(*NuevaRamaDer, Clave, RamaD, Posicion-Medio); //extrae el elemento mediano, último del nodo izquierdo *ClaveASubir = A->Clave[A->Cont]; //la Rama[0] del nuevo nodo es la rama derecha de la ClaveASubir (*NuevaRamaDer)->Rama[0] = A->Rama[A->Cont]; A->Cont = A->Cont - 1; } /* El procedimiento Borrar(), se invoca cuando se quiere eliminar un determinado registro del árbol, pero én realidad Borrar() pasa el control de la operación a BuscarClavePropagarFusion () que es quien generalmente controla todo el proceso. No obstante, cuando el nodo raiz se queda sin claves, bien porque sólo tiene una y ésta es eliminada o porque un proceso de fusión lo alcanza y baja su última clave, Borrar () interviene, obtiene la nueva raíz , libera el antiguo nodo y hace que el árbol disminuya en altura. */ void Borrar (Pagina** Raiz, TipoClave ClaveABorrar) { int Encontrada; Pagina* Auxi; BuscarClavePropagarFusion(*Raiz, ClaveABorrar, &Encontrada); if (! Encontrada) printf("La clave %d no está en el árbol\n",ClaveABorrar); else Apéndice B if ((*Raiz)->Cont == 0) //La raíz se ha quedado vacía { //Se sustituye la Raiz por Raiz->Rama[0] Auxi = *Raiz; *Raiz = (*Raiz)->Rama[0]; free(Auxi); } } /* BuscarClavePropagarFusion()es un procedimiento recursivo que, en primer lugar, busca la clave que se quiere eliminar. Una vez encontrada la clave, si el nodo es una hoja, llama al procedimiento Quitar() que la elimina del mismo. Si el nodo, en vez de una hoja, es un nodo interior, la clave a borrrar se sustituye por su inmediatamente mayor o sucesora, que se encuentra en un nodo hoja y, después, se suprime la clave sucesora en la hoja. A la salida de la recursividad el procedimiento BuscarClavePropagarFusion() comprueba, si los nodos tienen un número de claves correcto y cuando no es así invoca al procedimiento Arreglar() para que lo solucione. */ void BuscarClavePropagarFusion(Pagina* A, TipoClave Cl, int* Encontrada) { int P; TipoClave Sucesora; if (Arbolvacio(A)) *Encontrada = False; else { BuscarEnPagina(A, Cl, Encontrada, &P); if (*Encontrada) if (A->Rama[P-1] == NULL) //es una hoja Quitar(A, P); else { //No es hoja Sucesora = Menor(A->Rama[P]); //Encuentra la clave que sigue en valor a la actual A->Clave[P] = Sucesora; BuscarClavePropagarFusion(A->Rama[P], Sucesora, Encontrada); //Elimina la clave sucesora en su nodo } else //No ha sido localizada la clave BuscarClavePropagarFusion(A->Rama[P], Cl, Encontrada); /* Las llamadas recursivas devuelven control a este punto del procedimiento. En este momento hay que comprobar si el nodo hijo en el camino por el que se viene retrocediendo mantiene un número de claves igual o mayor que el mínimo necesario */ if ((A->Rama[P] != NULL) && (A->Rama[P]->Cont < m / 2)) /* No es hoja y su hijo tiene un número de claves menor que el mínimo necesario */ Arreglar(A, P); /* Cuando se llama a Arreglar() la página apuntada por A->Rama[P] está insuficientemente ocupada y hay que restaurarla moviendo elementos hacia ella desde alguna de sus hermanas o bien fusionando dos páginas. El proceso de fusión puede propagarse hacia arriba y llegar a disminuir la altura del árbol en una unidad */ } } TipoClave Menor(Pagina* A) { if (A->Rama[0] == NULL) return A->Clave[1]; else return Menor(A->Rama[0]); 15 16 Estructura de datos. Problemas en Pascal } void Arreglar(Pagina* A, int P) { /* A tiene la dirección del nodo antecedente del nodo A->Ramas[P] que es el que se ha quedado con menos claves que el mínimo */ if (P > 0) //Tiene hermano izquierdo if (A->Rama[P-1]->Cont > m / 2) /* Tiene mas claves que el mínimo y por tanto se podrá pasar al nodo derecho el elemento central separador entre ambos hermanos y subir una clave del nodo izquierdo como central */ MoverADrcha(A, P); else Combina(A, P); else //Solo tiene hermano derecho if (A->Rama[1]->Cont > m / 2) //Tiene mas claves que el mínimo MoverAIzqda(A, 1); else Combina(A, 1); } void Quitar(Pagina* A, int Posicion) { int J; for (J = Posicion + 1; J <= A->Cont; J++) { // Desplaza las claves y ramas una posición a la izquierda A->Clave[J-1] = A->Clave[J]; A->Rama[J-1] = A->Rama[J]; } A->Cont = A->Cont - 1; } /* El procedimiento Combina incrementa el contador de la página izquierda, baja la clave A->Clave[P] y mueve todas las claves y ramas de la página derecha a la izquierda. Libera la página derecha */ void Combina(Pagina* A, int P) { int J; Pagina* Auxider,* Auxiizq; Auxider = A->Rama[P]; Auxiizq = A->Rama[P-1]; Auxiizq->Cont = Auxiizq->Cont + 1; Auxiizq->Clave[Auxiizq->Cont] = A->Clave[P]; // baja la clave mediana desde el nodo padre Auxiizq->Rama[Auxiizq->Cont] = Auxider->Rama[0]; for (J = 1; J <= Auxider->Cont; J++) { Auxiizq->Cont = Auxiizq->Cont + 1; Auxiizq->Clave[Auxiizq->Cont] = Auxider->Clave[J]; Auxiizq->Rama[Auxiizq->Cont] = Auxider->Rama[J]; } Quitar(A, P); free(Auxider); } /* En este procedimiento se deja hueco en el nodo apuntado por A->Rama[p] que es el que tiene menos claves que el mínimo necesario, se inserta en el A->Clave[p] y se sube a esa posición la clave mayor del hermano izquierdo */ void MoverADrcha(Pagina* A, int P) { Apéndice B 17 int J; //Dejar hueco en el nodo con menos claves que el mínimo for (J = A->Rama[P]->Cont; J >= 1; J--) { A->Rama[P]->Clave[J+1] = A->Rama[P]->Clave[J]; A->Rama[P]->Rama[J+1] = A->Rama[P]->Rama[J]; } A->Rama[P]->Cont = A->Rama[P]->Cont+1; A->Rama[P]->Rama[1] = A->Rama[P]->Rama[0]; A->Rama[P]->Clave[1] = A->Clave[P]; //baja la clave del nodo padre /* Ahora sube la clave desde el hermano izquierdo al nodo padre, para reemplazar la que antes bajó */ A->Clave[P] = A->Rama[P-1]->Clave[A->Rama[P-1]->Cont]; A->Rama[P]->Rama[0] = A->Rama[P-1]->Rama[A->Rama[P-1]->Cont]; A->Rama[P-1]->Cont = A->Rama[P-1]->Cont-1; } void MoverAIzqda(Pagina* A, int P) { int J; // el nodo con menos claves que el mínimo es el apuntado por A->Rama[P-1] A->Rama[P-1]->Cont = A->Rama[P-1]->Cont + 1; A->Rama[P-1]->Clave[A->Rama[P-1]->Cont] = A->Clave[P]; A->Rama[P-1]->Rama[A->Rama[P-1]->Cont] = A->Rama[P]->Rama[0]; // el hermano derecho sera el nodo apuntado por A->Rama[P] A->Clave[P] = A->Rama[P]->Clave[1]; /* Sube al nodo padre la clave 1 del hermano derecho, sustituyendo a la que bajó */ A->Rama[P]->Rama[0] = A->Rama[P]->Rama[1]; A->Rama[P]->Cont = A->Rama[P]->Cont-1; for (J = 1; J <= A->Rama[P]->Cont; J++) { A->Rama[P]->Clave[J] = A->Rama[P]->Clave[J+1]; A->Rama[P]->Rama[J] = A->Rama[P]->Rama[J+1]; } } void Inorden(Pagina* Raiz) { int I; if (! Arbolvacio(Raiz)) { Inorden(Raiz->Rama[0]); for (I = 1; I <= Raiz->Cont; I++) { printf("%d ",Raiz->Clave[I]); Inorden(Raiz->Rama[I]); } } } Problema 15.13. a) Veamos el número n de claves que pueden almacenarse en las distintas alturas en función de m y h para el caso de almacenamiento máximo Altura 1 m Altura 2 m*(m+1) Altura 3 m*(m+1)2 …….. ……. …….. ……. Altura h m*(m+1)h-1 18 Estructura de datos. Problemas en Pascal Teniendo en cuenta que los resultados obtenidos forman parte de los términos de una progresión geométrica de razón m+1 y conociendo que la fórmula de la suma de los h primeros términos de una progresión geométrica es : s m(m 1) h 1 (m 1) m m 1 1 h después de simplificar queda la siguiente igualdad: n (m 1) 1 h Con lo que obviamente se tiene que n es ((m+1) ). Es decir el número de claves crece asintóticamente de acuerdo con una función potencial cuya base es el orden del árbol B más una unidad, y cuyo exponente es la altura del árbol B, independientemente del número exacto de claves que se almacene en cada nodo. b) A partir de la igualdad obtenida previamente en el apartado a que relaciona n con m y con h se tiene que el número máximo y mínimo de claves vienen dados respectivamente por: n n (m 1) h 1 m/2 ((m 1) / 2) h 1) ((m 1) / 2) h 1 (m 1) / 2 1 En la última igualdad se ha tenido en cuenta que m div 2 (m+1)div 2 –1. c) Para el caso que se nos plantean m = 9 (poner m = 9 es para que m+1 valga 10 y sea bastante sencillo poner las igualdades), con lo que para almacenar un millón de claves basta con establecer la siguiente igualdad para el caso de cada una de las alturas 1.000.000 =(9+1)h –1 1.000.000 = ((9+1) div 2)h –1 y entonces la altura h será respectivamente para los casos mínimo y máximo de h = log(1.000.000)= 6 h = log5(1.000.0000) = 6*1,430676 9 Si se compara con la altura dada por un árbol binario de búsqueda y considerando que un árbol de estas características almacena un total de n= 2h-1 claves se obtiene que la altura h es aproximadamente igual a 20, ya que 220 = 210*21.000*1.000 =1.000.000. Obsérvese la diferencia entre el número de accesos a nodos en el caso de un árbol B y un árbol binario; pasa de estar entre 6 y 9 a 20, si bien ambas siguen siendo logarítmicas. d) Para este análisis, de nuevo m = 9, con lo que para almacenar un total de cien millones de claves basta con establecer la siguiente igualdad para el caso de cada un de las alturas: 100.000.000 =(9+1)h –1 y 100.000.000 = ((9+1) div 2)h –1=5h –1 por consiguiente la altura h será, respectivamente, para los casos mínimo y máximo de h = log(100.000.000)= 8 h = log5(100.000.0000) = 8*1,430676 12 Si se compara con la altura dada por un árbol binario de búsqueda y teniendo en cuenta que un árbol de estas características almacena un total de n = 2h - 1 claves se obtiene que la altura h es aproximadamente igual a 20 + log2(100) 27, ya que 100*220 = 100*210*2100*1.000*1.000 =100.000.000 Observe que con sólo altura entre 8 y 9 se puede almacenar más del doble de claves que tiene España. e) Considere el caso de 4*512 Bytes. Teniendo en cuenta que un entero ocupa 2 Bytes y que hay que almacenar 2 enteros correspondientes a la clave y la dirección (se ignora que el número de direcciones en un nodo de un árbol B es una mas que de claves) y que se quiere que la altura sea mínima, entonces el número total de claves a almacenar será aproximadamente igual 4*512/4 que son 512 con lo que la altura mínima será log512 (100.000.000) 3 Para el caso de 2*512 Bytes y teniendo en cuenta las consideraciones anteriores, la altura máxima vendrá dada por log512 (100.000.000) 3 Tenga en cuenta éste último resultado, para observar la importancia que tiene el árbol B cuando realmente se implementa en un disco, ya que el número real de accesos a disco para la realización de una operación de búsqueda es sólo de tres.