C# Stack<T> (Pilha)
O Ctrl+Z desfaz a última coisa que você fez. Apertando de novo, desfaz a penúltima, e assim por diante: sempre da mais nova para a mais antiga. O botão Voltar do navegador funciona igual. Por trás dos dois existe uma pilha, e no C# ela se chama Stack<T>. É a irmã da fila, com a regra ao contrário.
- O que é uma pilha e a regra "último a entrar, primeiro a sair".
- Empilhar com
Push, desempilhar comPop, espiar o topo comPeek. - Em que ordem o
foreachmostra uma pilha (e por quenew Stack<T>(lista)parece "inverter"). - O que acontece numa pilha vazia:
InvalidOperationException: Stack empty. - Os jeitos seguros:
TryPopeTryPeek. - Três usos clássicos: desfazer, inverter uma palavra e conferir parênteses.
Todos os programas desta aula rodaram com o .NET 10 na máquina da Ana (usuária ana, computador notebook), na pasta ~/cs, com dotnet run arquivo.cs. Você pode repetir cada um no editor do guia.
A ideia: uma pilha de pratos
Na pia da Ana, os pratos sujos vão sendo colocados um em cima do outro. Para lavar, ela pega o de cima: o último que chegou. Pegar o prato de baixo derrubaria todos.
entra e sai por aqui
↓↑
[ prato branco ] ← topo (o último que entrou)
[ prato verde ]
[ prato azul ] ← base (o primeiro que entrou)
Essa regra é a LIFO, de Last In, First Out: "o último a entrar é o primeiro a sair". Compare com a fila (FIFO), em que sai o primeiro que chegou. Numa pilha você só mexe no topo:
| Método | Tradução | Na pia |
|---|---|---|
Push(x) | "empurrar", empilhar | Põe um prato no topo. |
Pop() | "estourar", desempilhar | Tira o prato do topo. O método devolve o prato tirado. |
Peek() | "espiar" | Olha qual prato está no topo, sem tirar. |
Count | "contagem" | Quantos pratos há na pilha. |
1. Empilhar, espiar e desempilhar
var pilha = new Stack<string>();
pilha.Push("prato azul");
pilha.Push("prato verde");
pilha.Push("prato branco");
Console.WriteLine($"Pratos: {pilha.Count}");
Console.WriteLine($"No topo: {pilha.Peek()}");
string lavado = pilha.Pop();
Console.WriteLine($"Lavei o {lavado}");
Console.WriteLine($"No topo: {pilha.Peek()}");
Console.WriteLine($"Pratos: {pilha.Count}");
- new Stack<string>()
- Cria uma pilha vazia de textos. O
Té o tipo do que vai na pilha (veja Generics). - pilha.Push("prato azul");
- Põe o prato azul no topo. Como a pilha estava vazia, ele fica também na base.
- pilha.Push("prato branco");
- O último
Push: o prato branco é o novo topo. - pilha.Peek()
- Devolve o topo, sem tirar.
- string lavado = pilha.Pop();
- Tira o topo e devolve. O prato verde passa a ser o topo.
$ dotnet run pratos.cs Pratos: 3 No topo: prato branco « o último que entrou Lavei o prato branco « o Pop tirou o topo No topo: prato verde Pratos: 2
2. A ordem de uma pilha
O foreach e o string.Join mostram a pilha do topo para a base: na mesma ordem em que os Pop tirariam os itens.
var pilha = new Stack<int>();
pilha.Push(1);
pilha.Push(2);
pilha.Push(3);
foreach (int n in pilha)
{
Console.WriteLine(n);
}
Console.WriteLine(string.Join(", ", pilha));
Console.WriteLine(string.Join(", ", pilha.ToArray()));
var outra = new Stack<int>([1, 2, 3]);
Console.WriteLine($"Topo da outra: {outra.Peek()}");
Console.WriteLine($"Itens ainda: {pilha.Count}");
$ dotnet run ordem.cs 3 « o topo primeiro 2 1 3, 2, 1 3, 2, 1 « o ToArray também começa pelo topo Topo da outra: 3 « o 3 foi o último a ser empilhado Itens ainda: 3 « o foreach só olhou: não tirou nada
- foreach (int n in pilha)
- Percorre do topo (3) até a base (1), sem tirar nada.
- pilha.ToArray()
- Copia a pilha num array, também do topo para a base.
- new Stack<int>([1, 2, 3])
- Cria a pilha empilhando os itens na ordem da lista: primeiro o 1, depois o 2, depois o 3. Por isso o 3 fica no topo, e a pilha "parece" invertida quando você a mostra.
Como a fila, a pilha não tem Add: Stack<int> p = [1, 2, 3]; dá error CS1061 (o erro está em Quando dá errado). Use o construtor, como acima, ou Push.
Missão: o histórico do navegador da Ana é uma pilha. Ela clicou duas vezes em Voltar. Antes do Console.WriteLine, tire duas páginas do topo com Pop, para que a página atual seja variaveis.html.
var historico = new Stack<string>();
historico.Push("inicio.html");
historico.Push("variaveis.html");
historico.Push("arrays.html");
historico.Push("list.html");
// a Ana clicou duas vezes em Voltar
Console.WriteLine($"Página atual: {historico.Peek()}");
var historico = new Stack<string>();
historico.Push("inicio.html");
historico.Push("variaveis.html");
historico.Push("arrays.html");
historico.Push("list.html");
historico.Pop();
historico.Pop();
Console.WriteLine($"Página atual: {historico.Peek()}");
3. A pilha vazia: Stack empty.
var pilha = new Stack<string>();
pilha.Push("prato azul");
Console.WriteLine(pilha.Pop());
Console.WriteLine(pilha.Pop());
$ dotnet run vazia.cs prato azul Unhandled exception. System.InvalidOperationException: Stack empty. at System.Collections.Generic.Stack`1.ThrowForEmptyStack() at System.Collections.Generic.Stack`1.Pop() at Program.<Main>$(String[] args) in /home/ana/cs/vazia.cs:line 5 $ echo $? 134
"Pilha vazia." O segundo Pop (linha 5) não tinha prato para tirar. O Peek numa pilha vazia dá a mesma mensagem, com Stack`1.Peek() no rastro. É o mesmo tipo de exceção da fila vazia, a InvalidOperationException: a operação não faz sentido no estado em que a pilha está.
Os métodos com Try não lançam exceção. Eles devolvem true ou false e entregam o item num out:
string texto = "";
var historico = new Stack<string>();
void Escrever(string novo)
{
historico.Push(texto);
texto += novo;
Console.WriteLine($"Texto: \"{texto}\"");
}
void Desfazer()
{
if (historico.TryPop(out string? anterior))
{
texto = anterior;
Console.WriteLine($"Desfeito: \"{texto}\"");
}
else
{
Console.WriteLine("Nada para desfazer.");
}
}
Escrever("Olá");
Escrever(", Ana");
Escrever("!!!");
Desfazer();
Desfazer();
Desfazer();
Desfazer();
$ dotnet run desfazer.cs Texto: "Olá" Texto: "Olá, Ana" Texto: "Olá, Ana!!!" Desfeito: "Olá, Ana" « desfez a última escrita Desfeito: "Olá" Desfeito: "" Nada para desfazer. « pilha vazia: o TryPop devolveu false, sem exceção
- void Escrever(string novo)
- Uma função local. Antes de mudar o texto, ela empilha como ele era: é isso que permite voltar depois.
- historico.Push(texto);
- Guarda a versão antiga no topo da pilha.
- if (historico.TryPop(out string? anterior))
- "Se conseguir tirar uma versão antiga, chame-a de
anterior." Numa pilha vazia, devolvefalse, e oelseavisa. - texto = anterior;
- Volta para a versão guardada. Como a pilha devolve a mais nova primeiro, cada
Desfazervolta um passo.
Existe também o TryPeek(out var x), que espia o topo sem tirar e devolve false numa pilha vazia.
4. Inverter uma palavra
Como a pilha devolve os itens ao contrário da ordem em que entraram, ela inverte coisas naturalmente. Empilhe as letras e desempilhe:
string palavra = "roma";
var pilha = new Stack<char>();
foreach (char letra in palavra)
pilha.Push(letra);
string invertida = "";
while (pilha.Count > 0)
invertida += pilha.Pop();
Console.WriteLine(invertida);
$ dotnet run inverter.cs amor
- foreach (char letra in palavra)
- Um
stringpode ser percorrido letra por letra (cada letra é um char). Entram r, o, m, a; o "a" fica no topo. - while (pilha.Count > 0)
- Enquanto houver letras, tire a do topo: a, m, o, r.
- invertida += pilha.Pop();
- Gruda cada letra no fim do texto novo.
5. Conferir parênteses
Um uso clássico, que os compiladores fazem o tempo todo: ver se cada (, [ e { foi fechado na ordem certa. A ideia: ao ver um símbolo que abre, empilhe; ao ver um que fecha, o topo da pilha tem de ser o par dele.
Console.WriteLine(Balanceado("(1 + 2) * [3 - {4 / 2}]"));
Console.WriteLine(Balanceado("(1 + 2]"));
Console.WriteLine(Balanceado("((1 + 2)"));
Console.WriteLine(Balanceado("1 + 2)"));
bool Balanceado(string expressao)
{
var abertos = new Stack<char>();
foreach (char c in expressao)
{
if (c == '(' || c == '[' || c == '{')
{
abertos.Push(c);
}
else if (c == ')' || c == ']' || c == '}')
{
if (!abertos.TryPop(out char aberto))
return false;
if ((c == ')' && aberto != '(') || (c == ']' && aberto != '[') || (c == '}' && aberto != '{'))
return false;
}
}
return abertos.Count == 0;
}
$ dotnet run parenteses.cs True « tudo fechado na ordem False « abriu com ( e fechou com ] False « sobrou um ( aberto False « fechou um ) que ninguém abriu
| Situação | O que a pilha mostra |
|---|---|
Fecha com o símbolo errado: (1 + 2] | No ], o topo é (, que não é o par dele. |
Abre e não fecha: ((1 + 2) | No fim, a pilha ainda tem um (: Count não é 0. |
Fecha sem ter aberto: 1 + 2) | No ), a pilha está vazia: o TryPop devolve false. |
Pilha ou fila?
Stack<T> (pilha) | Queue<T> (fila) | |
|---|---|---|
| Regra | Último a entrar, primeiro a sair (LIFO) | Primeiro a entrar, primeiro a sair (FIFO) |
| Pôr | Push | Enqueue |
| Tirar | Pop | Dequeue |
| Espiar | Peek (o topo) | Peek (o começo) |
| Usos | Desfazer, Voltar, parênteses, inverter | Atendimento, impressora, tarefas na ordem de chegada |
Quando dá errado
InvalidOperationException: Stack empty.
O que significa: um Pop() ou Peek() numa pilha sem itens.
Como consertar: teste pilha.Count > 0 antes, ou use TryPop/TryPeek (seção 3).
InvalidOperationException: Collection was modified after the enumerator was instantiated.
O que significa: você chamou Push ou Pop dentro de um foreach que percorre a mesma pilha. A pilha usa uma frase um pouco diferente da das outras coleções ("a coleção foi modificada depois que o enumerador foi criado"), mas é o mesmo problema:
$ dotnet run foreach.cs Unhandled exception. System.InvalidOperationException: Collection was modified after the enumerator was instantiated. at System.Collections.Generic.Stack`1.Enumerator.ThrowInvalidVersion() at System.Collections.Generic.Stack`1.Enumerator.MoveNext() at Program.<Main>$(String[] args) in /home/ana/cs/foreach.cs:line 2
Como consertar: para esvaziar a pilha, use while (pilha.TryPop(out var x)) em vez de foreach.
error CS1061: ‘Stack<int>’ não contém uma definição para "Add"…
O que significa: você escreveu Stack<int> p = [1, 2, 3]; ou new Stack<int> { 1, 2, 3 }. As duas formas chamam Add, que a pilha não tem.
$ dotnet run colexpr.cs /home/ana/cs/colexpr.cs(1,16): error CS1061: ‘Stack<int>’ não contém uma definição para "Add" e não foi possível encontrar nenhum método de extensão "Add" que aceite um primeiro argumento do tipo ‘Stack<int>’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Como consertar: new Stack<int>([1, 2, 3]) (lembrando que o 3 fica no topo) ou um Push para cada item.
O que significa: não é erro: é a regra da pilha. O foreach, o ToArray e os Pop começam pelo topo, o último que entrou.
Como consertar: se você precisa da ordem de chegada, a coleção certa é a fila ou a lista.
Referência rápida
| Escrita | O que faz |
|---|---|
new Stack<T>() | Pilha vazia |
new Stack<T>(colecao) | Empilha os itens na ordem da coleção (o último fica no topo) |
p.Push(x) | Põe x no topo |
p.Pop() | Tira e devolve o topo; InvalidOperationException se vazia |
p.Peek() | Devolve o topo sem tirar; InvalidOperationException se vazia |
p.TryPop(out var x) / p.TryPeek(out var x) | Sem exceção; devolvem bool |
p.Count / p.Clear() | Quantos itens / esvazia |
p.Contains(x) | x está na pilha? (olha um por um) |
p.ToArray(), foreach | Do topo para a base |
Exercícios
1. Empilhar
Complete para pôr o prato amarelo no topo da pilha:
2. Tirar sem exceção
Complete para tirar o topo só se a pilha não estiver vazia:
3. Você faz Push(1), Push(2), Push(3) e depois um Pop(). O que o Pop devolve?
4. O que string.Join(", ", new Stack<int>([1, 2, 3])) mostra?
5. Qual a diferença entre Peek() e Pop()?
Missão final: os parênteses da calculadora
Missão: a calculadora da Ana precisa recusar contas com parênteses errados. Complete a função Balanceado, que hoje sempre diz True: ao ver (, empilhe; ao ver ), desempilhe com TryPop e, se a pilha estava vazia, devolva false. No fim, a expressão só está certa se não sobrou nenhum parêntese aberto.
True False False
Console.WriteLine(Balanceado("(1 + (2 * 3))"));
Console.WriteLine(Balanceado("(1 + 2"));
Console.WriteLine(Balanceado("1 + 2)"));
bool Balanceado(string expressao)
{
var abertos = new Stack<char>();
foreach (char c in expressao)
{
if (c == '(')
{
// empilhe o parêntese aberto
}
else if (c == ')')
{
// tente desempilhar; se a pilha estava vazia, devolva false
}
}
return true; // troque: só está certo se não sobrou nenhum aberto
}
Console.WriteLine(Balanceado("(1 + (2 * 3))"));
Console.WriteLine(Balanceado("(1 + 2"));
Console.WriteLine(Balanceado("1 + 2)"));
bool Balanceado(string expressao)
{
var abertos = new Stack<char>();
foreach (char c in expressao)
{
if (c == '(')
{
abertos.Push(c);
}
else if (c == ')')
{
if (!abertos.TryPop(out _))
return false;
}
}
return abertos.Count == 0;
}
O out _ da solução é o descarte: "tire o topo, mas não preciso do valor". Aqui só interessa saber se havia um ( para tirar.
Resumo
- Uma
Stack<T>é uma pilha: o último a entrar é o primeiro a sair (LIFO). Você só mexe no topo. Pushempilha;Poptira e devolve o topo;Peekmostra o topo sem tirar.foreach,ToArrayestring.Joinvão do topo para a base;new Stack<T>(lista)deixa o último da lista no topo.PopePeeknuma pilha vazia lançamInvalidOperationException: Stack empty.;TryPopeTryPeekdevolvemfalse.- Mudar a pilha dentro do
foreachdáCollection was modified after the enumerator was instantiated. - Usos clássicos: desfazer, botão Voltar, inverter uma sequência e conferir parênteses.