Parte 6 — Datos y estructuras · ⏱️ Duración estimada: 90 min · Nivel: Intermedio ✅ Clase construida — 10 implementaciones del núcleo verificadas contra
casos.json.
Comprender el arreglo dinámico —la estructura que se esconde bajo list, Vec, ArrayList o el slice de Go— no como «una lista que crece por arte de magia», sino como un arreglo contiguo de tamaño fijo envuelto en una capa de contabilidad. La idea central es distinguir dos números que suelen confundirse: la longitud (len, cuántos elementos hay de verdad) y la capacidad (cap, cuántos caben antes de tener que mudarse a un bloque mayor). Mientras quede capacidad libre, agregar al final es un simple arr[len] = x; len++, en tiempo constante. Cuando la capacidad se agota, la estructura reserva un bloque nuevo —típicamente el doble— y copia todo lo viejo, en O(n). El objetivo profundo de esta clase es entender por qué, pese a esas copias periódicas y caras, agregar al final sale O(1) amortizado: la duplicación geométrica reparte el coste de cada copia entre tantas inserciones baratas que el promedio queda constante. Cormen lo demuestra con el método contable y el potencial para tablas dinámicas (CLRS, §17.4) y Sedgewick lo llama «resizing arrays». Invertir la secuencia, el ejercicio de hoy, obliga a recorrerla entera (O(n)) y a decidir si mutamos en sitio o construimos una copia.
Al finalizar, podrás:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | Lista dinámica | Crece según haga falta |
| 2 | Invertir | Recorrer al revés |
| 3 | Redimensionar | Añadir/quitar elementos |
list de Python, Vec<T> de Rust, ArrayList de Java, List<T> de C#, el slice de Go). Conserva la propiedad esencial del arreglo —acceso por índice en O(1), porque arr[i] sigue siendo base + i × tamaño— pero paga esa contigüidad con el coste de reubicarse cuando se llena. Recorrerlo entero cuesta O(n).len es cuántos elementos contiene; cap es cuántos caben en el bloque reservado antes de tener que crecer. Casi siempre cap ≥ len, y el hueco entre ambos es la reserva que hace baratos los próximos append. Sedgewick insiste en no confundirlos: la capacidad es un detalle de implementación, la longitud es lo que ve el usuario.append concreto —el que dispara el redimensionado— cueste O(n) al copiar todo el contenido a un bloque nuevo (CLRS, §17.4). Insertar o borrar en el medio, en cambio, es O(n) siempre, porque hay que desplazar todos los elementos posteriores.Piensa en las líneas que llegan de un archivo, las respuestas que teclea un usuario o los resultados que devuelve una consulta: cantidades que no conoces de antemano. Un arreglo fijo te obligaría a adivinar el tamaño máximo y a desperdiciar memoria «por si acaso», o a quedarte corto. El arreglo dinámico resuelve el dilema creciendo bajo demanda, y lo hace de forma económica precisamente porque no crece de a uno: duplica su capacidad, de modo que las mudanzas se vuelven cada vez más raras a medida que la estructura engorda. El problema de hoy —leer una lista de enteros y emitirla al revés unidos por guiones— es deliberadamente pequeño para que la atención caiga sobre la estructura: cómo se construye elemento a elemento con append y cómo se recorre para invertirla.
invertido=<elementos en orden inverso unidos por ->Especificación y verificación en casos.json:
| stdin | esperado |
|---|---|
1 2 3 |
invertido=3-2-1 |
5 |
invertido=5 |
10 20 30 40 |
invertido=40-30-20-10 |
LEER lista ; invertir ; ESCRIBIR unidos por -
Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json.
Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.
python/main.py · python main.pyimport sys
nums = [int(x) for x in sys.stdin.read().split()]
nums.reverse()
print("invertido=" + "-".join(str(x) for x in nums))
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
javascript/main.mjs · node main.mjsimport { readFileSync } from "node:fs";
const nums = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
nums.reverse();
console.log(`invertido=${nums.join("-")}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
typescript/main.ts · pnpm exec tsx main.tsimport { readFileSync } from "node:fs";
const nums: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
nums.reverse();
console.log(`invertido=${nums.join("-")}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
java/Main.java · java Main.javaimport java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] p = br.readLine().trim().split("\\s+");
List<Integer> nums = new ArrayList<>();
for (String s : p) nums.add(Integer.parseInt(s));
Collections.reverse(nums);
System.out.println("invertido=" + nums.stream().map(String::valueOf).collect(Collectors.joining("-")));
}
}
🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure
csharp/Program.cs · dotnet runusing System;
using System.Linq;
string[] p = Console.In.ReadToEnd()
.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var nums = p.Select(int.Parse).Reverse();
Console.WriteLine($"invertido={string.Join("-", nums)}");
🧬 El mismo programa en la familia .NET: F# · VB.NET
go/main.go · go run main.gopackage main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
f := strings.Fields(line)
for i, j := 0, len(f)-1; i < j; i, j = i+1, j-1 {
f[i], f[j] = f[j], f[i]
}
fmt.Printf("invertido=%s\n", strings.Join(f, "-"))
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
rust/main.rs · rustc main.rs -o main && ./mainuse std::io::Read;
fn main() {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
let mut nums: Vec<&str> = s.split_whitespace().collect();
nums.reverse();
println!("invertido={}", nums.join("-"));
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
c/main.c · cc main.c -o main && ./main#include <stdio.h>
int main(void) {
long v[1024];
int n = 0;
while (scanf("%ld", &v[n]) == 1) n++;
printf("invertido=");
for (int i = n - 1; i >= 0; i--) {
if (i < n - 1) printf("-");
printf("%ld", v[i]);
}
printf("\n");
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
sql/main.sql · sqlite3 :memory: < main.sql-- SQL: invierte con ORDER BY sobre la posición.
WITH nums(pos, x) AS (VALUES (1, 1), (2, 2), (3, 3))
SELECT 'invertido=' || group_concat(x, '-') AS resultado
FROM (SELECT x FROM nums ORDER BY pos DESC);
🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog
php/main.php · php main.php<?php
$nums = preg_split('/\s+/', trim(fgets(STDIN)));
$nums = array_reverse($nums);
echo "invertido=" . implode("-", $nums) . "\n";
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.
Sigamos el caso 1 2 3, que debe producir invertido=3-2-1. Todas las implementaciones construyen una secuencia dinámica, la invierten y unen sus elementos con guiones; conviene comparar tres que revelan modelos distintos de crecimiento y de mutación.
En Python, la comprensión [int(x) for x in ...split()] construye la lista incrementalmente: cada valor que aparece dispara un append interno, y CPython va duplicando la capacidad del bloque a medida que hace falta (empieza pequeño y crece por saltos, no de a uno). Con [1, 2, 3] en mano, nums.reverse() invierte en sitio: no crea una lista nueva, sino que intercambia el primero con el último y avanza hacia el centro —un solo intercambio para tres elementos, ya que el del medio se queda quieto—. Luego "-".join(...) recorre la lista ya invertida y produce 3-2-1. La mutación en sitio es la razón de que reverse() no devuelva nada (retorna None): modifica el objeto original, como advierte Ramalho en Fluent Python.
En Go, el ejemplo evita construir enteros y trabaja directamente sobre f, el slice de cadenas que devuelve strings.Fields. Un slice es la trinidad puntero+len+cap: aquí no crece, solo se recorre. El bucle for i, j := 0, len(f)-1; i < j; ... es el patrón clásico de inversión en sitio con dos índices que convergen: intercambia f[0] con f[2] y se detiene cuando i < j deja de cumplirse, sin tocar el elemento central. Luego strings.Join(f, "-") produce 3-2-1. Es el mismo algoritmo que el reverse() de Python, pero escrito a mano y visible.
En C, no hay lista dinámica en la biblioteca estándar, así que el ejemplo reserva un arreglo fijo generoso (long v[1024]) y lleva la longitud real en n, contándola mientras scanf devuelva 1. Aquí no se invierte la memoria: se recorre al revés con for (int i = n - 1; i >= 0; i--), imprimiendo cada valor y anteponiendo un guion salvo en el primero. El resultado, 3-2-1, es idéntico; la diferencia es que C te obliga a gestionar tú mismo la separación entre capacidad (1024) y longitud (n), justo la contabilidad que las listas dinámicas automatizan.
Los tres imprimen invertido=3-2-1; el verificador comprueba que las diez implementaciones coinciden carácter a carácter con lo que dicta casos.json.
| Clase de diferencia | Observación entre lenguajes |
|---|---|
| Sintáctica | list[::-1] (Python), .reverse() (JS/Rust), Collections.reverse (Java). |
| Semántica | Algunos invierten en sitio (mutando); otros crean una lista nueva. |
| Paradigmática | SQL invierte con ORDER BY descendente sobre una posición. |
Bajo la sintaxis se esconden decisiones de diseño muy distintas. En valor vs. referencia: el slice de Go, la list de Python, el ArrayList de Java y el List<T> de C# son referencias (o encabezados que apuntan a un arreglo compartido), de modo que invertir en sitio afecta a todo el que tenga esa misma referencia; el Vec<T> de Rust, en cambio, tiene un único dueño, y el sistema de préstamos impide que otro lo observe mientras lo mutas. En colecciones nativas: Python, JavaScript y PHP ofrecen la lista dinámica como el tipo secuencia por defecto —no hay arreglo fijo que valga—, mientras Go la separa deliberadamente del arreglo ([]T frente a [N]T) y C ni siquiera la incluye, obligando a malloc/realloc manuales. En coste: todas comparten el acceso O(1) por índice y la inversión O(n), pero difieren en el append: donde Python, Rust y Go crecen por duplicación geométrica (O(1) amortizado, CLRS §17.4), en C cada crecimiento manual con realloc puede o no copiar según haya hueco contiguo, y el programador carga con esa decisión.
El par arreglo-fijo/arreglo-dinámico reaparece en casi todos los lenguajes, a veces con nombres que engañan. En Go, [3]int es un arreglo de valor que se copia entero al pasarlo, mientras []int es un slice —una vista con len y cap sobre un arreglo subyacente— que append puede reasignar a otro bloque cuando se llena; confundirlos produce bugs sutiles cuando dos slices comparten el mismo arreglo. En Rust conviven [i64; 3] (fijo, en la pila) y Vec<i64> (dinámico, en el heap, con capacity() inspeccionable). En C++ están std::array<T, N> y std::vector<T>, este último la referencia canónica del arreglo dinámico. En Java el arreglo primitivo int[] es fijo y ArrayList<Integer> es el dinámico —con el coste extra del boxing—. Reconocer cuál tienes entre manos, y si su len es fijo o crece, es medio problema resuelto.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 090
Detalle en reto.md.
list.reverse() en Python, Collections.reverse en Java) creyendo que devuelve una lista nueva, y modificar el original sin querer → solución: si necesitas conservar el original, usa una operación que devuelva copia (reversed(lista), lista[::-1] en Python) o duplica antes de invertir.reverse() devuelva la lista → causa: en Python nums.reverse() retorna None; escribir x = nums.reverse() deja x vacío → solución: invierte y luego usa nums, o usa reversed()/slicing si quieres una expresión que devuelva valor.arr[i] con arr[n-1-i] invierte y vuelve a invertir, dejándolo igual → solución: intercambiar solo hasta la mitad (i < j, índices que convergen), como hace la versión de Go.append siempre es barato → causa: creer que agregar al final nunca cuesta → solución: entender que es O(1) amortizado; el append que dispara el redimensionado copia todo en O(n), aunque el promedio siga siendo constante (CLRS §17.4).len/cap) y de reubicaciones ocasionales.append es O(1) si a veces copia todo? Porque la capacidad se duplica, no se aumenta de a uno. Cada copia cara de n elementos viene precedida de n inserciones baratas que la «pagan» por adelantado; el análisis amortizado (método contable o del potencial, CLRS §17.4) reparte el coste y da un promedio constante por operación.len y cap? len es cuántos elementos usas; cap cuántos caben antes de crecer. En Go puedes ver ambos con len(s) y cap(s); en Rust, con .len() y .capacity(). Python oculta la capacidad, pero existe igual bajo el capó.Libros de la parte:
Libros de los lenguajes del núcleo:
⏮️ Clase 089 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 091 ⏭️