Estructura de Datos · C++11

Stack·Cola·Heap

Tres estructuras que mueven el mundo del software. Desde Ctrl+Z hasta Dijkstra.

C++11 o superior
📋 Prereq: arreglos, punteros, O(n)
🔧 g++ · VS Code · CLion
LIFO

Stack

El último en entrar es el primero en salir. La base de la recursión.

push · pop · top → O(1)
FIFO

Cola

El primero en llegar, el primero en salir. Justicia perfecta.

enqueue · dequeue → O(1)
PRIO

Heap

Árbol binario completo en arreglo. El máximo siempre accesible.

insert · extract → O(log n)
scroll
BLOQUE 01

Stack

⬆ LIFO — Last In, First Out
visualización interactiva — stack
30
20
10
Analogía visual

🍽 Pila de platos — el último plato que pusiste es el primero que sacas.

📞 Call Stack: cada llamada a función apila un frame. Al retornar, se desapila.

Operaciones fundamentales
OperaciónComplejidadDescripción
push(x)O(1)Agrega al tope
pop()O(1)Elimina el tope
top()O(1)Consulta el tope sin eliminar
isEmpty()O(1)Verifica si está vacío
size()O(1)Cantidad de elementos
// Stack con std::stack (STL)
#include <stack>

stack<int> s;
s.push(10);   // [10]
s.push(20);   // [10, 20]
s.push(30);   // [10, 20, 30]

s.top();     // → 30  (no elimina)
s.pop();     // elimina 30
s.top();     // → 20
s.size();    // → 2

// topIndex empieza en -1 (pila vacía)
// push: data[++topIndex] = val
// pop:  return data[topIndex--]
Casos de uso reales

Ctrl+Z / Ctrl+Y

Dos stacks: undo_stack y redo_stack. Cada acción se guarda en el tope. Deshacer = pop del undo + push al redo.

{ }

Validación de expresiones

Algoritmo Shunting Yard. Detecta si "({[]})" está balanceado en O(n). Parseo de expresiones matemáticas.

📞

Call Stack

El stack que ya usaste sin saberlo. Cada función crea un stack frame. La recursión es un stack implícito.

BLOQUE 02

Cola

➡ FIFO — First In, First Out
visualización interactiva — queue
← dequeue (FRONT) enqueue (REAR) →
10
20
30
Analogía visual

🏦 Fila del banco — el primero en llegar es el primero en ser atendido. Justicia perfecta: orden de llegada siempre respetado.

🔄 Cola circular: (rear+1)%N evita desperdiciar posiciones.

Operaciones fundamentales
OperaciónComplejidadDescripción
enqueue(x)O(1)Agrega al final (REAR)
dequeue()O(1)Elimina el frente (FRONT)
front()O(1)Consulta el frente
back()O(1)Consulta el final
isEmpty()O(1)¿Está vacía?
// Queue con std::queue (STL)
#include <queue>

queue<int> q;
q.push(10);   // enqueue al rear
q.push(20);
q.push(30);

q.front();   // → 10 (no elimina)
q.pop();     // dequeue: elimina 10
q.front();   // → 20

// Cola circular: aritmética módulo
// rearIdx = (rearIdx + 1) % 100
// frontIdx = (frontIdx + 1) % 100
Casos de uso reales
🖨

Cola de impresión

Trabajos de múltiples usuarios llegan a una impresora compartida. FIFO garantiza justicia en el orden de atención.

🌐

BFS en grafos

La cola es el corazón del Breadth-First Search. Explora nodo a nodo por niveles. Comparar con DFS (usa Stack).

⚙️

Scheduling de procesos

Sistemas operativos usan queues para round-robin scheduling. Paquetes de red, servidores web, eventos de UI.

BLOQUE 03

Heap

🎯 PRIORIDAD — El mayor (o menor) siempre en la raíz
Max-Heap — árbol binario completo en arreglo
Fórmulas de índices (base 0)
Hijo izq. de i:  2*i + 1
Hijo der. de i:  2*i + 2
Padre de i:    (i - 1) / 2
Operaciones fundamentales
OperaciónComplejidadDescripción
insert(x)O(log n)Push + heapify-up
extractMax()O(log n)Raíz al final + heapify-down
peekMax()O(1)Ver raíz (índice 0)
buildHeap()O(n)Desde arreglo desordenado
heapSort()O(n log n)Ordenamiento in-place
// priority_queue (STL) — Max-Heap por default
#include <queue>

