Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Pilas, Colas y Estructuras Lineales Restringidas

TAD Pila, Cola y Deque

Universidad Nacional de Río Negro

Prerrequisitos: TAD e interfaces .h, structs, punteros, malloc/free y complejidad básica. Compilá con gcc -Wall -Wextra -std=c11 -pedantic.

Objetivos: 1. Comparar las operaciones de pila, cola y deque. 2. Trazar la propiedad y liberación de los nodos que manipula cada operación.

Comprobación de salida: elegí una operación push o dequeue, formulá su invariante y trazá qué nodo debe liberar o conservar.

Desarrollo

Pilas (Stacks)

Una pila es una estructura de datos lineal que sigue el principio LIFO (Last In, First Out): el último elemento en entrar es el primero en salir. Es análogo a una pila de platos donde solo podés agregar o quitar platos desde la parte superior.

Operaciones de apilado (push) and desapilado (pop) en una Pila.

Figure 1:Operaciones de apilado (push) and desapilado (pop) en una Pila.

Operaciones Fundamentales

Implementación con Lista Enlazada

Estructura de una Pila implementada dinámicamente mediante nodos enlazados en el
heap.

Figure 2:Estructura de una Pila implementada dinámicamente mediante nodos enlazados en el heap.

Estructura de Datos
1
2
3
4
5
6
7
8
9
10
typedef struct nodo
{
    void *dato;
    struct nodo *siguiente;
} nodo_t;
struct pila
{
    nodo_t *tope;
    size_t tamanio;
};
Creación de una Pila
1
2
3
4
5
6
7
8
9
10
11
pila_t *pila_crear(void)
{
    pila_t *pila = malloc(sizeof(*pila));
    if (pila == NULL)
    {
        return NULL;
    }
    pila->tope = NULL;
    pila->tamanio = 0;
    return pila;
}
Apilar (Push)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool pila_push(pila_t *pila, void *dato)
{
    if (pila == NULL)
    {
        return false;
    }
    nodo_t *nuevo = malloc(sizeof(*nuevo));
    if (nuevo == NULL)
    {
        return false;
    }
    nuevo->dato = dato;
    nuevo->siguiente = pila->tope;
    pila->tope = nuevo;
    pila->tamanio++;
    return true;
}
Desapilar (Pop)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool pila_pop(pila_t *pila, void **dato)
{
    if (pila == NULL || pila->tope == NULL)
    {
        return false;
    }
    nodo_t *nodo_a_eliminar = pila->tope;
    if (dato != NULL)
    {
        *dato = nodo_a_eliminar->dato;
    }
    pila->tope = nodo_a_eliminar->siguiente;
    free(nodo_a_eliminar);
    pila->tamanio--;
    return true;
}
Ver Tope (Peek)
1
2
3
4
5
6
7
8
9
10
11
12
bool pila_peek(const pila_t *pila, void **dato)
{
    if (pila == NULL || pila->tope == NULL)
    {
        return false;
    }
    if (dato != NULL)
    {
        *dato = pila->tope->dato;
    }
    return true;
}
Verificar si está Vacía
1
2
3
4
bool pila_es_vacia(const pila_t *pila)
{
    return (pila == NULL) || (pila->tope == NULL);
}
Destruir Pila
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void pila_destruir(pila_t *pila, destruir_dato_fn destruir_dato)
{
    if (pila == NULL)
    {
        return;
    }
    while (pila->tope != NULL)
    {
        nodo_t *nodo_actual = pila->tope;
        pila->tope = nodo_actual->siguiente;
        if (destruir_dato != NULL && nodo_actual->dato != NULL)
        {
            destruir_dato(nodo_actual->dato);
        }
        free(nodo_actual);
    }
    free(pila);
}

Análisis de Complejidad de la Pila (Lista Enlazada)

OperaciónComplejidad TemporalComplejidad Espacial
pushO(1)O(1)O(1)O(1)
popO(1)O(1)O(1)O(1)
peekO(1)O(1)O(1)O(1)
es_vaciaO(1)O(1)O(1)O(1)

Implementación con Arreglo Dinámico

Una alternativa es implementar la pila usando un arreglo, donde el tope es el último elemento ocupado.

Estructura de una Pila implementada estáticamente mediante un arreglo y un
índice de tope.

Figure 3:Estructura de una Pila implementada estáticamente mediante un arreglo y un índice de tope.

