Estructuras de datos que todo programador debe conocer

D
DanisCh
(Actualizado: ) • 16 min de lectura
Estructuras de datos que todo programador debe conocer
Consejos para estudiar programación Empezar desde cero Python

Las estructuras de datos son formas organizadas de almacenar y gestionar información en un programa. Elegir la estructura correcta para cada problema puede ser la diferencia entre un código que funciona en milisegundos y uno que tarda minutos. No es exagerado decir que entender bien las estructuras de datos es una de las habilidades más importantes que puedes desarrollar como programador.

En esta guía vas a ver las estructuras de datos más fundamentales, cómo funcionan por dentro, cuándo usar cada una y cómo implementarlas en Python y JavaScript.

Por qué importa elegir la estructura correcta

Imagina que tienes una lista de un millón de usuarios y necesitas comprobar si un email concreto ya está registrado. Podrías recorrer la lista de principio a fin buscando ese email. Con un millón de usuarios, en el peor caso tendrías que hacer un millón de comparaciones. Con la estructura correcta (una tabla hash), esa misma búsqueda tarda lo mismo independientemente de si tienes 100 o 100 millones de usuarios.

# Búsqueda en lista: O(n) → el tiempo crece con el tamaño de los datos
emails_lista = ["ana@mail.com", "carlos@mail.com", ...]  # 1 millón de emails

# En el peor caso recorre TODA la lista
if "nuevo@mail.com" in emails_lista:
    print("Ya existe")

# Búsqueda en conjunto (tabla hash): O(1) → tiempo constante
emails_conjunto = {"ana@mail.com", "carlos@mail.com", ...}

# Instantáneo sin importar el tamaño
if "nuevo@mail.com" in emails_conjunto:
    print("Ya existe")

Este ejemplo ilustra por qué las estructuras de datos importan. Vamos a ver las principales.

Array (arreglo)

El array es la estructura de datos más básica y la que casi todos los lenguajes tienen de forma nativa. Almacena elementos en posiciones contiguas de memoria, identificadas por un índice que empieza en cero.

Característica clave: el acceso por índice es instantáneo porque el ordenador calcula directamente la posición en memoria (posición_base + índice × tamaño_elemento). No tiene que buscar el elemento.

# Python (listas)
numeros = [10, 20, 30, 40, 50]

# Acceso por índice: O(1) → instantáneo
print(numeros[0])    # 10
print(numeros[2])    # 30
print(numeros[-1])   # 50 (último elemento)

# Modificar por índice: O(1)
numeros[1] = 99
print(numeros)   # [10, 99, 30, 40, 50]

# Añadir al final: O(1) amortizado
numeros.append(60)

# Insertar en el medio: O(n) → hay que desplazar todos los elementos siguientes
numeros.insert(2, 25)   # inserta 25 en la posición 2

# Buscar un elemento: O(n) → hay que recorrer hasta encontrarlo
print(25 in numeros)    # True
print(numeros.index(25))  # devuelve el índice del elemento

# Eliminar del final: O(1)
numeros.pop()

# Eliminar del medio: O(n) → hay que desplazar elementos
numeros.pop(1)   # elimina el elemento en el índice 1
// JavaScript (arrays)
const numeros = [10, 20, 30, 40, 50];

console.log(numeros[0]);          // 10
console.log(numeros.length);      // 5
numeros.push(60);                 // añadir al final: O(1)
numeros.unshift(5);               // añadir al inicio: O(n) ← costoso
numeros.pop();                    // eliminar del final: O(1)
numeros.shift();                  // eliminar del inicio: O(n) ← costoso
console.log(numeros.indexOf(30)); // 1 (buscar: O(n))

Cuándo usar un array: cuando necesitas acceder a elementos por posición, cuando el orden importa, cuando iteras sobre todos los elementos frecuentemente. Es la estructura por defecto para la mayoría de colecciones ordenadas.

Cuándo NO es la mejor opción: cuando necesitas insertar o eliminar frecuentemente en el medio (muy lento), o cuando necesitas buscar elementos frecuentemente sin conocer su posición.

