Estruturas de Dados

Listas encadeadas, pilhas e filas

Aula interativa Prof. Raul de Araújo Lima · Sistemas de Informação · UniCatólica

Python carregando…

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ó, ou None se for o último.

Para inserir no meio, basta mudar duas referências. Nada é deslocado.

Em Python, uma variável que guarda um objeto guarda na verdade uma referência para ele. É exatamente isso que o campo 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 = iniciowhile p is not None:p = p.prox
Esquecer 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ó (ou None, se vazia);
  • fim: o último nó. Sem ele, inserir no fim exigiria percorrer tudo;
  • tamanho: atualizado a cada operação, para len() ser O(1).

Os métodos especiais __len__ e __str__ fazem len(lista) e print(lista) funcionarem.

Todo método precisa tratar a lista vazia: é o caso que mais causa erros como 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.

Compare as operações: inserir no início e no fim não visitam nenhum nó; inserir na posição, buscar e remover precisam andar pela lista. Quantos nós são visitados para remover o último?

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).

inserir na posição → O(n)buscar → O(n)remover → O(n)

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.

Regra prática: ao inserir, primeiro ligue o nó novo ao resto da lista; depois ligue a lista ao nó novo. Na dúvida, desenhe as setas no papel antes de programar.

Vetor × lista encadeada: quanto custa cada operação

OperaçãoVetor (list)Lista encadeada simples (com fim)
acessar a posição iO(1)O(n): é preciso andar i nós
inserir ou remover no inícioO(n): desloca todosO(1)
inserir no fimO(1) amortizadoO(1)
remover do fimO(1)O(n): precisa achar o penúltimo (O(1) na dupla)
inserir ou remover no meio, já estando no lugarO(n) deslocamentosO(1): só troca referências
buscar um valorO(n), ou O(log n) se ordenadoO(n): busca binária não funciona sem acesso direto
memória extrapouca (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.

Listas encadeadas são a base de pilhas, filas, tabelas hash com encadeamento e listas de adjacência de grafos, que veremos nas próximas aulas.

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.

Exemplos: a lista de músicas de um player (anterior/próxima), o histórico do navegador e 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).

Na remoção, compare com a lista simples (troque o tipo no seletor): a dupla não precisa do ponteiro 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.

Para percorrer uma volta, a condição de parada não é mais 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çãoEm 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) == 0O(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().

Execute: a última linha tenta desempilhar de uma pilha vazia. Leia a mensagem de erro. Como evitar? Teste 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.

(2 + 3) * 4 → 2 3 + 4 *2 + 3 * 4 → 2 3 4 * +5 + (1 + 2) * 4 − 3 → 5 1 2 + 4 * + 3 −

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.

O primeiro valor desempilhado é o segundo operando. Em 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 desfazer e guarda o estado atual em refazer;
  • 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).

Com 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).

Detalhe fácil de esquecer: quando a fila fica vazia, 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 %.

trás = (frente + tamanho) % capacidadefrente = (frente + 1) % capacidade

Enfileire até encher, desenfileire alguns e enfileire de novo: veja os novos elementos ocupando o início do vetor.

Esse é o buffer circular usado em teclados, placas de rede e players de áudio: memória fixa, sem alocação, tudo O(1). A desvantagem é a capacidade limitada.

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étodoO 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
Acessar o meio, 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?

1 → 2 → 3 → 4 → None→ 4 → 3 → 2 → 1 → None
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.

EntradaSaí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.

EntradaSaí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.

EntradaSaí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.

EntradaSaí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: mostra Atendendo: NOME para quem está na frente, ou Fila vazia;
  • fim: mostra Restam na fila: K e 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).

EntradaSaí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.