OpenCodex Blog

Publicación

- 12 min read

Estructuras de datos que todo desarrollador debe conocer

img of 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

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ónComplejidad temporal
Acceso por índiceO(1)
BuscarO(n)
Insertar al finalO(1) amortizado
Insertar en el centroO(n)
EliminarO(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ónComplejidad temporal
Acceso por índiceO(n)
BuscarO(n)
Insertar al principioO(1)
Insertar al finalO(n) sin puntero de cola
Eliminar en el encabezadoO(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ónComplejidad temporal
PushO(1)
PopO(1)
PeekO(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ónComplejidad temporal
EnqueueO(1)
DequeueO(1)
FrontO(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

  1. Una función hash convierte una clave en un índice numérico
  2. El valor se almacena en ese índice en la matriz subyacente
  3. 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)

OperationTime Complexity
ConsultaO(1)
InsertarO(1)
BorrarO(1)
BuscarO(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ónComplejidad temporal
BuscarO(log n)
InsertarO(log n)
BorrarO(log n)
Recorrido en ordenO(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ónComplejidad temporal
Añadir vérticeO(1)
Añadir aristaO(1)
Eliminar vérticeO(V + E)
Consultar aristaO(V) peor caso
RecorridoO(V + E)

Tabla comparativa

Estructura de datosAccesoBúsquedaInsertarBorrarCuando Utilizar
Array/MatrizO(1)O(n)O(n)O(n)Acceso rápido, tamaño fijo
Linked List/Lista enlazadaO(n)O(n)O(1)*O(1)*Inserciones/eliminaciones frecuentes
Stack/PilaO(n)O(n)O(1)O(1)Flujos de trabajo LIFO, funciones de deshacer
Queue/ColaO(n)O(n)O(1)O(1)Flujos de trabajo FIFO
Hash Table/Tabla HashN/AO(1) avgO(1) avgO(1) avgBú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/GrafoO(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:

  1. Las matrices ofrecen un acceso de complejidad O(1), pero las inserciones son costosas
  2. Las listas enlazadas destacan por sus inserciones de complejidad O(1) en posiciones conocidas
  3. Las pilas y las colas modelan con elegancia los flujos de trabajo LIFO y FIFO
  4. Las tablas hash permiten búsquedas casi instantáneas
  5. Los árboles mantienen el orden con operaciones de complejidad O(log n)
  6. 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.