|
| 1 | +# Vector |
| 2 | + |
| 3 | +> **Curso:** rust-data-structures · **Capítulo:** 01 · **Prerequisitos:** ninguno |
| 4 | +> **Código:** [`src/vector.rs`](../src/vector.rs) · **Video:** pendiente |
| 5 | +> **Lección en el sitio:** pendiente |
| 6 | +
|
| 7 | +## Introducción |
| 8 | + |
| 9 | +Un vector es un arreglo dinamico: una secuencia de elementos guardados en memoria |
| 10 | +contigua, con una longitud visible y una capacidad reservada. Es la primera |
| 11 | +estructura del curso porque obliga a entender tres ideas que regresan todo el |
| 12 | +tiempo: representacion interna, invariantes y costo real de una operacion. |
| 13 | + |
| 14 | +En este capitulo se asume que ya puedes leer Rust basico: `struct`, `impl`, |
| 15 | +genericos, `Option`, ownership y referencias. No se asume experiencia con |
| 16 | +gestion manual de memoria; precisamente por eso esta implementacion usa Rust |
| 17 | +seguro y deja la version con memoria sin inicializar como comparacion |
| 18 | +conceptual. |
| 19 | + |
| 20 | +## Motivación |
| 21 | + |
| 22 | +Imagina un motor de busqueda de reservas que recibe habitaciones candidatas. No |
| 23 | +sabe de antemano si encontrara dos, veinte o doscientas habitaciones, pero si |
| 24 | +necesita conservarlas en orden para filtrarlas, ordenarlas y mostrar resultados. |
| 25 | +Un arreglo de tamano fijo obliga a escoger una capacidad antes de conocer los |
| 26 | +datos. Una lista enlazada evita ese limite, pero paga con peor localidad de |
| 27 | +memoria y acceso por indice costoso. |
| 28 | + |
| 29 | +El vector resuelve el punto medio: guarda elementos contiguos como un arreglo, |
| 30 | +pero crece cuando se llena. Esa decision explica casi todo el capitulo: el |
| 31 | +acceso por indice es barato porque la memoria es contigua; `push` suele ser |
| 32 | +barato porque normalmente hay capacidad libre; insertar al frente es caro porque |
| 33 | +hay que mover los elementos. |
| 34 | + |
| 35 | +## Teoría |
| 36 | + |
| 37 | +### Historia |
| 38 | + |
| 39 | +Los arreglos dinamicos aparecen como respuesta practica a una limitacion muy |
| 40 | +antigua: muchos programas necesitan colecciones secuenciales cuyo tamano se |
| 41 | +descubre durante la ejecucion. Lenguajes y bibliotecas los han nombrado de |
| 42 | +formas distintas: `vector` en C++, `ArrayList` en Java, `list` en Python y |
| 43 | +`Vec<T>` en Rust. |
| 44 | + |
| 45 | +La idea central no cambia: reservar un bloque contiguo, recordar cuantos |
| 46 | +elementos estan inicializados y crecer a un bloque mayor cuando la longitud |
| 47 | +alcanza la capacidad. La tecnica es sencilla, pero poderosa: convierte muchas |
| 48 | +inserciones al final en tiempo amortizado constante. |
| 49 | + |
| 50 | +### Fundamentos |
| 51 | + |
| 52 | +Nuestro `Vector<T>` mantiene dos campos: |
| 53 | + |
| 54 | +- `items`: bloque contiguo de ranuras. |
| 55 | +- `len`: numero de elementos vivos. |
| 56 | + |
| 57 | +La capacidad es `items.len()`. La invariante principal es: |
| 58 | + |
| 59 | +```text |
| 60 | +0 <= len <= capacity |
| 61 | +``` |
| 62 | + |
| 63 | +Ademas, en esta implementacion segura, todas las posiciones `0..len` contienen |
| 64 | +`Some(T)` y todas las posiciones que quedan fuera de la longitud se tratan como |
| 65 | +espacio libre. Usamos `Option<T>` para que Rust pueda saber que ranuras tienen |
| 66 | +valor inicializado sin escribir `unsafe`. |
| 67 | + |
| 68 | +Cuando `push` encuentra espacio libre, escribe en `items[len]` y aumenta `len`. |
| 69 | +Cuando no hay espacio, `grow` reserva un bloque nuevo, normalmente del doble de |
| 70 | +capacidad, mueve los elementos y luego inserta el valor. Ese crecimiento ocasional |
| 71 | +es lo que hace que `push` tenga costo O(1) amortizado: algunas operaciones cuestan |
| 72 | +O(n), pero repartidas sobre muchas inserciones el costo promedio por `push` es |
| 73 | +constante. |
| 74 | + |
| 75 | +### Casos de uso |
| 76 | + |
| 77 | +Los vectores aparecen en casi todos los sistemas: |
| 78 | + |
| 79 | +- Resultados de busqueda antes de paginar o ordenar. |
| 80 | +- Buffers de eventos recibidos en lote. |
| 81 | +- Listas de IDs para filtrar entidades. |
| 82 | +- Tablas densas donde el indice es significativo. |
| 83 | +- Fronteras temporales en algoritmos, cuando no se necesita remover al frente. |
| 84 | + |
| 85 | +Tambien son la base de muchas estructuras futuras. Un heap binario suele |
| 86 | +representarse con un vector. Una cola circular puede usar un bloque contiguo. |
| 87 | +Muchas implementaciones de grafos usan vectores de listas de adyacencia. |
| 88 | + |
| 89 | +### Ventajas y limitaciones |
| 90 | + |
| 91 | +Ventajas: |
| 92 | + |
| 93 | +- Acceso por indice O(1). |
| 94 | +- Muy buena localidad de cache por memoria contigua. |
| 95 | +- Iteracion simple y rapida. |
| 96 | +- `push` al final O(1) amortizado. |
| 97 | +- Representacion facil de inspeccionar y probar. |
| 98 | + |
| 99 | +Limitaciones: |
| 100 | + |
| 101 | +- Insertar o remover al principio cuesta O(n). |
| 102 | +- Crecer puede mover todos los elementos. |
| 103 | +- La capacidad puede reservar memoria que aun no se usa. |
| 104 | +- Mantener punteros o referencias a elementos es delicado si el vector crece. |
| 105 | +- Nuestra version segura con `Option<T>` es pedagogica, no la mas eficiente. |
| 106 | + |
| 107 | +### Comparación con alternativas |
| 108 | + |
| 109 | +Un arreglo fijo es mejor cuando el tamano se conoce y no cambia. Un slice es una |
| 110 | +vista prestada sobre memoria existente, no una coleccion que crece. Una lista |
| 111 | +enlazada evita mover muchos elementos al insertar cerca del inicio, pero pierde |
| 112 | +localidad y acceso por indice barato. Un deque es mejor cuando se necesita |
| 113 | +insertar y remover por ambos extremos. |
| 114 | + |
| 115 | +El vector es la opcion correcta cuando el caso comun es agregar al final, |
| 116 | +recorrer en orden o acceder por indice. No es buena opcion cuando la operacion |
| 117 | +dominante es insertar al frente o remover constantemente del inicio. |
| 118 | + |
| 119 | +## Diagramas |
| 120 | + |
| 121 | +El diagrama principal vive en [`diagrams/01-vector.mmd`](../diagrams/01-vector.mmd). |
| 122 | + |
| 123 | +```mermaid |
| 124 | +flowchart LR |
| 125 | + title["Vector: longitud, capacidad y memoria contigua"] |
| 126 | +
|
| 127 | + subgraph metadata["Metadatos"] |
| 128 | + len["len = 3"] |
| 129 | + cap["capacity = 6"] |
| 130 | + end |
| 131 | +
|
| 132 | + subgraph memory["Bloque contiguo"] |
| 133 | + slot0["0: A"] |
| 134 | + slot1["1: B"] |
| 135 | + slot2["2: C"] |
| 136 | + slot3["3: libre"] |
| 137 | + slot4["4: libre"] |
| 138 | + slot5["5: libre"] |
| 139 | + end |
| 140 | +
|
| 141 | + len --> slot2 |
| 142 | + cap --> slot5 |
| 143 | + slot0 --> slot1 --> slot2 --> slot3 --> slot4 --> slot5 |
| 144 | +
|
| 145 | + push["push(D)"] |
| 146 | + push --> slot3 |
| 147 | +
|
| 148 | + grow["Si len == capacity: crecer y mover"] |
| 149 | + cap --> grow |
| 150 | +``` |
| 151 | + |
| 152 | +## Análisis de complejidad |
| 153 | + |
| 154 | +| Operación | Mejor caso | Caso promedio | Peor caso | Espacio | |
| 155 | +|-----------|------------|---------------|-----------|---------| |
| 156 | +| `new` | O(1) | O(1) | O(1) | O(1) | |
| 157 | +| `with_capacity(n)` | O(n) | O(n) | O(n) | O(n) | |
| 158 | +| `len` / `capacity` / `is_empty` | O(1) | O(1) | O(1) | O(1) | |
| 159 | +| `get` / `get_mut` | O(1) | O(1) | O(1) | O(1) | |
| 160 | +| `push` | O(1) | O(1) amortizado | O(n) | O(n) si crece | |
| 161 | +| `pop` | O(1) | O(1) | O(1) | O(1) | |
| 162 | +| `insert` | O(1) al final con espacio | O(n) | O(n) | O(n) si crece | |
| 163 | +| `remove` | O(1) al final | O(n) | O(n) | O(1) | |
| 164 | +| `clear` | O(n) | O(n) | O(n) | O(1) | |
| 165 | +| `iter` | O(1) crear, O(n) consumir | O(n) | O(n) | O(1) | |
| 166 | + |
| 167 | +`with_capacity(n)` es O(n) en esta version porque inicializa `n` ranuras con |
| 168 | +`None`. `Vec<T>` de la biblioteca estandar puede reservar memoria sin inicializar |
| 169 | +con tecnicas internas que requieren invariantes mas finas. |
| 170 | + |
| 171 | +La diferencia entre `push` e `insert(0, value)` es la leccion central. `push` |
| 172 | +normalmente escribe al final. `insert(0, value)` debe desplazar todos los |
| 173 | +elementos una posicion a la derecha. |
| 174 | + |
| 175 | +## Visualización interactiva (opcional) |
| 176 | + |
| 177 | +No aplica todavia. Este capitulo se entiende con el diagrama estatico y los |
| 178 | +benchmarks; una visualizacion interactiva se agregara cuando `academy-web` |
| 179 | +defina el mecanismo de playgrounds. |
| 180 | + |
| 181 | +## Implementación |
| 182 | + |
| 183 | +La implementacion completa vive en [`src/vector.rs`](../src/vector.rs). |
| 184 | + |
| 185 | +La representacion es deliberadamente segura: |
| 186 | + |
| 187 | +```rust |
| 188 | +pub struct Vector<T> { |
| 189 | + items: Box<[Option<T>]>, |
| 190 | + len: usize, |
| 191 | +} |
| 192 | +``` |
| 193 | + |
| 194 | +`items` es el bloque contiguo. `len` separa las ranuras ocupadas de la capacidad |
| 195 | +reservada. El costo educativo de usar `Option<T>` es que inicializamos ranuras |
| 196 | +libres; el beneficio es que evitamos `unsafe` mientras estudiamos la estructura. |
| 197 | + |
| 198 | +El crecimiento duplica capacidad: |
| 199 | + |
| 200 | +```rust |
| 201 | +let next_capacity = if self.capacity() == 0 { |
| 202 | + 1 |
| 203 | +} else { |
| 204 | + self.capacity() * 2 |
| 205 | +}; |
| 206 | +``` |
| 207 | + |
| 208 | +Duplicar no es magia: es una estrategia para que el numero total de movimientos |
| 209 | +tras muchas inserciones sea lineal, no cuadratico. Si crecieramos de uno en uno, |
| 210 | +cada `push` al llenar capacidad moveria casi todo otra vez. |
| 211 | + |
| 212 | +Insertar en medio muestra el costo que `push` esconde: |
| 213 | + |
| 214 | +```rust |
| 215 | +for current in (index..self.len).rev() { |
| 216 | + self.items[current + 1] = self.items[current].take(); |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | +Se recorre al reves para no sobrescribir valores que todavia deben moverse. |
| 221 | +Remover hace lo contrario: toma el elemento y desplaza lo que esta a la derecha |
| 222 | +una posicion a la izquierda. |
| 223 | + |
| 224 | +## Pruebas |
| 225 | + |
| 226 | +Las pruebas viven en [`tests/vector_test.rs`](../tests/vector_test.rs) y en el |
| 227 | +modulo interno de [`src/vector.rs`](../src/vector.rs). |
| 228 | + |
| 229 | +Cubren: |
| 230 | + |
| 231 | +- Vector vacio: longitud, capacidad y `pop` sobre vacio. |
| 232 | +- Crecimiento de capacidad. |
| 233 | +- Acceso dentro y fuera de rango. |
| 234 | +- Mutacion por indice con `get_mut`. |
| 235 | +- Insercion al medio y al final. |
| 236 | +- Remocion y preservacion de orden. |
| 237 | +- Iteracion. |
| 238 | +- `clear` conservando capacidad. |
| 239 | +- Comportamiento de ownership: `remove` transfiere propiedad y `clear` destruye |
| 240 | + los valores restantes. |
| 241 | + |
| 242 | +Los doc-comments tambien son pruebas: `cargo test --doc` compila y ejecuta los |
| 243 | +ejemplos de la API publica. |
| 244 | + |
| 245 | +## Benchmarks |
| 246 | + |
| 247 | +El benchmark vive en [`benches/vector_bench.rs`](../benches/vector_bench.rs) y |
| 248 | +se ejecuta con: |
| 249 | + |
| 250 | +```bash |
| 251 | +cargo bench --bench vector_bench |
| 252 | +``` |
| 253 | + |
| 254 | +Mide cuatro caminos: |
| 255 | + |
| 256 | +- crecimiento por `push`; |
| 257 | +- acceso por indice; |
| 258 | +- insercion al frente; |
| 259 | +- insercion al final con `insert(len, value)`. |
| 260 | + |
| 261 | +En una corrida local inicial con `SIZE = 20_000`, la insercion al frente fue |
| 262 | +varios ordenes de magnitud mas lenta que `push` o acceso por indice. El numero |
| 263 | +exacto no importa tanto como la relacion: mover miles de elementos en cada |
| 264 | +operacion domina el costo. |
| 265 | + |
| 266 | +## Ejercicios |
| 267 | + |
| 268 | +### Ejercicio 1: Trazar crecimiento `[Nivel 1]` |
| 269 | + |
| 270 | +Dado un vector vacio, inserta los valores `10, 20, 30, 40, 50` con `push`. |
| 271 | +Registra despues de cada insercion el par `(len, capacity)`. |
| 272 | + |
| 273 | +**Entrada/Salida esperada:** la salida debe mostrar la evolucion de longitud y |
| 274 | +capacidad. |
| 275 | + |
| 276 | +<details> |
| 277 | +<summary>Pista</summary> |
| 278 | +Observa que la primera capacidad pasa de 0 a 1, y despues se duplica. |
| 279 | +</details> |
| 280 | + |
| 281 | +### Ejercicio 2: Insertar manteniendo orden `[Nivel 2]` |
| 282 | + |
| 283 | +Implementa una funcion que reciba un `Vector<i32>` ordenado de menor a mayor e |
| 284 | +inserte un nuevo valor sin romper el orden. |
| 285 | + |
| 286 | +**Entrada/Salida esperada:** insertar `40` y luego `20` en `[10, 30, 50]` produce |
| 287 | +`[10, 20, 30, 40, 50]`. |
| 288 | + |
| 289 | +<details> |
| 290 | +<summary>Pista</summary> |
| 291 | +Busca el primer indice cuyo valor sea mayor o igual al nuevo valor. |
| 292 | +</details> |
| 293 | + |
| 294 | +### Ejercicio 3: Retener por prefijo `[Nivel 3]` |
| 295 | + |
| 296 | +Implementa una funcion que remueva de un `Vector<&str>` todos los valores que no |
| 297 | +empiezan con un prefijo dado. |
| 298 | + |
| 299 | +**Entrada/Salida esperada:** con prefijo `api`, el vector `["api", "api/v1", |
| 300 | +"api/v2", "admin", "assets"]` queda como `["api", "api/v1", "api/v2"]`. |
| 301 | + |
| 302 | +<details> |
| 303 | +<summary>Pista</summary> |
| 304 | +Cuando remueves en el indice actual, el siguiente elemento se desplaza a esa |
| 305 | +misma posicion. No incrementes el indice en ese caso. |
| 306 | +</details> |
| 307 | + |
| 308 | +### Ejercicio 4: Buffer de candidatos de reserva `[Nivel 4]` |
| 309 | + |
| 310 | +Disena una pequena estructura para guardar habitaciones candidatas de una busqueda |
| 311 | +de reservas. Debe permitir agregar candidatos, descartar candidatos por precio y |
| 312 | +recorrer los restantes en orden de llegada. Decide si `Vector` basta o si el |
| 313 | +problema pide otra estructura. |
| 314 | + |
| 315 | +**Entrada/Salida esperada:** no hay salida unica; explica tus invariantes y los |
| 316 | +tradeoffs. |
| 317 | + |
| 318 | +<details> |
| 319 | +<summary>Pista</summary> |
| 320 | +Si el orden de llegada importa y las remociones son pocas, `Vector` puede bastar. |
| 321 | +Si descartas miles de elementos al frente, considera otra estructura. |
| 322 | +</details> |
| 323 | + |
| 324 | +## Soluciones |
| 325 | + |
| 326 | +Soluciones ejecutables de niveles 1 a 3: |
| 327 | + |
| 328 | +- [`examples/soluciones/vector_trace_growth.rs`](../examples/soluciones/vector_trace_growth.rs) |
| 329 | +- [`examples/soluciones/vector_insert_sorted.rs`](../examples/soluciones/vector_insert_sorted.rs) |
| 330 | +- [`examples/soluciones/vector_retain_prefix.rs`](../examples/soluciones/vector_retain_prefix.rs) |
| 331 | + |
| 332 | +Discusion para el nivel 4: |
| 333 | + |
| 334 | +Un `Vector` es adecuado si el flujo natural es "recolectar candidatos, filtrar |
| 335 | +algunos y recorrer el resultado". Mantiene orden de llegada, es facil de paginar |
| 336 | +y aprovecha localidad. El costo aparece si el sistema descarta continuamente el |
| 337 | +primer elemento o necesita insertar por prioridad. En ese caso, un deque o heap |
| 338 | +podria expresar mejor el problema. La decision no depende de si una estructura |
| 339 | +es "mas avanzada"; depende de la operacion dominante. |
| 340 | + |
| 341 | +## Referencias |
| 342 | + |
| 343 | +- Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein, |
| 344 | + *Introduction to Algorithms*, secciones sobre analisis amortizado y arreglos |
| 345 | + dinamicos. |
| 346 | +- Rust Standard Library, `Vec<T>`: representacion publica, API y garantias de |
| 347 | + complejidad documentadas. |
| 348 | +- Bjarne Stroustrup, *The C++ Programming Language*, secciones sobre `vector` y |
| 349 | + memoria contigua. |
| 350 | +- Rustonomicon, capitulos sobre memoria sin inicializar y colecciones basadas en |
| 351 | + asignacion manual; referencia para entender por que una version industrial |
| 352 | + requiere invariantes mas estrictas. |
0 commit comments