|
| 1 | +# Stack |
| 2 | + |
| 3 | +> **Curso:** rust-data-structures · **Capítulo:** 03 · **Prerequisitos:** Capítulo 01, Vector; Capítulo 02, Linked List |
| 4 | +> **Código:** [`src/stack.rs`](../src/stack.rs) · **Video:** pendiente |
| 5 | +> **Lección en el sitio:** pendiente |
| 6 | +
|
| 7 | +## Introducción |
| 8 | + |
| 9 | +Una pila (*stack*) es una colección LIFO: *last in, first out*. El último valor |
| 10 | +que entra es el primero que sale. Esa regla es más importante que la forma de |
| 11 | +almacenamiento; una pila puede respaldarse con un vector, una lista enlazada u |
| 12 | +otra estructura. |
| 13 | + |
| 14 | +En este capítulo implementamos `Stack<T>` sobre el `Vector<T>` del capítulo 1. |
| 15 | +Así se ve una idea central del curso: muchas estructuras no son solo memoria, |
| 16 | +sino contratos de uso. El contrato de la pila es simple: `push`, `pop`, `peek`, |
| 17 | +underflow controlado y orden LIFO. |
| 18 | + |
| 19 | +## Motivación |
| 20 | + |
| 21 | +Cuando un editor ofrece "deshacer", guarda acciones en el orden en que ocurrieron, |
| 22 | +pero la siguiente acción que debe deshacer es la última. Cuando un parser revisa |
| 23 | +paréntesis, el cierre actual debe coincidir con la apertura más reciente. Cuando |
| 24 | +un recorrido usa DFS iterativo, el siguiente nodo sale del tope. |
| 25 | + |
| 26 | +En todos esos casos no necesitamos insertar en medio ni buscar por índice. Solo |
| 27 | +queremos trabajar con el tope. Una pila existe para expresar ese patrón sin |
| 28 | +exponer operaciones que distraen o rompen la intención. |
| 29 | + |
| 30 | +## Teoría |
| 31 | + |
| 32 | +### Historia |
| 33 | + |
| 34 | +Las pilas aparecen en arquitectura de computadoras, lenguajes de programación y |
| 35 | +algoritmos desde muy temprano. La pila de llamadas guarda frames de funciones: |
| 36 | +la función más reciente debe terminar antes de volver a la anterior. Las máquinas |
| 37 | +virtuales basadas en pila evalúan expresiones empujando operandos y aplicando |
| 38 | +operaciones sobre el tope. |
| 39 | + |
| 40 | +Su valor histórico y práctico viene de una restricción: al limitar el acceso al |
| 41 | +tope, la estructura se vuelve fácil de razonar. |
| 42 | + |
| 43 | +### Fundamentos |
| 44 | + |
| 45 | +La pila expone estas operaciones: |
| 46 | + |
| 47 | +- `push(value)`: agrega un valor al tope. |
| 48 | +- `pop()`: remueve el tope y lo devuelve. |
| 49 | +- `peek()`: lee el tope sin removerlo. |
| 50 | +- `peek_mut()`: modifica el tope sin cambiar la profundidad. |
| 51 | +- `clear()`: vacía la pila. |
| 52 | +- `iter()`: recorre desde la base hasta el tope. |
| 53 | + |
| 54 | +La invariante semántica es: |
| 55 | + |
| 56 | +```text |
| 57 | +si push(a) ocurre antes que push(b), entonces pop() devuelve b antes que a |
| 58 | +``` |
| 59 | + |
| 60 | +Nuestra implementación guarda el tope al final del vector interno. Eso hace que |
| 61 | +`push` use `Vector::push` y `pop` use `Vector::pop`, ambas operaciones eficientes |
| 62 | +para el final de la colección. |
| 63 | + |
| 64 | +### Casos de uso |
| 65 | + |
| 66 | +Usos clásicos: |
| 67 | + |
| 68 | +- Undo/redo. |
| 69 | +- Validación de paréntesis y delimitadores. |
| 70 | +- Evaluación de expresiones. |
| 71 | +- DFS iterativo. |
| 72 | +- Historial de navegación. |
| 73 | +- Call stack conceptual de un programa. |
| 74 | + |
| 75 | +### Ventajas y limitaciones |
| 76 | + |
| 77 | +Ventajas: |
| 78 | + |
| 79 | +- API mínima y clara. |
| 80 | +- `push`, `pop` y `peek` baratos. |
| 81 | +- Underflow representado con `Option<T>`, sin pánico. |
| 82 | +- Fácil de respaldar con diferentes estructuras. |
| 83 | + |
| 84 | +Limitaciones: |
| 85 | + |
| 86 | +- No permite acceso arbitrario sin romper la abstracción. |
| 87 | +- Iterar expone lectura, pero no debe reemplazar el contrato LIFO. |
| 88 | +- Un vector puede mover memoria al crecer. |
| 89 | +- Una lista enlazada puede evitar crecimiento por bloque, pero paga asignaciones |
| 90 | + por nodo y peor localidad. |
| 91 | + |
| 92 | +### Comparación con alternativas |
| 93 | + |
| 94 | +Una pila respaldada por vector aprovecha memoria contigua y `push/pop` al final. |
| 95 | +Una pila respaldada por lista enlazada puede hacer `push_front/pop_front` en O(1), |
| 96 | +pero asigna un nodo por valor. En la práctica, para valores pequeños y muchos |
| 97 | +recorridos, la localidad del vector suele ganar. |
| 98 | + |
| 99 | +Un deque también puede actuar como pila, pero expone más operaciones de las que |
| 100 | +el concepto necesita. La decisión no es solo rendimiento: una API más pequeña |
| 101 | +enseña y protege la intención. |
| 102 | + |
| 103 | +## Diagramas |
| 104 | + |
| 105 | +El diagrama principal vive en [`diagrams/03-stack.mmd`](../diagrams/03-stack.mmd). |
| 106 | + |
| 107 | +```mermaid |
| 108 | +flowchart TB |
| 109 | + title["Stack: LIFO sobre almacenamiento interno"] |
| 110 | +
|
| 111 | + top["top"] |
| 112 | + slot2["2: C"] |
| 113 | + slot1["1: B"] |
| 114 | + slot0["0: A"] |
| 115 | + base["base"] |
| 116 | +
|
| 117 | + top --> slot2 |
| 118 | + slot2 --> slot1 |
| 119 | + slot1 --> slot0 |
| 120 | + slot0 --> base |
| 121 | +
|
| 122 | + push["push(D)<br/>agrega sobre top"] |
| 123 | + pop["pop()<br/>remueve top"] |
| 124 | + peek["peek()<br/>lee top sin remover"] |
| 125 | +
|
| 126 | + push --> top |
| 127 | + pop --> top |
| 128 | + peek --> top |
| 129 | +``` |
| 130 | + |
| 131 | +## Análisis de complejidad |
| 132 | + |
| 133 | +| Operación | Mejor caso | Caso promedio | Peor caso | Espacio | |
| 134 | +|-----------|------------|---------------|-----------|---------| |
| 135 | +| `new` | O(1) | O(1) | O(1) | O(1) | |
| 136 | +| `with_capacity(n)` | O(n) | O(n) | O(n) | O(n) | |
| 137 | +| `len` / `capacity` / `is_empty` | O(1) | O(1) | O(1) | O(1) | |
| 138 | +| `push` | O(1) | O(1) amortizado | O(n) si crece | O(n) si crece | |
| 139 | +| `pop` | O(1) | O(1) | O(1) | O(1) | |
| 140 | +| `peek` / `peek_mut` | O(1) | O(1) | O(1) | O(1) | |
| 141 | +| `clear` | O(n) | O(n) | O(n) | O(1) | |
| 142 | +| `iter` | O(1) crear, O(n) consumir | O(n) | O(n) | O(1) | |
| 143 | + |
| 144 | +El peor caso de `push` aparece cuando el vector interno debe crecer. Aun así, el |
| 145 | +costo amortizado es O(1), igual que en el capítulo de `Vector`. |
| 146 | + |
| 147 | +## Visualización interactiva (opcional) |
| 148 | + |
| 149 | +No aplica todavía. La pila se entiende con el diagrama, los ejemplos de |
| 150 | +undo/parsing y los benchmarks; se agregará playground cuando `academy-web` tenga |
| 151 | +ese mecanismo definido. |
| 152 | + |
| 153 | +## Implementación |
| 154 | + |
| 155 | +La implementación vive en [`src/stack.rs`](../src/stack.rs). |
| 156 | + |
| 157 | +El tipo es pequeño: |
| 158 | + |
| 159 | +```rust |
| 160 | +pub struct Stack<T> { |
| 161 | + items: Vector<T>, |
| 162 | +} |
| 163 | +``` |
| 164 | + |
| 165 | +`push` delega al final del vector: |
| 166 | + |
| 167 | +```rust |
| 168 | +pub fn push(&mut self, value: T) { |
| 169 | + self.items.push(value); |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +`peek` calcula el último índice sin restar sobre cero: |
| 174 | + |
| 175 | +```rust |
| 176 | +self.items |
| 177 | + .len() |
| 178 | + .checked_sub(1) |
| 179 | + .and_then(|index| self.items.get(index)) |
| 180 | +``` |
| 181 | + |
| 182 | +Ese detalle importa: una pila vacía no debe provocar underflow aritmético ni |
| 183 | +pánico. Devuelve `None`, porque no hay tope. |
| 184 | + |
| 185 | +## Pruebas |
| 186 | + |
| 187 | +Las pruebas viven en [`tests/stack_test.rs`](../tests/stack_test.rs) y dentro de |
| 188 | +[`src/stack.rs`](../src/stack.rs). |
| 189 | + |
| 190 | +Cubren: |
| 191 | + |
| 192 | +- Underflow: `pop` y `peek` sobre pila vacía. |
| 193 | +- Orden LIFO. |
| 194 | +- `peek` sin remover. |
| 195 | +- `peek_mut` modificando solo el tope. |
| 196 | +- `clear` conservando capacidad. |
| 197 | +- Iteración desde base hasta tope. |
| 198 | +- Movimiento de ownership con `pop`. |
| 199 | +- Destrucción de valores restantes con `clear`. |
| 200 | + |
| 201 | +Los doc-comments se validan con `cargo test --doc`. |
| 202 | + |
| 203 | +## Benchmarks |
| 204 | + |
| 205 | +El benchmark vive en [`benches/stack_bench.rs`](../benches/stack_bench.rs) y se |
| 206 | +ejecuta con: |
| 207 | + |
| 208 | +```bash |
| 209 | +cargo bench --bench stack_bench |
| 210 | +``` |
| 211 | + |
| 212 | +Mide: |
| 213 | + |
| 214 | +- `push/pop` con stack respaldado por vector; |
| 215 | +- `push_front/pop_front` con lista enlazada como comparación; |
| 216 | +- `peek` repetido. |
| 217 | + |
| 218 | +La comparación existe para enseñar tradeoffs. Ambas estrategias pueden sostener |
| 219 | +operaciones LIFO baratas, pero el vector suele tener mejor localidad y la lista |
| 220 | +evita crecimiento por bloque a cambio de asignar nodos. |
| 221 | + |
| 222 | +## Ejercicios |
| 223 | + |
| 224 | +### Ejercicio 1: Trazar operaciones `[Nivel 1]` |
| 225 | + |
| 226 | +Ejecuta la secuencia `push(A)`, `push(B)`, `pop()` y registra el tope después de |
| 227 | +cada paso. |
| 228 | + |
| 229 | +**Entrada/Salida esperada:** `[Some("A"), Some("B"), Some("A")]`. |
| 230 | + |
| 231 | +<details> |
| 232 | +<summary>Pista</summary> |
| 233 | +`pop` remueve `B`, por eso `A` vuelve a quedar como tope. |
| 234 | +</details> |
| 235 | + |
| 236 | +### Ejercicio 2: Paréntesis balanceados `[Nivel 2]` |
| 237 | + |
| 238 | +Implementa una función que determine si una cadena tiene paréntesis, corchetes y |
| 239 | +llaves balanceados. |
| 240 | + |
| 241 | +**Entrada/Salida esperada:** `([]{})` es válido; `([)]` no lo es. |
| 242 | + |
| 243 | +<details> |
| 244 | +<summary>Pista</summary> |
| 245 | +Empuja aperturas y compara cada cierre contra la apertura más reciente. |
| 246 | +</details> |
| 247 | + |
| 248 | +### Ejercicio 3: Undo/redo `[Nivel 3]` |
| 249 | + |
| 250 | +Modela dos pilas: una para undo y otra para redo. Mover el tope de una pila a la |
| 251 | +otra debe preservar el valor exacto. |
| 252 | + |
| 253 | +**Entrada/Salida esperada:** deshacer y rehacer una acción deja la pila de undo |
| 254 | +con esa acción nuevamente en el tope. |
| 255 | + |
| 256 | +<details> |
| 257 | +<summary>Pista</summary> |
| 258 | +`pop` transfiere ownership; no necesitas clonar. |
| 259 | +</details> |
| 260 | + |
| 261 | +### Ejercicio 4: Parser iterativo `[Nivel 4]` |
| 262 | + |
| 263 | +Diseña un parser o recorrido de árbol que use una pila explícita en vez de |
| 264 | +recursión. Explica qué guardas en cada frame y por qué el orden LIFO es correcto. |
| 265 | + |
| 266 | +**Entrada/Salida esperada:** no hay una única solución; se evalúa el diseño y |
| 267 | +sus invariantes. |
| 268 | + |
| 269 | +<details> |
| 270 | +<summary>Pista</summary> |
| 271 | +Piensa en reemplazar el call stack por una estructura que tú controlas. |
| 272 | +</details> |
| 273 | + |
| 274 | +## Soluciones |
| 275 | + |
| 276 | +Soluciones ejecutables de niveles 1 a 3: |
| 277 | + |
| 278 | +- [`examples/soluciones/stack_trace_operations.rs`](../examples/soluciones/stack_trace_operations.rs) |
| 279 | +- [`examples/soluciones/stack_balanced_parentheses.rs`](../examples/soluciones/stack_balanced_parentheses.rs) |
| 280 | +- [`examples/soluciones/stack_undo_redo.rs`](../examples/soluciones/stack_undo_redo.rs) |
| 281 | + |
| 282 | +Discusión para el nivel 4: |
| 283 | + |
| 284 | +Una pila explícita es útil cuando quieres controlar el recorrido, evitar |
| 285 | +recursión profunda o guardar estado adicional por frame. El tradeoff es que el |
| 286 | +programa debe hacer visible lo que antes hacía el call stack: qué falta visitar, |
| 287 | +qué resultado parcial existe y cuándo se termina un frame. |
| 288 | + |
| 289 | +## Referencias |
| 290 | + |
| 291 | +- Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein, |
| 292 | + *Introduction to Algorithms*, secciones sobre pilas, colas y DFS. |
| 293 | +- Robert Sedgewick y Kevin Wayne, *Algorithms*, secciones introductorias de |
| 294 | + stacks y expresión de algoritmos con LIFO. |
| 295 | +- Rust Standard Library, `Vec<T>` y `Option<T>`, como base para una pila segura. |
| 296 | +- Rust Book, capítulos de ownership y borrowing, para entender por qué `pop` |
| 297 | + transfiere valores. |
0 commit comments