Lista enlazada (Linked List)

A diferencia del array, los elementos de una lista enlazada no están en posiciones contiguas de memoria. Cada elemento (nodo) guarda su valor y un puntero al siguiente nodo. El último nodo apunta a None/null.

# Visualización de una lista enlazada:
# [10 | →] → [20 | →] → [30 | →] → [40 | None]
# nodo1       nodo2       nodo3       nodo4
# Python: implementación de lista enlazada simple
class Nodo:
    def __init__(self, valor):
        self.valor    = valor
        self.siguiente = None   # puntero al siguiente nodo

class ListaEnlazada:
    def __init__(self):
        self.cabeza = None   # primer nodo

    def insertar_al_inicio(self, valor):
        nuevo = Nodo(valor)
        nuevo.siguiente = self.cabeza   # el nuevo apunta al antiguo primero
        self.cabeza = nuevo             # el nuevo es ahora el primero
        # O(1): no importa cuántos elementos haya

    def insertar_al_final(self, valor):
        nuevo = Nodo(valor)
        if not self.cabeza:
            self.cabeza = nuevo
            return
        actual = self.cabeza
        while actual.siguiente:    # recorrer hasta el último
            actual = actual.siguiente
        actual.siguiente = nuevo   # O(n): hay que llegar hasta el final

    def eliminar(self, valor):
        if not self.cabeza:
            return

        if self.cabeza.valor == valor:
            self.cabeza = self.cabeza.siguiente   # O(1) si es el primero
            return

        actual = self.cabeza
        while actual.siguiente:
            if actual.siguiente.valor == valor:
                actual.siguiente = actual.siguiente.siguiente  # saltar el nodo
                return
            actual = actual.siguiente
        # O(n): hay que buscar el elemento

    def imprimir(self):
        actual = self.cabeza
        elementos = []
        while actual:
            elementos.append(str(actual.valor))
            actual = actual.siguiente
        print(" → ".join(elementos))

# Usar la lista enlazada
lista = ListaEnlazada()
lista.insertar_al_inicio(30)
lista.insertar_al_inicio(20)
lista.insertar_al_inicio(10)
lista.imprimir()   # 10 → 20 → 30
lista.eliminar(20)
lista.imprimir()   # 10 → 30

Ventaja sobre el array: insertar o eliminar al inicio es O(1), porque no hay que desplazar elementos. Solo se cambia el puntero.

Desventaja sobre el array: el acceso por índice es O(n) porque no hay forma de saltar directamente a una posición: hay que recorrer desde el principio. Además, consume más memoria porque cada nodo guarda el valor y el puntero.

Cuándo usar una lista enlazada: cuando haces muchas inserciones y eliminaciones al inicio o en posiciones conocidas, y raramente accedes por índice. En la práctica, las listas enlazadas son más comunes como base de otras estructuras (pilas, colas) que como estructura directa.

Pila (Stack)

La pila sigue el principio LIFO: Last In, First Out (el último en entrar es el primero en salir). Piensa en una pila de platos: solo puedes coger el que está arriba, y solo puedes añadir uno encima del que ya está arriba.

Las operaciones fundamentales son push (añadir encima) y pop (quitar el de arriba). Ambas son O(1).

# Python: una lista funciona perfectamente como pila
pila = []

pila.append(1)   # push
pila.append(2)
pila.append(3)
print(pila)       # [1, 2, 3]
print(pila[-1])   # 3 ← ver el elemento superior sin sacarlo (peek)

elemento = pila.pop()   # pop: saca el último
print(elemento)         # 3
print(pila)             # [1, 2]
# Caso de uso clásico 1: verificar que los paréntesis están balanceados
def parentesis_balanceados(expresion):
    pila = []
    pares = {')': '(', ']': '[', '}': '{'}

    for caracter in expresion:
        if caracter in '([{':
            pila.append(caracter)    # apertura: añadir a la pila
        elif caracter in ')]}':
            if not pila or pila[-1] != pares[caracter]:
                return False         # cierre sin apertura correspondiente
            pila.pop()               # cierre correcto: sacar la apertura

    return len(pila) == 0   # True si todos los abiertos fueron cerrados

