Árboles B
Los árboles B son una adaptación de los árboles 2-3 a memoria externa, donde cada nodo se almacena en un bloque y entonces se ensancha el nodo a tamaño para aprovechar que el bloque se lee completo. Los nodos internos del árbol pueden almacenar hasta elementos (entendiendo que éstos requieren almacenar claves y punteros a los nodos hijos). Estas claves están replicadas en las hojas (esta variante, que suele ser la más conveniente, corresponde al llamado árbol B). Las hojas pueden almacenar hasta elementos (pues no almacenan punteros), pero suelen almacenar también elementos para poder incorporar un puntero a los datos asociados a cada clave. Incluso pueden almacenar elementos, para alguna constante , si se elige almacenar los datos asociados directamente en la hoja junto con la clave, para evitar otro acceso aleatorio en disco. Note que los "punteros" son aquí posiciones del archivo en disco donde se almacena el árbol B. Por simplicidad de exposición, diremos que los bloques tienen capacidad de almacenar hasta claves, tanto en las hojas como en los nodos internos.
Con una capacidad máxima de claves, el árbol B garantiza que los nodos, salvo la raíz, tienen al menos claves. Cada nodo interno con claves tiene hijos . Todas las claves del subárbol cumplen (entendiendo e ). Todas las hojas del árbol B están al mismo nivel, por lo que su altura es (está entre y ).
El mecanismo de búsqueda de un elemento en el árbol B es una extensión clara del mecanismo del árbol 2-3. Se lee el nodo raíz, con sus claves , y se busca entre ellas (en forma binaria o secuencial, no hay diferencia en el modelo de memoria externa). Una vez determinado que , la búsqueda continúa en el subárbol . La búsqueda requiere entonces leer páginas de disco.
La inserción de un elemento comienza con una búsqueda, donde se identifica la hoja donde debería estar su clave. El elemento se agrega en la hoja, y si ésta se pasa del tamaño máximo , se corta en dos hojas y de tamaño y , respectivamente, y la mediana de las claves (que será la máxima clave almacenada en ) se inserta en el nodo padre , que así reemplaza su antiguo hijo por dos hijos, y , separados por la nueva clave. Si el padre se pasa del tamaño , es decir pasa a tener claves y hijos, se repite la operación en forma casi idéntica: se corta en dos mitades y de claves y hijos, y la clave mediana se mueve hacia el padre de , reemplazando el nodo por los nodos y que flanquean la nueva clave de . Si a su vez se pasa del tamaño máximo, se repite el procedimiento de rebalse de nodos internos. Si finalmente esto ocurre en la raíz, se crea una nueva raíz del árbol con sólo 2 hijos, y la altura del árbol crece en 1.
Para el borrado, se elimina al elemento de la hoja. La clave borrada no necesita eliminarse de los nodos internos, aunque ya no exista más. Si la hoja pasa a tener menos de elementos, entonces se une con su anterior o siguiente hermana, y la clave que las separa en el padre se elimina. Si esto hace que el padre tenga menos de elementos, se repite el proceso de forma similar. La diferencia al unir dos nodos internos es que la clave que los separa en el nodo del padre se baja al nuevo nodo, para separar el último hijo del nodo izquierdo del primero del nodo derecho que se unen. Eventualmente se puede llegar a la raíz, que no requiere tener elementos. Sin embargo, si la raíz queda con cero elementos (y un hijo), se elimina y el hijo pasa a ser la raíz, con lo que el árbol B pierde altura.
En el borrado puede ocurrir que, cuando unimos un nodo con su hermano, el nodo resultante tenga más de elementos. En ese caso debemos volver a cortar el nodo que hemos creado, por su nueva mediana, y volver a insertar una nueva clave en el padre. En la práctica, esto implica que las claves se redistribuyen entre los nodos hermanos y la clave del padre que los separa se modifica. Cuando esto ocurre, el borrado no necesita seguirse propagando hacia arriba.
Como puede verse, tanto la inserción como el borrado cuestan operaciones de I/O. El árbol B garantiza un porcentaje mínimo de ocupación de las páginas de disco de 50%. Si los datos se insertan en forma uniformemente aleatoria, el porcentaje promedio de ocupación es de 69%.
Con algunas técnicas más refinadas para evitar cortar hojas cuando rebalsan, la ocupación puede sobrepasar el 80% promedio.
Por otro lado, note que podríamos mantener los primeros niveles del árbol B en memoria principal, de modo que la cantidad de I/Os para búsquedas y modificaciones se reduciría a .
📄️ Cota inferior
El costo de búsqueda de $O(\log_B \frac{M})$ es óptimo si se busca mediante