priority_queue<int> pq;
pq.push(30); pq.push(10); pq.push(50);
pq.top();   // → 50  (siempre el máximo)
pq.pop();   // extrae 50, O(log n)

// Min-Heap: usar greater<T>
priority_queue<
  int, vector<int>,
  greater<int>
> minPQ;
minPQ.push(30); minPQ.push(10);
minPQ.top();  // → 10 (mínimo)

// ¡std::priority_queue es Max-Heap por defecto!
Casos de uso reales
🗺️

Dijkstra / Prim / A*

Min-Heap para explorar siempre el nodo más cercano. Reduce O(V²) → O((V+E) log V). El heap hace la magia.

🏥

Sala de emergencias

Priority Queue: el paciente más grave se atiende primero, sin importar el orden de llegada. FIFO ya no alcanza.

📊

K-ésimo mayor

Min-Heap de tamaño k para encontrar el k-ésimo mayor en un stream. O(log k) por inserción vs O(n) con sort.

BLOQUE 04

Comparativa

Característica Stack Cola Heap
Principio LIFO FIFO Prioridad
Inserción O(1) O(1) O(log n)
Eliminación O(1) O(1) O(log n)
Ver el "mejor" O(1) tope O(1) frente O(1) raíz
Implementación base Arreglo / Lista Arreglo circular / Lista Arreglo (árbol implícito)
STL de C++ std::stack std::queue std::priority_queue
Uso principal Recursión, undo, parsing BFS, scheduling FIFO Dijkstra, scheduling por prioridad
Adaptador STL sobre deque/vector/list sobre deque/list sobre vector (heap sort)
Usa Stack cuando…
  • ✓ El orden debe invertirse (LIFO)
  • ✓ Necesitas revertir pasos (undo)
  • ✓ Conviertes recursión a iterativa
  • ✓ Parseas expresiones o paréntesis
  • ✓ Implementas DFS en grafos
Usa Cola cuando…
  • ✓ El orden de llegada importa
  • ✓ Necesitas explorar por niveles (BFS)
  • ✓ Modelás sistemas de espera
  • ✓ Procesás eventos en orden
  • ✓ Latencia y fairness son factores
Usa Heap cuando…
  • ✓ Los elementos tienen prioridad
  • ✓ Necesitas mín/máx repetidamente
  • ✓ Implementás Dijkstra / Prim / A*
  • ✓ Trabajás con k-ésimos elementos
  • ✓ Mergeás listas ordenadas
BLOQUE 05

Ejercicios

EJ 01Stack

Validador de paréntesis

Escribe una función que determine si los paréntesis, corchetes y llaves de un string están correctamente balanceados.
"({[]})" → ✓  |  "({[}])" → ✗

💡 Hint: push los abiertos. Al cerrar, compara con el tope. Al final el stack debe estar vacío.
EJ 02Cola

Números binarios del 1 al N

Usa una Queue para generar los primeros N números en representación binaria.
N=5 → 1, 10, 11, 100, 101

💡 Hint: enqueue "1". Para cada dequeue, enqueue num+"0" y num+"1". Refuerza el patrón BFS.
EJ 03Heap

Fusionar K listas ordenadas

Dadas K listas de enteros ordenadas, fusiónalasen una sola lista ordenada con O(N log K) donde N es el total de elementos.

💡 Hint: Min-Heap con el primer elem de cada lista. Extrae mínimo, inserta el siguiente de esa lista.
EJ 04Stack + getMin

Stack con getMin() en O(1)

Implementa un Stack que soporte getMin() en tiempo constante además de las operaciones básicas.

💡 Hint: stack auxiliar que guarda el mínimo actual en cada nivel del stack principal.
EJ 05 — TAREAQueue con Stacks

Queue implementada con 2 Stacks

Clásico de entrevistas: implementa una Queue usando exclusivamente dos Stacks. Sin arreglos auxiliares.