print(parentesis_balanceados("(a + b) * [c - d]"))   # True
print(parentesis_balanceados("(a + b] * c"))          # False
print(parentesis_balanceados("((a + b)"))             # False (falta cerrar)
# Caso de uso clásico 2: historial de navegación (botón "atrás")
class HistorialNavegacion:
    def __init__(self):
        self._historial = []
        self._adelante  = []

    def visitar(self, url):
        self._historial.append(url)
        self._adelante.clear()   # al visitar una nueva página, el historial adelante se borra
        print(f"Visitando: {url}")

    def atras(self):
        if len(self._historial) <= 1:
            print("No hay páginas anteriores")
            return
        actual = self._historial.pop()
        self._adelante.append(actual)
        print(f"Volviendo a: {self._historial[-1]}")

    def adelante(self):
        if not self._adelante:
            print("No hay páginas siguientes")
            return
        siguiente = self._adelante.pop()
        self._historial.append(siguiente)
        print(f"Avanzando a: {siguiente}")

nav = HistorialNavegacion()
nav.visitar("google.com")
nav.visitar("wikipedia.org")
nav.visitar("python.org")
nav.atras()     # Volviendo a: wikipedia.org
nav.atras()     # Volviendo a: google.com
nav.adelante()  # Avanzando a: wikipedia.org

Cuándo usar una pila: cuando necesitas procesar elementos en orden inverso al que llegaron, para implementar la función "deshacer" en editores, para evaluar expresiones matemáticas, para la gestión de llamadas a funciones (el call stack es una pila), y para algoritmos de recorrido en profundidad (DFS).

Cola (Queue)

La cola sigue el principio FIFO: First In, First Out (el primero en entrar es el primero en salir). Como la cola de un supermercado: el primero que llega es el primero que se atiende.

Las operaciones son enqueue (añadir al final) y dequeue (quitar del inicio). Ambas deben ser O(1).

# Python: usar deque (double-ended queue) de collections, más eficiente que una lista
from collections import deque

cola = deque()
cola.append(1)     # enqueue: añadir al final
cola.append(2)
cola.append(3)
print(cola)        # deque([1, 2, 3])

elemento = cola.popleft()   # dequeue: quitar del inicio → O(1)
print(elemento)             # 1
print(cola)                 # deque([2, 3])

# ¿Por qué no usar lista.pop(0)?
# lista.pop(0) es O(n): hay que desplazar todos los elementos
# deque.popleft() es O(1): operación constante
# Caso de uso: sistema de impresión (cola de trabajos)
from collections import deque

class ColaDeTrabajosImpresion:
    def __init__(self):
        self._cola = deque()

    def agregar_trabajo(self, documento):
        self._cola.append(documento)
        print(f"Trabajo '{documento}' añadido a la cola (posición {len(self._cola)})")

    def procesar_siguiente(self):
        if not self._cola:
            print("No hay trabajos en cola")
            return
        trabajo = self._cola.popleft()
        print(f"Imprimiendo: '{trabajo}'")
        return trabajo

    def trabajos_pendientes(self):
        return len(self._cola)

impresora = ColaDeTrabajoImpresion()
impresora.agregar_trabajo("informe_anual.pdf")
impresora.agregar_trabajo("factura_mayo.pdf")
impresora.agregar_trabajo("presentacion.pptx")
impresora.procesar_siguiente()   # Imprimiendo: 'informe_anual.pdf'
impresora.procesar_siguiente()   # Imprimiendo: 'factura_mayo.pdf'
print(f"Pendientes: {impresora.trabajos_pendientes()}")   # 1

Cuándo usar una cola: cuando necesitas procesar elementos en el mismo orden en que llegaron. Colas de tareas en servidores web, sistemas de mensajería, algoritmos de recorrido en anchura (BFS), gestión de eventos en interfaces gráficas.

