Fundamentos de Estructuras de Datos
Publicado el 1 de diciembre de 2025
Fundamentos Esenciales de Estructuras de Datos
Las estructuras de datos son la columna vertebral de cualquier programa eficiente. Elegir la estructura correcta puede transformar un programa lento en uno extremadamente rápido, mientras que una elección incorrecta puede resultar desastrosa para el rendimiento.
1. Definición de Estructuras de Datos
¿Qué son las Estructuras de Datos?
Las Estructuras de Datos son las implementaciones concretas utilizadas para materializar Tipos de Datos Abstractos (ADT), como contenedores, diccionarios y colas de prioridad.
Un ADT puede ser implementado por muchas estructuras de datos funcionalmente equivalentes.
Concepto Clave: Separación entre Interfaz e Implementación
Pro Tip
Cambiar la estructura de datos dentro de un proceso no afecta la corrección del programa, siempre que se reemplace una implementación correcta por otra implementación correcta diferente.
Esto permite optimizar el rendimiento sin reescribir la lógica del programa.
Ejemplo conceptual:
# ADT: Diccionario (interfaz)
# Operaciones: insert(), search(), delete()
# Implementación 1: Array ordenado
class DiccionarioArray:
def insert(self, key, value): pass
def search(self, key): pass
def delete(self, key): pass
# Implementación 2: Tabla Hash
class DiccionarioHash:
def insert(self, key, value): pass
def search(self, key): pass
def delete(self, key): pass
# Implementación 3: Árbol Binario de Búsqueda
class DiccionarioArbol:
def insert(self, key, value): pass
def search(self, key): pass
def delete(self, key): pass
# El programa usa la interfaz, no la implementación específica
diccionario = DiccionarioHash() # Podemos cambiar esto fácilmente
diccionario.insert("nombre", "Juan")
resultado = diccionario.search("nombre")
¿Qué significa que un ADT puede tener múltiples implementaciones?
2. Propósito y Relevancia
¿Por Qué Son Importantes?
Impacto en el Rendimiento
El propósito fundamental de una Estructura de Datos es gestionar los compromisos de rendimiento para la ejecución de diversas operaciones, mejorando dramáticamente el rendimiento total de un sistema.
Analogía Médica
Éxito
Integrar la estructura de datos correcta en un programa lento puede funcionar de manera similar a trasplantar partes nuevas en un paciente enfermo.
El máximo beneficio se obtiene al diseñar el programa basándose en las estructuras de datos apropiadas desde el principio.
Consecuencias de una Mala Elección
Advertencia
La selección de la estructura incorrecta puede ser desastrosa en términos de rendimiento.
Un programa que debería ejecutarse en segundos podría tomar horas o días si usa la estructura de datos equivocada.
Problema: Buscar si un elemento existe en una colección de 1,000,000 elementos
| Estructura | Operación | Complejidad | Tiempo (estimado) |
|---|---|---|---|
| Array desordenado | Búsqueda lineal | ~0.5 segundos | |
| Array ordenado | Búsqueda binaria | ~0.00002 segundos | |
| Tabla Hash | Búsqueda directa | ~0.000001 segundos |
Diferencia: ¡La tabla hash es 500,000 veces más rápida que el array desordenado!
Si esta operación se ejecuta 1,000 veces:
- Array desordenado: 8.3 minutos
- Array ordenado: 0.02 segundos
- Tabla Hash: 0.001 segundos
¿Cuál es el momento ideal para elegir la estructura de datos correcta?
3. Clasificación Fundamental: Contiguas vs. Enlazadas
Información
Las estructuras de datos se clasifican claramente como contiguas o enlazadas, dependiendo de si se basan en arrays (arreglos) o punteros.
Comparación Visual
| Característica | Contiguas (Arrays) | Enlazadas (Listas) |
|---|---|---|
| Memoria | Bloque continuo | Fragmentos dispersos |
| Acceso | Directo por índice | Secuencial |
| Tamaño | Fijo o dinámico | Flexible |
| Espacio extra | Mínimo | Punteros adicionales |
| Inserción/Eliminación | Costosa | Eficiente |
3.1 Estructuras Contiguas (Arrays)
Definición
Las estructuras contiguamente asignadas están compuestas por una única franja de memoria. El array es la estructura de datos contigua fundamental.
Propósito
Almacenar registros de datos de tamaño fijo, permitiendo que cada elemento sea localizado eficientemente por su índice.
Ejemplos
- Arrays unidimensionales
- Matrices (arrays multidimensionales)
- Tablas hash
- Heaps (montículos)
Representación en Memoria
Array: [10, 20, 30, 40, 50]
Memoria:
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
└────┴────┴────┴────┴────┘
↑
Dirección base: 1000
Acceso a arr[2]:
Dirección = 1000 + (2 × tamaño_elemento)
= 1000 + (2 × 4 bytes)
= 1008
Tiempo: O(1) - ¡Instantáneo!
Ventajas y Desventajas
✅ Ventajas:
-
Acceso en tiempo constante
- El índice de cada elemento se mapea directamente a una dirección de memoria
- Se pueden acceder a elementos arbitrarios instantáneamente si se conoce el índice
- Complejidad:
-
Eficiencia espacial
- Consisten puramente en datos
- Sin espacio desperdiciado en enlaces o información de formato
- Mejor localidad de caché (elementos contiguos en memoria)
-
Simplicidad
- Fácil de entender e implementar
- Soportado nativamente en todos los lenguajes
❌ Desventajas:
-
Limitación de tamaño
- Por defecto, no se puede ajustar su tamaño durante la ejecución
- Necesita saber el tamaño máximo de antemano
-
Inserción/Eliminación costosa
- Insertar o eliminar en el medio requiere desplazar elementos
- Complejidad:
-
Desperdicio de memoria
- Si se sobredimensiona, se desperdicia espacio
- Si se subdimensiona, puede quedarse sin espacio
💡 Solución: Arrays Dinámicos
class ArrayDinamico:
def __init__(self):
self.capacidad = 4
self.tamano = 0
self.datos = [None] * self.capacidad
def agregar(self, elemento):
if self.tamano == self.capacidad:
# ¡Duplicar el tamaño cuando se llena!
self.redimensionar(self.capacidad * 2)
self.datos[self.tamano] = elemento
self.tamano += 1
def redimensionar(self, nueva_capacidad):
nuevo_array = [None] * nueva_capacidad
for i in range(self.tamano):
nuevo_array[i] = self.datos[i]
self.datos = nuevo_array
self.capacidad = nueva_capacidad
# Análisis de complejidad:
# - Operación individual: O(n) en el peor caso
# - Costo amortizado para n inserciones: O(n)
# - Por lo tanto, cada inserción es O(1) amortizado Análisis del costo amortizado:
Aunque copiar el contenido es costoso, el procedimiento de duplicación asegura que el esfuerzo total para inserciones sigue siendo amortizado a .
| Tamaño | Operación | Costo |
|---|---|---|
| 1 → 2 | Copiar 1 | 1 |
| 2 → 4 | Copiar 2 | 2 |
| 4 → 8 | Copiar 4 | 4 |
| 8 → 16 | Copiar 8 | 8 |
| … | … | … |
| Total para inserciones | < |
Costo promedio por inserción:
3.2 Estructuras Enlazadas (Listas y Punteros)
Definición
Las estructuras de datos enlazadas están compuestas por fragmentos distintos de memoria unidos mediante punteros.
Un puntero representa la dirección de una ubicación en memoria.
Propósito
Ofrecer flexibilidad en la manipulación de la memoria y la gestión del tamaño de la estructura.
Ejemplos
- Listas enlazadas (simples, dobles, circulares)
- Árboles binarios
- Grafos (lista de adyacencia)
- Listas de saltos (skip lists)
Estructura de un Nodo
class Nodo:
def __init__(self, dato):
self.dato = dato # Campo de datos
self.siguiente = None # Puntero al siguiente nodo
class ListaEnlazada:
def **init**(self):
self.cabeza = None
def insertar_al_inicio(self, dato):
nuevo_nodo = Nodo(dato)
nuevo_nodo.siguiente = self.cabeza
self.cabeza = nuevo_nodo
def buscar(self, valor):
actual = self.cabeza
while actual is not None:
if actual.dato == valor:
return True
actual = actual.siguiente
return False
# Visualización:
# cabeza → [10|●] → [20|●] → [30|●] → [40|None]
# ↑ ↑ ↑ ↑
# dato=10 dato=20 dato=30 dato=40
Ventajas y Desventajas
✅ Ventajas:
-
Gestión de desbordamiento
- Nunca ocurre un desbordamiento a menos que la memoria física esté completamente llena
- Crecimiento dinámico sin necesidad de redimensionamiento
-
Inserción y eliminación eficientes
- Estas operaciones son generalmente más sencillas que en los arrays estáticos
- Solo requiere actualizar punteros: si tienes la posición
-
Flexibilidad estructural
- Fácil de reorganizar y reestructurar
- Permite estructuras complejas (árboles, grafos)
❌ Desventajas:
-
Costo de espacio
- Requieren memoria extra para almacenar los campos de punteros
- Típicamente 50% más espacio que arrays
-
Acceso secuencial
- No hay acceso directo por índice
- Buscar el elemento requiere recorrer nodos:
-
Localidad de caché pobre
- Los nodos están dispersos en memoria
- Menor rendimiento en sistemas modernos con caché
Comparación de memoria:
Array de 4 enteros (32 bits cada uno):
┌────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ Total: 16 bytes
└────┴────┴────┴────┘
Lista enlazada de 4 enteros (punteros de 64 bits):
┌────┬────────┐ ┌────┬────────┐ ┌────┬────────┐ ┌────┬────────┐
│ 10 │ ●────┼──→│ 20 │ ●────┼──→│ 30 │ ●────┼──→│ 40 │ None │
└────┴────────┘ └────┴────────┘ └────┴────────┘ └────┴────────┘
4b 8b 4b 8b 4b 8b 4b 8b
Total: 48 bytes (¡3 veces más espacio!)
¿Cuál es la principal ventaja de las estructuras enlazadas sobre los arrays?
4. Tipos de Datos Abstractos Fundamentales
4.1 Contenedores (Stacks y Queues)
Definición
Un contenedor es un ADT que permite el almacenamiento y la recuperación de elementos de datos independiente de su contenido.
Lo que distingue a los contenedores es el orden en que se recuperan los datos.
Pilas (Stacks) - LIFO
Last-In, First-Out
Las pilas soportan orden LIFO (Last-In, First-Out): el último elemento insertado es el primero en salir.
Operaciones principales:
push(x): Insertar elemento en la parte superiorpop(): Devolver y remover el elemento superior
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def pop(self):
if not self.is_empty():
return self.items.pop()
return None
def peek(self):
if not self.is_empty():
return self.items[-1]
return None
def is_empty(self):
return len(self.items) == 0
# Uso:
stack = Stack()
stack.push(10) # [10]
stack.push(20) # [10, 20]
stack.push(30) # [10, 20, 30]
print(stack.pop()) # 30 → [10, 20]
print(stack.peek()) # 20 (sin remover)
print(stack.pop()) # 20 → [10] Aplicaciones comunes:
- Evaluación de expresiones
- Algoritmos recursivos (call stack)
- Navegación del historial (botón “atrás”)
- Deshacer/rehacer en editores
Colas (Queues) - FIFO
First-In, First-Out
Las colas soportan orden FIFO (First-In, First-Out): el primer elemento insertado es el primero en salir.
Operaciones principales:
enqueue(x): Insertar elemento al finaldequeue(): Devolver y remover el elemento frontal
from collections import deque
class Queue:
def **init**(self):
self.items = deque()
def enqueue(self, item):
self.items.append(item) # Agregar al final
def dequeue(self):
if not self.is_empty():
return self.items.popleft() # Remover del frente
return None
def peek(self):
if not self.is_empty():
return self.items[0]
return None
def is_empty(self):
return len(self.items) == 0
# Uso:
queue = Queue()
queue.enqueue(10) # [10]
queue.enqueue(20) # [10, 20]
queue.enqueue(30) # [10, 20, 30]
print(queue.dequeue()) # 10 → [20, 30]
print(queue.peek()) # 20 (sin remover)
print(queue.dequeue()) # 20 → [30]
Aplicaciones comunes:
- Sistemas de impresión (print queue)
- Procesamiento de tareas (task queue)
- Búsqueda en amplitud (BFS)
- Simulación de líneas de espera
Problema: Evaluar expresiones aritméticas con paréntesis
def evaluar_expresion(expresion):
"""
Evalúa expresiones como: "3 + (2 * 5) - 1"
Usa stack para manejar paréntesis
"""
stack = Stack()
# Simplificado para demostración
for caracter in expresion:
if caracter == '(':
stack.push(caracter)
elif caracter == ')':
# Evaluar subexpresión
stack.pop()
return resultado
# Traza de ejecución para: 3 + (2 * (4 + 1))
# Paso 1: push '(' → Stack: ['(']
# Paso 2: push '(' → Stack: ['(', '(']
# Paso 3: pop ')' → Stack: ['('] [Evalúa: 4 + 1 = 5]
# Paso 4: pop ')' → Stack: [] [Evalúa: 2 * 5 = 10]
# Paso 5: Resultado final: 3 + 10 = 13 ¿Por qué LIFO?
En recursión, el último marco de función llamado es el primero que debe completarse. La estructura de pila natural de las llamadas recursivas coincide perfectamente con el ADT Stack.
Si insertamos elementos en orden [A, B, C, D] y luego los removemos, ¿qué orden obtendremos con una Pila (Stack)?
4.2 Diccionarios
Definición
El tipo de datos diccionario permite el acceso a elementos de datos por contenido o valor clave.
Propósito
Insertar una entrada para poder buscarla eficientemente más tarde mediante su clave.
Operaciones Fundamentales
| Operación | Descripción | Ejemplo |
|---|---|---|
search(D, k) | Dada una clave , retorna un puntero al elemento | search(dict, "edad") |
insert(D, x) | Agrega el elemento al diccionario | insert(dict, {"edad": 25}) |
delete(D, x) | Remueve el elemento del diccionario | delete(dict, "edad") |
Pro Tip
Una vez que los problemas se definen en términos de operaciones de diccionario, se puede ignorar la representación subyacente de la estructura de datos.
Esto permite cambiar la implementación sin afectar el resto del programa.
Implementaciones Comunes
| Implementación | Búsqueda | Inserción | Eliminación | Ventajas | Desventajas |
|---|---|---|---|---|---|
| Array desordenado | Simple | Búsqueda lenta | |||
| Array ordenado | Búsqueda rápida | Inserción/eliminación lentas | |||
| Lista enlazada | Inserción rápida | Búsqueda lenta | |||
| Árbol binario de búsqueda | Balanceado | Puede degenerar | |||
| Tabla Hash | promedio | promedio | promedio | Muy rápida | Peor caso |
Conclusión: La tabla hash es la implementación más eficiente para la mayoría de casos de uso prácticos.
# Diccionario como ADT - puede tener múltiples implementaciones
# Implementación con tabla hash de Python
estudiantes = {}
# Insert
estudiantes["Juan"] = {"edad": 20, "carrera": "Ingeniería"}
estudiantes["María"] = {"edad": 22, "carrera": "Medicina"}
estudiantes["Pedro"] = {"edad": 21, "carrera": "Derecho"}
# Search
if "Juan" in estudiantes:
print(estudiantes["Juan"]) # {'edad': 20, 'carrera': 'Ingeniería'}
# Delete
del estudiantes["Pedro"]
# Iterar
for nombre, info in estudiantes.items():
print(f"{nombre}: {info['edad']} años")
4.3 Tablas Hash (Hashing)
Definición
Un método muy práctico para mantener un diccionario que explota el acceso en tiempo constante de los arrays mediante una función hash que mapea claves grandes a índices enteros.
Propósito
Proporcionar tiempos de búsqueda, inserción y eliminación extremadamente rápidos en el caso promedio: .
¿Cómo Funciona?
def funcion_hash_simple(clave, tamano_tabla):
"""
Convierte una clave (string, número, etc.) en un índice
"""
# Para strings: suma de valores ASCII
hash_value = sum(ord(c) for c in str(clave))
# Módulo para mantenerlo dentro del rango
return hash_value % tamano_tabla
# Ejemplo:
tabla_tamano = 10
hash("Juan") = (74+117+97+110) % 10 = 398 % 10 = 8
hash("María") = (77+97+114+105+97) % 10 = 490 % 10 = 0
hash("Pedro") = (80+101+100+114+111) % 10 = 506 % 10 = 6
# Tabla hash resultante:
# [0] → María
# [1] → (vacío)
# [2] → (vacío)
# ...
# [6] → Pedro
# [7] → (vacío)
# [8] → Juan
# [9] → (vacío) Problema: Colisiones
Colisiones
El problema principal son las colisiones, donde dos claves distintas se mapean al mismo valor hash.
Ejemplo: Si hash("Ana") = 8 y hash("Juan") = 8, ¡ambos quieren ocupar la posición 8!
Soluciones a Colisiones
1. Encadenamiento (Chaining)
class HashTableChaining:
def __init__(self, tamano=10):
# Tabla de listas enlazadas (cubetas)
self.tamano = tamano
self.tabla = [[] for _ in range(tamano)]
def hash(self, clave):
return hash(clave) % self.tamano
def insert(self, clave, valor):
indice = self.hash(clave)
# Agregar a la lista en ese índice
for i, (k, v) in enumerate(self.tabla[indice]):
if k == clave:
self.tabla[indice][i] = (clave, valor)
return
self.tabla[indice].append((clave, valor))
def search(self, clave):
indice = self.hash(clave)
# Buscar en la lista
for k, v in self.tabla[indice]:
if k == clave:
return v
return None
# Visualización:
# [0] → []
# [1] → []
# [2] → [("Ana", 20), ("Luis", 22)] ← Colisión manejada
# [3] → []
# [4] → [("María", 25)]
# ...
2. Direccionamiento Abierto (Open Addressing)
class HashTableOpenAddressing:
def __init__(self, tamano=10):
self.tamano = tamano
self.claves = [None] * tamano
self.valores = [None] * tamano
def hash(self, clave):
return hash(clave) % self.tamano
def insert(self, clave, valor):
indice = self.hash(clave)
# Sondeo lineal: buscar siguiente posición libre
while self.claves[indice] is not None:
if self.claves[indice] == clave:
self.valores[indice] = valor
return
indice = (indice + 1) % self.tamano
self.claves[indice] = clave
self.valores[indice] = valor
def search(self, clave):
indice = self.hash(clave)
while self.claves[indice] is not None:
if self.claves[indice] == clave:
return self.valores[indice]
indice = (indice + 1) % self.tamano
return None
# Si hash("Ana") = 2 y está ocupado:
# Intenta índice 3, 4, 5... hasta encontrar espacio libre Comparación de Métodos de Resolución de Colisiones
| Método | Ventaja | Desventaja | Uso Típico |
|---|---|---|---|
| Encadenamiento | Simple, no tiene límite de elementos | Usa memoria extra (punteros) | Cuando el factor de carga es alto |
| Sondeo lineal | Buena localidad de caché | Clustering (agrupamiento) | Cuando hay suficiente espacio |
| Sondeo cuadrático | Reduce clustering | Puede no encontrar espacio | Balanceo entre ambos |
| Double hashing | Distribución uniforme | Más complejo | Aplicaciones críticas |
1. Detección de Duplicados
def encontrar_duplicados(arr):
"""
Tiempo: O(n) con hash vs O(n²) sin hash
"""
vistos = set() # Set usa tabla hash internamente
duplicados = []
for elemento in arr:
if elemento in vistos: # O(1) con hash
duplicados.append(elemento)
vistos.add(elemento)
return duplicados
# Ejemplo:
numeros = [1, 2, 3, 2, 4, 5, 3, 6]
print(encontrar_duplicados(numeros)) # [2, 3]
2. Detección de Plagio
- Se divide el texto en ventanas de palabras
- Se calcula el hash de cada ventana
- Se compara con una base de datos de hashes
- Si coincide el hash, se verifica el texto completo
3. Autenticación Criptográfica
import hashlib
def calcular_hash_archivo(ruta):
"""
Calcula hash SHA-256 de un archivo
Usado para verificar que no ha sido alterado
"""
sha256 = hashlib.sha256()
with open(ruta, 'rb') as f:
while chunk := f.read(8192):
sha256.update(chunk)
return sha256.hexdigest()
# Uso:
# hash_original = "a3b5c7d9..."
# hash_actual = calcular_hash_archivo("documento.pdf")
#
# if hash_original == hash_actual:
# print("✓ Archivo íntegro")
# else:
# print("✗ Archivo modificado o corrupto") 4. Cachés y Memorización
- Almacenar resultados de funciones costosas
- Clave: parámetros de entrada
- Valor: resultado calculado
- Tiempo: vs recalcular
¿Cuál es la principal ventaja de las tablas hash sobre otras implementaciones de diccionarios?
5. Cierre: La Importancia de la Optimización
Caso de Estudio: Optimización de Secuenciación de ADN
Problema Real
En la optimización de la secuenciación de ADN, la búsqueda de subcadenas se realizaba repetidamente en el bucle más interno del programa.
El rendimiento del programa completo dependía crucialmente de esta operación.
Evolución de la Solución
| Intento | Estructura de Datos | Resultado | Complejidad |
|---|---|---|---|
| 1 | Árbol de búsqueda binaria | ❌ Demasiado lento | por búsqueda |
| 2 | Tabla hash | ⚠️ Mejor, pero insuficiente | promedio |
| 3 | Árbol de sufijos comprimido | ✅ Óptimo | por consulta |
Contexto del problema:
- Buscar si una subcadena aparece en una secuencia de ADN
- La secuencia tiene millones de caracteres
- La operación se ejecuta millones de veces en un bucle
Intento 1: Árbol Binario de Búsqueda
Operación: Buscar subcadena
Complejidad: O(m log n)
donde m = longitud de la subcadena
n = número de subcadenas en el árbol
Para 1,000,000 de búsquedas:
Tiempo ≈ 1,000,000 × log(1,000,000) × 20
≈ 400,000,000 operaciones
≈ 7 minutos (demasiado lento)
Intento 2: Tabla Hash
Operación: Buscar subcadena
Complejidad: O(m) en promedio
donde m = longitud de la subcadena
Para 1,000,000 de búsquedas:
Tiempo ≈ 1,000,000 × 20
≈ 20,000,000 operaciones
≈ 20 segundos (mejor, pero aún lento para big data)
Intento 3: Árbol de Sufijos Comprimido
Operación: Buscar subcadena
Complejidad: O(m) para construir, O(1) para consultas subsecuentes
donde m = longitud de la subcadena
Preprocesamiento: Construir árbol de sufijos
Tiempo: O(n) donde n = longitud de la secuencia
Para 1,000,000 de búsquedas:
Tiempo ≈ n + 1,000,000 × 1
≈ 1,000,000 + 1,000,000
≈ 2,000,000 operaciones
≈ 2 segundos (¡10x más rápido!)
Lección clave:
Cada consulta subsecuente se realizaba en tiempo constante, optimizando el proceso de búsqueda que se ejecutaba en el bucle más interno.
Principio Fundamental de Optimización
Regla de Oro
En el diseño de algoritmos, identificar la operación que se realiza repetidamente y optimizar la estructura de datos para soportar esa operación específica es el factor clave para lograr el nivel de rendimiento requerido.
Estrategia de Optimización
-
Identificar el cuello de botella
- ¿Qué operación se ejecuta más veces?
- ¿Dónde se gasta la mayor parte del tiempo?
-
Analizar las operaciones requeridas
- ¿Necesitas búsquedas rápidas?
- ¿Inserciones/eliminaciones frecuentes?
- ¿Acceso secuencial u aleatorio?
-
Seleccionar la estructura apropiada
- Comparar complejidades de diferentes estructuras
- Considerar casos promedio vs peor caso
- Evaluar uso de memoria vs velocidad
-
Implementar y medir
- Probar con datos reales
- Medir rendimiento actual
- Iterar si es necesario
Guía Rápida: ¿Qué Estructura Usar?
¿Necesitas acceso por índice?
- ✅ Array/ArrayList
¿Necesitas buscar por clave?
- ✅ Tabla Hash (búsqueda rápida)
- ✅ Árbol Binario de Búsqueda (si necesitas orden)
¿Necesitas mantener orden?
- ✅ Array ordenado
- ✅ Árbol Binario de Búsqueda
- ✅ Árbol AVL/Rojo-Negro (balanceado)
¿Necesitas LIFO (último en entrar, primero en salir)?
- ✅ Stack (Pila)
¿Necesitas FIFO (primero en entrar, primero en salir)?
- ✅ Queue (Cola)
¿Inserciones/eliminaciones frecuentes en medio?
- ✅ Lista enlazada
- ✅ Skip list
¿Necesitas prioridad?
- ✅ Heap (Montículo)
- ✅ Cola de prioridad
¿Necesitas representar relaciones?
- ✅ Grafo (lista de adyacencia o matriz)
¿Necesitas búsqueda de prefijos?
- ✅ Trie (árbol de prefijos)
¿Necesitas búsqueda de subcadenas?
- ✅ Árbol de sufijos
Resumen de Conceptos Clave
Clasificación Principal
Estructuras Contiguas:
- Arrays, matrices, heaps
- ✅ Acceso
- ❌ Tamaño fijo o redimensionamiento costoso
Estructuras Enlazadas:
- Listas, árboles, grafos
- ✅ Tamaño dinámico
- ❌ Acceso
ADTs Fundamentales
| ADT | Orden | Operaciones | Uso |
|---|---|---|---|
| Stack | LIFO | push, pop | Recursión, deshacer |
| Queue | FIFO | enqueue, dequeue | Tareas, BFS |
| Diccionario | Por clave | search, insert, delete | Búsquedas |
Implementaciones de Diccionario
| Estructura | Búsqueda | Inserción | Mejor Para |
|---|---|---|---|
| Array desordenado | Pocas búsquedas | ||
| Array ordenado | Muchas búsquedas, pocas modificaciones | ||
| Tabla Hash | Mayoría de casos | ||
| Árbol BST | Necesitas orden |
Principios de Optimización
- 🎯 Identificar operación más frecuente
- 📊 Analizar complejidades
- 🔧 Elegir estructura apropiada
- 📈 Medir y optimizar
Regla de Oro
“La estructura de datos correcta puede transformar un programa de horas a segundos.”
Si tu programa realiza millones de búsquedas pero pocas inserciones, ¿qué estructura es más apropiada?
¡Felicitaciones!
Has completado los fundamentos de estructuras de datos. Ahora comprendes:
- La diferencia entre ADTs y estructuras de datos concretas
- Cuándo usar estructuras contiguas vs enlazadas
- Los contenedores fundamentales (Stack, Queue, Diccionario)
- Cómo las tablas hash optimizan las búsquedas
- La importancia de elegir la estructura correcta para el rendimiento
Próximo paso: Profundizar en estructuras avanzadas como árboles balanceados, grafos, y algoritmos de optimización específicos.