Publicación
- 12 min read
Estructuras de datos que todo desarrollador debe conocer

Estructuras de datos que todo desarrollador debe conocer (con ejemplos de código)
Las estructuras de datos son los pilares fundamentales de la ingeniería de software. Tanto si estás escribiendo un script sencillo como si estás entrenando un modelo de aprendizaje automático, las estructuras de datos que elijas determinan la eficiencia con la que se ejecuta tu código.
Si alguna vez te has preguntado por qué algunas soluciones son ultrarrápidas mientras que otras son muy lentas, la respuesta casi siempre radica en elegir la estructura de datos adecuada para el problema. En esta guía, desglosaremos las estructuras de datos esenciales que todo desarrollador debería dominar, con explicaciones claras, análisis de complejidad temporal y ejemplos prácticos de código.
Esta guía es perfecta para desarrolladores autodidactas, estudiantes de informática y programadores competitivos que se preparan para entrevistas técnicas.
Índice
- Por qué son importantes las estructuras de datos
- Array: La Clave
- Listas enlazadas: cadenas dinámicas
- Pilas: «último en entrar, primero en salir»
- Colas: primero en entrar, primero en salir
- Tablas hash: la magia de la búsqueda rápida
- Árboles: estructura jerárquica
- Grafos: mundos conectados
- Tabla comparativa
- ¿Qué estructura de datos deberías utilizar?
- Conclusión
Por qué son importantes las estructuras de datos
Antes de profundizar en estructuras concretas, veamos por qué son importantes:
- Rendimiento: una misma operación puede tardar unos milisegundos o minutos, dependiendo de la estructura de datos que utilices
- Eficiencia de memoria: algunas estructuras utilizan la memoria de forma inteligente; otras la desperdician de manera espectacular
- Claridad del código: la estructura adecuada hace que tu código exprese el problema de forma natural
Piénsalo: buscar un elemento en una matriz sin ordenar lleva un tiempo de O(n), pero en un árbol binario de búsqueda equilibrado, lleva O(log n). Para un millón de elementos, esa es la diferencia entre un millón de operaciones y veinte.
Array: La Clave
La matriz o array es la estructura de datos más básica en programación. Se trata de un bloque contiguo de memoria que almacena elementos del mismo tipo, a los que se puede acceder mediante su índice.
Cómo funcionan las matrices/array
Cuando se declara una matriz, el ordenador asigna un bloque fijo de memoria. Cada elemento se encuentra junto a sus vecinos, sin huecos ni punteros. Por eso las matrices ofrecen un acceso aleatorio de O(1).
Ejemplo de código en Python
# Crear una matriz
numbers = [10, 20, 30, 40, 50]
# Acceder a los elementos (O(1))
print(numbers[0]) # Output: 10
print(numbers[4]) # Output: 50
# Buscando (O(n))
if 30 in numbers:
print("Found!")
# Acceder al último elemento
print(numbers[-1]) # Output: 50
Ejemplo de código en C++
#include <iostream>
using namespace std;
int main() {
// Matriz estática
int numbers[5] = {10, 20, 30, 40, 50};
// Acceder a los elementos (O(1))
cout << numbers[0] << endl; // Output: 10
// Matriz dinámica (vector)
vector<int> dynamicNumbers = {10, 20, 30, 40, 50};
dynamicNumbers.push_back(60); // Añadir elemento al final
return 0;
}
Complejidad temporal
| Operación | Complejidad temporal |
|---|---|
| Acceso por índice | O(1) |
| Buscar | O(n) |
| Insertar al final | O(1) amortizado |
| Insertar en el centro | O(n) |
| Eliminar | O(n) |
Cuándo utilizar matrices
- Cuando se necesita un acceso aleatorio rápido a los elementos
- Cuando se conoce de antemano el tamaño exacto
- Cuando el rendimiento de la caché es importante (las matrices son compatibles con la caché)
Listas enlazadas: cadenas dinámicas
A diferencia de las matrices, las listas enlazadas almacenan los elementos en nodos repartidos por la memoria. Cada nodo contiene los datos y un puntero (o referencia) al siguiente nodo.
Tipos de listas enlazadas
- Lista enlazada simple: cada nodo apunta únicamente al nodo siguiente
- Lista enlazada doble: cada nodo apunta tanto al siguiente como al anterior
- Lista enlazada circular: el último nodo apunta de nuevo al primero
Ejemplo de código: Lista enlazada simple en Python
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
new_node = Node(data)
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def display(self):
elements = []
current = self.head
while current:
elements.append(str(current.data))
current = current.next
print(" -> ".join(elements))
# Uso
ll = LinkedList()
ll.append(10)
ll.append(20)
ll.append(30)
ll.display() # Output: 10 -> 20 -> 30
Complejidad temporal
| Operación | Complejidad temporal |
|---|---|
| Acceso por índice | O(n) |
| Buscar | O(n) |
| Insertar al principio | O(1) |
| Insertar al final | O(n) sin puntero de cola |
| Eliminar en el encabezado | O(1) |
Por qué son importantes las listas enlazadas
La principal ventaja de las listas enlazadas es que permiten insertar y eliminar elementos en tiempo O(1) en posiciones conocidas, sin necesidad de desplazar los elementos. Son ideales cuando:
- No se conoce de antemano el tamaño total
- Se añaden o eliminan elementos con frecuencia al principio
- Es necesario mantener colecciones ordenadas con modificaciones frecuentes
Pilas: «último en entrar, primero en salir»
Las pilas siguen el principio LIFO (último en entrar, primero en salir). Piensa en una pila de platos: se añaden por arriba y se retiran por arriba.
Aplicaciones en el mundo real
- Gestión de llamadas a funciones: la pila de llamadas almacena direcciones de retorno
- Mecanismos de deshacer: los editores de texto utilizan pilas para realizar un seguimiento de las acciones
- Evaluación de expresiones: los analizadores sintácticos utilizan pilas para el análisis sintáctico
- Historial del navegador: funcionalidad del botón Atrás
Ejemplo de código en Python
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def pop(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self.items.pop()
def peek(self):
return self.items[-1] if self.items else None
def is_empty(self):
return len(self.items) == 0
def size(self):
return len(self.items)
# Uso
stack = Stack()
stack.push(10)
stack.push(20)
stack.push(30)
print(stack.pop()) # Output: 30
print(stack.peek()) # Output: 20
Ejemplo de código: Equilibrio de paréntesis
def is_balanced(expression):
stack = Stack()
pairs = {')': '(', ']': '[', '}': '{'}
for char in expression:
if char in '([{':
stack.push(char)
elif char in ')]}':
if stack.is_empty() or stack.pop() != pairs[char]:
return False
return stack.is_empty()
# Tests
print(is_balanced("({[]})")) # Output: True
print(is_balanced("([)]")) # Output: False
Complejidad temporal
| Operación | Complejidad temporal |
|---|---|
| Push | O(1) |
| Pop | O(1) |
| Peek | O(1) |
Colas: primero en entrar, primero en salir
Las colas siguen el principio FIFO (primero en entrar, primero en salir). Al igual que en la cola de una cafetería, la primera persona en llegar es la primera en ser atendida.
Tipos de colas
- Cola simple: FIFO básico
- Cola circular: la última posición se conecta con la primera
- Cola de prioridad: los elementos tienen niveles de prioridad
- Deque (cola de doble extremo): añadir/eliminar desde ambos extremos
Ejemplo de código en Python
from collections import deque
class Queue:
def __init__(self):
self.items = deque()
def enqueue(self, item):
self.items.append(item)
def dequeue(self):
if self.is_empty():
raise IndexError("La cola está vacia")
return self.items.popleft()
def front(self):
return self.items[0] if self.items else None
def is_empty(self):
return len(self.items) == 0
def size(self):
return len(self.items)
# Uso
queue = Queue()
queue.enqueue("Alice")
queue.enqueue("Bob")
queue.enqueue("Charlie")
print(queue.dequeue()) # Output: Alice
print(queue.front()) # Output: Bob
Ejemplo de código: BFS utilizando una cola
from collections import deque
def bfs(graph, start):
visited = set()
queue = Queue()
queue.enqueue(start)
visited.add(start)
while not queue.is_empty():
node = queue.dequeue()
print(f"Visiting: {node}")
for neighbor in graph.get(node, []):
if neighbor not in visited:
visited.add(neighbor)
queue.enqueue(neighbor)
# Grafo de ejemplo (lista de adyacencia)
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
bfs(graph, 'A')
Complejidad temporal
| Operación | Complejidad temporal |
|---|---|
| Enqueue | O(1) |
| Dequeue | O(1) |
| Front | O(1) |
Tablas hash: la magia de la búsqueda rápida
Las tablas hash son los superhéroes de las estructuras de datos. Permiten realizar búsquedas, inserciones y eliminaciones en O(1) en el caso promedio, utilizando una función hash para calcular un índice en una matriz.
Cómo funcionan las tablas hash
- Una función hash convierte una clave en un índice numérico
- El valor se almacena en ese índice en la matriz subyacente
- Las colisiones (dos claves que apuntan al mismo índice) se gestionan mediante encadenamiento o direccionamiento abierto
Ejemplo de código en Python
# Uso del diccionario integrado de Python (hash table)
phonebook = {}
# Insert (O(1))
phonebook["Alice"] = "555-1234"
phonebook["Bob"] = "555-5678"
phonebook["Charlie"] = "555-9999"
# Lookup (O(1))
print(phonebook["Bob"]) # Output: 555-5678
# Comprobar si existe (O(1))
if "Alice" in phonebook:
print("Alice found!")
# Borrar (O(1))
del phonebook["Charlie"]
# Iterar
for name, number in phonebook.items():
print(f"{name}: {number}")
Ejemplo de código: Implementación personalizada de una tabla hash
class HashTable:
def __init__(self, size=100):
self.size = size
self.table = [[] for _ in range(size)]
def _hash(self, key):
return hash(key) % self.size
def put(self, key, value):
index = self._hash(key)
bucket = self.table[index]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
bucket.append((key, value))
def get(self, key):
index = self._hash(key)
bucket = self.table[index]
for k, v in bucket:
if k == key:
return v
return None
def delete(self, key):
index = self._hash(key)
bucket = self.table[index]
for i, (k, v) in enumerate(bucket):
if k == key:
del bucket[i]
return True
return False
# Uso
ht = HashTable()
ht.put("name", "Alice")
ht.put("age", 30)
print(ht.get("name")) # Output: Alice
Complejidad temporal (caso promedio)
| Operation | Time Complexity |
|---|---|
| Consulta | O(1) |
| Insertar | O(1) |
| Borrar | O(1) |
| Buscar | O(1) |
Cuándo son más adecuadas las tablas hash
- Las búsquedas rápidas son fundamentales
- Tienes pares clave-valor (como en los diccionarios)
- Estás creando un sistema de caché
- Necesitas realizar un seguimiento de los valores únicos (utilizando un conjunto)
Árboles: estructura jerárquica
Los árboles son estructuras de datos jerárquicas con un nodo raíz y nodos hijos. El más común es el árbol binario de búsqueda (BST), en el que cada nodo tiene como máximo dos hijos, y los hijos de la izquierda son más pequeños que el nodo padre.
Por qué son importantes los árboles
Los árboles sustentan algunas de las estructuras de datos más importantes en informática:
- Sistemas de archivos: directorios y archivos
- Bases de datos: índices para consultas rápidas
- Compiladores: árboles de sintaxis abstracta (AST)
- DOM: estructura de páginas web
Ejemplo de código: Árbol de búsqueda binaria en Python
class TreeNode:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
class BST:
def __init__(self):
self.root = None
def insert(self, key):
self.root = self._insert_recursive(self.root, key)
def _insert_recursive(self, node, key):
if node is None:
return TreeNode(key)
if key < node.key:
node.left = self._insert_recursive(node.left, key)
else:
node.right = self._insert_recursive(node.right, key)
return node
def search(self, key):
return self._search_recursive(self.root, key)
def _search_recursive(self, node, key):
if node is None or node.key == key:
return node
if key < node.key:
return self._search_recursive(node.left, key)
return self._search_recursive(node.right, key)
def inorder(self):
result = []
self._inorder_recursive(self.root, result)
return result
def _inorder_recursive(self, node, result):
if node:
self._inorder_recursive(node.left, result)
result.append(node.key)
self._inorder_recursive(node.right, result)
# Uso
bst = BST()
for value in [50, 30, 70, 20, 40, 60, 80]:
bst.insert(value)
print(bst.inorder()) # Output: [20, 30, 40, 50, 60, 70, 80]
print(bst.search(40)) # Output: TreeNode with key=40
Complejidad temporal (BST equilibrada)
| Operación | Complejidad temporal |
|---|---|
| Buscar | O(log n) |
| Insertar | O(log n) |
| Borrar | O(log n) |
| Recorrido en orden | O(n) |
Árboles AVL y árboles rojo-negro
Los árboles rojo-negro estándar pueden degradarse a O(n) si los elementos se insertan en orden. Los árboles autoequilibrados, como los árboles AVL y los árboles rojo-negro, mantienen una complejidad de O(log n) al equilibrarse automáticamente durante las inserciones.
Grafos: mundos conectados
Los grafos están formados por vértices (nodos) conectados por aristas. Son perfectos para modelar relaciones y redes: conexiones sociales, mapas de carreteras, redes informáticas y dependencias.
Tipos de grafos
- Dirigido vs No dirigido: los arcos tienen dirección o no
- Ponderados vs no ponderados: los arcos tienen pesos o no
- Cíclicos vs acíclicos: contienen ciclos o no
- Esparsos vs densos: pocos o muchos arcos
Ejemplo de código: Lista de adyacencia en Python
class Graph:
def __init__(self):
self.graph = {}
def add_vertex(self, vertex):
if vertex not in self.graph:
self.graph[vertex] = []
def add_edge(self, v1, v2, directed=False):
self.add_vertex(v1)
self.add_vertex(v2)
self.graph[v1].append(v2)
if not directed:
self.graph[v2].append(v1)
def display(self):
for vertex in self.graph:
print(f"{vertex} -> {self.graph[vertex]}")
# Uso: Crear una red social
g = Graph()
g.add_edge("Alice", "Bob")
g.add_edge("Alice", "Charlie")
g.add_edge("Bob", "Diana")
g.add_edge("Charlie", "Diana")
g.display()
# Output:
# Alice -> ['Bob', 'Charlie']
# Bob -> ['Alice', 'Diana']
# Charlie -> ['Alice', 'Diana']
# Diana -> ['Bob', 'Charlie']
Ejemplo de código: DFS y BFS
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
print(f"Visiting: {start}")
for neighbor in graph.get(start, []):
if neighbor not in visited:
dfs(graph, neighbor, visited)
return visited
def bfs(graph, start):
visited = set()
queue = [start]
visited.add(start)
while queue:
vertex = queue.pop(0)
print(f"Visiting: {vertex}")
for neighbor in graph.get(vertex, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
# Ejemplo grafo
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
print("DFS:")
dfs(graph, 'A')
print("\nBFS:")
bfs(graph, 'A')
Aplicaciones del recorrido de grafos
- DFS: Búsqueda de caminos, detección de ciclos, ordenación topológica
- BFS: Camino más corto (sin ponderar), recorrido por niveles, búsqueda de componentes conectados
Complejidad temporal (lista de adyacencia)
| Operación | Complejidad temporal |
|---|---|
| Añadir vértice | O(1) |
| Añadir arista | O(1) |
| Eliminar vértice | O(V + E) |
| Consultar arista | O(V) peor caso |
| Recorrido | O(V + E) |
Tabla comparativa
| Estructura de datos | Acceso | Búsqueda | Insertar | Borrar | Cuando Utilizar |
|---|---|---|---|---|---|
| Array/Matriz | O(1) | O(n) | O(n) | O(n) | Acceso rápido, tamaño fijo |
| Linked List/Lista enlazada | O(n) | O(n) | O(1)* | O(1)* | Inserciones/eliminaciones frecuentes |
| Stack/Pila | O(n) | O(n) | O(1) | O(1) | Flujos de trabajo LIFO, funciones de deshacer |
| Queue/Cola | O(n) | O(n) | O(1) | O(1) | Flujos de trabajo FIFO |
| Hash Table/Tabla Hash | N/A | O(1) avg | O(1) avg | O(1) avg | Búsquedas rápidas, datos clave-valor |
| BST (balanced) | O(log n) | O(log n) | O(log n) | O(log n) | Datos ordenados, consultas de rango |
| Graph/Grafo | O(V + E) | O(V + E) | O(1) | O(E) | Redes, relaciones |
*En posición conocida (principio o fin)
¿Qué estructura de datos deberías utilizar?
La elección de la estructura de datos adecuada depende de tu problema en concreto. A continuación una guía práctica para tomar decisiones:
Necesito almacenar elementos y acceder a ellos por índice
→ Utiliza una matriz (o una matriz dinámica, como las listas de Python)
Necesito añadir o eliminar elementos con frecuencia al principio
→ Utiliza una lista enlazada o un deque
Necesito procesar los elementos en el orden en que llegan
→ Utiliza una cola
Necesito procesar los elementos en orden inverso
→ Utiliza una pila
Necesito búsquedas rápidas por clave
→ Utiliza una tabla hash
Necesito datos ordenados con consultas de rango
→ Utiliza un árbol BST equilibrado (como TreeSet en Java)
Necesito modelar relaciones/redes
→ Utiliza un grafo
Conclusión
Las estructuras de datos son la base de una programación eficiente. Puntos clave:
- Las matrices ofrecen un acceso de complejidad O(1), pero las inserciones son costosas
- Las listas enlazadas destacan por sus inserciones de complejidad O(1) en posiciones conocidas
- Las pilas y las colas modelan con elegancia los flujos de trabajo LIFO y FIFO
- Las tablas hash permiten búsquedas casi instantáneas
- Los árboles mantienen el orden con operaciones de complejidad O(log n)
- Los grafos modelan relaciones y redes complejas
Domina estos fundamentos y escribirás mejor código, ya sea para resolver retos de programación competitiva, prepararte para entrevistas técnicas o desarrollar sistemas de producción.
En futuras entradas, profundizaremos en estructuras de datos avanzadas como Heaps, Tries y Segment Trees, así como en técnicas algorítmicas que combinan estas estructuras para una resolución de problemas eficaz.
Si esta guía te ha resultado útil, compártela con tus compañeros desarrolladores.