Tabla hash (Diccionario / Objeto)

La tabla hash es probablemente la estructura de datos más útil y más usada en el desarrollo cotidiano. Almacena pares clave-valor y permite buscar, insertar y eliminar por clave en tiempo O(1) de media.

Cómo funciona por dentro: cuando insertas un par clave-valor, la tabla aplica una función hash a la clave para calcular en qué "casilla" del array interno guardar el valor. Cuando buscas por clave, aplica la misma función hash y va directamente a esa casilla, sin recorrer nada.

# Python: los diccionarios son tablas hash
agenda = {}

# Insertar: O(1)
agenda["ana@mail.com"]    = {"nombre": "Ana García",   "telefono": "600111222"}
agenda["carlos@mail.com"] = {"nombre": "Carlos López", "telefono": "600333444"}

# Buscar por clave: O(1) ← aquí está la magia
contacto = agenda.get("ana@mail.com")
print(contacto["nombre"])   # "Ana García"

# Comprobar existencia: O(1)
if "nuevo@mail.com" in agenda:
    print("Ya existe")

# Eliminar: O(1)
del agenda["carlos@mail.com"]

# Recorrer: O(n)
for email, datos in agenda.items():
    print(f"{email}: {datos['nombre']}")
# Caso de uso clásico: contar frecuencias en O(n)
def contar_palabras(texto):
    frecuencia = {}
    palabras = texto.lower().split()

    for palabra in palabras:
        frecuencia[palabra] = frecuencia.get(palabra, 0) + 1

    return frecuencia

texto = "el gato y el perro y el gato"
print(contar_palabras(texto))
# {"el": 3, "gato": 2, "y": 2, "perro": 1}

# Con Counter (más conciso):
from collections import Counter
print(Counter(texto.lower().split()))
# Caso de uso: caché (memoización) con tabla hash
def fibonacci_con_cache(n, cache={}):
    if n <= 1:
        return n
    if n in cache:
        return cache[n]   # O(1): ya calculado
    resultado = fibonacci_con_cache(n-1, cache) + fibonacci_con_cache(n-2, cache)
    cache[n] = resultado   # guardar para no recalcular
    return resultado

print(fibonacci_con_cache(50))  # instantáneo gracias al caché
# Sin caché, fibonacci(50) tardaría días en ejecutarse

Cuándo usar una tabla hash: cuando necesitas búsquedas frecuentes por clave, para eliminar duplicados, para contar frecuencias, para implementar cachés y para cualquier caso donde la relación es clave → valor.

Conjunto (Set)

Un conjunto es como una tabla hash pero que solo guarda claves, sin valores asociados. Garantiza que no hay elementos duplicados y permite operaciones matemáticas de conjuntos (unión, intersección, diferencia) de forma eficiente.

# Python: sets
emails_registrados = {"ana@mail.com", "carlos@mail.com", "maria@mail.com"}

# Buscar: O(1)
if "ana@mail.com" in emails_registrados:
    print("Email ya registrado")

# Añadir: O(1)
emails_registrados.add("jorge@mail.com")

# Los duplicados se ignoran automáticamente
emails_registrados.add("ana@mail.com")   # ya existe, no hace nada
print(len(emails_registrados))           # sigue siendo 4

# Eliminar duplicados de una lista: truco clásico
con_duplicados = [1, 2, 2, 3, 3, 3, 4]
sin_duplicados = list(set(con_duplicados))   # [1, 2, 3, 4]

# Operaciones de conjuntos
a = {1, 2, 3, 4, 5}
b = {3, 4, 5, 6, 7}

print(a & b)    # {3, 4, 5}        intersección: elementos en ambos
print(a | b)    # {1,2,3,4,5,6,7}  unión: todos los elementos
print(a - b)    # {1, 2}           diferencia: en a pero no en b
print(a ^ b)    # {1, 2, 6, 7}     diferencia simétrica: en uno pero no en ambos
# Caso de uso: encontrar elementos comunes entre dos listas
def amigos_en_comun(amigos_ana, amigos_carlos):
    return set(amigos_ana) & set(amigos_carlos)

