Stack·Cola·Heap
Tres estructuras que mueven el mundo del software. Desde Ctrl+Z hasta Dijkstra.
Stack
El último en entrar es el primero en salir. La base de la recursión.
Cola
El primero en llegar, el primero en salir. Justicia perfecta.
Heap
Árbol binario completo en arreglo. El máximo siempre accesible.
Stack
🍽 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.
| Operación | Complejidad | Descripció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--]
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.
Cola
🏦 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.
| Operación | Complejidad | Descripció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
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.
Heap
i: 2*i + 1Hijo der. de
i: 2*i + 2Padre de
i: (i - 1) / 2
| Operación | Complejidad | Descripció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!
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.
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) |
- ✓ El orden debe invertirse (LIFO)
- ✓ Necesitas revertir pasos (undo)
- ✓ Conviertes recursión a iterativa
- ✓ Parseas expresiones o paréntesis
- ✓ Implementas DFS en grafos
- ✓ 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
- ✓ 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
Ejercicios
Validador de paréntesis
Escribe una función que determine si los paréntesis, corchetes y llaves de un string están correctamente balanceados."({[]})" → ✓ | "({[}])" → ✗
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
num+"0" y num+"1". Refuerza el patrón BFS.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.
Stack con getMin() en O(1)
Implementa un Stack que soporte getMin() en tiempo constante además de las operaciones básicas.
Queue implementada con 2 Stacks
Clásico de entrevistas: implementa una Queue usando exclusivamente dos Stacks. Sin arreglos auxiliares.
getMin() y getMax() ambas en O(1)
Implementa una MinMaxStack que soporte getMin() y getMax() simultáneamente en O(1).
Cierre
¿Recuerdan las preguntas del inicio de la clase?
Ahora tienen las herramientas para responderlas todas.
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.
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.
▶ 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) }
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.
▶ 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 }
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.
▶ 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 }
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.
▶ 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 }
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.
▶ 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 }
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)
▶ 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 }
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)
▶ 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 }