💡 Hint: stack inbox para encolar, stack outbox para desencolar. Vuelca outbox cuando está vacío.
EJ 06 — DESAFÍOMinMaxStack

getMin() y getMax() ambas en O(1)

Implementa una MinMaxStack que soporte getMin() y getMax() simultáneamente en O(1).

💡 Hint: dos stacks auxiliares, uno para mínimos y otro para máximos. Analiza el trade-off de memoria.
Práctica adicional — LeetCode
#23 Merge K Sorted Lists
#215 Kth Largest Element in an Array
#739 Daily Temperatures
Tags: Stack · Queue · Heap · Priority Queue
BLOQUE 06

Cierre

¿Recuerdan las preguntas del inicio de la clase?

Ahora tienen las herramientas para responderlas todas.

"¿Cómo funciona el Ctrl+Z?"
Stack de operaciones
"¿Cómo funciona una cola de impresión?"
Queue FIFO
"¿Cómo elige Dijkstra el próximo nodo?"
Min-Heap / Priority Queue
Lo que sigue — estructuras que se construyen sobre lo visto hoy
Stack →
BST · DFS iterativo · Backtracking · Conversión recursión/iterativa
Cola →
BFS · Grafos · Observer Pattern · Cola de eventos de sistema
Heap →
Dijkstra · Prim · A* · Huffman Coding · Sistemas de recomendación
Todas →
Command Pattern · Deque · Árbol binario · Teoría de colas (Queueing Theory)
BLOQUE 07

Problemas

7 problemas de dificultad progresiva. Intentá resolver cada uno antes de ver la solución. El código es autodescriptivo: comentarios explican cada decisión de diseño.

● Fácil — aplicación directa de la estructura ● Medio — combina estructuras o requiere insight ● Difícil — clásicos de entrevistas / competencias
Fácil Stack P-01

Palíndromo con Stack

Dada una cadena, determiná si es un palíndromo usando un std::stack. No uses la función reverse() ni índices.

💡 Apilá todos los caracteres. Al desapilar obtenés la cadena al revés. Compará carácter a carácter.
 Ver solución en C++
// =====================================================
// P-01 — Palíndromo con Stack
// =====================================================
// IDEA CLAVE: un Stack invierte el orden de los elementos
// (LIFO). Si apilamos cada carácter y luego comparamos
// el desapilado contra el original → detectamos simetría.
// Complejidad: O(N) tiempo · O(N) espacio
// =====================================================
#include <iostream>
#include <stack>
#include <string>
using namespace std;

bool esPalindromo(const string& s) {
    stack<char> pila;

    // Paso 1: apilar todos los caracteres
    for (char c : s)
        pila.push(c);

    // Paso 2: desapilar da los caracteres en orden INVERSO.
    // Comparamos con el original (izq→der).
    // Si difieren en algún punto → no es palíndromo.
    for (char c : s) {
        if (c != pila.top()) return false;
        pila.pop();
    }
    return true;
}

int main() {
    cout << esPalindromo("racecar") << "\n"; // 1 (true)
    cout << esPalindromo("hello")   << "\n"; // 0 (false)
    cout << esPalindromo("level")   << "\n"; // 1 (true)
    cout << esPalindromo("abcba")   << "\n"; // 1 (true)
}
Fácil Queue P-02

Rotar cola K posiciones

Dada una cola de enteros y un número K, rotá la cola K posiciones hacia la izquierda usando solo operaciones de Queue.

💡 Rotar 1 posición = sacar el frente y encolarlo al final. Repetir K veces. Cuidado con K > tamaño.
 Ver solución en C++
// =====================================================
// P-02 — Rotar cola K posiciones a la izquierda
// =====================================================
// IDEA CLAVE: rotar 1 vez = tomar el FRONT y encolarlo
// al REAR. Repetir K veces rota K posiciones.
// k % n evita rotaciones redundantes si K > tamaño.
// Complejidad: O(K) tiempo · O(1) espacio extra
// =====================================================
#include <iostream>
#include <queue>
using namespace std;

void rotarCola(queue<int>& q, int k) {
    if (q.empty()) return;

    // Normalizar K: rotar n veces devuelve la cola original
    k = k % (int)q.size();

    for (int i = 0; i < k; i++) {
        // El frente (más antiguo) pasa al final
        q.push(q.front());
        q.pop();
    }
}

void imprimir(queue<int> q) {
    while (!q.empty()) {
        cout << q.front() << " ";
        q.pop();
    }
    cout << "\n";
}

int main() {
    queue<int> q;
    for (int v : {1,2,3,4,5}) q.push(v);

    // Original: 1 2 3 4 5
    rotarCola(q, 2); imprimir(q); // 3 4 5 1 2
    rotarCola(q, 3); imprimir(q); // 1 2 3 4 5 (volvió al original)
    rotarCola(q, 7); imprimir(q); // equiv a k=2 → 3 4 5 1 2
}
Medio Stack P-03

Evaluar expresión postfija (RPN)

Dada una expresión en notación postfija (ej: "3 4 + 2 *" = 14), evaluala y devolvé el resultado usando un Stack de enteros.

💡 Número → push. Operador → pop dos operandos, operar, push resultado. El primero en salir es el operando derecho.
 Ver solución en C++
// =====================================================
// P-03 — Evaluar expresión en Notación Postfija (RPN)
// =====================================================
// Notación postfija: operadores van DESPUÉS de operandos.
// "3 4 +"  →  3 + 4  =  7
// "3 4 + 2 *"  →  (3+4)*2  =  14
//
// ALGORITMO:
//   Token es número  → push al stack
//   Token es operador → pop derecho, pop izquierdo,
//                       operar, push resultado
//
// ¿Por qué funciona? La pila mantiene los resultados
// parciales en el orden correcto para los siguientes ops.
// Complejidad: O(N) tiempo · O(N) espacio
// =====================================================
#include <iostream>
#include <stack>
#include <sstream>
#include <string>
using namespace std;

int evaluarRPN(const string& expr) {
    stack<int> nums;
    istringstream tokens(expr);
    string token;

    while (tokens >> token) {
        if (token == "+" || token == "-" ||
            token == "*" || token == "/") {

            // LIFO: el primero en salir es el operando DERECHO
            int der = nums.top(); nums.pop();
            int izq = nums.top(); nums.pop();

            if      (token == "+") nums.push(izq + der);
            else if (token == "-") nums.push(izq - der);
            else if (token == "*") nums.push(izq * der);
            else if (token == "/") nums.push(izq / der);
        } else {
            // Es un número: convertir string → int y apilar
            nums.push(stoi(token));
        }
    }
    // El único valor que queda es el resultado final
    return nums.top();
}

int main() {
    cout << evaluarRPN("3 4 + 2 *")         << "\n"; // 14
    cout << evaluarRPN("5 1 2 + 4 * + 3 -") << "\n"; // 14
    cout << evaluarRPN("2 3 4 * +")          << "\n"; // 14
}
Medio Heap P-04

K-ésimo mayor elemento

Dado un arreglo de enteros y un número K, encontrá el K-ésimo mayor elemento en O(N log K). No se puede ordenar el arreglo completo.

💡 Min-Heap de tamaño máximo K. Si entra un elemento, expulsá el mínimo. Al final el tope es el K-ésimo mayor.
 Ver solución en C++
// =====================================================
// P-04 — K-ésimo mayor elemento con Min-Heap
// =====================================================
// ¿Por qué Min-Heap y no Max-Heap?
//   Mantenemos un "club de los K mejores".
//   El portero del club es el más débil de ese grupo
//   (mínimo del Min-Heap). Si llega alguien mejor,
//   echamos al mínimo y dejamos entrar al nuevo.
//   Al final, el portero (tope) ES el K-ésimo mayor.
//
// Sort completo: O(N log N)  ← innecesario
// Min-Heap K:   O(N log K)  ← N inserciones, heap de tamaño K
// =====================================================
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

int kesimoMayor(const vector<int>& arr, int k) {
    // priority_queue con greater<int> → Min-Heap
    // El mínimo queda en la cima (acceso O(1))
    priority_queue<int, vector<int>, greater<int>> minHeap;

    for (int num : arr) {
        minHeap.push(num);

        // Si el heap tiene más de K elementos,
        // el nuevo mínimo definitivamente no es K-ésimo mayor
        if ((int)minHeap.size() > k)
            minHeap.pop(); // expulsar el menor del grupo
    }
    // La cima es el menor de los K mayores = K-ésimo mayor
    return minHeap.top();
}

