Menu

Stack in C#: pila con Push, Pop, Peek, undo e parentesi bilanciate

Stack<T> è una collezione last in, first out: l'elemento aggiunto per ultimo esce per primo. Impara Push, Pop e Peek, l'eccezione della pila vuota e TryPop, perché una pila si enumera al contrario, e due usi classici: una cronologia di annullamento e il controllo delle parentesi bilanciate.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Uno Stack<T> è una pila: metti gli elementi in cima con Push e li togli dalla cima con Pop, quindi l'ultimo elemento entrato è il primo a uscire (LIFO). Si può raggiungere solo la cima, e ogni operazione su di essa richiede tempo costante.

Push, Pop e Peek

Output:

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

green è stato inserito per ultimo, quindi esce per primo. Peek restituisce la cima senza modificare la pila, ed è così che controlli cosa ti darebbe Pop prima di decidere di prenderlo.

L'eccezione della pila vuota e TryPop

Estrarre o guardare la cima di una pila vuota lancia InvalidOperationException. Succede soprattutto nei parser e negli algoritmi che ricevono un input con più elementi di chiusura che di apertura.

Output:

Caught InvalidOperationException
True 10
False 0

TryPop e TryPeek (.NET Core 2.0 e successivi) restituiscono false su una pila vuota e impostano la variabile out al valore predefinito, qui 0. Su .NET Framework, controlla prima Count > 0.

Ordine di iterazione: prima la cima

Enumerare una pila non rimuove nulla, e procede dalla cima verso il basso, nell'ordine in cui Pop restituirebbe gli elementi:

Output:

checkout products home 
checkout > products > home
True
home
checkout

La copia invertita coglie molti di sorpresa: il costruttore accetta qualsiasi IEnumerable<T> e ne inserisce gli elementi in ordine, e una pila si enumera partendo dalla cima, quindi la vecchia cima finisce in fondo alla copia. Invertire prima la sequenza (il Reverse() di LINQ restituisce gli elementi partendo dal fondo) dà una copia con la stessa cima.

Anche inserire una lista di elementi in una nuova pila li inverte, il che è un modo rapido per invertire una sequenza: new Stack<char>("hello") restituisce con Pop o, l, l, e, h.

Esempio: una cronologia di annullamento

Gli editor tengono ogni modifica in una pila. Annulla estrae la modifica più recente e la ripristina; ripeti usa una seconda pila con le modifiche annullate.

Output:

Hello, world!
Hello, world
Hello
Hello, world

Memorizzare istantanee complete è la versione più semplice. Gli editor veri inseriscono invece piccoli oggetti comando (cosa è stato inserito, e dove), ognuno con un metodo per annullare se stesso, ma le due pile funzionano allo stesso modo.

Esempio: parentesi bilanciate

Verificare che (, [ e { vengano chiuse nell'ordine giusto è l'esercizio standard sulle pile, e la stessa logica si trova dentro ogni compilatore e parser JSON.

Output:

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

I tre controlli di fallimento corrispondono ai tre modi in cui le parentesi possono essere sbagliate: una chiusura senza nulla di aperto (a)b(, intercettata da Count == 0 invece che da un'eccezione di Pop), una chiusura del tipo sbagliato ((]) e aperture mai chiuse (((a), intercettate dal controllo finale).

Altri usi

  • Ricerca in profondità. Sostituisci la coda di una ricerca in ampiezza con una pila e la visita va in profondità prima che in larghezza. Una pila esplicita sostituisce anche la ricorsione quando l'input è abbastanza profondo da rischiare una StackOverflowException, che non si può catturare.
  • Valutare espressioni. La notazione postfissa (3 4 + 2 *) si valuta inserendo i numeri ed estraendone due per ogni operatore.
  • Backtracking. La cronologia di navigazione, la risoluzione di labirinti e gli stati di un parser inseriscono una posizione e tornano indietro a essa quando arrivano a un vicolo cieco.

Vedi Queue per la controparte first in, first out.

Stack, Queue e List a confronto

Stack<T>Queue<T>List<T>
Ordine di uscitaPrima il più recentePrima il più vecchioQualsiasi, per indice
AggiungerePushEnqueueAdd, Insert
RimuoverePop (cima)Dequeue (testa)Remove, RemoveAt
GuardarePeekPeeklist[i]
Varianti sicureTryPop, TryPeekTryDequeue, TryPeeknon necessarie

Per più thread, ConcurrentStack<T> di System.Collections.Concurrent offre Push, TryPop e TryPeek senza lock.

Errori comuni

  • Estrarre senza controllare. Una pila vuota lancia InvalidOperationException; controlla Count o usa TryPop.
  • Aspettarsi che foreach parta dal primo elemento inserito. Parte dalla cima.
  • Copiare con new Stack<T>(stack). La copia è invertita.
  • Inserire dentro un foreach sulla stessa pila. Lancia un'eccezione; usa un ciclo while (stack.Count > 0).

Domande frequenti

Cos'è uno Stack in C#?

Stack<T> in System.Collections.Generic è una collezione last in, first out (LIFO). Push mette un elemento in cima, Pop rimuove e restituisce l'elemento in cima, e Peek restituisce l'elemento in cima senza rimuoverlo. Tutte e tre vengono eseguite in tempo costante.

Cosa succede se fai Pop su uno stack vuoto in C#?

Pop e Peek lanciano InvalidOperationException quando lo stack è vuoto. Controlla prima stack.Count > 0, oppure usa TryPop(out var item) e TryPeek(out var item), che restituiscono false invece di lanciare un'eccezione (.NET Core 2.0 e successivi).

In che ordine foreach percorre uno Stack?

Dalla cima verso il basso: per primo viene l'elemento inserito più di recente, nello stesso ordine in cui li restituirebbe Pop. ToArray() usa lo stesso ordine. Una conseguenza è che new Stack<T>(otherStack) produce una copia invertita, perché il costruttore inserisce gli elementi nell'ordine in cui li enumera.

Che differenza c'è tra uno Stack e una Queue in C#?

Uno Stack<T> restituisce per primo l'elemento più recente (last in, first out), mentre una Queue<T> restituisce per primo quello più vecchio (first in, first out). Usa uno stack per la cronologia di annullamento, le strutture annidate e la ricerca in profondità; usa una coda per elaborare il lavoro in ordine di arrivo e per la ricerca in ampiezza.

Come controllo se le parentesi sono bilanciate in C#?

Scorri la stringa una volta. Inserisci ogni parentesi di apertura in uno Stack<char>. Per ogni parentesi di chiusura, lo stack non deve essere vuoto e la sua cima deve essere la parentesi di apertura corrispondente, che a quel punto estrai. La stringa è bilanciata quando la scansione termina con lo stack vuoto.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA