Menu

Stack en C#: Push, Pop, Peek, deshacer y paréntesis equilibrados

Stack<T> es una colección en la que el último que entra es el primero que sale: el elemento añadido más recientemente sale primero. Aprende Push, Pop y Peek, la excepción de la pila vacía y TryPop, por qué una pila se enumera al revés, y dos usos clásicos: un historial para deshacer y comprobar paréntesis equilibrados.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Un Stack<T> es un montón: pones elementos encima con Push y los quitas de arriba con Pop, así que el último que entra es el primero que sale (LIFO). Solo se puede llegar al de arriba, y cada operación sobre él tarda un tiempo constante.

Push, Pop y Peek

Salida:

On top: green
Count: 3
Took green
On top: red
Took red
Took blue
Count: 0

green se apiló el último, así que sale primero. Peek devuelve el de arriba sin cambiar la pila, y así puedes ver lo que te daría Pop antes de decidir sacarlo.

La excepción de la pila vacía y TryPop

Hacer pop o peek sobre una pila vacía lanza InvalidOperationException. Suele aparecer en parsers y algoritmos que reciben una entrada con más elementos de cierre que de apertura.

Salida:

Caught InvalidOperationException
True 10
False 0

TryPop y TryPeek (.NET Core 2.0 y posteriores) devuelven false con una pila vacía y ponen la variable out en el valor por defecto, aquí 0. En .NET Framework, comprueba antes Count > 0.

Orden de iteración: primero el de arriba

Enumerar una pila no quita nada, y va de arriba abajo, en el orden en que Pop devolvería los elementos:

Salida:

checkout products home 
checkout > products > home
True
home
checkout

La copia invertida pilla a la gente por sorpresa: el constructor recibe cualquier IEnumerable<T> y apila sus elementos en orden, y una pila se enumera empezando por arriba, así que el antiguo elemento de arriba acaba abajo del todo en la copia. Invertir antes la secuencia (el Reverse() de LINQ devuelve los elementos empezando por abajo) da una copia con el mismo elemento arriba.

Apilar una lista de elementos en una pila nueva también los invierte, lo que es una forma rápida de invertir una secuencia: new Stack<char>("hello") devuelve con pop o, l, l, e, h.

Ejemplo: un historial para deshacer

Los editores guardan cada cambio en una pila. Deshacer saca el cambio más reciente y lo revierte; rehacer mantiene una segunda pila de cambios deshechos.

Salida:

Hello, world!
Hello, world
Hello
Hello, world

Guardar instantáneas completas es la versión más sencilla. Los editores reales apilan en su lugar pequeños objetos de comando (qué se insertó y dónde), cada uno con un método para revertirse, pero las dos pilas funcionan igual.

Ejemplo: paréntesis equilibrados

Comprobar que (, [ y { se cierran en el orden correcto es el ejercicio clásico de pilas, y la misma lógica está dentro de todos los compiladores y parsers de JSON.

Salida:

"f(a[i], {x: 1})" -> True
"(]" -> False
"((a)" -> False
"a)b(" -> False
"" -> True

Las tres comprobaciones de fallo corresponden a las tres formas en que los paréntesis salen mal: un cierre sin nada abierto (a)b(, detectado con Count == 0 en lugar de con una excepción de Pop), un cierre del tipo equivocado ((]) y aperturas que nunca se cierran (((a), detectadas con la comprobación final).

Otros usos

  • Búsqueda en profundidad. Sustituye la cola de una búsqueda en anchura por una pila y el recorrido va en profundidad antes que en anchura. Una pila explícita también sustituye a la recursividad cuando la entrada es lo bastante profunda como para arriesgarse a una StackOverflowException, que no se puede capturar.
  • Evaluar expresiones. La notación postfija (3 4 + 2 *) se evalúa apilando números y sacando dos por cada operador.
  • Vuelta atrás. El historial de navegación, la resolución de laberintos y los estados de un parser apilan una posición y vuelven a ella con pop al llegar a un callejón sin salida.

Consulta Queue para la colección equivalente en la que el primero que entra es el primero que sale.

Stack frente a Queue frente a List

Stack<T>Queue<T>List<T>
Orden de salidaEl más nuevo primeroEl más antiguo primeroCualquiera, por índice
AñadirPushEnqueueAdd, Insert
QuitarPop (arriba)Dequeue (principio)Remove, RemoveAt
MirarPeekPeeklist[i]
Variantes segurasTryPop, TryPeekTryDequeue, TryPeekno hacen falta

Para varios hilos, ConcurrentStack<T> de System.Collections.Concurrent ofrece Push, TryPop y TryPeek sin locks.

Errores comunes

  • Hacer pop sin comprobar. Una pila vacía lanza InvalidOperationException; comprueba Count o usa TryPop.
  • Esperar que foreach empiece por el primer elemento apilado. Empieza por el de arriba.
  • Copiar con new Stack<T>(stack). La copia sale invertida.
  • Apilar dentro de un foreach sobre la misma pila. Lanza una excepción; usa un bucle while (stack.Count > 0).

Preguntas frecuentes

¿Qué es un Stack en C#?

Stack<T> de System.Collections.Generic es una colección LIFO (last in, first out, el último que entra es el primero que sale). Push pone un elemento encima, Pop quita y devuelve el elemento de arriba, y Peek devuelve el elemento de arriba sin quitarlo. Las tres se ejecutan en tiempo constante.

¿Qué ocurre al hacer Pop sobre una pila vacía en C#?

Pop y Peek lanzan InvalidOperationException cuando la pila está vacía. Comprueba antes stack.Count > 0, o usa TryPop(out var item) y TryPeek(out var item), que devuelven false en lugar de lanzar una excepción (.NET Core 2.0 y posteriores).

¿En qué orden recorre foreach un Stack?

De arriba abajo: el elemento añadido más recientemente va primero, el mismo orden en que los devolvería Pop. ToArray() usa el mismo orden. Una consecuencia es que new Stack<T>(otherStack) produce una copia invertida, porque el constructor apila los elementos en el orden en que los enumera.

¿Qué diferencia hay entre un Stack y una Queue en C#?

Un Stack<T> devuelve primero el elemento más nuevo (el último que entra es el primero que sale), mientras que una Queue<T> devuelve primero el más antiguo (el primero que entra es el primero que sale). Usa una pila para el historial de deshacer, las estructuras anidadas y la búsqueda en profundidad; usa una cola para procesar trabajo en orden de llegada y para la búsqueda en anchura.

¿Cómo compruebo si los paréntesis están equilibrados en C#?

Recorre el string una vez. Apila cada paréntesis de apertura en un Stack<char>. Para cada paréntesis de cierre, la pila no debe estar vacía y su elemento de arriba debe ser el de apertura correspondiente, que entonces sacas. El string está equilibrado cuando el recorrido termina con la pila vacía.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR