Pilhas (Stacks)

⏱ 17 min de leituraFácilPython

A pilha é a estrutura que você usa desde o primeiro dia de programação sem nunca ter escrito uma. Toda função que você chama entra numa pilha. Todo Ctrl + Z desfaz o topo de uma pilha. Todo stack trace que você lê é uma pilha impressa de cima para baixo. Aprender a estrutura é fácil, ela tem três operações e todas são O(1). O que este tópico entrega é o outro lado: reconhecer o problema que pede uma pilha antes de alguém dizer que é uma pilha.

Uma lista com uma porta só

Uma pilha é uma coleção com uma regra única: o último que entra é o primeiro que sai. É o LIFO, de last in, first out. Não existe entrar no meio, não existe sair pelo fundo, não existe olhar o terceiro elemento. Só existe o topo.

A melhor imagem disso vale mais que qualquer definição. Você está no supermercado:

A fila do caixa é FIFO

Quem chegou primeiro é atendido primeiro. Quem chega vai para trás e espera. É uma fila, e a regra dela é a ordem de chegada.

A sacola de compras é LIFO

Você empilha os produtos um sobre o outro. Na hora de tirar, sai primeiro o que você colocou por último, porque é ele que está por cima. É uma pilha.

As duas estruturas guardam a mesma coisa e entregam em ordens opostas. Trocar uma pela outra troca a resposta do problema inteiro, e é por isso que a fila tem um tópico só dela.

O que faz a pilha ser útil não é guardar dados, é guardar contexto na ordem certa para desfazer. Sempre que um problema tiver a forma "abri alguma coisa e preciso fechar na ordem inversa", a pilha já é metade da solução:

  • Histórico do navegador: cada página visitada empilha; o botão voltar desempilha.
  • Undo e redo: cada ação empilha; desfazer tira do topo. Com duas pilhas, o que sai de uma entra na outra, e você ganha o redo de graça.
  • Chamadas de função: cada função chamada empilha; cada return desempilha.
  • Expressões: cada abertura empilha; cada fechamento tem que casar com o topo.
  • Inverter uma sequência: empilhe tudo e desempilhe tudo.

push, pop e peek: tudo acontece no topo

São três operações que fazem trabalho e uma que faz pergunta:

OperaçãoO que fazCusto
push(x)põe x no topoO(1)
pop()tira o topo e devolveO(1)
peek()olha o topo sem tirarO(1)
esta_vazia()o topo existe?O(1)

A distinção que mais derruba gente na prática é entre pop e peek. O pop é destrutivo: depois dele o elemento não está mais lá. O peek é uma consulta: chame dez vezes seguidas e ele devolve o mesmo valor dez vezes. Se você precisa do valor do topo e vai precisar dele de novo depois, ou você usa peek, ou você guarda o resultado do pop numa variável.

O motivo de todas serem O(1) é sempre o mesmo: você nunca percorre nada. A pilha sabe onde está o topo, e o topo é o único lugar em que ela mexe. Não existe busca, não existe deslocamento, não existe cascata. Se quiser ver isso acontecendo antes de continuar lendo, role até o visualizador de parênteses balanceados, na seção "Parênteses balanceados", e olhe a torre da direita: ela é a pilha desenhada como todo mundo desenha no quadro, com o topo na primeira linha, e cada passo do algoritmo é um push ou um pop nela.

A quarta operação parece decoração e não é. Ela é a proteção das outras duas:

Python
def pop(self):
    if self.esta_vazia():          # sem isto, IndexError
        return None
    ...

Sem esse if, pop numa pilha vazia estoura, e peek numa pilha vazia também. Numa implementação sobre array, o efeito é ainda mais sutil: o ponteiro do topo vai para -2, -3, e a próxima leitura devolve lixo em vez de erro. Em Python isso acontece de verdade, porque índice negativo é válido e conta do fim para o começo: com items = [10, 20, 30], items[-2] devolve 20 sem reclamar de nada. Em Java, C# ou Go o mesmo descuido vira uma exceção de índice fora do intervalo, que pelo menos é barulhenta. O caso silencioso é o pior dos dois: passa no teste feliz e quebra em produção.

Existe uma quinta operação, esta_cheia(), e ela só faz sentido quando o array por baixo tem tamanho fixo. Com array dinâmico ou lista ligada, a pilha só enche quando a memória acaba, e aí o erro não é seu.

Pilha sobre array: um ponteiro que sobe e desce

A implementação mais comum guarda os elementos num array e mantém um inteiro apontando para a última posição usada. Ele começa em -1, fora do array de propósito: é assim que "vazia" vira uma comparação e não um caso especial.

Python
class Pilha:
    def __init__(self):
        self.items = [None] * 4       # capacidade inicial
        self.topo = -1                # nada dentro ainda

    def esta_vazia(self):
        return self.topo == -1

    def push(self, item):
        if self.topo + 1 == len(self.items):
            self._dobrar()            # sem espaço: cresce
        self.topo += 1
        self.items[self.topo] = item

    def pop(self):
        if self.esta_vazia():
            return None
        item = self.items[self.topo]
        self.items[self.topo] = None  # solta a referência
        self.topo -= 1
        return item

    def peek(self):
        return None if self.esta_vazia() else self.items[self.topo]

    def _dobrar(self):
        novo = [None] * (len(self.items) * 2)   # bloco novo, o dobro do tamanho
        for i in range(len(self.items)):        # <- esta passada é O(n)
            novo[i] = self.items[i]
        self.items = novo

Três detalhes desse código valem mais que o código em si.

A capacidade inicial 4 não é aleatória. É o número que aparece em implementações reais de array dinâmico, porque é pequeno o bastante para não desperdiçar e grande o bastante para evitar as primeiras cópias: a List<T> do .NET começa em 4 e vai para 8, 16, 32, e a list do CPython também salta para 4 no primeiro append. Nem todo mundo usa 4, a ArrayList do Java começa em 10, mas o formato é sempre o mesmo, um bloco pequeno que dobra.

O _dobrar() é a única operação cara da estrutura. Quando o array enche, não dá para "esticar" o bloco: aloca-se um bloco novo com o dobro do tamanho e copia-se tudo, um elemento de cada vez. Essa passada é O(n). Como ela acontece cada vez mais raramente conforme a pilha cresce, o custo se dilui e o push fica O(1) amortizado. Amortizado quer dizer "na média das operações", não "sempre": um push específico pode custar caro.

O self.items[self.topo] = None é opcional para números e importante para objetos. Se o array guarda inteiros primitivos, apagar a posição é trocar um número por outro, e não muda nada: mover o ponteiro já basta, e o próximo push sobrescreve o valor antigo. Mas se o array guarda referências para objetos, deixar a referência lá segura o objeto na memória e impede o coletor de lixo de liberá-lo. Você acha que desempilhou, mas o objeto continua vivo.

Repare no que não acontece no pop: a capacidade não diminui. Um array que cresceu para 1024 posições continua ocupando 1024 posições mesmo com a pilha vazia. A memória só volta quando você joga a pilha inteira fora. É o preço da memória contígua, e é o mesmo assunto do tópico de Arrays.

Pilha sobre lista ligada: sem resize, com um ponteiro a mais

A outra implementação troca o array por nós encadeados, o assunto do tópico de Listas Encadeadas. Cada nó guarda um valor e um ponteiro para o nó de baixo, e a pilha guarda só o head, que é o topo:

Python
class No:
    def __init__(self, valor, proximo=None):
        self.valor = valor
        self.proximo = proximo

class Pilha:
    def __init__(self):
        self.head = None              # pilha vazia

    def push(self, item):
        self.head = No(item, self.head)   # o novo aponta para o antigo

    def pop(self):
        if self.head is None:
            return None
        no = self.head
        self.head = no.proximo        # o topo desce um degrau
        return no.valor

O push inteiro é uma linha porque a operação é literalmente "crie um nó cujo próximo é o topo atual, e passe a chamar ele de topo". Nunca existe resize, nunca existe cópia, então não tem O(n) escondido em lugar nenhum. E o pop de verdade encolhe: o nó desempilhado fica sem ninguém apontando para ele e o coletor de lixo leva embora.

A conta não fecha só de vantagens. Cada item deixou de ser um valor solto e virou um objeto com um campo a mais. Em uma máquina de 64 bits, são 8 bytes só do ponteiro proximo, mais o cabeçalho do objeto, para cada elemento. E os nós ficam espalhados pela memória, então some o prêmio de cache que o array tinha.

Também some o acesso ao meio. No array, items[3] é uma conta; na lista ligada, chegar no quarto nó é andar por três ponteiros. A analogia é a do telefone sem fio: você só enxerga quem está de mãos dadas com você. O terceiro nó não faz ideia de que o primeiro existe.

Só que, para uma pilha, isso não custa nada: o contrato dela é acessar só o topo. A limitação da lista ligada cai justamente onde a pilha não precisa de nada.

A mesma pilha, dois porões: array dinâmico e lista ligada
OperaçãoSobre arraySobre lista ligada
push
empilhar no topo
O(1) amortizadoquando a capacidade acaba, o array dobra e copia tudo para o bloco novo: essa passada isolada é O(n)O(1) semprecria um nó, aponta o next dele para o topo atual e move o head; nunca copia nada
pop
tirar do topo e devolver
O(1)recua o ponteiro do topo uma casa; a capacidade alocada continua exatamente onde estavaO(1)guarda o head, aponta o head para o next e devolve o valor; o nó antigo vai para o coletor de lixo
peek
espiar sem tirar
O(1)lê items[topo] e não mexe no ponteiro; é a diferença inteira entre peek e popO(1)lê head.valor e não mexe no head
esta_vazia
a pergunta que protege as outras
O(1)topo == -1, com o topo começando fora do array de propósitoO(1)head is None
memória por item
o que cada elemento custa
só o valormais a capacidade ociosa: depois de dobrar, até metade do bloco pode estar vaziavalor + 1 ponteirocada item vira um objeto com o campo next; em 64 bits são 8 bytes só de ponteiro, mais o cabeçalho do objeto
encolher
quando a pilha esvazia
não encolhea capacidade só volta quando você joga o array inteiro fora; remover itens não devolve memóriaencolhe de verdadecada pop solta um nó, e a memória volta assim que o coletor passa
acesso ao meio
fora do contrato da pilha
O(1) se você burlara fórmula do índice existe, mas a interface de pilha esconde isso de propósito: o contrato é só o topoO(n)só dá para chegar andando de nó em nó, como no telefone sem fio: você só enxerga quem está de mãos dadas com você
Nenhuma das duas ganha em tudo. O array paga o resize e a capacidade ociosa, e ganha memória compacta e amiga do cache. A lista ligada nunca copia e devolve memória a cada pop, e paga um ponteiro por item. As duas entregam push, pop e peek em O(1), que é o que a pilha promete.

Leia a tabela de cima para baixo e repare que as quatro primeiras linhas são empate: push, pop, peek e esta_vazia custam O(1) nas duas colunas, com a única ressalva do "amortizado" no push sobre array. A diferença só aparece nas três últimas linhas, e é sempre a mesma troca dita de três jeitos: o array gasta menos por item mas guarda capacidade ociosa e nunca encolhe; a lista ligada paga um ponteiro por item e devolve memória a cada pop.

O resumo honesto é o do rodapé: as duas entregam push, pop e peek em O(1), que é o que a pilha promete. A escolha entre elas é sobre memória, não sobre velocidade das operações. E a última linha, "acesso ao meio", é a que mais engana: ela parece uma derrota da lista ligada, mas é irrelevante aqui, porque o contrato da pilha proíbe esse acesso nas duas implementações.

Na vida real você quase nunca implementa isso na mão, e vale saber o que a linguagem oferece. Em Java, a classe Stack é legado: ela herda de Vector, é sincronizada e a própria documentação recomenda usar Deque no lugar, normalmente com ArrayDeque. Em Python, uma list já é uma pilha: append é o push e pop() sem argumento é o pop. O que não vale é usar pop(0) achando que é pilha, porque isso é remover do começo e custa O(n).

Parênteses balanceados: o primeiro problema de verdade

Este é o problema clássico da pilha, o LeetCode 20: dada uma string com (, ), [, ], { e }, diga se ela está balanceada. Três regras no enunciado:

  1. toda abertura fecha com o mesmo tipo;
  2. o fechamento vem na ordem correta;
  3. todo fechamento tem uma abertura correspondente.

