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.
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()) # 3Aná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 nPilha 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()) # 2Pilha 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()) # 2Resumo 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()) # FalseLeetCode 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()) # FalseEstendendo 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 30Principais 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.
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
- Implementação e Aplicações de Pilhas
- Implementação de Filas e Deque
- Padrão da Pilha Monotônica
- Simulação Mútua de Pilha e Fila