Skip to content

Commit f520cf2

Browse files
committed
feat: add vector chapter
1 parent 40dd06b commit f520cf2

5 files changed

Lines changed: 362 additions & 7 deletions

File tree

.github/workflows/docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ jobs:
2020
test -s AGENTS.md
2121
test -s ROADMAP.md
2222
test -s docs/SUMMARY.md
23+
test -s docs/01-vector.md
2324
test -s examples/README.md
2425
test -s tests/README.md
2526
test -s benches/README.md
27+
test -s benches/vector_bench.rs
2628
test -s diagrams/README.md
29+
test -s diagrams/01-vector.mmd
2730
test -s assets/README.md
2831
2932
grep -q "Vector" docs/SUMMARY.md

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ un algoritmo sea necesario para explicar la propia estructura.
3030

3131
| # | Capitulo | Modulo | Estado |
3232
|---|----------|--------|--------|
33-
| 01 | Vector | `src/vector.rs` | planned |
33+
| 01 | Vector | `src/vector.rs` | benchmarked |
3434
| 02 | Linked List | `src/linked_list.rs` | planned |
3535
| 03 | Stack | `src/stack.rs` | planned |
3636
| 04 | Queue | `src/queue.rs` | planned |

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ El checklist detallado vive en
1919

2020
| # | Capitulo | Estado |
2121
|---|----------|--------|
22-
| 01 | Vector | planned |
22+
| 01 | Vector | benchmarked |
2323
| 02 | Linked List | planned |
2424
| 03 | Stack | planned |
2525
| 04 | Queue | planned |

docs/01-vector.md

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

Comments
 (0)