Emtix

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:

Ejemplo de intercambiabilidad de implementaciones
# 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")
Quiz

¿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

EstructuraOperaciónComplejidadTiempo (estimado)
Array desordenadoBúsqueda linealO(n)O(n)~0.5 segundos
Array ordenadoBúsqueda binariaO(logn)O(\log n)~0.00002 segundos
Tabla HashBúsqueda directaO(1)O(1)~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
Quiz

¿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ísticaContiguas (Arrays)Enlazadas (Listas)
MemoriaBloque continuoFragmentos dispersos
AccesoDirecto por índice O(1)O(1)Secuencial O(n)O(n)
TamañoFijo o dinámicoFlexible
Espacio extraMínimoPunteros adicionales
Inserción/EliminaciónCostosa O(n)O(n)Eficiente O(1)O(1)

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

Visualización de array 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:

  1. 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: O(1)O(1)
  2. Eficiencia espacial

    • Consisten puramente en datos
    • Sin espacio desperdiciado en enlaces o información de formato
    • Mejor localidad de caché (elementos contiguos en memoria)
  3. Simplicidad

    • Fácil de entender e implementar
    • Soportado nativamente en todos los lenguajes

❌ Desventajas:

  1. 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
  2. Inserción/Eliminación costosa

    • Insertar o eliminar en el medio requiere desplazar elementos
    • Complejidad: O(n)O(n)
  3. Desperdicio de memoria

    • Si se sobredimensiona, se desperdicia espacio
    • Si se subdimensiona, puede quedarse sin espacio

💡 Solución: Arrays Dinámicos

Estrategia de redimensionamiento
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 nn inserciones sigue siendo amortizado a O(n)O(n).

TamañoOperaciónCosto
1 → 2Copiar 11
2 → 4Copiar 22
4 → 8Copiar 44
8 → 16Copiar 88
Total para nn inserciones< 2n2n

Costo promedio por inserción: 2n/n=2=O(1)2n / n = 2 = O(1)

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

Definición de nodo en lista enlazada
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:

  1. 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
  2. Inserción y eliminación eficientes

    • Estas operaciones son generalmente más sencillas que en los arrays estáticos
    • Solo requiere actualizar punteros: O(1)O(1) si tienes la posición
  3. Flexibilidad estructural

    • Fácil de reorganizar y reestructurar
    • Permite estructuras complejas (árboles, grafos)

❌ Desventajas:

  1. Costo de espacio

    • Requieren memoria extra para almacenar los campos de punteros
    • Típicamente 50% más espacio que arrays
  2. Acceso secuencial

    • No hay acceso directo por índice
    • Buscar el elemento ii requiere recorrer ii nodos: O(n)O(n)
  3. Localidad de caché pobre

    • Los nodos están dispersos en memoria
    • Menor rendimiento en sistemas modernos con caché

Comparación de memoria:

Comparación de uso 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!)
Quiz

¿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 xx en la parte superior
  • pop(): Devolver y remover el elemento superior
Implementación y uso de Stack
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 xx al final
  • dequeue(): Devolver y remover el elemento frontal
Implementación y uso de Queue
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

Evaluación de expresiones con stack
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.

Quiz

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ónDescripciónEjemplo
search(D, k)Dada una clave kk, retorna un puntero al elementosearch(dict, "edad")
insert(D, x)Agrega el elemento xx al diccionarioinsert(dict, {"edad": 25})
delete(D, x)Remueve el elemento xx del diccionariodelete(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ónBúsquedaInserciónEliminaciónVentajasDesventajas
Array desordenadoO(n)O(n)O(1)O(1)O(n)O(n)SimpleBúsqueda lenta
Array ordenadoO(logn)O(\log n)O(n)O(n)O(n)O(n)Búsqueda rápidaInserción/eliminación lentas
Lista enlazadaO(n)O(n)O(1)O(1)O(n)O(n)Inserción rápidaBúsqueda lenta
Árbol binario de búsquedaO(logn)O(\log n)O(logn)O(\log n)O(logn)O(\log n)BalanceadoPuede degenerar
Tabla HashO(1)O(1) promedioO(1)O(1) promedioO(1)O(1) promedioMuy rápidaPeor caso O(n)O(n)

Conclusión: La tabla hash es la implementación más eficiente para la mayoría de casos de uso prácticos.

Ejemplo de uso de diccionario
# 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: O(1)O(1).

¿Cómo Funciona?

Concepto básico de función hash
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)

Implementación con encadenamiento
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)

Implementación con sondeo lineal
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étodoVentajaDesventajaUso Típico
EncadenamientoSimple, no tiene límite de elementosUsa memoria extra (punteros)Cuando el factor de carga es alto
Sondeo linealBuena localidad de cachéClustering (agrupamiento)Cuando hay suficiente espacio
Sondeo cuadráticoReduce clusteringPuede no encontrar espacioBalanceo entre ambos
Double hashingDistribución uniformeMás complejoAplicaciones críticas

1. Detección de Duplicados

Ejemplo: Encontrar duplicados en un array
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 kk 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

Verificación de integridad de archivos
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: O(1)O(1) vs recalcular
Quiz

¿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

IntentoEstructura de DatosResultadoComplejidad
1Árbol de búsqueda binaria❌ Demasiado lentoO(logn)O(\log n) por búsqueda
2Tabla hash⚠️ Mejor, pero insuficienteO(1)O(1) promedio
3Árbol de sufijos comprimido✅ ÓptimoO(1)O(1) 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

Análisis de rendimiento
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

Mejora con 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

Solución óptima

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

  1. Identificar el cuello de botella

    • ¿Qué operación se ejecuta más veces?
    • ¿Dónde se gasta la mayor parte del tiempo?
  2. Analizar las operaciones requeridas

    • ¿Necesitas búsquedas rápidas?
    • ¿Inserciones/eliminaciones frecuentes?
    • ¿Acceso secuencial u aleatorio?
  3. Seleccionar la estructura apropiada

    • Comparar complejidades de diferentes estructuras
    • Considerar casos promedio vs peor caso
    • Evaluar uso de memoria vs velocidad
  4. 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 O(1)O(1)
  • ❌ Tamaño fijo o redimensionamiento costoso

Estructuras Enlazadas:

  • Listas, árboles, grafos
  • ✅ Tamaño dinámico
  • ❌ Acceso O(n)O(n)

ADTs Fundamentales

ADTOrdenOperacionesUso
StackLIFOpush, popRecursión, deshacer
QueueFIFOenqueue, dequeueTareas, BFS
DiccionarioPor clavesearch, insert, deleteBúsquedas

Implementaciones de Diccionario

EstructuraBúsquedaInserciónMejor Para
Array desordenadoO(n)O(n)O(1)O(1)Pocas búsquedas
Array ordenadoO(logn)O(\log n)O(n)O(n)Muchas búsquedas, pocas modificaciones
Tabla HashO(1)O(1)O(1)O(1)Mayoría de casos
Árbol BSTO(logn)O(\log n)O(logn)O(\log n)Necesitas orden

Principios de Optimización

  1. 🎯 Identificar operación más frecuente
  2. 📊 Analizar complejidades
  3. 🔧 Elegir estructura apropiada
  4. 📈 Medir y optimizar

Regla de Oro

“La estructura de datos correcta puede transformar un programa de horas a segundos.”

Quiz

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.