Estructuras de Datos

Las estructuras de datos son formas de organizar y almacenar información en un programa de computadora para que pueda ser accedida y modificada de manera eficiente. Como programadores, es esencial entender las distintas estructuras de datos disponibles y saber cuándo aplicar cada una para optimizar el rendimiento y la utilidad de nuestros programas.

Una estructura de datos es una forma particular de organizar datos en la memoria de la computadora para que puedan ser usados de manera eficiente. Las estructuras de datos vienen en muchas formas, como arrays, listas, pilas, colas, grafos, árboles, hashes, etc.

Cada estructura organiza los datos de acuerdo a un modelo lógico específico y soporta operaciones eficientes para acceder, modificar, agregar y borrar elementos según ese modelo. Por ejemplo, un array organiza los elementos de manera secuencial en memoria para facilitar el acceso aleatorio por índices. Una lista enlazada conecta elementos en memoria usando “nodos” con referencias al siguiente nodo para facilitar la inserción y eliminación.

Al elegir la estructura de datos apropiada para la tarea a resolver, podemos escribir programas más eficientes y reducir la complejidad computacional, utilizando menos recursos como memoria y procesamiento.

Las estructuras de datos nos ayudan a:

  • Organizar grandes cantidades de datos para que sean más fáciles de acceder y modificar.

  • Modelar relaciones complejas entre datos, como con grafos y árboles.

  • Acceder y modificar datos de manera eficiente, optimizando el rendimiento.

  • Reutilizar código y estructuras de datos existentes en lugar de tener que reescribir soluciones desde cero.

Por ejemplo, un programa que debe almacenar miles de registros de usuarios se beneficia usando una estructura de datos hash para asociar cada usuario a datos como nombre, apellido, email, etc. De esta manera se pueden encontrar usuarios específicos muy rápido sin tener que iterar sobre toda la colección.

Otro ejemplo son los árboles de búsqueda binaria, que permiten encontrar elementos muy rápido en conjuntos ordenados de millones de elementos. Esto se logra descartando mitades del árbol a medida que se busca el elemento deseado.


Tipos de estructuras de datos

Existen muchos tipos de estructuras de datos. A continuación, se presentan algunas categorías útiles para clasificarlas.

Según relación entre elementos

  • Lineales: Los elementos se organizan secuencialmente uno después del otro. Por ejemplo, arrays, listas, pilas, colas.

  • No lineales: Los elementos se organizan en una jerarquía o grafo. Este es el caso de los árboles y grafos.

Según tipo de elementos

  • Homogéneas: Almacenan un solo tipo de datos. Por ejemplo, arrays en un lenguaje como Java.

  • Heterogéneas: Permiten diferentes tipos de datos. Objetos, registros son ejemplos de esta clasificación.

Según modo de acceso

  • Acceso secuencial: Sólo se puede acceder a los elementos en orden secuencial. Por ejemplo, listas enlazadas.

  • Acceso directo: Se puede acceder a cualquier elemento directamente por su posición. En este grupo se encuentran los arrays.

  • Acceso asociativo: Se accede a elementos por una clave asociada. Aquí se encuentran los diccionarios, hashes.

Según su funcionalidad

  • Arrays: Acceso rápido a elementos por índice pero difícil insertar/eliminar.

  • Listas: Fácil insertar/eliminar en cualquier posición pero acceso secuencial lento.

  • Pilas: Acceso LIFO (último en entrar, primero en salir). Útil para deshacer/rehacer.

  • Colas: Acceso FIFO (primero en entrar, primero en salir). Útil para procesamiento de eventos.

  • Árboles: Permiten modelar relaciones jerárquicas como con directorios de archivos o dependencias de tareas.

  • Grafos: Permiten modelar redes de interconexión como mapas, relaciones sociales, etc.

  • Hashes / Diccionarios: Asocian elementos con claves únicas para acceso ultra rápido.

Esta clasificación no es exhaustiva pero da una idea de la diversidad de estructuras de datos y sus diferentes propiedades que nos permiten modelar problemas complejos de manera eficiente.


Ejemplo

Veamos un ejemplo en Python para ver cómo se crea y utiliza una estructura de datos. Supongamos que queremos representar una cola de impresión donde las impresiones se procesan en orden de llegada (FIFO).

Primero, definimos una clase PrintQueue para representar nuestra cola:

class PrintQueue:
    def __init__(self):
        self.items = []

    def enqueue(self, item):
        self.items.append(item)

    def dequeue(self):
        return self.items.pop(0)

    def is_empty(self):
        return len(self.items) == 0

Luego la utilizamos para agregar impresiones y procesarlas en orden:

print_queue = PrintQueue()

print_queue.enqueue("documento1.doc")
print_queue.enqueue("imagen3.jpg")
print_queue.enqueue("presentacion.ppt")

while not print_queue.is_empty():
    next_item = print_queue.dequeue()
    print(f"Imprimiendo {next_item}")

Esto imprimirá:

Imprimiendo documento1.doc
Imprimiendo imagen3.jpg
Imprimiendo presentacion.ppt

Con una estructura de datos como la cola implementamos la lógica FIFO de una forma sencilla y reutilizable. ¡Y esto es sólo una muestra, las posibilidades son infinitas!


Conclusión

Las estructuras de datos son herramientas fundamentales en programación que nos permiten organizar información de forma óptima para resolver problemas complejos. Conocer los distintos tipos de estructuras disponibles, como arrays, listas, pilas, colas, hashes, grafos y árboles, nos permite construir programas más eficientes. ¡Espero que esta introducción te haya dado algunos conocimientos y herramientas para comenzar a dominar este apasionante tema!

1 - Arreglos

Los arreglos (arrays en inglés) son estructuras de datos fundamentales en programación que permiten almacenar y organizar colecciones de datos del mismo tipo. Dominar el uso de arrays es esencial para cualquier programador.

Un array es una estructura de datos que representa un conjunto de elementos, los cuales se acceden a través de índices numéricos contiguos que van desde 0 hasta el tamaño del array menos 1. Los arrays proveen acceso rápido y directo a los elementos en base a su posición.

En lenguajes como Python y Ruby, los arrays se conocen como ’listas’ (lists). En Javascript se les conoce como ‘arreglos’ (arrays).

Los arrays son típicamente homogéneos, almacenando elementos del mismo tipo como enteros, cadenas, etc. Algunos lenguajes permiten arrays heterogéneos con valores de distintos tipos.

Diagrama de un array

Creación de arrays

La manera de crear arrays varía según el lenguaje de programación:

MI_ARRAY = ["A", "B", "C"] # array literal
mi_array = list(range(5))  # array a partir de rango

Al crear un array literal se inicializan sus elementos directamente. Al construir un array vacío se especifica su tamaño pero sus elementos son inicializados con un valor default (0 para números, null para objetos, etc).

Acceder y modificar elementos

Los elementos individuales se acceden rápidamente por su índice utilizando corchetes []:

my_array = ['a', 'b', 'c']
print(my_array[0]) # 'a'
print(my_array[2]) # 'c'
my_array[2] = 'z'
print(my_array[2]) # 'z'

Los índices comienzan en 0, por lo que en un array de tamaño N, los índices válidos están entre 0 y N-1.

Acceder a un índice inválido causa un error, por ejemplo, acceder al índice 3 en un array de tamaño 3. Esto se conoce como “index out of bounds”.

Recorrer un array

Podemos recorrer todos los elementos usando un ciclo for:

letras = ['a', 'b', 'c']

for i in range(len(letras)):
    print(letras[i])

Esto imprime cada elemento en orden. len() devuelve la longitud total del array.

Otra forma es iterando directamente sobre los elementos:

letras = ['a', 'b', 'c']

for letra in letras:
    print(letra)

Buscar en un array

Podemos buscar un elemento en un array mediante un ciclo y comparando elemento por elemento:

letras = ['a', 'b', 'c']

def buscar_en_array(array, elemento):
    for i in range(len(array)):
        if array[i] == elemento:
            return i
    return False

print(buscar_en_array(letras, 'b')) # 1
print(buscar_en_array(letras, 'z')) # False

Devuelve el índice si se encuentra o False si no se encuentra.

Array multidimensional

Los arrays pueden tener más de una dimensión, por ejemplo matrices 2D, cubos 3D, etc.

Un array 2D se puede ver como una tabla con filas y columnas. Para acceder a un elemento se especifican dos índices, uno para la fila y otro para la columna:

matrix = [
  [1, 2, 3],
  [4, 5, 6]
]

print(matrix[0][2]) # 3
print(matrix[1][0]) # 4

Pueden tener más dimensiones, por ejemplo un array 3D para representar pixeles en una imagen.


Conclusión

Los arrays son estructuras de datos fundamentales en programación que proveen un acceso eficiente a elementos en memoria mediante índices numéricos. Tener un buen dominio de arrays, matrices y sus usos es indispensable para cualquier programador.


2 - Mapas (Diccionarios)

Los mapas (también llamados hashes o diccionarios) son estructuras de datos que asocian claves con valores. Permiten un acceso ultra rápido a elementos mediante claves únicas. En Python se conocen como diccionarios.

Un diccionario, o mapa, consiste en una colección de pares clave-valor. La clave se utiliza para acceder al valor asociado. Las claves deben ser únicas dentro de un diccionario. Los valores pueden repetirse.

Diagrama de un diccionario


Operaciones principales

  • Añadir/actualizar: Inserta un par clave-valor. Si la clave existía, su valor es reemplazado.
    diccionario['clave'] = 'valor'
    
  • Obtener valor: Acceder al valor dada una clave.
    valor = diccionario['clave']
    
  • Eliminar: Remueve un par clave-valor del diccionario.
    del diccionario['clave']
    
  • Recorrer: Iterar sobre las claves, valores o pares del diccionario.
    for clave in diccionario:
      print(clave, diccionario[clave]) # clave, valor
    

Creación de un diccionario o mapa

La sintaxis para crear mapas o diccionarios en Python es la siguiente:

diccionario_vacio = {}

persona = {
  'nombre': 'Juan',
  'edad': 25
}

Ejemplos de uso

Los diccionarios son útiles en muchos casos. A continuación se mencionan algunos de ellos.

Objetos y mapeos

Podemos modelar objetos y entidades con atributos clave-valor:

producto = {
  'nombre': 'Smartphone',
  'precio': 500,
  'marca': 'XYZ'
}

Conteos y frecuencias

Contar ocurrencias de elementos en secuencias:

texto = "Hola mundo mundo"

frecuencias = {}

for palabra in texto.split():
    if palabra in frecuencias:
        frecuencias[palabra] += 1
    else:
        frecuencias[palabra] = 1

print(frecuencias)
# {'Hola': 1, 'mundo': 2}

Almacenar y acceder a datos

Como alternativa de alta performance a lists y arrays.


Conclusión

Los diccionarios son estructuras de datos versátiles gracias a su rápido acceso basado en claves únicas. Tienen usos en casi todos los programas, por lo que dominar diccionarios es indispensable en cualquier lenguaje.


3 - Listas enlazadas

Las listas enlazadas (linked lists en inglés) son una estructura de datos dinámica y flexible que permite almacenar eficientemente colecciones de elementos. A diferencia de los arrays, las listas enlazadas no requieren reservar un tamaño fijo de antemano.

Una lista enlazada se compone de nodos donde cada nodo tiene dos partes:

  • Dato o información
  • Referencia al siguiente nodo

Los nodos se organizan de forma secuencial, cada uno apuntando al siguiente. El último nodo apunta a nulo para indicar el final.

Esta estructura dinámica permite inserción y eliminación eficiente de nodos.


Tipos de listas enlazadas