Estructura de Datos
1
2
3
4
5
6
struct pila
{
    void **elementos;
    size_t tope;      // Próximo índice libre / Cantidad de elementos
    size_t capacidad; // Capacidad total del arreglo
};
Creación con Capacidad Inicial
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pila_t *pila_crear_arreglo(size_t capacidad_inicial)
{
    if (capacidad_inicial == 0)
    {
        return NULL;
    }
    pila_t *pila = malloc(sizeof(*pila));
    if (pila == NULL)
    {
        return NULL;
    }
    pila->elementos = malloc(capacidad_inicial * sizeof(*(pila->elementos)));
    if (pila->elementos == NULL)
    {
        free(pila);
        return NULL;
    }
    pila->tope = 0;
    pila->capacidad = capacidad_inicial;
    return pila;
}
Apilar con Redimensionamiento
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static bool pila_redimensionar(pila_t *pila)
{
    size_t nueva_capacidad = pila->capacidad * 2;
    void **nuevo_arreglo =
        realloc(pila->elementos, nueva_capacidad * sizeof(*nuevo_arreglo));
    if (nuevo_arreglo == NULL)
    {
        return false;
    }
    pila->elementos = nuevo_arreglo;
    pila->capacidad = nueva_capacidad;
    return true;
}
bool pila_push_arreglo(pila_t *pila, void *dato)
{
    if (pila == NULL)
    {
        return false;
    }
    if (pila->tope >= pila->capacidad)
    {
        if (!pila_redimensionar(pila))
        {
            return false;
        }
    }
    pila->elementos[pila->tope] = dato;
    pila->tope++;
    return true;
}
Desapilar (Arreglo)
1
2
3
4
5
6
7
8
9
10
11
12
13
bool pila_pop_arreglo(pila_t *pila, void **dato)
{
    if (pila == NULL || pila->tope == 0)
    {
        return false;
    }
    pila->tope--;
    if (dato != NULL)
    {
        *dato = pila->elementos[pila->tope];
    }
    return true;
}

Análisis de Complejidad (Arreglo)

OperaciónComplejidad TemporalComplejidad Espacial
pushO(1)O(1) amortizadoO(1)O(1)
popO(1)O(1)O(1)O(1)
peekO(1)O(1)O(1)O(1)
es_vaciaO(1)O(1)O(1)O(1)

Aplicaciones de Pilas

Las pilas aparecen naturalmente en numerosos contextos de programación:

  1. Gestión de llamadas a funciones: La pila de ejecución (call stack) mantiene los registros de activación.

  2. Evaluación de expresiones: Conversión de notación infija a postfija, evaluación de expresiones postfijas.

  3. Backtracking: Algoritmos de búsqueda en profundidad, resolución de laberintos.

  4. Deshacer/Rehacer: Editores de texto mantienen pilas de operaciones.

  5. Parsing: Análisis sintáctico de lenguajes de programación (verificación de paréntesis balanceados).

Ejemplo: Verificación de Paréntesis Balanceados
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
bool parentesis_balanceados(const char *expresion)
{
    if (expresion == NULL)
    {
        return false;
    }
    pila_t *pila = pila_crear();
    if (pila == NULL)
    {
        return false;
    }
    for (size_t i = 0; expresion[i] != '\0'; i++)
    {
        char caracter = expresion[i];
        if (caracter == '(')
        {
            if (!pila_push(pila, caracter))
            {
                pila_destruir(pila);
                return false;
            }
        }
        else if (caracter == ')')
        {
            if (pila_es_vacia(pila))
            {
                pila_destruir(pila);
                return false;
            }
            int temporal;
            pila_pop(pila, &temporal);
        }
    }
    bool resultado = pila_es_vacia(pila);
    pila_destruir(pila);
    return resultado;
}

Ejercicios de Pilas

Ejercicio 1: Inversión de una cadena con pila
Ejercicio 2: Validar expresiones con múltiples delimitadores
Ejercicio 3: Evaluación de expresiones postfijas

Colas (Queues)

Una cola es una estructura de datos lineal que sigue el principio FIFO (First In, First Out): el primer elemento en entrar es el primero en salir. Es análogo a una fila de personas donde quien llega primero es atendido primero.

Estructura de una Cola implementada dinámicamente mediante nodos enlazados en el
heap con punteros a inicio y fin.

Figure 4:Estructura de una Cola implementada dinámicamente mediante nodos enlazados en el heap con punteros a inicio y fin.

Implementación con Lista Enlazada