amigos_de_ana    = ["María", "Jorge", "Lucía", "Pedro", "Sara"]
amigos_de_carlos = ["Jorge", "Sara", "Tomás", "Lucía"]

comunes = amigos_en_comun(amigos_de_ana, amigos_de_carlos)
print(comunes)   # {"Jorge", "Sara", "Lucía"}

Árbol (Tree)

Un árbol es una estructura jerárquica donde cada elemento (nodo) puede tener un nodo padre y varios nodos hijos. El nodo sin padre es la raíz. Los nodos sin hijos son las hojas.

El árbol más importante para un programador principiante es el árbol binario de búsqueda (BST): cada nodo tiene como máximo dos hijos, y todos los valores del subárbol izquierdo son menores que el nodo actual, mientras que todos los del subárbol derecho son mayores.

#       8          ← raíz
#      / \
#     3   10       ← nodos internos
#    / \    \
#   1   6    14    ← hojas
#      / \
#     4   7

# Propiedad del BST: para cualquier nodo n
# todos los nodos del subárbol izquierdo tienen valor < n
# todos los nodos del subárbol derecho tienen valor > n

# Esto permite buscar en O(log n): en cada paso, se descarta la mitad del árbol
# Python: árbol binario de búsqueda
class NodoArbol:
    def __init__(self, valor):
        self.valor    = valor
        self.izquierda = None
        self.derecha   = None

class ArbolBinarioBusqueda:
    def __init__(self):
        self.raiz = None

    def insertar(self, valor):
        self.raiz = self._insertar(self.raiz, valor)

    def _insertar(self, nodo, valor):
        if nodo is None:
            return NodoArbol(valor)
        if valor < nodo.valor:
            nodo.izquierda = self._insertar(nodo.izquierda, valor)
        elif valor > nodo.valor:
            nodo.derecha = self._insertar(nodo.derecha, valor)
        return nodo   # si es igual, no se inserta (no hay duplicados)

    def buscar(self, valor):
        return self._buscar(self.raiz, valor)

    def _buscar(self, nodo, valor):
        if nodo is None:
            return False   # llegamos a una hoja sin encontrarlo
        if valor == nodo.valor:
            return True
        if valor < nodo.valor:
            return self._buscar(nodo.izquierda, valor)   # buscar en la izquierda
        return self._buscar(nodo.derecha, valor)          # buscar en la derecha

    def recorrido_ordenado(self):
        """Recorre el árbol en orden: izquierda → raíz → derecha. Devuelve los valores ordenados."""
        resultado = []
        self._inorden(self.raiz, resultado)
        return resultado

    def _inorden(self, nodo, resultado):
        if nodo:
            self._inorden(nodo.izquierda, resultado)
            resultado.append(nodo.valor)
            self._inorden(nodo.derecha, resultado)

arbol = ArbolBinarioBusqueda()
for valor in [8, 3, 10, 1, 6, 14, 4, 7]:
    arbol.insertar(valor)

print(arbol.buscar(6))   # True
print(arbol.buscar(5))   # False
print(arbol.recorrido_ordenado())   # [1, 3, 4, 6, 7, 8, 10, 14] ← ordenados!

Cuándo aparecen los árboles en la práctica: el sistema de archivos de tu ordenador es un árbol, el DOM de HTML es un árbol, los menús con submenús son árboles, los sistemas de bases de datos usan árboles B internamente para los índices.

Grafo (Graph)

Un grafo es la estructura de datos más general: un conjunto de nodos (vértices) conectados por aristas (edges). A diferencia del árbol, en un grafo puede haber ciclos (puedes volver al punto de partida siguiendo conexiones) y un nodo puede tener cualquier número de conexiones en cualquier dirección.

# Formas de representar un grafo:

# 1. Lista de adyacencia (la más común): diccionario donde cada clave es un nodo
#    y su valor es la lista de nodos con los que está conectado
grafo = {
    "A": ["B", "C"],
    "B": ["A", "D", "E"],
    "C": ["A", "F"],
    "D": ["B"],
    "E": ["B", "F"],
    "F": ["C", "E"]
}