Existen varios tipos:

  • Simplemente enlazada: Cada nodo apunta al siguiente. Son útiles para colas (queues) y pilas (stacks).

Diagrama de una lista enlazada simple

  • Doblemente enlazada: Cada nodo tiene referencia al siguiente y al anterior. Permiten recorrer en ambos sentidos.

Diagrama de una lista doblemente enlazada

  • Circular: El último nodo apunta al primero formando un ciclo. Útiles para buffers circulares.

Diagrama de una lista circular


Operaciones comunes

  • Insertar: Agregar nodos al inicio, final o medio de la lista.

  • Eliminar: Quitar nodos por valor o posición.

  • Buscar: Encontrar nodos por valor recorriendo la lista.

  • Recorrer: Iterar los nodos accediendo por las referencias.


Implementación

Las listas enlazadas se pueden implementar de la siguiente manera:

Usa la clase ListNode para representar nodos:

class ListNode:
    def __init__(self, value):
        self.value = value
        self.next = None

Luego para crear y usar una lista se define una clase LinkedList con métodos para las operaciones.

class LinkedList:
    def __init__(self):
        self.head = None

    def add_to_start(self, new_value):
        new_node = ListNode(new_value)
        new_node.next = self.head
        self.head = new_node

    def print(self):
        current = self.head
        while current != None:
            print(current.value)
            current = current.next

    def search(self, searched_value):
        current = self.head
        while current != None:
            if current.value == searched_value:
                return True
            current = current.next
        return False

    #...other methods

Con esta clase LinkedList podemos crear una lista, agregar nodos, imprimirla, buscar elementos, etc.

Se podrían agregar otros métodos como insertar al final, eliminar por valor, obtener por índice, etc. Pero esto da una idea de cómo encapsular la funcionalidad de la lista enlazada en una clase. Como práctica, podés intentar agregar estos métodos por tu cuenta, ¡no dudes en dejar tus comentarios y contactarte si necesitas ayuda!


Ventajas y desventajas

Ventajas:

  • Insertar y eliminar nodos es eficiente.
  • No requiere definir tamaño fijo como los arrays.
  • Estructura dinámica y flexible.

Desventajas:

  • Mayor uso de memoria por tener que almacenar referencias.
  • El acceso a elementos por índice es más costoso al ser secuencial.

Ejemplos de uso

  • Implementar estructuras como pilas (stacks) y colas (queues).
  • En listas doblemente enlazadas, recorrer la lista en el sentido ambos sentidos.
  • Blockchains como la de Bitcoin.
  • Reproducir elementos en orden como playlists de música.

Conclusión

Las listas enlazadas son una estructura de datos versátil para almacenar secuencias dinámicas de elementos. Tener un buen manejo de estas listas, sus operaciones y usos es indispensable para cualquier programador.


4 - Pilas

Las pilas (stacks en inglés) son una estructura de datos abstracta que funciona bajo el principio LIFO (last in, first out), donde el último elemento en entrar es el primero en salir.

La naturaleza LIFO de las pilas se debe a que sólo se puede acceder y manipular el elemento superior. La operación de colocar un elemento sobre la pila se conoce como “push”, mientras que sacar un elemento de la pila es un “pop”. El funcionamiento LIFO provoca que el último elemento colocado en una pila sea el primero en ser retirado.

Estructura de una pila


Operaciones principales

Las operaciones primarias que soporta una estructura de pila son:

  • Push: agrega un elemento encima de la pila.
  • Pop: saca el elemento de la pila que se encuentra en la cima.
  • Peek: permite acceder al elemento de la cima sin sacarlo de la pila.
  • isEmpty: consulta si la pila se encuentra vacía.

La mayoría de los lenguajes como Python y Java proveen implementaciones de pilas en sus librerías estándar.


Implementación

Una pila puede implementarse utilizando una lista enlazada de manera que cada node apunte al nodo anterior.

class Node:
    def __init__(self, value):
        self.value = value
        self.previous = None


