|
| 1 | +# B-Tree |
| 2 | + |
| 3 | +> **Curso:** rust-data-structures · **Capitulo:** 09 · **Prerequisitos:** Vector, Graph, busqueda binaria y orden total |
| 4 | +> **Codigo:** [`src/btree.rs`](../src/btree.rs) · **Video:** pendiente |
| 5 | +> **Leccion en el sitio:** pendiente |
| 6 | +
|
| 7 | +## Introduccion |
| 8 | + |
| 9 | +Un B-tree es un arbol de busqueda multiway. A diferencia de un arbol binario, |
| 10 | +cada nodo puede guardar muchas claves y muchos hijos. Esa decision no es un |
| 11 | +detalle cosmetico: esta pensada para memoria real, paginas de disco, cache y |
| 12 | +bloques contiguos. |
| 13 | + |
| 14 | +En este capitulo implementamos un B-tree educativo de claves ordenadas. Se |
| 15 | +comporta como un conjunto: no guarda duplicados. La implementacion incluye |
| 16 | +busqueda, insercion, split de nodos llenos e iteracion ordenada. La eliminacion |
| 17 | +se estudia como estrategia, pero no se expone como API hasta que el capitulo |
| 18 | +pueda cubrirla con la misma calidad que insercion. |
| 19 | + |
| 20 | +## Motivacion |
| 21 | + |
| 22 | +Un arbol binario de busqueda puede degradarse si no se balancea. Un red-black |
| 23 | +tree mantiene balance con rotaciones y colores. Un B-tree toma otro camino: |
| 24 | +reduce altura guardando mas claves por nodo. |
| 25 | + |
| 26 | +Esa forma lo vuelve canonico para indices de bases de datos y sistemas de |
| 27 | +archivos. Cuando leer un nodo cuesta traer una pagina, conviene que cada pagina |
| 28 | +contenga muchas claves utiles. Menos altura significa menos saltos. |
| 29 | + |
| 30 | +## Teoria |
| 31 | + |
| 32 | +### Historia |
| 33 | + |
| 34 | +Los B-trees nacieron para almacenamiento secundario. En disco, el costo dominante |
| 35 | +no era comparar enteros sino leer bloques. La estructura fue disenada para |
| 36 | +mantener un arbol bajo, ancho y ordenado. |
| 37 | + |
| 38 | +Aunque hoy muchos datos viven en memoria, la idea sigue vigente: localidad, |
| 39 | +paginas, caches y estructuras ordenadas siguen importando. Por eso B-tree vuelve |
| 40 | +a aparecer mas adelante en internals de bases de datos. |
| 41 | + |
| 42 | +### Fundamentos |
| 43 | + |
| 44 | +Un B-tree con grado minimo `t` mantiene estas reglas: |
| 45 | + |
| 46 | +- Cada nodo puede guardar hasta `2t - 1` claves. |
| 47 | +- Cada nodo interno con `k` claves tiene `k + 1` hijos. |
| 48 | +- Las claves dentro de cada nodo estan ordenadas. |
| 49 | +- Los hijos separan rangos: hijo izquierdo menor, hijo derecho mayor. |
| 50 | +- La raiz puede tener menos claves que los demas nodos. |
| 51 | +- Cuando un nodo lleno recibe otra clave, se divide y sube su mediana. |
| 52 | + |
| 53 | +Este capitulo usa `t = 2` por defecto para que los splits sean faciles de ver. |
| 54 | +Tambien permite `with_min_degree(t)` para observar como cambia la capacidad del |
| 55 | +nodo. |
| 56 | + |
| 57 | +### Insercion |
| 58 | + |
| 59 | +La insercion sigue una regla practica: no bajamos hacia un hijo lleno. Si el |
| 60 | +hijo esta lleno, primero lo dividimos. Eso garantiza que al llegar a una hoja |
| 61 | +haya espacio para insertar. |
| 62 | + |
| 63 | +La operacion critica es `split_child`: |
| 64 | + |
| 65 | +```text |
| 66 | +antes: [10 | 20 | 30] con t = 2 |
| 67 | +sube: 20 |
| 68 | +despues: [10] 20 [30] |
| 69 | +``` |
| 70 | + |
| 71 | +Si insertamos `40`, el camino baja al hijo derecho y queda `[30 | 40]`. |
| 72 | + |
| 73 | +### Busqueda |
| 74 | + |
| 75 | +Buscar una clave compara dentro del nodo. Si la encuentra, termina. Si no la |
| 76 | +encuentra y el nodo es hoja, falla. Si el nodo es interno, baja al hijo cuyo |
| 77 | +rango podria contener la clave. |
| 78 | + |
| 79 | +La implementacion usa `binary_search` dentro de cada nodo. En un sistema real, |
| 80 | +el tamano de nodo se escoge por pagina o cache line; aqui se escoge por claridad |
| 81 | +educativa. |
| 82 | + |
| 83 | +### Eliminacion |
| 84 | + |
| 85 | +Eliminar en B-tree es mas delicado que insertar. No basta con quitar una clave: |
| 86 | +hay que conservar ocupacion minima, pedir prestado a hermanos o fusionar nodos. |
| 87 | + |
| 88 | +Estrategia general: |
| 89 | + |
| 90 | +1. Si la clave esta en una hoja, se puede remover directamente si el nodo queda |
| 91 | + con suficientes claves. |
| 92 | +2. Si la clave esta en un nodo interno, se reemplaza por predecesor o sucesor. |
| 93 | +3. Antes de bajar a un hijo con pocas claves, se repara: prestamo de hermano o |
| 94 | + merge. |
| 95 | +4. Si la raiz queda vacia, su unico hijo se convierte en nueva raiz. |
| 96 | + |
| 97 | +No exponemos `remove` todavia porque un capitulo canonico no debe publicar una |
| 98 | +eliminacion incompleta. La estrategia queda explicada para que el estudiante vea |
| 99 | +el mapa antes de implementarla en una iteracion futura. |
| 100 | + |
| 101 | +### Casos de uso |
| 102 | + |
| 103 | +Usos clasicos: |
| 104 | + |
| 105 | +- Indices de bases de datos. |
| 106 | +- Sistemas de archivos. |
| 107 | +- Mapas ordenados. |
| 108 | +- Indices por rango. |
| 109 | +- Almacenamiento por paginas. |
| 110 | +- Estructuras persistentes que favorecen bajo numero de lecturas. |
| 111 | + |
| 112 | +### Comparacion con alternativas |
| 113 | + |
| 114 | +Un arbol binario de busqueda es mas pequeno conceptualmente, pero sin balance |
| 115 | +puede degradarse. Un red-black tree mantiene altura logaritmica con rotaciones |
| 116 | +locales; suele ser buena opcion en memoria. Un skip list usa niveles |
| 117 | +probabilisticos y es simple de implementar concurrentemente en algunos disenos. |
| 118 | +Un hashmap busca rapido por clave exacta promedio, pero no mantiene orden ni |
| 119 | +rangos naturales. |
| 120 | + |
| 121 | +El B-tree brilla cuando importan orden, rangos y localidad. No es "mejor que un |
| 122 | +hashmap"; resuelve otro problema. |
| 123 | + |
| 124 | +## Diagramas |
| 125 | + |
| 126 | +El diagrama principal vive en [`diagrams/09-btree.mmd`](../diagrams/09-btree.mmd). |
| 127 | + |
| 128 | +```mermaid |
| 129 | +flowchart TB |
| 130 | + title["B-tree: claves ordenadas y split"] |
| 131 | +
|
| 132 | + subgraph before["Antes: raiz llena (t = 2, max = 3 claves)"] |
| 133 | + root_before["[10 | 20 | 30]"] |
| 134 | + end |
| 135 | +
|
| 136 | + insert["insert(40)<br/>la raiz llena se divide"] |
| 137 | +
|
| 138 | + subgraph after["Despues del split"] |
| 139 | + root_after["[20]"] |
| 140 | + left["[10]"] |
| 141 | + right["[30 | 40]"] |
| 142 | + root_after --> left |
| 143 | + root_after --> right |
| 144 | + end |
| 145 | +
|
| 146 | + before --> insert --> after |
| 147 | +
|
| 148 | + search["contains(30)<br/>compara en raiz y baja a un hijo"] |
| 149 | + iter["iter()<br/>hijo izquierdo, clave raiz, hijo derecho"] |
| 150 | + locality["Nodo multiway<br/>varias claves por pagina/cache line"] |
| 151 | +
|
| 152 | + root_after --> search |
| 153 | + root_after --> iter |
| 154 | + root_after --> locality |
| 155 | +``` |
| 156 | + |
| 157 | +## Analisis de complejidad |
| 158 | + |
| 159 | +Sea `n` el numero de claves y `t` el grado minimo. |
| 160 | + |
| 161 | +| Operacion | Mejor caso | Caso promedio | Peor caso | Espacio | |
| 162 | +|-----------|------------|---------------|-----------|---------| |
| 163 | +| `new` | O(1) | O(1) | O(1) | O(1) | |
| 164 | +| `with_min_degree` | O(1) | O(1) | O(1) | O(1) | |
| 165 | +| `len` / `is_empty` | O(1) | O(1) | O(1) | O(1) | |
| 166 | +| `contains` | O(1) | O(log n) | O(log n) | O(1) | |
| 167 | +| `insert` | O(log n) | O(log n) | O(log n) | O(t) por split | |
| 168 | +| `iter` | O(n) | O(n) | O(n) | O(n) referencias | |
| 169 | +| `height` | O(log n) | O(log n) | O(log n) | O(1) | |
| 170 | + |
| 171 | +La implementacion materializa referencias en `iter()` para mantener el codigo |
| 172 | +del capitulo enfocado en representacion e invariantes. Un iterador productivo |
| 173 | +podria usar una pila explicita y no reservar `Vec<&T>`. |
| 174 | + |
| 175 | +## Visualizacion interactiva (opcional) |
| 176 | + |
| 177 | +No aplica todavia. El B-tree es buen candidato para una visualizacion futura: |
| 178 | +insertar claves una por una, ver nodos llenos, observar la mediana que sube y |
| 179 | +comparar alturas con diferentes grados minimos. |
| 180 | + |
| 181 | +## Implementacion |
| 182 | + |
| 183 | +La implementacion vive en [`src/btree.rs`](../src/btree.rs). |
| 184 | + |
| 185 | +El tipo publico conserva raiz, grado minimo y longitud: |
| 186 | + |
| 187 | +```rust |
| 188 | +pub struct BTree<T> { |
| 189 | + root: Node<T>, |
| 190 | + min_degree: usize, |
| 191 | + len: usize, |
| 192 | +} |
| 193 | +``` |
| 194 | + |
| 195 | +Cada nodo guarda claves, hijos y si es hoja: |
| 196 | + |
| 197 | +```rust |
| 198 | +struct Node<T> { |
| 199 | + keys: Vec<T>, |
| 200 | + children: Vec<Node<T>>, |
| 201 | + leaf: bool, |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +La raiz empieza como hoja vacia. Antes de insertar, `insert` verifica duplicados |
| 206 | +con `contains`. Si la raiz esta llena, se crea una nueva raiz interna y se |
| 207 | +divide la raiz anterior como primer hijo. Despues la insercion baja por un nodo |
| 208 | +que no esta lleno. |
| 209 | + |
| 210 | +El split usa `Vec::split_off` para separar las claves derechas y `pop` para |
| 211 | +extraer la mediana. No usa `unsafe`. |
| 212 | + |
| 213 | +## Pruebas |
| 214 | + |
| 215 | +Las pruebas viven en [`tests/btree_test.rs`](../tests/btree_test.rs) y dentro de |
| 216 | +[`src/btree.rs`](../src/btree.rs). |
| 217 | + |
| 218 | +Cubren: |
| 219 | + |
| 220 | +- Insercion y busqueda. |
| 221 | +- Conteo de claves. |
| 222 | +- Politica de duplicados. |
| 223 | +- Split de raiz. |
| 224 | +- Iteracion ordenada despues de multiples splits. |
| 225 | +- Validacion de grado minimo. |
| 226 | +- Relacion entre grado minimo y capacidad antes de split. |
| 227 | + |
| 228 | +Los doc-comments se validan con `cargo test --doc`. |
| 229 | + |
| 230 | +## Benchmarks |
| 231 | + |
| 232 | +El benchmark vive en [`benches/btree_bench.rs`](../benches/btree_bench.rs) y se |
| 233 | +ejecuta con: |
| 234 | + |
| 235 | +```bash |
| 236 | +cargo bench --bench btree_bench |
| 237 | +``` |
| 238 | + |
| 239 | +Mide: |
| 240 | + |
| 241 | +- insercion ordenada en el B-tree educativo; |
| 242 | +- insercion pseudoaleatoria en el B-tree educativo; |
| 243 | +- insercion pseudoaleatoria en `std::collections::BTreeSet`; |
| 244 | +- busqueda en el B-tree educativo; |
| 245 | +- busqueda en `BTreeSet`. |
| 246 | + |
| 247 | +El benchmark no pretende superar a la biblioteca estandar. Sirve para observar |
| 248 | +como la estructura mantiene busqueda e insercion ordenadas con altura baja y |
| 249 | +nodos multiway. |
| 250 | + |
| 251 | +## Ejercicios |
| 252 | + |
| 253 | +### Ejercicio 1: Trazar un split `[Nivel 1]` |
| 254 | + |
| 255 | +Con grado minimo `2`, inserta `1`, `2`, `3` y `4`. Explica por que la raiz se |
| 256 | +divide al insertar la cuarta clave. |
| 257 | + |
| 258 | +**Entrada/Salida esperada:** la altura final es `2` y la raiz tiene `1` clave. |
| 259 | + |
| 260 | +<details> |
| 261 | +<summary>Pista</summary> |
| 262 | +Con `t = 2`, un nodo puede tener como maximo `3` claves. |
| 263 | +</details> |
| 264 | + |
| 265 | +### Ejercicio 2: Indice ordenado `[Nivel 2]` |
| 266 | + |
| 267 | +Inserta claves en desorden y usa `iter()` para producirlas ordenadas. |
| 268 | + |
| 269 | +**Entrada/Salida esperada:** insertar `[30, 10, 20, 40]` produce |
| 270 | +`[10, 20, 30, 40]`. |
| 271 | + |
| 272 | +<details> |
| 273 | +<summary>Pista</summary> |
| 274 | +El recorrido in-order visita hijo izquierdo, clave, hijo derecho. |
| 275 | +</details> |
| 276 | + |
| 277 | +### Ejercicio 3: Politica de duplicados `[Nivel 3]` |
| 278 | + |
| 279 | +Inserta dos veces la misma clave y verifica que `len` no cambie. |
| 280 | + |
| 281 | +**Entrada/Salida esperada:** la primera insercion devuelve `true`; la segunda, |
| 282 | +`false`. |
| 283 | + |
| 284 | +<details> |
| 285 | +<summary>Pista</summary> |
| 286 | +Este B-tree modela un conjunto de claves, no un multiset. |
| 287 | +</details> |
| 288 | + |
| 289 | +### Ejercicio 4: Disenar eliminacion `[Nivel 4]` |
| 290 | + |
| 291 | +Describe como implementarias `remove` sin romper las invariantes de ocupacion. |
| 292 | +Incluye prestamo de hermanos, merge y cambio de raiz. |
| 293 | + |
| 294 | +**Entrada/Salida esperada:** no hay una unica solucion; se evalua que el plan |
| 295 | +mantenga ocupacion minima y orden. |
| 296 | + |
| 297 | +<details> |
| 298 | +<summary>Pista</summary> |
| 299 | +Antes de bajar a un hijo con pocas claves, reparalo. |
| 300 | +</details> |
| 301 | + |
| 302 | +## Soluciones |
| 303 | + |
| 304 | +Soluciones ejecutables de niveles 1 a 3: |
| 305 | + |
| 306 | +- [`examples/soluciones/btree_trace_split.rs`](../examples/soluciones/btree_trace_split.rs) |
| 307 | +- [`examples/soluciones/btree_ordered_index.rs`](../examples/soluciones/btree_ordered_index.rs) |
| 308 | +- [`examples/soluciones/btree_duplicate_policy.rs`](../examples/soluciones/btree_duplicate_policy.rs) |
| 309 | + |
| 310 | +Discusion para el nivel 4: |
| 311 | + |
| 312 | +Una eliminacion completa debe mantener el arbol balanceado y todos los nodos, |
| 313 | +salvo la raiz, con ocupacion suficiente. La forma robusta es reparar antes de |
| 314 | +bajar: si el hijo tiene pocas claves, toma prestado de un hermano con excedente |
| 315 | +o fusiona con un hermano y baja una clave del padre. Si borras una clave interna, |
| 316 | +reemplazala por predecesor o sucesor desde un subarbol con espacio suficiente. |
| 317 | + |
| 318 | +## Referencias |
| 319 | + |
| 320 | +- Rudolf Bayer y Edward M. McCreight, articulo original sobre B-trees. |
| 321 | +- Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest y Clifford Stein, |
| 322 | + *Introduction to Algorithms*, capitulo de B-trees. |
| 323 | +- Rust Standard Library, `std::collections::BTreeSet`. |
| 324 | +- RFC-0001 §10 y §14: ubicacion curricular y anatomia de capitulos. |
0 commit comments