# Visualización:
# A — B — D
# |   |
# C   E
# |   |
# F———┘

# 2. Matriz de adyacencia: una cuadrícula de 0s y 1s
# matriz[i][j] = 1 significa que hay una arista entre el nodo i y el nodo j
# Recorrido en anchura (BFS): explora nivel por nivel
# Útil para encontrar el camino más corto entre dos nodos
from collections import deque

def bfs(grafo, inicio, destino):
    visitados = {inicio}
    cola = deque([[inicio]])   # cola de caminos, no solo de nodos

    while cola:
        camino = cola.popleft()
        nodo_actual = camino[-1]

        if nodo_actual == destino:
            return camino   # encontramos el camino más corto

        for vecino in grafo[nodo_actual]:
            if vecino not in visitados:
                visitados.add(vecino)
                cola.append(camino + [vecino])

    return None   # no hay camino

print(bfs(grafo, "A", "F"))   # ["A", "C", "F"] ← camino más corto

Cuándo aparecen los grafos en la práctica: las redes sociales son grafos (usuarios conectados por amistades), los mapas de navegación son grafos (ciudades conectadas por carreteras), internet es un grafo (servidores conectados por cables), los sistemas de recomendación usan grafos para encontrar similitudes.

Tabla comparativa de operaciones

EstructuraAccesoBúsquedaInserciónEliminaciónMejor para
ArrayO(1)O(n)O(n)*O(n)*Acceso por índice, iteración
Lista enlazadaO(n)O(n)O(1)**O(1)**Inserciones/eliminaciones frecuentes al inicio
PilaO(n)O(n)O(1)O(1)Procesamiento LIFO, deshacer acciones
ColaO(n)O(n)O(1)O(1)Procesamiento FIFO, colas de tareas
Tabla hashO(1)O(1)O(1)O(1)Búsqueda por clave, conteo, caché
Conjunto—O(1)O(1)O(1)Verificar pertenencia, eliminar duplicados
BSTO(log n)O(log n)O(log n)O(log n)Datos ordenados con búsqueda eficiente

* O(1) al insertar o eliminar al final del array. O(n) en el medio o al inicio.

** O(1) si ya tienes la referencia al nodo anterior. O(n) si hay que buscar primero.

Cómo elegir la estructura correcta

Estas preguntas te ayudan a elegir rápido:

  • ¿Necesitas acceder a elementos por posición? → Array/lista
  • ¿Necesitas buscar por clave frecuentemente? → Tabla hash (diccionario)
  • ¿Necesitas verificar si algo existe sin importar el orden? → Conjunto (set)
  • ¿Necesitas procesar en orden inverso al que llegó? → Pila
  • ¿Necesitas procesar en el mismo orden en que llegó? → Cola
  • ¿Los datos tienen jerarquía (padre-hijo)? → Árbol
  • ¿Los datos tienen conexiones arbitrarias entre sí? → Grafo
  • ¿Haces muchas inserciones al inicio? → Lista enlazada

Resumen

  • Los arrays son la estructura base: acceso instantáneo por índice, pero insertar o eliminar en el medio es lento.
  • Las listas enlazadas permiten insertar y eliminar al inicio en O(1), pero el acceso por índice es lento.
  • Las pilas (LIFO) son perfectas para procesar en orden inverso, implementar "deshacer" o gestionar llamadas anidadas.
  • Las colas (FIFO) son perfectas para procesar tareas en el orden en que llegaron.
  • Las tablas hash (diccionarios) ofrecen búsqueda, inserción y eliminación en O(1). Son la estructura más usada en el desarrollo cotidiano.
  • Los conjuntos garantizan unicidad y permiten operaciones de conjuntos matemáticos eficientemente.
  • Los árboles binarios de búsqueda mantienen los datos ordenados con operaciones en O(log n).
  • Los grafos representan conexiones arbitrarias entre elementos: redes sociales, mapas, internet.

 

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a