A tentação de quem nunca viu é contar: se o número de ( for igual ao de ), está válido. Contador não resolve, e o contraexemplo cabe em quatro caracteres: )( tem um de cada e é inválido, porque a ordem está trocada. Um contador esquece a ordem, e ordem é o problema inteiro.

A regra 2 é o que entrega a estrutura. "Ordem correta" quer dizer que, quando eu encontro um fechamento, ele tem que casar com a abertura mais recente que ainda não fechou. Mais recente que ainda está aberta é exatamente a definição de topo de pilha.

Visualizador · a pilha em ação: parênteses balanceados
passo 1 de 8
Expressão
0
{
·
1
[
·
2
(
·
3
)
·
4
]
·
5
}
·

Começo com a pilha vazia e 6 caracteres para ler, da esquerda para a direita.

Pilha (topo em cima)

pilha vazia

base da pilha
solucao.py
1def valida(s):
2 pilha = []
3 pares = {")": "(", "]": "[", "}": "{"}
4 for c in s:
5 if c in pares:
6 if not pilha or pilha[-1] != pares[c]:
7 return False
8 pilha.pop()
9 else:
10 pilha.append(c)
11 return not pilha
Variáveis
c-
pilha[-1]vazia
len(pilha)0
veredito…
caracteres (n)6
empilhados (push)0
desempilhados (pop)0
altura máxima0

←→ passo · espaço roda

Comece pelo preset Aninhado, que roda a expressão {[()]}. Repare na torre da direita subindo até 3 e depois descendo até zero, e no painel de baixo: 3 push, 3 pop, altura máxima 3.

Agora clique em Lado a lado, que roda ()[]{}. Mesma quantidade de caracteres, mesmos 3 push e 3 pop, e a altura máxima despenca para 1, porque cada par fecha antes do próximo abrir. Essa diferença é a resposta para "quanta memória a pilha usa": não é o tamanho da entrada, é o aninhamento máximo dela.

Os outros três presets são os casos que quebram, e vale prever a resposta antes de clicar:

  • Cruzado (([)]): para no passo 4 de 4. O ) pede ( no topo, mas o topo é [. Tipos trocados, e nem adianta continuar lendo.
  • Sobra aberto (([]): o [] fecha certinho, a expressão acaba e a pilha não esvazia. Inválida por falta de fechamento.
  • Fecha sem abrir ()(): morre no primeiro caractere, com a pilha vazia. Um fechamento sem ninguém para casar.

Esses três casos são o esqueleto da solução, porque são exatamente as três formas de dar errado:

Python
def valida(s):
    pilha = []
    pares = {")": "(", "]": "[", "}": "{"}     # fechamento -> abertura
    for c in s:
        if c in pares:                        # é fechamento
            if not pilha or pilha[-1] != pares[c]:
                return False                  # vazia, ou tipo trocado
            pilha.pop()
        else:                                 # é abertura
            pilha.append(c)
    return not pilha                          # sobrou aberto?

O dicionário pares tem o fechamento como chave e a abertura como valor, e essa escolha merece um parágrafo. O raciocínio: quando você lê uma abertura, não há nada a verificar, você só empilha. É no fechamento que existe uma pergunta, e a pergunta é "qual abertura eu esperava aqui?". Mapear fechamento para abertura responde isso em O(1). O caminho contrário é uma chave fraca, porque não há decisão a tomar na abertura.

Existem duas variações que aparecem muito e valem conhecer:

  • Sem dicionário nenhum: um if para cada um dos três fechamentos, comparando o pop com a abertura esperada. Mais verboso, mesma lógica, e evita a estrutura extra.
  • Empilhar o fechamento esperado: ao ver (, empilhe ). Assim, quando um fechamento aparece, o pop tem que ser igual a ele, sem tradução nenhuma no meio. É elegante e costuma render um código mais curto.

Duas bordas a mais valem o teste manual no visualizador: apague tudo do campo da expressão e veja o que acontece com a string vazia, que é válida por definição porque o laço não roda nenhuma vez; e digite um caractere só, (, para ver a pilha ficar com um item pendurado até o fim. O botão Sortear válida gera expressões balanceadas ao acaso, útil para conferir que a altura máxima acompanha o aninhamento e não o tamanho.

As duas linhas que mais somem em entrevista são a proteção not pilha antes do pilha[-1], que evita o IndexError em ), e o return not pilha do final, que é o único jeito de reprovar ([. Sem a última linha, o algoritmo devolve válido para qualquer expressão que só abra. Teste os dois casos antes de submeter.

Complexidade: O(n) de tempo, porque cada caractere é lido uma vez e faz no máximo um push e um pop. O espaço é O(n) no pior caso, que é a expressão totalmente aninhada, do tipo ((((())))), em que a pilha chega à metade do tamanho da entrada antes de começar a descer. O caso mais extremo de todos é a entrada que só abre, ((((((: ela empilha os n caracteres e nunca desempilha nenhum, e ainda assim é lida uma vez só. Para ()()(), o espaço real é O(1), e é exatamente isso que o contador de altura máxima do visualizador está mostrando: 1 no preset Lado a lado, 3 no Aninhado, com o mesmo número de caracteres nos dois.

A pilha que você já usava sem saber: a call stack

Toda linguagem mantém uma pilha para executar o seu programa. Quando a() chama b(), o frame de a não sai: ele fica parado na pilha, com as variáveis locais e o ponto exato onde parou, esperando b terminar. Só quando b retorna é que a volta a andar.

É por isso que o stack trace tem a forma que tem. A linha de cima é onde a exceção estourou, e cada linha abaixo é quem chamou quem, até o ponto de entrada do programa. Controller chamou service, service chamou o repositório, o repositório estourou. O caminho inteiro está ali.

Ler o stack trace substitui um monte de código defensivo. É comum ver try/catch espalhado por todo canto só para acrescentar mensagens do tipo "cheguei aqui", quando o próprio rastro já diz exatamente por onde a execução passou. E não são só funções: atribuições, operações matemáticas e até o print acontecem sobre essa mesma pilha.

Quando uma função chama a si mesma, a call stack fica visível. O exercício aqui é calcular uma potência de três jeitos e ver que os passos são sempre os mesmos: um laço simples, uma recursão, e uma pilha explícita escrita na mão.

Python
def potencia_laco(x, n):      # 1. sem pilha nenhuma, O(1) de espaço
    r = 1
    for _ in range(n):
        r *= x
    return r

def potencia(x, n):           # 2. recursão: a pilha existe, você só não a escreveu
    if n == 1:                # caso base
        return x
    return x * potencia(x, n - 1)

Vale dizer na cara: potência não precisa de pilha, o laço de cima resolve em O(1) de espaço e é a versão que você escreveria em produção. O exemplo está aqui porque é pequeno o bastante para caber na tela inteira e mostrar, lado a lado, o que a recursão faz por baixo. É didática, não é recomendação.

Visualizador · a mesma potência com duas pilhas
passo 1 de 9
O que a call stack está fazendo

Chamei potencia(2, 3): um frame novo entra no topo da pilha, com x = 2 e n = 3.

expoente (n)3
altura máxima da pilha1
empilhamentos1
memória extraO(n)
Call stack (topo em cima)
potencia(2, 3)acabou de entrar
base da pilha
recursivo.py
1def potencia(x, n):
2 if n == 1:
3 return x
4 return x * potencia(x, n - 1)
Variáveis
topopotencia(2, 3)
frames1
return-
2^3-

←→ passo · espaço roda

Rode primeiro no modo Recursão (call stack) com o padrão 2³, até o último passo. Acompanhe a ida: cada chamada empilha um frame e ele fica parado, porque ainda não sabe a resposta, ele depende da chamada de baixo. Quando n chega a 1, o caso base devolve 2 e a pilha começa a se desfazer de cima para baixo, multiplicando: 2, depois 4, depois 8. O campo return no painel de variáveis mostra exatamente esses três valores, nessa ordem.

Agora clique em Pilha explícita, sem mexer em nada mais, e rode de novo até o fim. O mesmo 2³, resolvido com uma pilha que você escreveu na mão: empilhe a base n vezes, depois desempilhe multiplicando. Compare os dois painéis de estatísticas no último passo. Os dois modos fazem 3 empilhamentos e chegam à altura máxima 3. A recursão não é mágica, ela é uma pilha que você não escreveu.

Repare no caso base if n == 1. Ele funciona para todo n ≥ 1 e quebra em n = 0: a recursão passa direto pelo 1, vai para 0, -1, -2, e só para quando a call stack estoura. É a falha número um em recursão, o caso base que não cobre a menor entrada possível. Aqui o certo seria if n == 0: return 1, que já engloba o outro. Por isso o visualizador começa o expoente em 1: para o caso base que está no texto ser sempre alcançado.

Isso tem duas consequências práticas:

Recursão custa memória, e o custo é O(profundidade). Cada frame ocupa espaço até a recursão desenrolar. Por isso, uma recursão de 100 mil níveis estoura antes de terminar. Em Python, o limite padrão é de 1000 chamadas aninhadas, e passar disso levanta RecursionError. Em Java e C#, o sintoma tem outro nome, StackOverflowError, e a mesma causa.

Toda recursão pode virar um laço com pilha explícita. Trocar a call stack por uma pilha sua move a memória da stack para a heap e some com o limite de profundidade. É exatamente essa troca que aparece nos percursos em árvore e no DFS em grafos: a versão iterativa do DFS é a versão recursiva com um stack = [] no lugar da chamada. Aumente o expoente no visualizador até 8 e olhe a altura máxima subir junto: é essa altura que estoura.

Dois testes rápidos fecham o assunto no visualizador. Coloque o expoente em 1: a recursão cai direto no caso base, com um frame só e altura máxima 1, que é a menor entrada válida. Depois coloque base 9 e expoente 8: o resultado passa de 43 milhões (43046721 no painel), mas a altura máxima continua sendo 8. A memória da recursão acompanha a profundidade, não o tamanho do resultado, e essa é a única coisa que a call stack cobra de você.

Inverter, desfazer e avaliar: três padrões diretos

Com push, pop e peek na mão, três problemas clássicos viram quase nada de código.

Inverter uma sequência

Empilhe tudo e desempilhe tudo. Como o último a entrar é o primeiro a sair, a ordem sai invertida sozinha:

Python
def inverter(nums):
    pilha = []
    for x in nums:            # 4, 3, 2, 1 entram nessa ordem
        pilha.append(x)

    saida = []
    while pilha:              # o topo sai primeiro: 1, 2, 3, 4
        saida.append(pilha.pop())
    return saida

Vale medir o preço: o tempo é O(n), mas o espaço vira O(n), porque a pilha auxiliar cresce junto com a entrada. Cada operação continua sendo O(1), e ainda assim a complexidade de espaço do algoritmo mudou de patamar. Reverter com dois ponteiros no próprio array custa O(1) de espaço e resolve o mesmo problema, então a versão com pilha vale como ferramenta de raciocínio, não como a melhor solução para inverter arrays.

Desfazer e refazer com duas pilhas

Uma pilha só resolve o undo. Para ganhar o redo, use duas: toda ação executada entra na pilha de desfazer; quando o usuário desfaz, a ação sai dessa pilha e entra na de refazer; quando ele refaz, o caminho é o inverso. E quando ele executa uma ação nova, a pilha de refazer é esvaziada, que é exatamente o comportamento que todo editor de texto tem.

Avaliar uma expressão em notação polonesa reversa

Na notação pós-fixada, o operador vem depois dos operandos: ["2", "1", "+", "3", "*"] quer dizer (2 + 1) * 3 = 9. Não existem parênteses, e mesmo assim não existe ambiguidade, porque a ordem já está codificada na posição. O algoritmo inteiro cabe num laço:

Python
def avaliar_rpn(tokens):                      # LeetCode 150
    pilha = []
    for t in tokens:
        if t in ("+", "-", "*", "/"):
            b = pilha.pop()                   # o SEGUNDO operando sai primeiro
            a = pilha.pop()
            pilha.append(aplicar(t, a, b))
        else:
            pilha.append(int(t))
    return pilha.pop()                        # sobra exatamente um: a resposta

def aplicar(op, a, b):
    if op == "+": return a + b
    if op == "-": return a - b
    if op == "*": return a * b
    return int(a / b)                         # trunca em direção ao zero

O detalhe que erra quem escreve rápido está nas duas primeiras linhas do if: o primeiro pop devolve o operando da direita. Trocar a ordem não muda nada em + e *, e inverte o resultado de - e /. Rode ["4", "2", "-"] de cabeça: a resposta certa é 2, e a ordem trocada devolve -2.

O segundo detalhe é a última linha do aplicar, e ela reprova mais submissões do que a lógica da pilha inteira. O enunciado do LeetCode 150 manda truncar em direção ao zero, e o // do Python não faz isso: ele arredonda para baixo. Com -7 e 2, -7 // 2 dá -4, enquanto o esperado é -3. Por isso int(a / b), que corta a parte decimal e ignora o sinal. Em Java e C#, a divisão inteira já trunca em direção ao zero e o problema não existe. É o tipo de armadilha que só aparece quando o teste tem número negativo.

Pilha monotônica: o próximo maior elemento em O(n)

Aqui a pilha deixa de ser estrutura auxiliar e vira técnica. É o padrão que mais cai em entrevista, e o mais bonito de ver rodando.

O problema: para cada posição de um array, ache o primeiro valor à direita que é maior que ela. Se não existir, a resposta é -1. Para [6, 8, 0, 1, 3], que é o preset Do GeeksforGeeks no visualizador logo abaixo, a resposta é [8, -1, 1, 3, -1].

A força bruta é óbvia e é O(n²): para cada i, varra tudo à direita até achar alguém maior.

Python
def forca_bruta(nums):
    resp = [-1] * len(nums)
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[j] > nums[i]:
                resp[i] = nums[j]
                break
    return resp

A virada de chave é olhar o problema de trás para frente. Em vez de perguntar "quem resolve o i?", pergunte: quando eu chego num valor novo, quem é que ele resolve? Um valor grande que aparece resolve, de uma vez só, todo mundo menor que estava esperando. E quem estava esperando fica guardado em uma pilha que se mantém decrescente, do fundo para o topo.

Como reconhecer o padrão no enunciado. Quase todo problema de pilha monotônica se disfarça com uma dessas frases: "o próximo maior/menor elemento", "o anterior maior/menor", "quantos dias até ficar mais quente", "por quanto tempo este valor foi o maior", "o maior retângulo", "o span da ação". O gatilho comum é sempre o mesmo: para cada posição, encontre o primeiro vizinho que satisfaz uma comparação. Se a resposta de cada elemento depende de "quem é o próximo que...", e a resposta ingênua é um laço dentro do outro, tente a pilha antes de qualquer outra coisa.

Visualizador · pilha monotônica: o próximo maior elemento
passo 1 de 29
nums, o array de entrada
0
73
·
1
74
·
2
75
·
3
71
·
4
69
·
5
72
·
6
76
·
7
73
·
resp, a resposta sendo preenchida
0
-1
1
-1
2
-1
3
-1
4
-1
5
-1
6
-1
7
-1

esperando na pilhajá respondidoainda no -1 provisório

Começo com as 8 respostas em -1. Esse é o palpite padrão: se ninguém maior aparecer à direita, o -1 fica.

Pilha de índices (topo em cima)

pilha vazia

base da pilha
monotonica.py
1def proximo_maior(nums):
2 resp = [-1] * len(nums)
3 pilha = [] # índices em espera
4 for i, v in enumerate(nums):
5 while pilha and nums[pilha[-1]] < v:
6 resp[pilha.pop()] = v
7 pilha.append(i)
8 return resp
Variáveis
i-
v-
nums[pilha[-1]]vazia
respondidos0 de 8
tamanho (n)8
comparações até aqui0
força bruta faria11
empilhados (push)0
desempilhados (pop)0

←→ passo · espaço roda

O padrão abre nas temperaturas do LeetCode 739, [73, 74, 75, 71, 69, 72, 76, 73], e a resposta completa é [74, 75, 76, 72, 72, 76, -1, -1]. Rode passo a passo e pare em dois momentos:

  • Índice 5, valor 72. Ele chega e resolve dois de uma vez: primeiro o 69 do índice 4, depois o 71 do índice 3. São dois passos seguidos do visualizador, um por pop, mas nenhum deles avança o i: é a mesma visita ao 72 pagando duas dívidas. Depois o topo vira o 75, que não é menor que 72, o algoritmo para o while e o 75 continua esperando embaixo.
  • Índice 6, valor 76. Desempilha o 72 e depois o 75, e agora a pilha fica vazia: repare que não existe um passo de "paro o while" aqui, porque não sobrou topo para comparar. Quem entra depois, 76 e 73, é quem sobra no fim com -1, porque ninguém maior apareceu à direita deles.

Repare também no que a pilha não faz. Quando o topo não é menor que o valor atual, o algoritmo para na hora, sem olhar o resto: como a pilha é decrescente, todo mundo abaixo do topo é ainda maior. Essa é a garantia que sustenta a complexidade.

O visualizador grava o valor do próximo maior, que é a versão clássica do problema. O LeetCode 739 pede a distância em dias, ou seja i - j em vez de v. O caminho da pilha é idêntico, muda só o que você escreve dentro do resp. Acompanhe o passo a passo aqui e depois troque essa única linha na sua solução.

Python
def proximo_maior(nums):
    resp = [-1] * len(nums)
    pilha = []                                # guarda ÍNDICES em espera
    for i, v in enumerate(nums):
        while pilha and nums[pilha[-1]] < v:
            resp[pilha.pop()] = v             # v é o próximo maior de quem sai
        pilha.append(i)
    return resp

O laço aninhado engana. Tem um for com um while dentro, e a leitura apressada diz O(n²). Só que cada índice entra na pilha exatamente uma vez e sai no máximo uma vez: são no máximo 2n operações de pilha no total, não n por iteração. Por isso é O(n) de tempo e O(n) de espaço.

Os dois contadores do visualizador existem para você conferir isso na tela. Rode cada um dos cinco presets até o último passo e compare o campo comparações até aqui com o força bruta faria, que já vem calculado para o array inteiro:

PresetnPilhaForça bruta
Temperaturas 73 74 75 71 69 72 76 7381011
Do GeeksforGeeks 6 8 0 1 3566
Pior caso da força bruta 8 7 6 5 4 3 2 18728
Crescente 1 2 3 4 5 6 7 8877
Tudo igual 4 4 4 4436

O preset Pior caso da força bruta é o que fecha o argumento: com o array decrescente, nenhuma resposta existe, então a força bruta varre o array inteiro para cada posição e faz 28 comparações, que é n(n-1)/2 com n = 8. A pilha faz 7, uma por elemento a partir do segundo, e empilha os 8 sem desempilhar nenhum. Aumente o array e a distância cresce junto: digite 12 11 10 9 8 7 6 5 4 3 2 1, que é o limite de 12 itens do visualizador, e os contadores viram 11 contra 66. A pilha cresceu de 7 para 11, a força bruta de 28 para 66.

Repare que em três dos cinco presets a força bruta empata ou quase empata. Isso não é defeito da tabela, é a coisa mais importante que ela ensina: com n pequeno, e principalmente quando a resposta está quase sempre no vizinho imediato, a força bruta não perde nada. A pilha monotônica não é mais rápida em todo array, ela é a que nunca degenera. Big O é sobre a garantia, não sobre o caso simpático, e é por isso que o único preset em que a diferença explode é justamente o pior caso.

O preset Crescente é o empate perfeito, porque a força bruta acha a resposta no primeiro vizinho e sai do laço na primeira comparação. Já Tudo igual é a borda que separa as duas versões do algoritmo: com 4 4 4 4, a comparação é <, ou seja, estritamente maior, e valores iguais não resolvem ninguém. Todas as respostas ficam -1, e a pilha termina com os quatro índices dentro. Trocar < por <= mudaria a pergunta para "próximo maior ou igual" e essas quatro respostas viravam 4, 4, 4, -1. É uma tecla só de diferença, então leia o enunciado com atenção antes de escolher.

Antes de seguir, três exercícios de previsão. Responda antes de rodar, depois confira na tela:

  1. Digite 5 1 2 3 4. Quantas vezes o 5 é comparado, e qual é a resposta dele?
  2. Digite um número só, 7. Qual é a resposta, e quantos push e quantos pop acontecem?
  3. Apague tudo, deixando o campo vazio. O que o algoritmo tem para fazer?

Respostas. (1) O 5 é comparado quatro vezes, uma em cada índice de 1 a 4, porque ele fica no fundo da pilha e todo mundo que chega esbarra nele antes de desistir. A resposta dele é -1, já que ninguém à direita é maior. O total dá 7 comparações, e a força bruta também faz 7: um número grande logo no começo é o caso em que a pilha mais trabalha à toa. (2) Com um elemento só, a resposta é -1, com 1 push e 0 pop, e zero comparações, porque a pilha nunca chega a ter um topo para comparar. (3) Com o array vazio não existe nem pergunta a fazer: o for não roda, a pilha continua vazia e resp sai vazia também. Os três casos são exatamente os que costumam faltar nos testes que você escreve.

O mesmo esqueleto resolve uma família inteira de problemas, mudando muito pouco:

  • Próximo menor elemento: inverta a comparação para >, e a pilha passa a ser crescente.
  • Próximo maior à esquerda: mesmo código, percorrendo o array de trás para frente. Vale para as quatro combinações: maior ou menor, à direita ou à esquerda, é sempre o mesmo esqueleto com o sinal e o sentido trocados.
  • Daily Temperatures (LeetCode 739): em vez de gravar v, grave i - j, a distância em dias. Por isso a pilha guarda índice e não valor.
  • Next Greater Element II (LeetCode 503), a versão circular: rode o laço 2n vezes usando nums[i % n], e só empilhe na primeira volta (if i < n). A segunda volta existe só para resolver quem ficou pendurado, e como cada índice ainda entra uma vez só, continua O(n).
  • Largest Rectangle in Histogram (LeetCode 84): a pilha guarda as barras crescentes e, quando uma barra menor chega, cada barra que sai calcula a área do retângulo que ela conseguia formar. É o mesmo padrão levado ao limite, e o truque de implementação é acrescentar uma barra de altura 0 no fim para forçar o esvaziamento da pilha.

As armadilhas que pegam todo mundo

Mexer na pilha sem perguntar se ela existe. pilha[-1] e pilha.pop() numa lista vazia levantam IndexError em Python, e o equivalente nas outras linguagens. A condição if pilha and ... vem antes, sempre, e o curto-circuito do and faz o resto.

Esquecer de checar a pilha no fim. Metade dos problemas de pilha tem uma segunda pergunta depois do laço: sobrou alguém? Em parênteses balanceados, é a diferença entre reprovar e aprovar ([. Na pilha monotônica, é quem fica com -1.

Confundir pop com peek. Se você deu pop e depois precisou do valor de novo, ele já foi. Guarde numa variável ou use peek.

Achar que a pilha sobre array devolve memória. Ela não devolve. A capacidade fica onde chegou, e só volta quando o objeto inteiro é descartado. Em uma pilha que teve um pico de 1 milhão de itens e agora tem 3, o array continua com 1 milhão de posições.

Confiar em recursão profunda. A call stack tem limite, e ele é bem menor do que a memória disponível. Python para em 1000 níveis por padrão. Se a profundidade depende da entrada, prefira a versão iterativa com pilha explícita.

Usar pilha onde o problema é fila. Se a ordem que importa é a de chegada, LIFO devolve tudo ao contrário. E, em Python, lista.pop(0) para simular uma fila custa O(n) por remoção, porque desloca todo o resto: o certo é collections.deque, que é o assunto de Filas e Deques.

Empilhar valores quando você precisa de índices. Na pilha monotônica, guardar o valor funciona para "qual é o próximo maior", mas não para "a quantos passos ele está". O índice carrega as duas informações, o valor carrega uma. Na dúvida, empilhe o índice.

Como praticar

A ordem abaixo é a dos problemas listados no fim desta página, e ela é proposital: os dois primeiros treinam a estrutura, o terceiro treina projetar com ela, o quarto treina a avaliação de expressão e o último treina a pilha monotônica.

  1. Valid Parentheses (20) é o problema desta página. Antes de submeter, teste ), ([ e a string vazia, que valem os três casos de borda de uma vez.
  2. Baseball Game (682) é a estrutura pura, sem truque nenhum: um token numérico é um push, o C é um pop, o D empilha o dobro do topo e o + empilha a soma dos dois últimos. O + é o exercício: você precisa ler dois valores sem destruir nenhum, então ou você usa índices (p[-1] + p[-2]), ou dá um pop, lê o novo topo e devolve o que tirou. Se você fizer dois pop e esquecer de repor, o placar sai errado e o teste que pega isso é justamente um com dois + seguidos. No fim, some a pilha inteira.
  3. Min Stack (155) é o salto: entregar o mínimo em O(1) sem varrer a pilha. A saída é a mesma ideia do undo e redo, duas pilhas, uma com os valores e outra com o mínimo até aquele ponto. O invariante é: no push(x), empilhe em mins o menor entre x e o topo atual de mins, e no pop, desempilhe das duas juntas. Assim mins[-1] é sempre o mínimo do estado atual, sem busca nenhuma. A armadilha é valor repetido: se você só empilhar em mins quando x < mins[-1], uma pilha com dois valores mínimos iguais perde o mínimo no primeiro pop. Ou use <=, ou empilhe sempre, que é mais simples e gasta a mesma O(n) de memória. Teste com push(-2), push(0), push(-2), pop(), getMin(), que tem que devolver -2.
  4. Evaluate Reverse Polish Notation (150) é a avaliação de expressão da seção anterior, código pronto e tudo. Os dois pontos que reprovam: a ordem dos pop, porque o primeiro é o operando da direita, e a divisão, que precisa truncar em direção ao zero (int(a / b) em Python, nunca //).
  5. Daily Temperatures (739) é a pilha monotônica com uma linha de diferença: grave a distância i - j em vez do valor. É o mesmo array do visualizador, então dá para conferir o caminho passo a passo antes de escrever qualquer código.

Um roteiro que funciona para os cinco: escreva a força bruta primeiro, calcule o Big O dela e só então pergunte o que a pilha economiza. Foi assim que a solução dos parênteses apareceu nesta página, e é assim que o padrão fica na cabeça em vez de virar decoreba.

Daqui, dois caminhos se abrem. Um é a estrutura irmã, a fila, que troca LIFO por FIFO e resolve a outra metade dos problemas de ordem. O outro é a recursão, que é a call stack desta página vista de dentro, e a porta de entrada para árvores e grafos.

Vídeo da aula

Direto do canal da comunidade Craft & Code Club · 2:26:51.

Problemas para praticar

Na ordem em que recomendamos resolver. Marque os que você já fez, fica salvo aqui.

FácilValid ParenthesesLeetCode 20↗
FácilBaseball GameLeetCode 682↗
MédioMin StackLeetCode 155↗
MédioDaily TemperaturesLeetCode 739↗

Referências

Artigos e materiais externos para se aprofundar.

Travou em algum passo? Traga sua questão para o Discord da comunidade ou para os encontros semanais.

Entrar
Concluiu este tópico?
Marque para acompanhar seu progresso.

Este tópico também tem página própria, fora deste roadmap: Pilhas (Stacks).