Listas encadeadas, pilhas e filas
No vetor, os elementos ficam colados uns nos outros. Hoje vamos espalhá-los pela memória e ligá-los por referências, e usar essa ideia para construir as duas estruturas mais usadas da computação: a pilha e a fila.
Navegação: ← → trocam de etapa · V alterna entre etapas e página única · M sumário · F tela cheia · + − tamanho do texto · dentro do código, Ctrl+Enter executa.
Todo código desta aula pode ser editado e executado aqui mesmo. Altere os exemplos à vontade; o botão ↺ restaura o original. Suas alterações ficam salvas neste navegador; em Progresso você pode baixá-las em um arquivo e continuar em outro computador.
As visualizações mostram cada ligação sendo feita: escolha a operação, use Próximo passo ou Animar. Nos exercícios, o botão ✓ Verificar testa seu código com várias entradas.
Vetor ou corrente de nós?
Na aula passada vimos que, no vetor, inserir ou remover no início desloca todos os elementos: O(n).
A lista encadeada resolve isso guardando cada elemento em um nó separado, que pode estar em qualquer lugar da memória. Cada nó guarda:
- o valor;
- uma referência (
prox) para o próximo nó, ouNonese for o último.
Para inserir no meio, basta mudar duas referências. Nada é deslocado.
prox faz: aponta para outro nó.Percorrendo a lista
Não há índices: só conhecemos o primeiro nó. Para visitar os outros, uma variável p segue as referências até chegar a None.
Esse laço é o “for” das listas encadeadas e aparece em quase toda operação:
p = p.prox deixa o laço preso no mesmo nó para sempre. E nunca ande com o próprio inicio: você perderia a entrada da lista.A classe ListaEncadeada
Uma classe guarda a “entrada” da lista e esconde os detalhes dos nós:
inicio: o primeiro nó (ouNone, se vazia);fim: o último nó. Sem ele, inserir no fim exigiria percorrer tudo;tamanho: atualizado a cada operação, paralen()ser O(1).
Os métodos especiais __len__ e __str__ fazem len(lista) e print(lista) funcionarem.
AttributeError: 'NoneType' object has no attribute 'prox'.Lista encadeada passo a passo
Escolha a operação e acompanhe cada linha de código. As etiquetas mostram para onde apontam início, fim, p e ant. A lista resultante vira o ponto de partida da próxima operação.
Inserir na posição, buscar e remover
Inserir na posição pos
Ande até o nó anterior (posição pos − 1) e faça duas ligações, nesta ordem: o novo aponta para o sucessor; o anterior aponta para o novo.
Remover um valor
Ande com dois ponteiros: p procura o valor e ant fica um passo atrás. Achou? ant.prox = p.prox: o anterior “pula” o nó removido.
Casos especiais: remover o primeiro (não há anterior: muda inicio) e remover o último (atualize fim).
Cuidado com a ordem das ligações
Um nó só continua acessível enquanto alguém aponta para ele. Se você sobrescrever a única referência para o resto da lista, ela se perde.
No exemplo errado, a.prox = novo acontece antes de guardar quem vinha depois de a. Resultado: o 20 e o 30 saem da lista, e novo.prox = a.prox faz o novo nó apontar para ele mesmo, um laço sem fim.
Vetor × lista encadeada: quanto custa cada operação
| Operação | Vetor (list) | Lista encadeada simples (com fim) |
|---|---|---|
| acessar a posição i | O(1) | O(n): é preciso andar i nós |
| inserir ou remover no início | O(n): desloca todos | O(1) |
| inserir no fim | O(1) amortizado | O(1) |
| remover do fim | O(1) | O(n): precisa achar o penúltimo (O(1) na dupla) |
| inserir ou remover no meio, já estando no lugar | O(n) deslocamentos | O(1): só troca referências |
| buscar um valor | O(n), ou O(log n) se ordenado | O(n): busca binária não funciona sem acesso direto |
| memória extra | pouca (algum espaço reservado) | uma referência por nó (duas na dupla) |
Não existe vencedor absoluto. A lista encadeada brilha quando há muitas inserções e remoções nas pontas ou em lugares que já conhecemos; o vetor, quando precisamos de acesso por posição.
Lista duplamente encadeada
Cada nó guarda duas referências: prox (para frente) e ant (para trás).
- Dá para percorrer nos dois sentidos.
- Com um nó em mãos, removê-lo é O(1): ele mesmo sabe quem é o anterior.
- Remover do fim também fica O(1):
fim.anté o novo último.
O custo: mais memória por nó e o dobro de ligações para manter corretas em cada operação.
collections.deque do Python, que usa uma lista dupla de blocos.Lista dupla passo a passo
As setas ⇄ representam o par prox/ant. Repare que cada inserção no meio mexe em quatro referências, não em duas (e a remoção, em duas, não em uma).
ant andando atrás de p, porque cada nó já sabe quem vem antes.Lista circular: o rodízio
Na lista circular, o último nó aponta de volta para o primeiro. Não existe None no fim: andando sempre para frente, damos voltas.
Útil para tudo que funciona em rodízio: a vez dos jogadores, o escalonador de processos de um sistema operacional (cada processo usa a CPU um pouco e passa a vez), playlists em repetição.
p is None (isso nunca acontece!), e sim voltar ao nó inicial.Pilha: o último a entrar é o primeiro a sair
Uma pilha (stack) só permite mexer no topo, como uma pilha de pratos. Política LIFO: last in, first out.
| Operação | Em Python (list) | Custo |
|---|---|---|
| empilhar (push) | p.append(x) | O(1) |
| desempilhar (pop) | p.pop() | O(1) |
| ver o topo (peek) | p[-1] | O(1) |
| está vazia? | not p ou len(p) == 0 | O(1) |
O topo é o fim da list: lá, inserir e remover não deslocam ninguém.
Pilha com nós
Também dá para montar a pilha sobre uma lista encadeada: o topo é o início da lista. Empilhar é inserir no início e desempilhar é remover do início, ambos O(1).
Desempilhar de uma pilha vazia é um erro de quem usa a estrutura. A classe avisa lançando uma exceção com raise, como a list faz em [].pop().
p.vazia() antes.Repare: quem usa a classe só enxerga empilhar, desempilhar, espiar e vazia. Isso é um tipo abstrato de dados: o “o quê” separado do “como”. Trocar a implementação (list ou nós) não muda o resto do programa.
Aplicação: parênteses balanceados
Todo compilador verifica se ( ), [ ] e { } estão bem aninhados. O último que abriu tem de ser o primeiro a fechar: é uma pilha.
Três jeitos de errar, três testes:
- fecha com a pilha vazia:
(a+b)); - fecha com o par errado no topo:
([)]; - termina com a pilha não vazia:
((x).
Aplicação: expressões pós-fixas
Na notação pós-fixa (polonesa reversa), o operador vem depois dos operandos: 3 4 + significa 3 + 4. Não precisa de parênteses nem de regras de precedência, e uma pilha avalia tudo em uma passada.
Calculadoras HP, a máquina virtual do Java e o próprio Python (que compila seu código para instruções de uma pilha) usam essa ideia.
8 2 -, desempilhamos 2 e depois 8, e calculamos 8 − 2.Aplicação: desfazer e refazer
O Ctrl+Z de qualquer editor usa duas pilhas:
- cada ação empilha o estado anterior em
desfazer; - desfazer tira o topo de
desfazere guarda o estado atual emrefazer; - refazer faz o caminho inverso;
- uma ação nova esvazia
refazer: o futuro alternativo se perde.
O botão “voltar” do navegador funciona do mesmo jeito, com páginas no lugar de textos. E a pilha de chamadas de funções, que veremos em recursão, também.
Fila: o primeiro a entrar é o primeiro a sair
Na fila (queue), quem chega entra no fim e quem é atendido sai da frente. Política FIFO: first in, first out.
Exemplos: fila de impressão, requisições a um servidor, mensagens entre programas, a ordem de visita em um grafo (busca em largura, mais adiante).
list, append é O(1), mas tirar da frente com pop(0) desloca todos os outros: O(n). Para filas, use collections.deque, que faz as duas pontas em O(1).Fila com nós
Sobre uma lista encadeada simples com inicio e fim:
- enfileirar = inserir no fim: O(1);
- desenfileirar = remover do início: O(1).
Por que não o contrário (entrar no início e sair pelo fim)? Porque remover do fim de uma lista simples exige achar o penúltimo: O(n).
fim também precisa voltar a None. Senão, o próximo enfileirar liga o nó novo a um nó que já saiu.Fila circular em vetor
Dá para fazer uma fila O(1) em um vetor de tamanho fixo sem deslocar ninguém: em vez de mover os elementos, movemos o índice da frente. Quando um índice passa do fim do vetor, volta ao 0 com o operador %.
Enfileire até encher, desenfileire alguns e enfileire de novo: veja os novos elementos ocupando o início do vetor.
collections.deque: pilha e fila prontas
deque (double-ended queue, “fila de duas pontas”) permite inserir e remover nas duas pontas em O(1). Serve como pilha, como fila ou como os dois.
| Método | O que faz |
|---|---|
append(x) / pop() | direita (fim) |
appendleft(x) / popleft() | esquerda (frente) |
d[0], d[-1] | espia as pontas |
rotate(k) | gira k posições (circular!) |
deque(maxlen=n) | guarda só os n últimos |
d[i], é O(n) em um deque. Ele é feito para as pontas.Desafio: inverter uma lista encadeada
Inverta a lista sem criar nós novos e sem usar listas do Python: apenas mude os campos prox, para que cada nó aponte para o anterior.
Dica: ande com três referências. p é o nó atual, ant é o que ficou para trás e prox guarda o resto da lista antes de você mudar p.prox.
Depois de resolver: qual é a complexidade? Quanta memória extra ela usa?
Mostrar uma solução
Qual estrutura usar?
Leia a situação para a turma, colete as respostas e só então clique.
Qual será a saída?
Leia o código, escreva sua resposta e só então clique. Errou? Copie o código para o laboratório e execute.
Prática 1: listas encadeadas
Escreva sua solução no editor. ▶ Executar roda com a entrada que você quiser; ✓ Verificar roda os testes automáticos e mostra o que falta. Tente antes de abrir a solução!
1. Tamanho, soma e maior básico
O código já lê os números e monta uma lista encadeada com eles. Percorrendo os nós (sem usar a lista numeros), mostre o tamanho, a soma e o maior valor.
| Entrada | Saída esperada |
|---|---|
4 8 1 | Tamanho: 3 Soma: 13 Maior: 8 |
Mostrar uma solução
2. Remover todas as ocorrências médio
Complete o método remover_todos(x): ele remove todos os nós com valor x e devolve quantos removeu. Cuidado com os casos em que o removido é o primeiro, o último ou todos. O programa principal já está pronto e testa também se fim ficou certo, inserindo um 0 no fim.
| Entrada | Saída esperada |
|---|---|
1 2 3 2 4 2 | Removidos: 2 Lista: 1 -> 3 -> 4 -> None Com 0 no fim: 1 -> 3 -> 4 -> 0 -> None |
Mostrar uma solução
Prática 2: pilhas
Use uma list como pilha: só append, pop e [-1].
3. Delimitadores balanceados básico
Leia uma linha de texto e verifique se ( ), [ ] e { } estão balanceados e bem aninhados. Outros caracteres são ignorados. Mostre Balanceada ou Não balanceada.
| Entrada | Saída esperada |
|---|---|
{[a+(b*c)]-d} | Balanceada |
([)] | Não balanceada |
Mostrar uma solução
4. Calculadora pós-fixa médio
Leia uma expressão pós-fixa com inteiros não negativos e os operadores + - * / separados por espaço (/ é a divisão inteira, //). Mostre Resultado: R. Se faltarem operandos para algum operador, ou sobrar mais de um valor no fim, mostre Expressão inválida.
| Entrada | Saída esperada |
|---|---|
3 4 + 2 * | Resultado: 14 |
2 3 | Expressão inválida |
Mostrar uma solução
Prática 3: filas
Use collections.deque (ou a sua classe Fila): nada de pop(0) em list.
5. Fila de atendimento médio
Leia comandos, um por linha, até fim:
chega NOME: a pessoa entra na fila (o nome pode ter espaços);atende: mostraAtendendo: NOMEpara quem está na frente, ouFila vazia;fim: mostraRestam na fila: Ke termina.
Mostrar uma solução
6. A roda da eliminação desafio
Há n pessoas numeradas de 1 a n em roda. Começando pela pessoa 1, conta-se k pessoas (a primeira contada é a 1) e a k-ésima sai; a contagem recomeça pela pessoa seguinte. Leia n k e mostre a ordem de eliminação e o sobrevivente (problema de Josephus).
| Entrada | Saída esperada |
|---|---|
7 3 | Eliminados: 3 6 2 7 5 1 Sobrevivente: 4 |
Dica: em um deque, passar as k − 1 primeiras pessoas da frente para o fim simula a roda.
Mostrar uma solução
Laboratório livre
Espaço para testar ideias durante a aula. Sugestões: acrescente à ListaEncadeada um método remover_fim() e conte quantos nós ele visita; implemente uma pilha com dois métodos e use-a para inverter uma palavra; simule uma fila de impressão em que cada documento leva um tempo diferente.