Skip to content

Commit a0c5b9e

Browse files
committed
feat: add trie chapter
1 parent cf020bf commit a0c5b9e

7 files changed

Lines changed: 338 additions & 9 deletions

File tree

.github/workflows/docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
test -s docs/04-queue.md
2727
test -s docs/05-deque.md
2828
test -s docs/06-heap.md
29+
test -s docs/07-trie.md
2930
test -s examples/README.md
3031
test -s tests/README.md
3132
test -s benches/README.md
@@ -35,13 +36,15 @@ jobs:
3536
test -s benches/queue_bench.rs
3637
test -s benches/deque_bench.rs
3738
test -s benches/heap_bench.rs
39+
test -s benches/trie_bench.rs
3840
test -s diagrams/README.md
3941
test -s diagrams/01-vector.mmd
4042
test -s diagrams/02-linked-list.mmd
4143
test -s diagrams/03-stack.mmd
4244
test -s diagrams/04-queue.mmd
4345
test -s diagrams/05-deque.mmd
4446
test -s diagrams/06-heap.mmd
47+
test -s diagrams/07-trie.mmd
4548
test -s assets/README.md
4649
4750
grep -q "Vector" docs/SUMMARY.md

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ un algoritmo sea necesario para explicar la propia estructura.
3636
| 04 | Queue | `src/queue.rs` | benchmarked |
3737
| 05 | Deque | `src/deque.rs` | benchmarked |
3838
| 06 | Heap | `src/heap.rs` | benchmarked |
39-
| 07 | Trie | `src/trie.rs` | planned |
39+
| 07 | Trie | `src/trie.rs` | benchmarked |
4040
| 08 | Graph | `src/graph.rs` | planned |
4141
| 09 | B-Tree | `src/btree.rs` | planned |
4242
| 10 | HashMap | `src/hashmap.rs` | planned |

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ El checklist detallado vive en
2525
| 04 | Queue | benchmarked |
2626
| 05 | Deque | benchmarked |
2727
| 06 | Heap | benchmarked |
28-
| 07 | Trie | planned |
28+
| 07 | Trie | benchmarked |
2929
| 08 | Graph | planned |
3030
| 09 | B-Tree | planned |
3131
| 10 | HashMap | planned |

benches/trie_bench.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,7 @@ fn bench_sorted_vector_prefix_search(words: &[String]) -> Duration {
7171
}
7272

7373
fn generate_words() -> Vec<String> {
74-
(0..SIZE).map(|index| format!("course-{index:05}")).collect()
74+
(0..SIZE)
75+
.map(|index| format!("course-{index:05}"))
76+
.collect()
7577
}

docs/07-trie.md

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
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.

docs/superpowers/plans/2026-07-14-rust-data-structures-course.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,11 @@ For each chapter, complete the following checklist before moving to the next str
258258
- Create: `examples/trie_advanced.rs`
259259
- Create: `examples/trie_real_case.rs`
260260

261-
- [ ] Teach prefix trees, terminal markers, insertion, lookup, deletion, prefix search, and memory tradeoffs.
262-
- [ ] Compare against hashmap, sorted vector, B-tree, radix tree, and finite automata.
263-
- [ ] Include real cases: autocomplete, dictionary lookup, routing prefixes.
264-
- [ ] Include tests for empty strings, shared prefixes, deletion without breaking siblings, prefix queries, and Unicode policy.
265-
- [ ] Include benchmarks for lookup and prefix search.
261+
- [x] Teach prefix trees, terminal markers, insertion, lookup, deletion, prefix search, and memory tradeoffs.
262+
- [x] Compare against hashmap, sorted vector, B-tree, radix tree, and finite automata.
263+
- [x] Include real cases: autocomplete, dictionary lookup, routing prefixes.
264+
- [x] Include tests for empty strings, shared prefixes, deletion without breaking siblings, prefix queries, and Unicode policy.
265+
- [x] Include benchmarks for lookup and prefix search.
266266

267267
### Task 12: Graph
268268

0 commit comments

Comments
 (0)