Estructura de Datos
1
2
3
4
5
6
7
8
9
10
11
typedef struct nodo
{
    void *dato;
    struct nodo *siguiente;
} nodo_t;
struct cola
{
    nodo_t *frente;
    nodo_t *final;
    size_t tamanio;
};
Creación de una Cola
1
2
3
4
5
6
7
8
9
10
11
12
cola_t *cola_crear(void)
{
    cola_t *cola = malloc(sizeof(*cola));
    if (cola == NULL)
    {
        return NULL;
    }
    cola->frente = NULL;
    cola->final = NULL;
    cola->tamanio = 0;
    return cola;
}
Encolar (Enqueue)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bool cola_enqueue(cola_t *cola, void *dato)
{
    if (cola == NULL)
    {
        return false;
    }
    nodo_t *nuevo = malloc(sizeof(*nuevo));
    if (nuevo == NULL)
    {
        return false;
    }
    nuevo->dato = dato;
    nuevo->siguiente = NULL;
    if (cola->final == NULL)
    {
        cola->frente = nuevo;
        cola->final = nuevo;
    }
    else
    {
        cola->final->siguiente = nuevo;
        cola->final = nuevo;
    }
    cola->tamanio++;
    return true;
}
Desencolar (Dequeue)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool cola_dequeue(cola_t *cola, void **dato)
{
    if (cola == NULL || cola->frente == NULL)
    {
        return false;
    }
    nodo_t *nodo_a_eliminar = cola->frente;
    if (dato != NULL)
    {
        *dato = nodo_a_eliminar->dato;
    }
    cola->frente = nodo_a_eliminar->siguiente;
    if (cola->frente == NULL)
    {
        cola->final = NULL;
    }
    free(nodo_a_eliminar);
    cola->tamanio--;
    return true;
}
Ver Frente (Peek)
1
2
3
4
5
6
7
8
9
10
11
12
bool cola_peek(const cola_t *cola, void **dato)
{
    if (cola == NULL || cola->frente == NULL)
    {
        return false;
    }
    if (dato != NULL)
    {
        *dato = cola->frente->dato;
    }
    return true;
}
Destruir Cola
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void cola_destruir(cola_t *cola, destruir_dato_fn destruir_dato)
{
    if (cola == NULL)
    {
        return;
    }
    while (cola->frente != NULL)
    {
        nodo_t *nodo_actual = cola->frente;
        cola->frente = nodo_actual->siguiente;
        if (destruir_dato != NULL && nodo_actual->dato != NULL)
        {
            destruir_dato(nodo_actual->dato);
        }
        free(nodo_actual);
    }
    free(cola);
}

Análisis de Complejidad de la Cola (Lista Enlazada)

OperaciónComplejidad TemporalComplejidad Espacial
enqueueO(1)O(1)O(1)O(1)
dequeueO(1)O(1)O(1)O(1)
peekO(1)O(1)O(1)O(1)
es_vaciaO(1)O(1)O(1)O(1)

Implementación con Arreglo Circular

Una implementación eficiente de cola con arreglo usa la técnica de arreglo circular, donde los índices “dan la vuelta” al final del arreglo.

Implementación eficiente de Cola sobre un arreglo circular para evitar el
desplazamiento costoso de elementos.

Figure 5:Implementación eficiente de Cola sobre un arreglo circular para evitar el desplazamiento costoso de elementos.

Estructura de Datos
1
2
3
4
5
6
7
8
struct cola
{
    void **elementos;
    size_t frente;
    size_t final;
    size_t tamanio;
    size_t capacidad;
};
Creación de Cola Circular
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cola_t *cola_crear_circular(size_t capacidad_inicial)
{
    if (capacidad_inicial == 0)
    {
        return NULL;
    }
    cola_t *cola = malloc(sizeof(*cola));
    if (cola == NULL)
    {
        return NULL;
    }
    cola->elementos = malloc(capacidad_inicial * sizeof(*(cola->elementos)));
    if (cola->elementos == NULL)
    {
        free(cola);
        return NULL;
    }
    cola->frente = 0;
    cola->final = 0;
    cola->tamanio = 0;
    cola->capacidad = capacidad_inicial;
    return cola;
}
Encolar en Arreglo Circular
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static bool cola_redimensionar_circular(cola_t *cola)
{
    size_t nueva_capacidad = cola->capacidad * 2;
    void **nuevo_arreglo = malloc(nueva_capacidad * sizeof(*nuevo_arreglo));
    if (nuevo_arreglo == NULL)
    {
        return false;
    }
    for (size_t i = 0; i < cola->tamanio; i++)
    {
        size_t indice = (cola->frente + i) % cola->capacidad;
        nuevo_arreglo[i] = cola->elementos[indice];
    }
    free(cola->elementos);
    cola->elementos = nuevo_arreglo;
    cola->frente = 0;
    cola->final = cola->tamanio;
    cola->capacidad = nueva_capacidad;
    return true;
}
bool cola_enqueue_circular(cola_t *cola, void *dato)
{
    if (cola == NULL)
    {
        return false;
    }
    if (cola->tamanio == cola->capacidad)
    {
        if (!cola_redimensionar_circular(cola))
        {
            return false;
        }
    }
    cola->elementos[cola->final] = dato;
    cola->final = (cola->final + 1) % cola->capacidad;
    cola->tamanio++;
    return true;
}
Desencolar en Arreglo Circular
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool cola_dequeue_circular(cola_t *cola, void **dato)
{
    if (cola == NULL || cola->tamanio == 0)
    {
        return false;
    }
    if (dato != NULL)
    {
        *dato = cola->elementos[cola->frente];
    }
    cola->frente = (cola->frente + 1) % cola->capacidad;
    cola->tamanio--;
    return true;
}