class Stack:
    def __init__(self):
        self.top = None
        self.size = 0

    def push(self, value):
        new_node = Node(value)
        if self.top is None:
            self.top = new_node
        else:
            new_node.previous = self.top
            self.top = new_node
        self.size += 1

    def pop(self):
        if self.top is None:
            return None
        top_node = self.top
        self.top = self.top.previous
        self.size -= 1
        return top_node.value

    def peek(self):
        if self.top is None:
            return None
        return self.top.value

    def is_empty(self):
        return self.top is None  # Returns true if top is None

    def __len__(self):
        return self.size

    def __str__(self):
        values = []
        current = self.top
        while current:
            values.append(str(current.value))
            current = current.previous
        return "\n".join(values)

Ejemplos de uso

Las pilas tienen muchos usos en programación:

  • Pila de ejecución (call stack): registra las llamadas a funciones pendientes de resolver. Implementa el comportamiento LIFO esperado.

  • Pila de navegador: permite volver atrás (undo) en el historial de navegación de forma similar a una pila LIFO.

  • Ejecución de expresiones matemáticas: mediante una pila se puede verificar paréntesis, corchetes, llaves, etc.

  • Algoritmos y estructuras de datos: como en el algoritmo quicksort y en la implementación de buses de datos (datapaths).


Conclusión

Las pilas son estructuras de datos versátiles gracias a su principio de funcionamiento LIFO. Tener un buen dominio de pilas, sus usos y aplicaciones es esencial en la ciencia de la computación.


5 - Colas

Las colas (queues en inglés) son una estructura de datos abstracta que funciona bajo el principio FIFO (first in, first out), donde el primer elemento en entrar es también el primero en salir. Las colas se utilizan para ordenar elementos de forma que el que llega primero es procesado primero. Comprender su funcionamiento es esencial para cualquier programador.

La naturaleza FIFO (first in, first out) de las colas se debe a que sólo se puede acceder y manipular el elemento inicial. Cuando se agrega un elemento a la cola se conoce como “enqueue”, mientras que eliminar un elemento se denomina “dequeue”.

Esto hace que el primer elemento en ser añadido a la cola también sea el primero en ser retirado, de ahí su comportamiento FIFO.

Diagrama de una cola


Operaciones principales

Las operaciones básicas de una cola son:

  • Enqueue: Agrega un elemento al final de la cola.
  • Dequeue: Saca el elemento del frente de la cola.
  • Peek: Obtiene el elemento al frente sin sacarlo.
  • isEmpty: Consulta si la cola está vacía.

Implementación

Al igual que las pilas, las colas se pueden implementar usando listas enlazadas. Se agrega al final y se saca del frente manteniendo referencias a ambos extremos.

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None


class Queue:
    def __init__(self):
        self.front = None
        self.end = None
        self.size = 0

    def enqueue(self, value):
        new_node = Node(value)
        if self.end is None:
            self.end = new_node
            self.front = new_node
            return
        self.end.next = new_node
        self.end = new_node
        self.size += 1

    def dequeue(self):
        if self.is_empty():
            return None
        value = self.front.value
        self.front = self.front.next
        if self.front is None:
            self.end = None
        self.size -= 1
        return value

    def peek(self):
        if self.is_empty():
            return None
        return self.front.value

    def is_empty(self):
        return self.front is None  # Returns true if front is None

    def __len__(self):
        return self.size

    def __str__(self):
        values = []
        current = self.front
        while current:
            values.append(str(current.value))
            current = current.next
        return "\n".join(values)

Ejemplos de uso

Algunos usos comunes de colas:

  • Colas de impresión donde primero en entrar, primero en imprimir.
  • Colas de tareas en sistemas operativos para orden de ejecución.
  • Simulaciones donde se debe respetar orden de llegada como en bancos.
  • Canales de mensajes como los de RabbitMQ o Kafka.
  • Buffers circulares en audio para streaming.

Conclusión

Las colas son estructuras versátiles gracias a su principio FIFO. Tener un buen manejo de colas, implementación y aplicaciones reforzará tus habilidades como programador.