|
| 1 | +# Trie |
| 2 | + |
| 3 | +> **Curso:** rust-data-structures · **Capitulo:** 07 · **Prerequisitos:** Capitulo 01, Vector; Capitulo 10 del curriculum, strings basicos |
| 4 | +> **Codigo:** [`src/trie.rs`](../src/trie.rs) · **Video:** pendiente |
| 5 | +> **Leccion en el sitio:** pendiente |
| 6 | +
|
| 7 | +## Introduccion |
| 8 | + |
| 9 | +Un trie es un arbol de prefijos. Cada arista representa una parte de una clave y |
| 10 | +cada nodo puede marcar si el camino hasta ahi forma una palabra terminal. Su |
| 11 | +fuerza aparece cuando muchas claves comparten prefijos: `car`, `cart` y `cat` |
| 12 | +comparten trabajo en vez de duplicarlo por completo. |
| 13 | + |
| 14 | +En este capitulo implementamos `Trie` para palabras representadas como |
| 15 | +secuencias de `char` de Rust. Eso significa que soportamos valores escalares |
| 16 | +Unicode, pero no hacemos normalizacion Unicode: `"é"` y `"e\u{301}"` son claves |
| 17 | +distintas. |
| 18 | + |
| 19 | +## Motivacion |
| 20 | + |
| 21 | +Un `HashMap` responde muy bien a busquedas exactas, pero no sabe naturalmente |
| 22 | +contestar "dame todas las palabras que empiezan con `app`". Un vector ordenado |
| 23 | +puede hacerlo con busqueda binaria y escaneo, pero sigue comparando strings |
| 24 | +completos o rangos. Un trie guarda el prefijo como camino compartido. |
| 25 | + |
| 26 | +La idea no es que el trie reemplace a todos los mapas. Es una estructura para |
| 27 | +cuando el prefijo es parte central del problema: autocomplete, diccionarios, |
| 28 | +rutas, comandos, prefijos de red o indices de texto acotados. |
| 29 | + |
| 30 | +## Teoria |
| 31 | + |
| 32 | +### Historia |
| 33 | + |
| 34 | +Los tries aparecen en busqueda de cadenas, compiladores, diccionarios, |
| 35 | +autocompletado y ruteo. Su nombre viene de *retrieval*. La idea es convertir una |
| 36 | +clave secuencial en un camino: cada paso consume una unidad de la clave y decide |
| 37 | +que rama tomar. |
| 38 | + |
| 39 | +Esa representacion sacrifica memoria por estructura. Un nodo puede tener pocos |
| 40 | +hijos, y guardar mapas por nodo puede ser costoso. A cambio, las operaciones |
| 41 | +dependen de la longitud de la clave, no del numero total de palabras. |
| 42 | + |
| 43 | +### Fundamentos |
| 44 | + |
| 45 | +Operaciones principales: |
| 46 | + |
| 47 | +- `insert(word)`: crea el camino y marca terminal. |
| 48 | +- `contains(word)`: busca palabra exacta y exige terminal. |
| 49 | +- `starts_with(prefix)`: busca camino, aunque no sea terminal. |
| 50 | +- `words_with_prefix(prefix)`: recorre el subarbol del prefijo. |
| 51 | +- `remove(word)`: desmarca terminal y poda ramas muertas. |
| 52 | +- `node_count()`: muestra el costo de representacion. |
| 53 | + |
| 54 | +La invariante principal es: |
| 55 | + |
| 56 | +```text |
| 57 | +una palabra existe si su camino existe y el ultimo nodo esta marcado terminal |
| 58 | +``` |
| 59 | + |
| 60 | +Por eso `contains("ca")` puede ser falso aunque `starts_with("ca")` sea |
| 61 | +verdadero. |
| 62 | + |
| 63 | +### Politica Unicode |
| 64 | + |
| 65 | +Este trie recorre `word.chars()`. En Rust, `char` representa un valor escalar |
| 66 | +Unicode, no un byte. Eso permite guardar `"niño"` o `"🚀rust"` sin romper indices |
| 67 | +por byte. |
| 68 | + |
| 69 | +Pero no hacemos normalizacion. Las cadenas canonicas equivalentes para humanos |
| 70 | +pueden tener representaciones distintas y, por tanto, claves distintas. Esa |
| 71 | +decision mantiene el capitulo enfocado en tries; normalizacion Unicode pertenece |
| 72 | +a un tema de strings y texto. |
| 73 | + |
| 74 | +### Casos de uso |
| 75 | + |
| 76 | +Usos clasicos: |
| 77 | + |
| 78 | +- Autocomplete. |
| 79 | +- Diccionarios y validacion de palabras. |
| 80 | +- Prefijos de rutas. |
| 81 | +- Comandos de CLI. |
| 82 | +- Tablas de prefijos. |
| 83 | +- Filtros por prefijo en interfaces de busqueda. |
| 84 | + |
| 85 | +### Ventajas y limitaciones |
| 86 | + |
| 87 | +Ventajas: |
| 88 | + |
| 89 | +- Busqueda por prefijo natural. |
| 90 | +- Operaciones proporcionales a la longitud de la clave. |
| 91 | +- Prefijos compartidos entre palabras. |
| 92 | +- Puede enumerar resultados desde un prefijo. |
| 93 | +- Borrado puede podar ramas que ya no se usan. |
| 94 | + |
| 95 | +Limitaciones: |
| 96 | + |
| 97 | +- Puede consumir mas memoria que un `HashMap`. |
| 98 | +- Cada nodo necesita una estructura para hijos. |
| 99 | +- No ofrece hashing de clave completa. |
| 100 | +- La salida lexicografica depende del orden de `char`, no de reglas humanas de |
| 101 | + idioma. |
| 102 | +- Sin normalizacion Unicode, claves visualmente parecidas pueden ser distintas. |
| 103 | + |
| 104 | +### Comparacion con alternativas |
| 105 | + |
| 106 | +Un `HashMap<String, V>` es excelente para busqueda exacta promedio O(1), pero no |
| 107 | +resuelve prefijos sin escanear. Un vector ordenado puede hacer busqueda por |
| 108 | +rango y tiene buena localidad, pero insertar cuesta mover elementos. Un B-tree |
| 109 | +mantiene orden y rangos con buen comportamiento de memoria, pero no comparte |
| 110 | +prefijos internamente. |
| 111 | + |
| 112 | +Un radix tree comprime caminos de un solo hijo y puede ahorrar memoria. Un |
| 113 | +automata finito puede representar lenguajes o diccionarios con otra clase de |
| 114 | +optimizaciones. Este capitulo usa un trie sin compresion porque ensena la |
| 115 | +invariante base con claridad. |
| 116 | + |
| 117 | +## Diagramas |
| 118 | + |
| 119 | +El diagrama principal vive en [`diagrams/07-trie.mmd`](../diagrams/07-trie.mmd). |
| 120 | + |
| 121 | +```mermaid |
| 122 | +flowchart TB |
| 123 | + title["Trie: prefijos compartidos y terminales"] |
| 124 | +
|
| 125 | + root["root"] |
| 126 | + c["c"] |
| 127 | + a["a"] |
| 128 | + r["r terminal: car"] |
| 129 | + t1["t terminal: cart"] |
| 130 | + t2["t terminal: cat"] |
| 131 | + d["d"] |
| 132 | + o["o"] |
| 133 | + g["g terminal: dog"] |
| 134 | +
|
| 135 | + root --> c |
| 136 | + c --> a |
| 137 | + a --> r |
| 138 | + r --> t1 |
| 139 | + a --> t2 |
| 140 | +
|
| 141 | + root --> d |
| 142 | + d --> o |
| 143 | + o --> g |
| 144 | +
|
| 145 | + insert["insert(car)<br/>marca terminal al final"] |
| 146 | + lookup["contains(ca)<br/>existe prefijo, no terminal"] |
| 147 | + prefix["words_with_prefix(ca)<br/>recorre subarbol"] |
| 148 | + remove["remove(car)<br/>desmarca terminal y poda si queda sin hijos"] |
| 149 | +
|
| 150 | + insert --> r |
| 151 | + lookup --> a |
| 152 | + prefix --> a |
| 153 | + remove --> r |
| 154 | +``` |
| 155 | + |
| 156 | +## Analisis de complejidad |
| 157 | + |
| 158 | +Sea `m` la longitud de la clave en `char`, `a` el numero de hijos de un nodo y |
| 159 | +`r` el tamano de la salida. |
| 160 | + |
| 161 | +| Operacion | Mejor caso | Caso promedio | Peor caso | Espacio | |
| 162 | +|-----------|------------|---------------|-----------|---------| |
| 163 | +| `new` | O(1) | O(1) | O(1) | O(1) | |
| 164 | +| `len` / `is_empty` | O(1) | O(1) | O(1) | O(1) | |
| 165 | +| `insert` | O(m log a) | O(m log a) | O(m log a) | O(m) nodos nuevos | |
| 166 | +| `contains` | O(m log a) | O(m log a) | O(m log a) | O(1) | |
| 167 | +| `starts_with` | O(m log a) | O(m log a) | O(m log a) | O(1) | |
| 168 | +| `words_with_prefix` | O(m log a) | O(m log a + r) | O(n) | O(r) | |
| 169 | +| `remove` | O(m log a) | O(m log a) | O(m log a) | O(1) extra | |
| 170 | +| `clear` | O(n) | O(n) | O(n) | O(1) | |
| 171 | +| `node_count` | O(n) | O(n) | O(n) | O(1) | |
| 172 | + |
| 173 | +Usamos `BTreeMap<char, Node>` para que la salida sea determinista y ordenada por |
| 174 | +`char`. Eso hace que buscar hijos sea O(log a). Un arreglo fijo o un hashmap por |
| 175 | +nodo tendrian otros tradeoffs. |
| 176 | + |
| 177 | +## Visualizacion interactiva (opcional) |
| 178 | + |
| 179 | +No aplica todavia. El trie se entiende con el diagrama de prefijos compartidos, |
| 180 | +los ejemplos de autocomplete y las pruebas de borrado; se agregara playground |
| 181 | +cuando `academy-web` tenga ese mecanismo definido. |
| 182 | + |
| 183 | +## Implementacion |
| 184 | + |
| 185 | +La implementacion vive en [`src/trie.rs`](../src/trie.rs). |
| 186 | + |
| 187 | +El tipo publico conserva solo raiz y contador: |
| 188 | + |
| 189 | +```rust |
| 190 | +pub struct Trie { |
| 191 | + root: Node, |
| 192 | + len: usize, |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +Cada nodo guarda terminalidad e hijos: |
| 197 | + |
| 198 | +```rust |
| 199 | +struct Node { |
| 200 | + is_terminal: bool, |
| 201 | + children: BTreeMap<char, Node>, |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +`insert` camina por `word.chars()` y crea nodos si faltan. `contains` reutiliza |
| 206 | +`find_node`, pero exige que el nodo final sea terminal. `starts_with` solo exige |
| 207 | +que el camino exista. |
| 208 | + |
| 209 | +`remove` es recursivo: desmarca la terminal y devuelve si el hijo quedo vacio |
| 210 | +para poder podarlo. Esa poda es el detalle que evita dejar ramas muertas despues |
| 211 | +de borrar palabras. |
| 212 | + |
| 213 | +## Pruebas |
| 214 | + |
| 215 | +Las pruebas viven en [`tests/trie_test.rs`](../tests/trie_test.rs) y dentro de |
| 216 | +[`src/trie.rs`](../src/trie.rs). |
| 217 | + |
| 218 | +Cubren: |
| 219 | + |
| 220 | +- Insercion y busqueda exacta. |
| 221 | +- Diferencia entre palabra y prefijo. |
| 222 | +- Cadena vacia como palabra terminal valida. |
| 223 | +- Prefijos compartidos. |
| 224 | +- Borrado sin romper hermanos. |
| 225 | +- Poda de ramas muertas. |
| 226 | +- Busqueda por prefijo ordenada. |
| 227 | +- Politica Unicode por `char` sin normalizacion. |
| 228 | +- Reuso despues de `clear`. |
| 229 | + |
| 230 | +Los doc-comments se validan con `cargo test --doc`. |
| 231 | + |
| 232 | +## Benchmarks |
| 233 | + |
| 234 | +El benchmark vive en [`benches/trie_bench.rs`](../benches/trie_bench.rs) y se |
| 235 | +ejecuta con: |
| 236 | + |
| 237 | +```bash |
| 238 | +cargo bench --bench trie_bench |
| 239 | +``` |
| 240 | + |
| 241 | +Mide: |
| 242 | + |
| 243 | +- lookup exacto en trie; |
| 244 | +- lookup exacto con `HashSet`; |
| 245 | +- busqueda de prefijo en trie; |
| 246 | +- busqueda de prefijo en vector ordenado. |
| 247 | + |
| 248 | +La comparacion ensena el tradeoff: `HashSet` suele ganar busqueda exacta; el |
| 249 | +trie existe cuando el prefijo es una operacion de primera clase. |
| 250 | + |
| 251 | +## Ejercicios |
| 252 | + |
| 253 | +### Ejercicio 1: Trazar prefijos `[Nivel 1]` |
| 254 | + |
| 255 | +Inserta `car`, `cart` y `cat`. Explica por que `contains("ca")` es falso pero |
| 256 | +`starts_with("ca")` es verdadero. |
| 257 | + |
| 258 | +**Entrada/Salida esperada:** `words_with_prefix("car")` devuelve |
| 259 | +`["car", "cart"]`. |
| 260 | + |
| 261 | +<details> |
| 262 | +<summary>Pista</summary> |
| 263 | +El nodo `a` existe, pero no esta marcado como terminal. |
| 264 | +</details> |
| 265 | + |
| 266 | +### Ejercicio 2: Autocomplete limitado `[Nivel 2]` |
| 267 | + |
| 268 | +Implementa una funcion que devuelva como maximo `k` sugerencias para un prefijo. |
| 269 | + |
| 270 | +**Entrada/Salida esperada:** con `app`, `apple`, `apply`, `apt`, `backend` y |
| 271 | +prefijo `ap`, las primeras 3 sugerencias son `["app", "apple", "apply"]`. |
| 272 | + |
| 273 | +<details> |
| 274 | +<summary>Pista</summary> |
| 275 | +Puedes tomar los primeros `k` elementos de `words_with_prefix`. |
| 276 | +</details> |
| 277 | + |
| 278 | +### Ejercicio 3: Politica Unicode `[Nivel 3]` |
| 279 | + |
| 280 | +Inserta `"é"`, `"e\u{301}"` y `"🚀rust"`. Comprueba que las dos primeras claves |
| 281 | +son distintas y que el prefijo `"🚀"` encuentra `"🚀rust"`. |
| 282 | + |
| 283 | +**Entrada/Salida esperada:** ambas formas de `e` existen como palabras |
| 284 | +separadas. |
| 285 | + |
| 286 | +<details> |
| 287 | +<summary>Pista</summary> |
| 288 | +El trie recorre `chars()`: no compara equivalencia canonica humana. |
| 289 | +</details> |
| 290 | + |
| 291 | +### Ejercicio 4: Radix tree `[Nivel 4]` |
| 292 | + |
| 293 | +Disena como comprimirias caminos de un solo hijo para convertir este trie en un |
| 294 | +radix tree. Explica que cambia en `insert`, `contains` y `remove`. |
| 295 | + |
| 296 | +**Entrada/Salida esperada:** no hay una unica solucion; se evalua la claridad de |
| 297 | +las invariantes. |
| 298 | + |
| 299 | +<details> |
| 300 | +<summary>Pista</summary> |
| 301 | +Un nodo podria guardar segmentos de string en vez de un solo `char`. |
| 302 | +</details> |
| 303 | + |
| 304 | +## Soluciones |
| 305 | + |
| 306 | +Soluciones ejecutables de niveles 1 a 3: |
| 307 | + |
| 308 | +- [`examples/soluciones/trie_trace_prefixes.rs`](../examples/soluciones/trie_trace_prefixes.rs) |
| 309 | +- [`examples/soluciones/trie_autocomplete.rs`](../examples/soluciones/trie_autocomplete.rs) |
| 310 | +- [`examples/soluciones/trie_unicode_policy.rs`](../examples/soluciones/trie_unicode_policy.rs) |
| 311 | + |
| 312 | +Discusion para el nivel 4: |
| 313 | + |
| 314 | +Un radix tree reduce memoria al compactar cadenas de nodos con un solo hijo. |
| 315 | +El costo es que cada operacion debe comparar segmentos, partir nodos cuando hay |
| 316 | +divergencia y cuidar mas casos en borrado. La invariante deja de ser "un nodo |
| 317 | +por char" y pasa a ser "un nodo por segmento comun". |
| 318 | + |
| 319 | +## Referencias |
| 320 | + |
| 321 | +- Robert Sedgewick y Kevin Wayne, *Algorithms*, secciones de tries. |
| 322 | +- Donald Knuth, *The Art of Computer Programming*, busqueda digital. |
| 323 | +- Rust Standard Library, `BTreeMap` y `str::chars`. |
| 324 | +- Unicode Standard, normalizacion canonica como tema separado de la estructura. |
0 commit comments