Análisis de Complejidad (Arreglo Circular)

OperaciónComplejidad TemporalComplejidad Espacial
enqueueO(1)O(1) amortizadoO(1)O(1)
dequeueO(1)O(1)O(1)O(1)
peekO(1)O(1)O(1)O(1)
es_vaciaO(1)O(1)O(1)O(1)

Aplicaciones de Colas

Las colas modelan situaciones donde el orden de llegada importa:

  1. Sistemas operativos: Scheduling de procesos, colas de impresión.

  2. Redes: Buffers de transmisión, enrutamiento de paquetes.

  3. Algoritmos de grafos: Búsqueda en anchura (BFS).

  4. Simulaciones: Modelado de filas de espera, teoría de colas.

  5. Procesamiento asíncrono: Cola de tareas, sistemas de mensajería.

Ejercicios de Colas

Ejercicio 1: Simulador de cola de impresión
Ejercicio 2: Implementación de cola con dos pilas
Ejercicio 3: Invertir los primeros K elementos de una cola

Comparación: Pilas vs Colas

AspectoPila (LIFO)Cola (FIFO)
PolíticaLast In, First OutFirst In, First Out
AnalogíaPila de platosFila de personas
Operacionespush, pop, peekenqueue, dequeue, peek
ComplejidadO(1)O(1) todasO(1)O(1) todas
Aplicación típicaBacktracking, parsingScheduling, BFS
Implementación simpleLista (un puntero)Lista (dos punteros)
Implementación arregloÍndice topeArreglo circular

Deques (Double-Ended Queues)

Un deque (pronunciado “deck”) es una generalización que permite insertar y extraer elementos en ambos extremos.

Representación de una Cola de Doble Extremo (Deque), permitiendo inserciones y
eliminaciones por ambos extremos.

Figure 6:Representación de una Cola de Doble Extremo (Deque), permitiendo inserciones y eliminaciones por ambos extremos.

Operaciones

Aplicaciones de Deques

Ejercicios de Deques

Ejercicio 1: Verificar palíndromo con Deque
Ejercicio 2: Simulación de una Pila usando un Deque
Ejercicio 3: Máximo en una ventana deslizante

Comparación de Implementaciones

Lista Enlazada vs Arreglo

CriterioLista EnlazadaArreglo (Circular)
MemoriaOverhead por punterosCompacta, localidad de caché
TamañoDinámico sin límiteRequiere redimensionamiento
OperacionesSiempre O(1)O(1)O(1)O(1) amortizado
Complejidad códigoMediaAlta (aritmética modular)
Uso típicoTamaño impredecibleTamaño acotado

Panorama de Estructuras de Datos

Las pilas y colas son solo el comienzo. Existe un ecosistema rico de estructuras de datos, cada una optimizada para diferentes patrones de acceso.

Clasificación por Restricciones de Acceso

  1. Acceso Completamente Restringido:

    • Pilas: solo el tope es accesible

    • Colas: solo frente y final

  2. Acceso Parcialmente Restringido:

    • Deques: ambos extremos

    • Colas de Prioridad: elemento de máxima prioridad

  3. Acceso Indexado:

    • Arreglos: acceso por índice en O(1)O(1)

    • Listas: acceso secuencial en O(n)O(n)

  4. Acceso por Clave:

    • Tablas Hash: búsqueda en O(1)O(1) promedio

    • Árboles Binarios de Búsqueda: búsqueda en O(log⁡n)O(\log n)

Estructuras Avanzadas

Árboles:

Grafos:

Tablas Hash:

Estructuras Especializadas:

Ejercicios de Autoevaluación

Síntesis y Resumen

Resumen

Los Tipos de Datos Abstractos son una herramienta fundamental para construir software modular y mantenible. En este apunte hemos cubierto:

Referencias y Lecturas de Pilas y Colas

Referencias y Lecturas de Pilas y Colas

Para profundizar en el estudio de los TADs y estructuras de datos, se recomiendan las siguientes referencias:

Para aspectos específicos de gestión de memoria y su impacto en la implementación de TADs, consultá: