Há alguns anos, na entrevista de emprego do Google, um amigo recebeu o seguinte desafio: “implemente uma fila com duas pilhas”. Achei o desafio interessante, então decidi pensar sobre esse problema (para o problema inverso veja o próximo post). Antes de continuar lendo, pense como você resolveria esse desafio.
...
Pronto?
Vamos começar com uma breve revisão de pilhas e filas.
Pilhas e filas
Uma pilha é uma coleção de elementos baseada no conceito de Last In First Out (LIFO), ou seja, o último elemento a ser adicionado é o primeiro a sair. Ela possui duas operações básicas: empilhar (push - adiciona um elemento no final da pilha) e desempilhar (pop - remove e devolve o último elemento da pilha).
Analogamente, uma fila é uma estrutura de dados baseada em First In First Out (FIFO), ou seja, o primeiro elemento a ser adicionado é o primeiro a sair (pense em uma fila de banco, por exemplo). Ela também possui duas operações básicas: insere (adiciona um elemento no final da fila) e remove (remove o primeiro elemento da fila).
Implementação
Vamos usar a seguinte implementação de uma pilha em Python (não se preocupe se você não souber Python - o resto do código não depende de nenhuma funcionalidade específica):
class Pilha(object):
def __init__(self):
self.elementos = []
def empilha(self, elemento):
self.elementos.append(elemento)
def desempilha(self):
return self.elementos.pop()
def vazio(self):
return len(self.elementos) == 0
Adicionei uma função extra vazio
para dizer se a pilha está vazia ou não. Agora, vamos à implementação da fila com duas pilhas:
class Fila(object):
def __init__(self):
self.normal = Pilha()
self.invertida = Pilha()
def insere(self, elemento):
transfere(self.invertida, self.normal)
self.normal.empilha(elemento)
def remove(self):
transfere(self.normal, self.invertida)
return self.invertida.desempilha()
def vazio(self):
return self.normal.vazio() and self.invertida.vazio()
def transfere(origem, destino):
while (not origem.vazio()):
destino.empilha(origem.desempilha())
Explicação
O “truque” é usar uma pilha como a fila normal e outra como a fila invertida. Após qualquer operação todos os elementos estão ou na fila normal ou na fila invertida, ou seja, sempre ao menos uma das duas pilhas está vazia. Esse invariante é essencial para o funcionamento da nossa implementação. Vamos então entender cada função.
A função auxiliar transfere
desempilha todos os elementos da origem
e os empilha, em ordem, no destino
. Assumindo que uma das pilhas está vazia temos dois casos:
- A pilha
origem
está vazia: nada acontece. - A pilha
destino
está vazia: os elementos daorigem
são removidos de trás para a frente e adicionados nessa ordem na pilhadestino
. Em outras palavras, a pilhaorigem
é invertida na pilhadestino
.
Assim, podemos entender que a função transfere
sempre inverte o conteúdo da origem
e adiciona no destino
. Note que ao final dessa operação a pilha origem
sempre está vazia, assim o invariante de que uma das pilhas está vazia continua válido.
A função insere
deve adicionar um elemento no final da fila. A função empilha
insere um elemento no final de uma pilha. Assim, basta empilhar o novo elemento na pilha normal. Novamente, considerando nosso invariante, temos dois casos:
- A pilha invertida está vazia: a função
transfere
não faz nada e todos os elementos já estão em ordem na pilha normal. - A pilha normal está vazia: a função
transfere
inverte a pilha invertida (ou seja, coloca na ordem correta) e adiciona na pilha normal.
Em ambos os casos, após a chamada de transfere
a pilha invertida está vazia e a pilha normal contém todos os elementos. O novo elemento é adicionado ao final da pilha normal e o invariante ainda é válido.
Finalmente a função remove
deve remover o primeiro elemento da fila. Esse é o motivo de termos uma pilha que guarda a fila invertida, pois remover o último elemento da fila invertida (desempilhar) é o mesmo que remover o primeiro elemento da fila não invertida. Agora queremos garantir que os elementos estejam na pilha invertida, ou seja, invertemos e transferimos os elementos da pilha normal para a pilha invertida. De acordo com o nosso invariante temos, mais uma vez:
- A pilha invertida está vazia: a função
transfere
inverte a pilha normal e transfere os elementos para a pilha invertida. - A pilha normal está vazia: a função
transfere
não faz nada.
O último elemento da pilha invertida (primeiro elemento da fila) é então removido e assim o invariante continua verdadeiro. Como a função vazio
não altera nenhuma das pilhas e na condição inicial ambas as pilhas estão vazias, o invariante é sempre verdadeiro.
Simulação
Para facilitar o entendimento vamos mostrar uma simulação. Vamos inserir os números de 0 a 4 na fila. A partir do terceiro elemento, ao adicionar um elemento vamos imediatamente remover um elemento da fila.
Inicialmente inserimos os números 0, 1 e 2 na fila. Como a pilha invertida (I) está vazia, eles simplesmente serão empilhados na pilha normal (N):
0 | |
I | N |
→
1 | |
0 | |
I | N |
→
2 | |
1 | |
0 | |
I | N |
Ao remover um elemento todos os elementos da pilha normal são transferidos para a invertida
2 | |
1 | |
0 | |
I | N |
→
1 | |
2 | 0 |
I | N |
→
1 | |
2 | 0 |
I | N |
→
0 | |
1 | |
2 | |
I | N |
e então o último elemento é removido (desempilhado).
0 | |
1 | |
2 | |
I | N |
→
1 | |
2 | |
I | N |
Note que na remoção o número 0, ou seja o primeiro elemento adicionado, foi removido. Adicionamos então os números 3 e 4. Para isso os elementos da pilha invertida são transferidos para a pilha normal
1 | |
2 | |
I | N |
→
2 | 1 |
I | N |
→
2 | |
1 | |
I | N |
e os novos elementos são adicionado (empilhado) no final da pilha normal (tecnicamente seria necessário transferir os elementos da pilha invertida para a normal, mas a pilha invertida já está vazia então vamos ignorar esse passo)
2 | |
1 | |
I | N |
→
3 | |
2 | |
1 | |
I | N |
→
4 | |
3 | |
2 | |
1 | |
I | N |
Vamos agora remover todos os elementos até que a fila esteja vazia. Para isso devemos novamente transferir os elementos da pilha normal para a invertida
4 | |
3 | |
2 | |
1 | |
I | N |
→
1 | |
2 | |
3 | |
4 | |
I | N |
e então desempilhar os elementos do último para o primeiro
2 | |
3 | |
4 | |
I | N |
→
3 | |
4 | |
I | N |
→
4 | |
I | N |
→
I | N |
Se voltarmos à simulação veremos que os elementos foram removidos da fila na mesma ordem em que foram inseridos, ou seja, temos uma FIFO (o primeiro a entrar é o primeiro a sair).
Essa foi a nossa implementação de uma fila com duas pilhas. E aí, você saberia implementar uma pilha com duas filas?