|
| 1 | +# Linked List |
| 2 | + |
| 3 | +> **Curso:** rust-data-structures · **Capítulo:** 02 · **Prerequisitos:** Capítulo 01, Vector |
| 4 | +> **Código:** [`src/linked_list.rs`](../src/linked_list.rs) · **Video:** pendiente |
| 5 | +> **Lección en el sitio:** pendiente |
| 6 | +
|
| 7 | +## Introducción |
| 8 | + |
| 9 | +Una lista enlazada representa una secuencia como nodos conectados. Cada nodo |
| 10 | +guarda un valor y un enlace al siguiente nodo. A diferencia del vector, sus |
| 11 | +elementos no viven necesariamente en memoria contigua: la estructura se recorre |
| 12 | +siguiendo enlaces. |
| 13 | + |
| 14 | +Este capítulo enseña listas enlazadas simples en Rust seguro. El objetivo no es |
| 15 | +decir que son "mejores" que los vectores, sino entender qué ganan y qué pagan: |
| 16 | +inserción barata al frente, recorrido secuencial natural y peor localidad de |
| 17 | +memoria. |
| 18 | + |
| 19 | +## Motivación |
| 20 | + |
| 21 | +Imagina una cola de reintentos para trabajos fallidos: procesas el primer trabajo, |
| 22 | +quizá agregas otros al final, y rara vez necesitas acceso aleatorio por índice. |
| 23 | +Un vector puede resolverlo, pero remover del frente obliga a desplazar elementos. |
| 24 | +Una lista enlazada permite tomar el primer nodo cambiando un enlace. |
| 25 | + |
| 26 | +La lista enlazada existe para casos donde la forma natural del problema es |
| 27 | +seguir enlaces y mover nodos, no saltar a posiciones arbitrarias. Esa diferencia |
| 28 | +es la razón por la que aparece justo después de `Vector`: enseña el contraste |
| 29 | +entre memoria contigua y nodos dispersos. |
| 30 | + |
| 31 | +## Teoría |
| 32 | + |
| 33 | +### Historia |
| 34 | + |
| 35 | +Las listas enlazadas son una de las estructuras clásicas de la programación de |
| 36 | +sistemas. Aparecen temprano porque permiten construir colecciones dinámicas sin |
| 37 | +tener que mover todos los elementos cuando se inserta al frente o cuando se |
| 38 | +reconecta una parte de la estructura. |
| 39 | + |
| 40 | +En lenguajes con punteros manuales, una lista enlazada suele enseñarse como |
| 41 | +"nodo + puntero". En Rust, el mismo concepto obliga a ser más preciso: ¿quién es |
| 42 | +dueño del nodo?, ¿quién destruye la cadena?, ¿puede existir un enlace colgante? |
| 43 | +Por eso este capítulo es una buena introducción a ownership aplicado. |
| 44 | + |
| 45 | +### Fundamentos |
| 46 | + |
| 47 | +Nuestra lista usa una representación simple: |
| 48 | + |
| 49 | +```rust |
| 50 | +pub struct LinkedList<T> { |
| 51 | + head: Option<Box<Node<T>>>, |
| 52 | + len: usize, |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +Cada `Node<T>` contiene un valor y un `next`. La lista posee el primer nodo. Cada |
| 57 | +nodo posee el siguiente. Cuando la lista se destruye, la cadena completa se |
| 58 | +destruye por ownership. |
| 59 | + |
| 60 | +Invariantes principales: |
| 61 | + |
| 62 | +- `len` es el número de nodos alcanzables desde `head`. |
| 63 | +- Si `len == 0`, `head == None`. |
| 64 | +- Si `len > 0`, `head == Some(...)`. |
| 65 | +- El último nodo tiene `next == None`. |
| 66 | + |
| 67 | +Esta implementación no guarda un puntero al último nodo. Eso mantiene el diseño |
| 68 | +seguro y fácil de enseñar, pero hace que `push_back`, `pop_back` y `back` sean |
| 69 | +O(n): hay que recorrer desde `head`. |
| 70 | + |
| 71 | +### Casos de uso |
| 72 | + |
| 73 | +Las listas enlazadas sirven cuando el patrón dominante es secuencial: |
| 74 | + |
| 75 | +- Pilas con `push_front` y `pop_front`. |
| 76 | +- Colas simples cuando se acepta recorrer para insertar al final. |
| 77 | +- Cadenas de manejadores o pasos de procesamiento. |
| 78 | +- Estructuras donde los nodos se reconectan con frecuencia. |
| 79 | +- Fundamento conceptual para estructuras más avanzadas como skip lists. |
| 80 | + |
| 81 | +También son valiosas pedagógicamente: muestran ownership, movimiento de valores, |
| 82 | +destrucción de nodos y el costo real de perder localidad de memoria. |
| 83 | + |
| 84 | +### Ventajas y limitaciones |
| 85 | + |
| 86 | +Ventajas: |
| 87 | + |
| 88 | +- `push_front` y `pop_front` son O(1). |
| 89 | +- No requiere mover todos los elementos al insertar al frente. |
| 90 | +- Cada nodo puede tener vida y ownership claros. |
| 91 | +- Es una base conceptual para estructuras enlazadas más complejas. |
| 92 | + |
| 93 | +Limitaciones: |
| 94 | + |
| 95 | +- Acceso por índice es O(n) si se implementa. |
| 96 | +- `push_back` es O(n) en esta versión sin puntero a cola. |
| 97 | +- Peor localidad de caché que un vector. |
| 98 | +- Más asignaciones pequeñas. |
| 99 | +- Más complejidad conceptual por nodos y enlaces. |
| 100 | + |
| 101 | +### Comparación con alternativas |
| 102 | + |
| 103 | +Un vector suele ser mejor si necesitas recorrer muchos elementos, acceder por |
| 104 | +índice o aprovechar caché. Una lista enlazada gana cuando la operación central |
| 105 | +está cerca de la cabeza y no necesitas acceso aleatorio. |
| 106 | + |
| 107 | +Un deque es mejor si necesitas operaciones eficientes en ambos extremos. Una |
| 108 | +lista doblemente enlazada puede remover desde ambos lados con más flexibilidad, |
| 109 | +pero introduce enlaces hacia atrás y más invariantes. Una lista intrusiva puede |
| 110 | +evitar asignaciones por nodo, pero ya pertenece a programación de bajo nivel y |
| 111 | +normalmente requiere `unsafe`. |
| 112 | + |
| 113 | +## Diagramas |
| 114 | + |
| 115 | +El diagrama principal vive en |
| 116 | +[`diagrams/02-linked-list.mmd`](../diagrams/02-linked-list.mmd). |
| 117 | + |
| 118 | +```mermaid |
| 119 | +flowchart LR |
| 120 | + title["Linked List: nodos enlazados por ownership"] |
| 121 | +
|
| 122 | + list["LinkedList<br/>head + len"] |
| 123 | + node0["Node A<br/>value + next"] |
| 124 | + node1["Node B<br/>value + next"] |
| 125 | + node2["Node C<br/>value + next"] |
| 126 | + none["None"] |
| 127 | +
|
| 128 | + list --> node0 |
| 129 | + node0 --> node1 |
| 130 | + node1 --> node2 |
| 131 | + node2 --> none |
| 132 | +
|
| 133 | + push_front["push_front(X)<br/>nuevo nodo apunta al head anterior"] |
| 134 | + push_front --> list |
| 135 | +
|
| 136 | + push_back["push_back(X)<br/>recorre hasta next = None"] |
| 137 | + push_back --> node2 |
| 138 | +``` |
| 139 | + |
| 140 | +## Análisis de complejidad |
| 141 | + |
| 142 | +| Operación | Mejor caso | Caso promedio | Peor caso | Espacio | |
| 143 | +|-----------|------------|---------------|-----------|---------| |
| 144 | +| `new` | O(1) | O(1) | O(1) | O(1) | |
| 145 | +| `len` / `is_empty` | O(1) | O(1) | O(1) | O(1) | |
| 146 | +| `push_front` | O(1) | O(1) | O(1) | O(1) por nodo | |
| 147 | +| `push_back` | O(1) en lista vacía | O(n) | O(n) | O(1) por nodo | |
| 148 | +| `pop_front` | O(1) | O(1) | O(1) | O(1) | |
| 149 | +| `pop_back` | O(1) en 0/1 nodo | O(n) | O(n) | O(1) | |
| 150 | +| `front` | O(1) | O(1) | O(1) | O(1) | |
| 151 | +| `back` | O(1) en 0/1 nodo | O(n) | O(n) | O(1) | |
| 152 | +| `remove` | O(1) si índice 0 | O(n) | O(n) | O(1) | |
| 153 | +| `clear` | O(n) | O(n) | O(n) | O(1) adicional | |
| 154 | +| `iter` | O(1) crear, O(n) consumir | O(n) | O(n) | O(1) | |
| 155 | + |
| 156 | +El costo de `push_back` es deliberado: al no guardar `tail`, la lista debe |
| 157 | +recorrer todos los nodos hasta encontrar `next == None`. Esa decisión simplifica |
| 158 | +la implementación segura y deja visible el tradeoff. |
| 159 | + |
| 160 | +## Visualización interactiva (opcional) |
| 161 | + |
| 162 | +No aplica todavía. El movimiento de nodos se entiende con diagrama, ejemplos y |
| 163 | +pruebas; una visualización se agregará cuando `academy-web` tenga definido el |
| 164 | +mecanismo de playgrounds. |
| 165 | + |
| 166 | +## Implementación |
| 167 | + |
| 168 | +La implementación completa vive en [`src/linked_list.rs`](../src/linked_list.rs). |
| 169 | + |
| 170 | +El núcleo es: |
| 171 | + |
| 172 | +```rust |
| 173 | +struct Node<T> { |
| 174 | + value: T, |
| 175 | + next: Option<Box<Node<T>>>, |
| 176 | +} |
| 177 | +``` |
| 178 | + |
| 179 | +`push_front` toma el `head` actual y lo convierte en `next` del nuevo nodo. Esa |
| 180 | +operación no recorre nada: |
| 181 | + |
| 182 | +```rust |
| 183 | +let next = self.head.take(); |
| 184 | +self.head = Some(Box::new(Node { value, next })); |
| 185 | +``` |
| 186 | + |
| 187 | +`push_back`, en cambio, camina por enlaces mutables hasta encontrar el hueco: |
| 188 | + |
| 189 | +```rust |
| 190 | +let mut cursor = &mut self.head; |
| 191 | +while let Some(node) = cursor { |
| 192 | + cursor = &mut node.next; |
| 193 | +} |
| 194 | +*cursor = Some(new_node); |
| 195 | +``` |
| 196 | + |
| 197 | +El iterador guarda el próximo nodo como referencia: |
| 198 | + |
| 199 | +```rust |
| 200 | +pub struct Iter<'a, T> { |
| 201 | + next: Option<&'a Node<T>>, |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +Cada llamada a `next()` devuelve el valor actual y avanza al enlace siguiente. |
| 206 | +No copia ni mueve valores. |
| 207 | + |
| 208 | +## Pruebas |
| 209 | + |
| 210 | +Las pruebas viven en [`tests/linked_list_test.rs`](../tests/linked_list_test.rs) |
| 211 | +y dentro del módulo de implementación. |
| 212 | + |
| 213 | +Cubren: |
| 214 | + |
| 215 | +- Lista vacía y transiciones de un solo elemento. |
| 216 | +- `push_front` / `pop_front`. |
| 217 | +- `push_back` / `pop_back`. |
| 218 | +- `front` y `back`. |
| 219 | +- Remoción de cabeza, medio, cola e índice inexistente. |
| 220 | +- Iteración en orden. |
| 221 | +- `clear` y destrucción de valores. |
| 222 | +- Consistencia de `len` tras operaciones mixtas. |
| 223 | + |
| 224 | +Los doc-comments también se verifican con `cargo test --doc`. |
| 225 | + |
| 226 | +## Benchmarks |
| 227 | + |
| 228 | +El benchmark vive en |
| 229 | +[`benches/linked_list_bench.rs`](../benches/linked_list_bench.rs) y se ejecuta |
| 230 | +con: |
| 231 | + |
| 232 | +```bash |
| 233 | +cargo bench --bench linked_list_bench |
| 234 | +``` |
| 235 | + |
| 236 | +Mide: |
| 237 | + |
| 238 | +- `push_front`; |
| 239 | +- `push_back`; |
| 240 | +- `pop_front`; |
| 241 | +- iteración sobre lista enlazada; |
| 242 | +- iteración sobre vector como comparación de localidad. |
| 243 | + |
| 244 | +El resultado esperado es que `push_front` y `pop_front` sean baratos, mientras |
| 245 | +que `push_back` sea mucho más caro por recorrer desde la cabeza. La comparación |
| 246 | +con vector ayuda a recordar que una estructura con operaciones asintóticas |
| 247 | +atractivas puede perder en práctica por localidad de memoria. |
| 248 | + |
| 249 | +## Ejercicios |
| 250 | + |
| 251 | +### Ejercicio 1: Invertir entrada `[Nivel 1]` |
| 252 | + |
| 253 | +Construye una lista que reciba los valores `1, 2, 3, 4` y los deje en orden |
| 254 | +inverso usando solo `push_front`. |
| 255 | + |
| 256 | +**Entrada/Salida esperada:** `[4, 3, 2, 1]`. |
| 257 | + |
| 258 | +<details> |
| 259 | +<summary>Pista</summary> |
| 260 | +Cada nuevo valor insertado al frente queda antes de todos los anteriores. |
| 261 | +</details> |
| 262 | + |
| 263 | +### Ejercicio 2: Mover frente al final `[Nivel 2]` |
| 264 | + |
| 265 | +Implementa una función que tome el primer elemento de una lista y lo agregue al |
| 266 | +final. |
| 267 | + |
| 268 | +**Entrada/Salida esperada:** `["a", "b", "c"]` queda como `["b", "c", "a"]`. |
| 269 | + |
| 270 | +<details> |
| 271 | +<summary>Pista</summary> |
| 272 | +Combina `pop_front` con `push_back`. |
| 273 | +</details> |
| 274 | + |
| 275 | +### Ejercicio 3: Remover alternados `[Nivel 3]` |
| 276 | + |
| 277 | +Remueve cada segundo elemento de una lista. Para `1..=8`, el resultado debe ser |
| 278 | +`[1, 3, 5, 7]`. |
| 279 | + |
| 280 | +**Entrada/Salida esperada:** `[1, 3, 5, 7]`. |
| 281 | + |
| 282 | +<details> |
| 283 | +<summary>Pista</summary> |
| 284 | +Después de remover en un índice, el siguiente elemento se desplaza a ese mismo |
| 285 | +índice lógico. |
| 286 | +</details> |
| 287 | + |
| 288 | +### Ejercicio 4: Cola de reintentos `[Nivel 4]` |
| 289 | + |
| 290 | +Diseña una cola de reintentos para trabajos fallidos. Decide si basta esta lista |
| 291 | +simple, si necesitas guardar `tail`, o si conviene otra estructura. Explica las |
| 292 | +operaciones dominantes y sus costos. |
| 293 | + |
| 294 | +**Entrada/Salida esperada:** no hay salida única; se evalúa el razonamiento. |
| 295 | + |
| 296 | +<details> |
| 297 | +<summary>Pista</summary> |
| 298 | +Si insertas al final todo el tiempo, una lista simple sin `tail` te cobra O(n) |
| 299 | +por cada inserción. |
| 300 | +</details> |
| 301 | + |
| 302 | +## Soluciones |
| 303 | + |
| 304 | +Soluciones ejecutables de niveles 1 a 3: |
| 305 | + |
| 306 | +- [`examples/soluciones/linked_list_reverse_input.rs`](../examples/soluciones/linked_list_reverse_input.rs) |
| 307 | +- [`examples/soluciones/linked_list_move_front_to_back.rs`](../examples/soluciones/linked_list_move_front_to_back.rs) |
| 308 | +- [`examples/soluciones/linked_list_remove_every_other.rs`](../examples/soluciones/linked_list_remove_every_other.rs) |
| 309 | + |
| 310 | +Discusión para el nivel 4: |
| 311 | + |
| 312 | +Una lista simple es adecuada si procesas por el frente y agregas poco al final. |
| 313 | +Si cada trabajo fallido se reencola al final constantemente, guardar un `tail` |
| 314 | +vuelve natural la operación, pero en Rust seguro ese diseño requiere más cuidado: |
| 315 | +un puntero crudo exigiría `unsafe`, y una solución con `Rc<RefCell<_>>` cambia el |
| 316 | +modelo de ownership. Un `VecDeque` puede ser la estructura práctica correcta si |
| 317 | +solo necesitas una cola eficiente en ambos extremos. |
| 318 | + |
| 319 | +## Referencias |
| 320 | + |
| 321 | +- Donald Knuth, *The Art of Computer Programming*, Vol. 1, secciones sobre |
| 322 | + estructuras enlazadas. |
| 323 | +- Robert Sedgewick y Kevin Wayne, *Algorithms*, secciones sobre listas enlazadas |
| 324 | + y pilas/colas. |
| 325 | +- Rust Standard Library, `std::collections::LinkedList`, documentación y notas de |
| 326 | + uso. |
| 327 | +- Rustonomicon, capítulos sobre ownership, punteros y por qué estructuras |
| 328 | + enlazadas avanzadas suelen requerir invariantes más estrictas. |
0 commit comments