06 — Data Structures Basics: cómo se implementa en cada familia
06 — Data Structures Basics: how it is implemented in each family
ES: Guía de implementación del módulo. Explica cómo y por qué el mismo
Nodeproduce una lista, una pila y una cola en cada familia de lenguajes. No añade requisitos: los criterios están en06_Data_Structures_Basics.mdy, si esta guía y la especificación discrepan, manda la especificación.EN: Implementation guide for the module. It explains how and why the same
Nodeyields a list, a stack and a queue in each language family. It adds no requirements: the criteria live in06_Data_Structures_Basics.mdand, should this guide and the specification disagree, the specification wins.
1. La solución canónica, en llano / The canonical solution, in plain words
ES: El módulo no pide cuatro estructuras: pide un tipo de celda —Node, con un valor y un enlace— y tres formas de nombrar sus extremos. La lista guarda head y tail, la pila guarda top, la cola guarda front y rear; las tres llevan además un contador. Eso es todo el estado: los punteros son lo que distingue una estructura de otra, y el contador es lo que hace size y is_empty gratuitos.
- Lista:
insert_headengancha un nodo delante;insert_taillo engancha detrás porque guardatail;deleterecorre desdeheadcon unprevioushasta la primera aparición. - Pila:
pushengancha delante detop;popmuevetopal siguiente. Es una lista en la que solo se toca un extremo. - Cola:
enqueueengancha detrás derear;dequeuemuevefrontal siguiente y, si la cola se vacía, dejarearausente.
Lo que la especificación fija de verdad: el Node único y compartido, init obligatorio antes de cualquier consulta, ausencia del enlace en la representación nativa, un indicador de fallo por tipo de retorno declarado en el README, delete en $O(n)$ y el resto en $O(1)$, los casos como pasos sucesivos sobre la misma instancia, y que pila y cola no deleguen en la lista ni en una colección de la biblioteca. Lo que es solo forma: si la celda se muta o se reconstruye, si init es un constructor que devuelve el valor o un procedimiento que lo rellena, y cómo se llaman los tipos cuando hay conflicto léxico.
EN: The module does not ask for four structures: it asks for one cell type —Node, with a value and a link— and three ways of naming its ends. The list keeps head and tail, the stack keeps top, the queue keeps front and rear; all three also keep a counter. That is the whole state: the pointers are what makes one structure different from another, and the counter is what makes size and is_empty free.
What the specification actually fixes: the single shared Node, init before any query, link absence in the native representation, one failure indicator per return type declared in the README, delete in $O(n)$ and everything else in $O(1)$, the cases as successive steps on the same instance, and stacks and queues that do not delegate to the list or to a library collection. What is just form: whether the cell is mutated or rebuilt, whether init is a constructor returning the value or a procedure filling it, and how the types are named when there is a lexical conflict.
2. Los ejes que cambian en este módulo / The axes that change here
| Eje / Axis | Pregunta / Question | Si sí / If yes | Si no / If no |
|---|---|---|---|
| Mutabilidad de la celda | ¿Se puede actualizar el enlace de un nodo ya creado? | Los punteros se reasignan: insert_tail y enqueue son $O(1)$ de verdad |
Cada operación devuelve nodo y estructura nuevos: insert_tail y enqueue bajan a $O(n)$ y se declara |
Forma de init |
¿El tipo se declara y luego se rellena, o se construye devolviendo el valor? | Procedimiento sobre una instancia ya declarada (Make-style) |
Constructor que devuelve el valor inicializado |
| Ausencia del enlace | ¿La representación nativa del lenguaje admite «no hay siguiente»? | Se usa tal cual (null, nil, None, Nothing, #f, (), puntero nulo) |
Se declara la representación equivalente en el README |
| Indicador de fallo | ¿Qué se devuelve al extraer de una estructura vacía? | El valor centinela del tipo de retorno (-1 en números) o el fallo booleano |
— |
| Contador | ¿La estructura puede llevar su tamaño? | size e is_empty son $O(1)$ en las tres variantes |
Sin contador habría que recorrer: no lo permite ningún lenguaje del repo |
ES: El primer eje es el que cambia la complejidad, y no es una preferencia: guardar tail o rear solo sirve si se puede escribir tail.next = node. Donde no se puede, el último enlace está al final de una cadena que hay que reconstruir, y la operación se vuelve $O(n)$.
EN: The first axis is the one that changes complexity, and it is not a preference: keeping tail or rear only pays off if tail.next = node can be written. Where it cannot, the last link sits at the end of a chain that must be rebuilt, and the operation becomes $O(n)$.
3. Las familias: cómo definen la celda y sus extremos / The families: how they define the cell and its ends
| Familia / Family | Cómo define la celda / How it defines the cell | Qué puede mutar / What it may mutate | Ausencia del enlace / Link absence | Variante / Variant |
|---|---|---|---|---|
| F1 OO con null | Una clase con campos; el enlace es una referencia | Los campos de la instancia | Referencia nula | 4.1 |
| F2 Scripting dinámico | Un objeto o registro sin tipos declarados | Los campos del objeto | Valor nulo del lenguaje | 4.1 |
| F3 C-lineage | Un registro y un puntero al mismo registro | El registro y el puntero | Puntero nulo | 4.2 |
| F4 Sistemas modernos | Registro con el enlace como referencia u opción del lenguaje | El registro, si el tipo lo permite | Ausencia tipada del propio lenguaje | 4.2 |
| F5 Funcional ML/Haskell | Un tipo algebraico o registro inmutable | Nada: se devuelve un valor nuevo | La opción nativa del lenguaje, solo en el enlace | 4.3 |
| F6 Lisp | Un registro o un par | Nada, salvo celdas mutables explícitas | Lista vacía, falso o nulo | 4.3 |
| F7 BEAM | Un registro o mapa | Nada: se devuelve un valor nuevo | Valor ausente del dominio | 4.3 |
| F8 Multi-paradigma | Depende del tipo elegido | Según el tipo | Según el tipo | 4.1 o 4.3, declarada en el README |
| F9 Lógico | Un término con el valor y el enlace como argumentos | Nada | Fallo de unificación | Variante relacional, declarada en el README |
ES: Ninguna familia cambia qué se guarda: siempre un valor, un enlace y los punteros de la estructura. Lo que cambia es si el enlace se escribe o se devuelve, y por tanto si los extremos cuestan $O(1)$ o $O(n)$. EN: No family changes what is stored: always a value, a link and the structure’s pointers. What changes is whether the link is written or returned, and therefore whether the ends cost $O(1)$ or $O(n)$.
4. Las variaciones, en pseudocódigo / The variations, in pseudocode
4.1 La variante del módulo: celda mutable con punteros / The module’s variant: mutable cell with pointers
ES: Es la de la especificación, resumida en las operaciones que deciden la forma. El estado de cada estructura son sus punteros y el contador; los enlaces se reasignan. EN: This is the specification’s own, reduced to the operations that decide the shape. Each structure’s state is its pointers and the counter; links are reassigned.
Node
value, next
init(value)
this.value = value
this.next = absent # ausencia nativa del lenguaje
return this
LinkedList: head, tail, count
insert_head(v): node = Node.init(v); node.next = head; head = node
if tail is absent: tail = node; count += 1
insert_tail(v): node = Node.init(v)
if tail is absent: head = node else tail.next = node
tail = node; count += 1 # O(1) por tener `tail`
delete(v): previous = absent; current = head
while current is not absent
if current.value == v
previous ? previous.next = current.next : head = current.next
if tail == current: tail = previous
count -= 1; return success
previous = current; current = current.next
return failure # O(n): recorre
Stack: top, count
push(v): node = Node.init(v); node.next = top; top = node; count += 1
pop(): if top is absent: return failure
value = top.value; top = top.next; count -= 1; return value
Queue: front, rear, count
enqueue(v): node = Node.init(v)
if rear is absent: front = node else rear.next = node
rear = node; count += 1 # O(1) por tener `rear`
dequeue(): if front is absent: return failure
value = front.value; front = front.next
if front is absent: rear = absent # la cola se vació
count -= 1; return value
ES: Los dos if ... is absent de insert_tail y enqueue son el caso «estructura vacía», y el rear = absent de dequeue es el que evita que el siguiente enqueue escriba sobre un nodo que ya no está en la cola. Sin esas tres guardas la estructura funciona hasta que deja de hacerlo.
EN: The two if ... is absent checks in insert_tail and enqueue are the empty-single case, and rear = absent in dequeue is what stops the next enqueue from writing through a node that has left the queue. Without those three guards the structure works until it stops working.
4.2 La de la celda preasignada: init como procedimiento / Pre-allocated cell: init as a procedure
ES: Cuando el lenguaje no tiene constructores que devuelvan valores, la instancia se declara y init la rellena. El enlace es un puntero nulo y la ausencia, ese mismo valor.
EN: When the language has no constructors that return values, the instance is declared and init fills it. The link is a null pointer and absence is that same value.
procedure Node.init(nb: out Node; value: int)
nb.value = value
nb.next = null
procedure LinkedList.init(lb: out LinkedList)
lb.head = null; lb.tail = null; lb.count = 0
procedure insert_tail(lb: in out LinkedList; value: int)
node = allocate(Node); Node.init(node, value)
if lb.tail == null: lb.head = node
else: lb.tail.next = node # escribe a través del puntero
lb.tail = node; lb.count += 1
ES: La diferencia con 4.1 no está en el algoritmo sino en quién posee el valor: aquí toda operación recibe in out la estructura y la muta, y la ausencia del enlace se comprueba con == null, no con un tipo opcional.
EN: The difference from 4.1 is not in the algorithm but in who owns the value: here every operation takes the structure as in out and mutates it, and link absence is checked with == null, not with an optional type.
4.3 La variante por casos: la celda no se muta / The clause-based variant: the cell is not mutated
ES: Si el nodo es inmutable, set_next devuelve un nodo nuevo y toda operación devuelve también la estructura nueva. Los extremos siguen existiendo, pero el último enlace ya no se puede escribir: se reconstruye el camino, y por eso insert_tail y enqueue pasan a $O(n)$.
EN: If the node is immutable, set_next returns a new node and every operation also returns the new structure. The ends still exist, but the last link can no longer be written: the path is rebuilt, which is why insert_tail and enqueue drop to $O(n)$.
insert_tail(xs, v) # reconstruye el camino hasta el final
xs == empty -> single(init(v))
single(x) -> init(x, next = init(v))
node(x, rest) -> node(x, next = insert_tail(rest, v))
delete(xs, v) # devuelve (éxito, estructura)
xs == empty -> (failure, xs)
node(x, rest) where x == v -> (success, rest)
node(x, rest) -> let (ok, rest') = delete(rest, v) in (ok, node(x, rest'))
push(s, v) -> Stack(top = node(v, next = s.top), count = s.count + 1) # devuelve la pila nueva
pop(s) -> s.top == absent ? (failure, s)
: (value(s.top), Stack(top = next(s.top), count - 1))
ES: El contador sigue dentro de la estructura devuelta, así que size e is_empty siguen siendo $O(1)$; lo que se paga es la reconstrucción de los nodos, no la lectura de los extremos.
EN: The counter still travels inside the returned structure, so size and is_empty remain $O(1)$; what is paid for is node rebuilding, not reading the ends.
ES: En las tres variantes se guarda lo mismo y se responde lo mismo; lo que se mueve es quién escribe el enlace (la estructura, el puntero o el valor devuelto) y, con ello, si los extremos cuestan $O(1)$ o $O(n)$. EN: All three variants store the same and answer the same; what moves is who writes the link (the structure, the pointer, or the returned value) and, with it, whether the ends cost $O(1)$ or $O(n)$.
5. Consecuencias, leídas desde el pseudocódigo / Consequences, read from the pseudocode
ES: El coste de cada operación se lee directamente del código, no de una tabla aparte:
- 4.1 y 4.2 — todo $O(1)$ salvo
delete.insert_head,insert_tail,push,enqueue,pop,dequeue,peek,size,is_emptyyget_headtocan punteros y un contador.deleterecorre conprevious/currenty es $O(n)$, como fija la especificación. La memoria es un nodo por elemento, sin capacidad reservada. - 4.2 — el coste oculto es la reserva. Cada inserción pide memoria nueva; en un lenguaje con gestión manual, ese
allocatees también parte del coste y de la responsabilidad de liberar. - 4.3 — los extremos cuestan $O(n)$.
insert_tailyenqueuereconstruyen el camino hasta el último nodo, y cada reconstrucción asigna $O(n)$ nodos. Las lecturas de extremos (get_head,peek,pop,dequeue) siguen en $O(1)$ porque están en la cabeza,topofront;sizeeis_emptytambién, porque el contador viaja en el valor devuelto.deletees $O(n)$ en todas las variantes. - El contador no es decoración. Sin él,
sizesería un recorrido yis_emptyuna comparación con la ausencia; con él, ambos son una lectura. En 4.3 es lo único que evita convertir tres operaciones en $O(n)$.
Coste de expresión medido (líneas de la fuente del módulo por lenguaje, 2026-10-17): de 94 en ada a 371 en clojure, 844 en rust, 893 en cobol y 1167 en assembly. Cuatro estructuras, el mismo Node y los mismos casos.
EN: The cost of each operation reads straight off the code: in 4.1 and 4.2 everything is $O(1)$ except delete ($O(n)$ by specification), with one node per element and no reserved capacity; 4.2 adds the allocation cost; in 4.3 the ends cost $O(n)$ because the path to the last node is rebuilt with $O(n)$ allocations, while head reads, size and is_empty stay $O(1)$ thanks to the counter, and delete remains $O(n)$ everywhere. Measured expression cost: 94 lines in Ada to 1167 in Assembly.
6. Trampas para quien llega nuevo / Traps for newcomers
- Compartir el
Nodeno es envolver la lista.StackyQueueusan el mismo tipo de celda, pero llevan sus propios punteros y su propio contador; sipushacaba llamando ainsert_head, el módulo ya no implementa tres ADT. initno es opcional. La instancia se declara sin valores definidos y soloinitla deja usable; consultaris_emptyantes es un caso fuera de contrato, no un fallo que haya que tolerar.- El indicador depende del tipo de retorno. Un número que puede faltar devuelve el centinela (
-1); una operación de éxito/fallo devuelve el booleano del lenguaje. Mezclarlos es lo que hace ambiguo el contrato. - La ausencia del enlace no es un indicador de fallo.
nextausente significa «fin de cadena» y usa la representación nativa;-1no es un enlace ausente. - Tres guardas que se olvidan siempre: el primer
insert_tailsobre una lista vacía, el primerenqueuesobre una cola vacía, yrear = absental sacar el último elemento. deletedel último elemento debe movertail. Si no, el siguienteinsert_tailescribe a través de un nodo que ya no está y la lista queda rota.- Con celdas inmutables,
insert_tailyenqueueno son $O(1)$. No es un defecto de la implementación: es lo que cuesta no poder escribir el enlace. Se declara en el README, con la misma honestidad con la que se declara una desviación. - Los valores de prueba son positivos para no confundirse con el centinela de fallo.
- Nada de colecciones de la biblioteca ni de imports de otros módulos: la estructura se construye desde cero o el módulo no existe.
7. Dónde seguir / Where next
| Capa / Layer | Documento / Document |
|---|---|
| Qué se exige / What is required | 06_Data_Structures_Basics.md |
| Las familias en general / Families in general | PARADIGM_FAMILIES.md |
| Qué obliga tu lenguaje / What your language mandates | idioms/{lang}.md |
| Qué se verificó en los 50 / What was verified across the 50 | 06_Audited_Data_Structures_Basics.md |