DSA Interview Prep · Aula

Simulação Mútua de Pilha e Fila

Implemente uma fila usando duas pilhas e uma pilha usando duas filas, explicando o custo amortizado de cada abordagem.

Aula 4 de 413 etapas

Simulação Mútua de Pilha e Fila é uma aula grátis de DSA Interview Prep no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de DSA Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DSA Interview Prep inclui 4 aulas no total.

Por que simular uma estrutura com a outra?

Implementar uma fila usando duas pilhas e uma pilha usando duas filas são perguntas clássicas de entrevistas sobre projeto. Elas avaliam sua compreensão das invariantes de ambas as estruturas de dados e sua capacidade de manter a garantia de uma estrutura usando as operações básicas de outra. Os entrevistadores também usam essas perguntas como ponto de partida para discutir a complexidade amortizada.

A ideia principal é: pilhas são LIFO e filas são FIFO. Para converter uma na outra, é necessário inverter a ordem — e inverter uma pilha em outra pilha produz a ordem original de inserção, que é FIFO.

Fila usando duas pilhas (abordagem preguiçosa)

A abordagem preguiçosa usa uma pilha inbox para as inserções e uma pilha outbox para as remoções. Quando a remoção da fila é chamada, se outbox estiver vazia, transfira todos os elementos de inbox para outbox — essa inversão restaura a ordem FIFO. Se outbox não estiver vazia, remova o elemento diretamente dela. As transferências ocorrem sob demanda, amortizando o custo de transferência O(n) ao longo de muitas operações.

class MyQueue:
    def __init__(self):
        self.inbox  = []
        self.outbox = []

    def push(self, x):
        self.inbox.append(x)

    def _transfer(self):
        if not self.outbox:
            while self.inbox:
                self.outbox.append(self.inbox.pop())

    def pop(self):
        self._transfer()
        return self.outbox.pop()

    def peek(self):
        self._transfer()
        return self.outbox[-1]

    def empty(self):
        return not self.inbox and not self.outbox

q = MyQueue()
q.push(1); q.push(2); q.push(3)
print(q.peek())  # 1
print(q.pop())   # 1
print(q.pop())   # 2
q.push(4)
print(q.pop())   # 3

Análise amortizada em O(1) para uma fila criada com pilhas

Cada elemento é transferido de inbox para outbox no máximo uma vez. Como remover elementos de outbox custa O(1) e as transferências ocorrem apenas quando outbox está vazia, o trabalho total para n operações de push e n operações de pop é de no máximo 2n operações de pilha — O(n) no total e O(1) amortizado por operação. Isso significa que operações individuais podem custar O(n) no pior caso, mas a média é O(1).

# Trace transfer costs for 10 push/pop interleaved
class TrackedQueue:
    def __init__(self):
        self.inbox = []; self.outbox = []; self.transfers = 0

    def push(self, x): self.inbox.append(x)

    def pop(self):
        if not self.outbox:
            while self.inbox:
                self.outbox.append(self.inbox.pop())
                self.transfers += 1
        return self.outbox.pop()

q = TrackedQueue()
for i in range(5):
    q.push(i)
for _ in range(5):
    q.pop()
q.push(10); q.push(20)
q.pop()
print('Total transfer operations:', q.transfers)  # at most n

Pilha usando duas filas (remoção preguiçosa)

Implementar uma pilha com duas filas é menos natural, pois filas são FIFO. A abordagem de remoção preguiçosa mantém uma fila principal e uma fila temporária. Em push, insira o elemento na fila principal (O(1)). Em pop ou peek, remova todos os elementos, exceto o último, para a fila temporária, salve o último elemento e, em seguida, troque as filas. Isso custa O(n) por remoção, mas O(1) por inserção.

from collections import deque

class MyStack:
    def __init__(self):
        self.main = deque()
        self.temp = deque()

    def push(self, x):
        self.main.append(x)   # O(1)

    def pop(self):
        # Move all but last element to temp
        while len(self.main) > 1:
            self.temp.append(self.main.popleft())
        val = self.main.popleft()   # the 'top'
        self.main, self.temp = self.temp, self.main  # swap
        return val

    def top(self):
        while len(self.main) > 1:
            self.temp.append(self.main.popleft())
        val = self.main[0]
        self.temp.append(self.main.popleft())
        self.main, self.temp = self.temp, self.main
        return val

    def empty(self):
        return len(self.main) == 0