int main() {
    vector<int> v1 = {3,2,1,5,6,4};
    cout << kesimoMayor(v1, 2) << "\n"; // 5  (2do mayor)
    cout << kesimoMayor(v1, 4) << "\n"; // 3  (4to mayor)

    vector<int> v2 = {3,2,3,1,2,4,5,5,6};
    cout << kesimoMayor(v2, 4) << "\n"; // 4
}
Medio Stack P-05

Stack con getMin() en O(1)

Implementá un Stack que soporte push(), pop() y getMin() todas en tiempo O(1). Sin recorrer el stack para encontrar el mínimo.

💡 Dos stacks en paralelo: uno con datos, otro con el mínimo actual en cada nivel. Al hacer pop, ambos retroceden juntos.
 Ver solución en C++
// =====================================================
// P-05 — Stack con getMin() en O(1)
// =====================================================
// TRUCO: stack auxiliar de mínimos sincronizado.
// En cada push, el stack de mínimos registra cuál es el
// mínimo global en ese momento. Al hacer pop, ambos
// stacks retroceden juntos → el historial se preserva.
//
// Push [5,3,7,2,4]:
//   datos: 5 3 7 2 4
//   mins:  5 3 3 2 2  ← el mínimo en cada "nivel"
//
// Complejidad: O(1) todas las operaciones · O(N) espacio
// =====================================================
#include <iostream>
#include <stack>
#include <stdexcept>
using namespace std;

class MinStack {
    stack<int> datos; // stack principal
    stack<int> mins;  // mínimo en cada nivel del stack principal

public:
    void push(int val) {
        datos.push(val);
        // El mínimo nuevo es: val si es el 1ro, o min(val, mínimo actual)
        int nuevoMin = mins.empty() ? val : min(val, mins.top());
        mins.push(nuevoMin);
    }

    void pop() {
        if (datos.empty()) throw runtime_error("Stack vacío");
        datos.pop();
        mins.pop(); // el historial de mínimos retrocede en sincronía
    }

    int top()    { return datos.top(); }
    int getMin() { return mins.top(); } // O(1): la respuesta ya está guardada
    bool empty() { return datos.empty(); }
};

int main() {
    MinStack ms;
    for (int v : {5,3,7,2,4}) ms.push(v);

    cout << ms.getMin() << "\n"; // 2 (mínimo actual)
    ms.pop(); // saca 4
    ms.pop(); // saca 2
    cout << ms.getMin() << "\n"; // 3 (nuevo mínimo)
    ms.pop(); // saca 7
    cout << ms.getMin() << "\n"; // 3
    ms.pop(); // saca 3
    cout << ms.getMin() << "\n"; // 5
}
Difícil Stack P-06

Rectángulo mayor en histograma

Dado un histograma como arreglo de alturas, encontrá el área del rectángulo más grande que puede trazarse dentro del histograma. (LeetCode #84)

💡 Stack monotónico creciente de índices. Cuando una barra rompe la monotonía, calculá el área de todo lo que no puede extenderse más.
 Ver solución en C++
// =====================================================
// P-06 — Rectángulo mayor en histograma
// =====================================================
// IDEA: Stack Monotónico Creciente de índices.
//
// Invariante: el stack guarda índices cuyas alturas
// están en orden CRECIENTE de abajo a arriba.
//
// Cuando la barra actual es MENOR que el tope del stack,
// la barra del tope ya NO puede extenderse hacia la derecha.
// → Calculamos su área máxima y la removemos.
//
// Área de barra i: altura[i] × ancho
// Ancho = desde el nuevo tope del stack hasta i (exclusivo)
//
// Truco del centinela: agregar altura=0 al final para
// forzar el vaciado del stack y calcular las áreas finales.
//
// Complejidad: O(N) — cada índice entra y sale del stack una vez
// =====================================================
#include <iostream>
#include <vector>
#include <stack>
using namespace std;

int maxAreaHistograma(vector<int> alturas) {
    alturas.push_back(0); // centinela: fuerza el vaciado final del stack
    stack<int> st;       // guarda índices (no alturas directamente)
    int maxArea = 0;

    for (int i = 0; i < (int)alturas.size(); i++) {
        // Mientras la barra actual rompe la monotonía creciente
        while (!st.empty() && alturas[st.top()] > alturas[i]) {
            int altura = alturas[st.top()]; st.pop();

            // Ancho: si el stack quedó vacío, el rectángulo llega hasta 0
            // Si no, llega desde el nuevo tope hasta i
            int ancho = st.empty() ? i : (i - st.top() - 1);

            maxArea = max(maxArea, altura * ancho);
        }
        st.push(i); // mantener el stack monotónico
    }
    return maxArea;
}

int main() {
    // [2,1,5,6,2,3]: el rect más grande es 5×2=10 (barras 5 y 6)
    cout << maxAreaHistograma({2,1,5,6,2,3}) << "\n"; // 10
    cout << maxAreaHistograma({2,4})                 << "\n"; // 4
    cout << maxAreaHistograma({6,2,5,4,5,1,6})     << "\n"; // 12
}
Difícil Heap P-07

Mediana en tiempo real

Implementá una clase que reciba un stream de números enteros y responda la mediana actual en O(log N) por inserción. (LeetCode #295)

💡 Dos heaps: Max-Heap para la mitad inferior, Min-Heap para la superior. Mantené sus tamaños balanceados. La mediana vive en los topes.
 Ver solución en C++
// =====================================================
// P-07 — Mediana en tiempo real con dos Heaps
// =====================================================
// IDEA: partir el stream en dos mitades con dos heaps:
//
//   maxHeap → mitad INFERIOR (el mayor es el tope)
//   minHeap → mitad SUPERIOR (el menor es el tope)
//
//   Visualización para [1, 3, 5, 7, 9]:
//     maxHeap:  1  3          ← tope = 3
//     minHeap:     5  7  9   ← tope = 5
//     Mediana = 5 (centro exacto)
//
// INVARIANTE: |maxHeap| == |minHeap|  o  |maxHeap| == |minHeap| + 1
//   → Si tamaños iguales: mediana = (tope_max + tope_min) / 2.0
//   → Si maxHeap tiene 1 extra: mediana = tope_max
//
// Inserción: O(log N)  |  getMedian: O(1)
// =====================================================
#include <iostream>
#include <queue>
using namespace std;

class MedianaStream {
    // Max-Heap: guarda la mitad inferior del stream
    priority_queue<int> maxHeap;

    // Min-Heap: guarda la mitad superior del stream
    priority_queue<int, vector<int>, greater<int>> minHeap;

public:
    void insertar(int num) {
        // Paso 1: asignar num al heap correcto
        // → Si num es ≤ al mayor de la mitad baja, va a maxHeap
        //   (o si maxHeap está vacío)
        if (maxHeap.empty() || num <= maxHeap.top())
            maxHeap.push(num);
        else
            minHeap.push(num);

        // Paso 2: rebalancear para mantener el invariante de tamaños
        // maxHeap puede tener a lo sumo 1 elemento más que minHeap
        if (maxHeap.size() > minHeap.size() + 1) {
            // maxHeap tiene demasiados: mover el mayor a minHeap
            minHeap.push(maxHeap.top()); maxHeap.pop();
        } else if (minHeap.size() > maxHeap.size()) {
            // minHeap tiene demasiados: mover el menor a maxHeap
            maxHeap.push(minHeap.top()); minHeap.pop();
        }
    }

    double mediana() const {
        if (maxHeap.size() == minHeap.size())
            return (maxHeap.top() + minHeap.top()) / 2.0; // promedio central
        return maxHeap.top(); // maxHeap tiene el elemento del medio
    }
};

int main() {
    MedianaStream ms;
    for (int num : {5, 15, 1, 3, 8, 7, 9, 2}) {
        ms.insertar(num);
        cout << "Insertar " << num
             << " → mediana: " << ms.mediana() << "\n";
    }
    // 5→5 | 15→10 | 1→5 | 3→4 | 8→5 | 7→6 | 9→7 | 2→6
}