s = MyStack()
s.push(1); s.push(2); s.push(3)
print(s.top())  # 3
print(s.pop())  # 3
print(s.pop())  # 2

Pilha usando uma fila (rotacionar ao inserir)

Uma implementação elegante com uma fila: em push, insira o novo elemento e, em seguida, rotacione a fila para que o novo elemento fique na frente. Rotacionar significa remover e inserir novamente todos os elementos que já estavam na fila antes da inserção. Assim, pop e peek custam O(1) (basta remover ou consultar o primeiro elemento). Push custa O(n) — a compensação oposta à da versão com duas filas.

from collections import deque

class MyStackOneQueue:
    def __init__(self):
        self.q = deque()

    def push(self, x):
        self.q.append(x)
        # Rotate: move all preceding elements behind x
        for _ in range(len(self.q) - 1):
            self.q.append(self.q.popleft())

    def pop(self):
        return self.q.popleft()

    def top(self):
        return self.q[0]

    def empty(self):
        return len(self.q) == 0

s = MyStackOneQueue()
s.push(1); s.push(2); s.push(3)
print(s.top())  # 3
print(s.pop())  # 3
print(s.top())  # 2

Resumo das compensações: qual variante escolher?

Para uma fila criada com duas pilhas: push O(1), pop/peek O(1) amortizado — prefira-a quando as operações de pop forem frequentes. Para uma pilha criada com duas filas: push O(1), pop O(n) — prefira-a quando houver muito mais operações de push do que de pop. Para uma pilha criada com uma fila: push O(n), pop O(1) — prefira-a quando as operações de pop predominarem. Expresse essas compensações claramente em uma entrevista para demonstrar que você pensa além de simplesmente dizer «funciona».

print('Queue from 2 stacks: push O(1), pop O(1) amortised')
print('Stack from 2 queues: push O(1), pop O(n)')
print('Stack from 1 queue:  push O(n), pop O(1)')

Por que inverter restaura a ordem FIFO?

Quando os elementos 1, 2 e 3 são inseridos em uma pilha (caixa de entrada), eles ficam organizados da base ao topo como 1, 2 e 3. Remover todos eles para uma segunda pilha (caixa de saída) inverte a ordem: a caixa de saída fica com 3 na base e 1 no topo. Remover elementos da caixa de saída produz 1, depois 2 e depois 3 — exatamente a ordem de inserção FIFO. É por isso que exatamente duas inversões (duas pilhas) restauram FIFO, enquanto uma única pilha produziria LIFO.

# Demonstrate double-reversal = FIFO
inbox  = [1, 2, 3]   # pushed in this order
outbox = []
while inbox:
    outbox.append(inbox.pop())
print('outbox (one reversal):', outbox)  # [3, 2, 1] top-to-bottom

# Pop from outbox gives FIFO
result = []
while outbox:
    result.append(outbox.pop())
print('dequeued:', result)  # [1, 2, 3] — FIFO!

LeetCode 232: implementar uma fila usando pilhas

LeetCode 232 é o problema direto de «fila criada com duas pilhas». A solução esperada é a transferência preguiçosa para a caixa de saída. Em uma entrevista, explique que cada elemento passa da caixa de entrada para a caixa de saída no máximo uma vez, tornando todas as operações O(1) amortizadas. Mencione que chamadas individuais de pop podem custar O(n) no pior caso, quando a caixa de saída está vazia, mas a média ao longo de n operações é O(1).

class MyQueue:
    def __init__(self):
        self.inbox  = []
        self.outbox = []

    def push(self, x):
        self.inbox.append(x)

    def pop(self):
        self.peek()             # ensure outbox is populated
        return self.outbox.pop()

    def peek(self):
        if not self.outbox:
            while self.inbox:   # transfer lazily
                self.outbox.append(self.inbox.pop())
        return self.outbox[-1]

    def empty(self):
        return not self.inbox and not self.outbox

# Simulation
q = MyQueue()
q.push(1); q.push(2)
print(q.peek())  # 1
print(q.pop())   # 1
print(q.empty()) # False

LeetCode 225: implementar uma pilha usando filas

LeetCode 225 é o problema de «pilha criada com filas». A solução de uma fila com rotação ao usar push é a mais simples. Depois de inserir o elemento x, rotacione a fila, movendo para trás de x todos os elementos que já estavam nela. Isso custa O(n) por push, mas faz com que top e pop custem O(1). Explique a compensação e confirme que ela corresponde às restrições, por exemplo, uma carga de trabalho com poucas operações de push ou muitas operações de pop.

from collections import deque

class MyStack:
    def __init__(self):
        self.q = deque()

    def push(self, x):       # O(n)
        self.q.append(x)
        for _ in range(len(self.q) - 1):
            self.q.append(self.q.popleft())

    def pop(self):           # O(1)
        return self.q.popleft()

    def top(self):           # O(1)
        return self.q[0]

    def empty(self):
        return len(self.q) == 0

s = MyStack()
s.push(1); s.push(2); s.push(3)
print(s.top())  # 3
print(s.pop())  # 3
print(s.top())  # 2
print(s.empty()) # False

Estendendo para três pilhas em um único vetor

Um desafio de projeto relacionado: implemente três pilhas usando um único vetor. Uma abordagem divide o vetor em três seções fixas de mesmo tamanho. Uma abordagem mais flexível usa armazenamento intercalado com ponteiros, fazendo cada pilha crescer a partir de sua região e copiando os dados quando os limites colidem. Isso testa o gerenciamento de vetores dinâmicos e é uma questão comum em entrevistas de nível sênior. A abordagem das seções fixas é mais simples, mas desperdiça espaço se as pilhas crescerem de forma desigual.

class ThreeStacks:
    def __init__(self, size):
        self.data = [0] * (3 * size)
        self.tops = [-1, -1, -1]  # relative top of each stack
        self.size = size

    def push(self, stack_num, val):
        self.tops[stack_num] += 1
        if self.tops[stack_num] >= self.size:
            raise OverflowError('stack full')
        self.data[stack_num * self.size + self.tops[stack_num]] = val

    def pop(self, stack_num):
        if self.tops[stack_num] < 0:
            raise IndexError('stack empty')
        val = self.data[stack_num * self.size + self.tops[stack_num]]
        self.tops[stack_num] -= 1
        return val

ts = ThreeStacks(5)
ts.push(0, 10); ts.push(1, 20); ts.push(2, 30)
print(ts.pop(0), ts.pop(1), ts.pop(2))  # 10 20 30

Principais conclusões: padrões de simulação

Os problemas de simulação mútua ensinam um princípio mais amplo: qualquer estrutura de dados pode ser construída a partir de outra, desde que haja armazenamento intermediário e operações de reversão suficientes. O custo da simulação depende de quais operações você otimiza — sempre é possível tornar push O(1) ou pop O(1), mas tornar ambos O(1) exige amortização ou várias estruturas auxiliares.

Em uma entrevista, pergunte sempre: "Quais operações são mais frequentes?" Isso orienta a escolha da variante de implementação e demonstra uma visão de nível sênior sobre os requisitos operacionais.

Verificação rápida

Teste sua compreensão dos conceitos de Estruturas de dados e algoritmos — preparação para entrevistas de programação abordados nesta lição.

Recapitulação da lição

Nesta lição, você aprendeu: uma fila criada a partir de duas pilhas consegue fazer pop amortizado em O(1) ao transferir elementos da entrada para a saída sob demanda, uma pilha criada a partir de uma fila consegue fazer pop em O(1) ao girar a fila a cada push (push O(n)) e a escolha da operação que deve ser O(1) depende do padrão de uso. Em seguida, exploraremos os componentes internos dos mapas hash e o tratamento de colisões.

Grátis para começar

Aprenda Python com um tutor de IA — grátis

Escreva e execute código real no seu navegador, obtenha ajuda instantânea de um tutor de IA 24/7 e continue de onde parou na web ou no app.

Cursos
30
Aulas
120

Perguntas Frequentes

A aula “Simulação Mútua de Pilha e Fila” é grátis?

Sim — o texto completo de “Simulação Mútua de Pilha e Fila” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de DSA Interview Prep, atualize para CoddyKit PRO. O curso de DSA Interview Prep inclui 4 aulas no total.

O que vou aprender em “Simulação Mútua de Pilha e Fila”?

Implemente uma fila usando duas pilhas e uma pilha usando duas filas, explicando o custo amortizado de cada abordagem. Você pratica DSA Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar DSA Interview Prep?

Nenhuma experiência prévia é necessária. DSA Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.

Quanto tempo leva a aula “Simulação Mútua de Pilha e Fila”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de DSA Interview Prep?

Sim. Cada aula de DSA Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Implementação e Aplicações de Pilhas
  2. Implementação de Filas e Deque
  3. Padrão da Pilha Monotônica
  4. Simulação Mútua de Pilha e Fila
← Voltar para DSA Interview Prep