Lista Encadeada
Atividade Estrutura de Dados - Anhanguera - Unidade 1
count_nodesEnunciado. Implementar uma lista encadeada em Python. A atividade consiste em implementar uma
função chamada count_nodes, que recebe uma lista encadeada como parâmetro e retorna o número de
nós presentes na lista. A função percorre a lista usando um loop enquanto incrementa um
contador; ao final do percurso, o valor do contador é retornado.
Uma lista encadeada deve ser criada, alguns elementos adicionados com o método append, a lista
impressa, e então count_nodes chamada passando a lista como argumento — exibindo na tela o número
de nós presentes.
Espelho da atividade:
class Node:
def __init__(self, data):
class LinkedList:
def __init__(self):
def append(self, data):
def print_list(self):
def count_nodes(linked_list):
Sobre a nomenclatura: os nomes de classes, métodos e da função seguem exatamente o espelho fornecido (em inglês). Comentários, docstrings e variáveis internas estão em português.
É uma sequência de nós espalhados pela memória, em que cada nó guarda um dado e uma
referência para o próximo nó. A lista em si conhece apenas o primeiro nó — o head.
head
|
v
+------+------+ +------+------+ +------+------+
| data | next |---->| data | next |---->| data | next |----> None
| 10 | | | 20 | | | 30 | |
+------+------+ +------+------+ +------+------+
no 1 no 2 no 3 fim da lista
O None no último nó é o que marca o fim da lista — é exatamente essa condição que os loops
de append, print_list e count_nodes vão usar para saber quando parar.
| Característica | Lista encadeada | list do Python (vetor) |
|---|---|---|
| Memória | nós espalhados, ligados por referências | bloco contíguo |
| Acessar o i-ésimo elemento | O(n) — precisa caminhar até ele | O(1) — cálculo direto |
| Inserir no início | O(1) | O(n) — desloca todos os elementos |
| Saber o tamanho | O(n) — precisa contar | O(1) — o tamanho é armazenado |
| Crescer | sem realocação | pode precisar realocar o bloco |
A última linha é o motivo de existir esta atividade: em uma lista encadeada não existe um campo
com o tamanho, então descobrir quantos nós há só é possível percorrendo a estrutura de ponta a
ponta. É esse percurso que a count_nodes implementa.
O decorador adicionar_metodo é um recurso didático que permite escrever um método por célula,
com a explicação em Markdown logo acima. O resultado é idêntico a declarar todos os métodos dentro
do corpo da classe — que é como eles aparecem no arquivo .py de entrega.
import time
def adicionar_metodo(classe):
'''Decorador didatico: anexa a funcao decorada a classe informada como um metodo.'''
def decorador(funcao):
setattr(classe, funcao.__name__, funcao)
return funcao
return decorador
print("Ambiente pronto.")NodeUm nó é a peça mais simples da estrutura: dois campos, nada mais.
+-------------------+
| data | next |
+-------------------+
| |
| +---> referencia para o proximo no (ou None)
+--------------> o valor armazenado
O next começa sempre como None: um nó recém-criado ainda não está ligado a ninguém.
Quem faz essa ligação é o método append.
class Node:
'''Representa um no (elemento) da lista encadeada.'''
def __init__(self, data):
self.data = data # o dado armazenado no no
self.next = None # referencia para o proximo no (None = ainda nao ligado)
def __repr__(self):
return f"Node({self.data!r})"Antes de usar a classe LinkedList, vale ver o encadeamento acontecendo manualmente —
é literalmente atribuir o next de um nó apontando para outro.
primeiro = Node(10)
segundo = Node(20)
print("Antes de ligar:")
print(" primeiro:", primeiro, "| primeiro.next:", primeiro.next)
primeiro.next = segundo # aqui nasce o "encadeamento"
print("\nDepois de ligar:")
print(" primeiro:", primeiro, "| primeiro.next:", primeiro.next)
print(" segundo :", segundo, "| segundo.next :", segundo.next, "(fim da lista)")
print("\nAcessando o dado do segundo no a partir do primeiro:", primeiro.next.data)LinkedListA lista guarda apenas o head — a referência para o primeiro nó. Todo o resto é alcançado
seguindo os next.
self.head = None significa lista vazia, e essa é a condição que os métodos verificam antes
de qualquer percurso.
class LinkedList:
'''Lista simplesmente encadeada: guarda apenas a referencia para o primeiro no.'''
def __init__(self):
self.head = None # inicio da lista; None significa lista vaziaappend — adicionar no finalO método precisa tratar dois casos:
Caso 1 — lista vazia. Não há para onde caminhar; o novo nó vira o próprio head.
antes: head ----> None
append(10)
depois: head ----> [ 10 | None ]
Caso 2 — lista com elementos. É preciso caminhar até o último nó (aquele cujo next é
None) e ligar o novo nó nele.
antes: head ----> [ 10 | * ] ----> [ 20 | None ]
^
|
no_atual para aqui: next e None = ultimo no
append(30)
depois: head ----> [ 10 | * ] ----> [ 20 | * ] ----> [ 30 | None ]
Atenção à condição do loop: usamos while no_atual.next is not None (e não
while no_atual is not None). A diferença é essencial: precisamos parar no último nó para
poder alterá-lo, e não passar direto por ele até o None.
Custo: O(n) — o percurso até o fim cresce com o tamanho da lista.
@adicionar_metodo(LinkedList)
def append(self, data):
'''Adiciona um novo no com 'data' no FINAL da lista. Custo O(n).'''
novo_no = Node(data)
# Caso 1: lista vazia -> o novo no passa a ser o inicio
if self.head is None:
self.head = novo_no
return
# Caso 2: percorre ate o ULTIMO no (aquele cujo next e None)
no_atual = self.head
while no_atual.next is not None:
no_atual = no_atual.next
# Liga o ultimo no ao novo no
no_atual.next = novo_no# Teste do append
lista_de_teste = LinkedList()
print("Lista recem-criada. head =", lista_de_teste.head, "(lista vazia)")
lista_de_teste.append(10)
print("Apos append(10): head =", lista_de_teste.head)
lista_de_teste.append(20)
lista_de_teste.append(30)
print("Apos append(20) e append(30):")
print(" head ->", lista_de_teste.head)
print(" head.next ->", lista_de_teste.head.next)
print(" head.next.next ->", lista_de_teste.head.next.next)
print(" head.next.next.next ->", lista_de_teste.head.next.next.next, "(fim da lista)")print_list — imprimir a listaO padrão de percurso aqui é o que se repetirá em count_nodes:
no_atual = self.head # comeca no inicio
while no_atual is not None: # enquanto nao chegar ao fim
...faz algo com no_atual... # neste caso: guarda o dado
no_atual = no_atual.next # avanca
Note que agora a condição é while no_atual is not None — queremos visitar todos os nós,
inclusive o último, então só paramos quando a referência vira None.
Custo: O(n).
@adicionar_metodo(LinkedList)
def print_list(self):
'''Imprime a lista no formato: 10 -> 20 -> 30 -> None. Custo O(n).'''
elementos = []
no_atual = self.head
while no_atual is not None:
elementos.append(str(no_atual.data))
no_atual = no_atual.next
elementos.append("None") # marca visualmente o fim da lista
print(" -> ".join(elementos))# Teste do print_list
lista_de_teste.print_list()
lista_vazia_de_teste = LinkedList()
print("Lista vazia:", end=" ")
lista_vazia_de_teste.print_list()count_nodes — o objetivo da atividadeConforme o enunciado, ela percorre a lista com um loop enquanto incrementa um contador e devolve o contador ao final.
Três detalhes fazem a função funcionar:
contador = 0 — começa do zero, o que já resolve o caso da lista vazia automaticamente;no_atual = linked_list.head — o ponto de partida é o início da lista;no_atual = no_atual.next — avançar dentro do loop. Esquecer esta linha é o erro mais comum
e produz um loop infinito, já que a condição nunca deixaria de ser verdadeira.10 -> 20 -> 30| Iteração | no_atual antes | contador depois | no_atual depois | Continua? |
|---|---|---|---|---|
| 1 | Node(10) | 1 | Node(20) | sim |
| 2 | Node(20) | 2 | Node(30) | sim |
| 3 | Node(30) | 3 | None | não — loop encerra |
Resultado: 3 nós.
A função é definida fora da classe, como uma função independente que recebe a lista por parâmetro — exatamente como pede o espelho da atividade.
def count_nodes(linked_list):
'''Percorre a lista encadeada com um loop e devolve o numero de nos. Custo O(n).'''
contador = 0 # inicializa o contador
no_atual = linked_list.head # comeca pelo primeiro no
while no_atual is not None: # enquanto nao chegar ao fim da lista
contador += 1 # incrementa o contador
no_atual = no_atual.next # avanca para o proximo no
return contador # devolve o total contadoVersão instrumentada da mesma função, apenas para visualizar o rastreio da tabela acima (não faz parte da entrega).
def count_nodes_com_rastreio(linked_list):
'''Mesma logica de count_nodes, imprimindo o estado a cada iteracao.'''
contador = 0
no_atual = linked_list.head
iteracao = 0
print(f"{'iteracao':<10}{'no_atual':<16}{'contador':<10}proximo")
print("-" * 48)
while no_atual is not None:
iteracao += 1
contador += 1
proximo = no_atual.next
print(f"{iteracao:<10}{str(no_atual):<16}{contador:<10}{proximo}")
no_atual = proximo
print("-" * 48)
print(f"Loop encerrado: no_atual e None. Contador final = {contador}")
return contador
count_nodes_com_rastreio(lista_de_teste)Exatamente o roteiro pedido: criar a lista, adicionar elementos com append, imprimir a lista,
chamar count_nodes passando a lista como argumento e exibir o resultado.
# Cria a lista encadeada
lista = LinkedList()
# Adiciona alguns elementos usando o metodo append
for valor in [10, 20, 30, 40, 50]:
lista.append(valor)
# Imprime a lista
print("Lista encadeada:")
lista.print_list()
# Chama a funcao count_nodes passando a lista encadeada como argumento
quantidade_de_nos = count_nodes(lista)
# Exibe o resultado na tela
print(f"Numero de nos presentes na lista: {quantidade_de_nos}")Os dois casos que costumam quebrar implementações de lista encadeada: a lista vazia e a lista com um único elemento.
lista_vazia = LinkedList()
print("Lista vazia .............:", end=" ")
lista_vazia.print_list()
print(" count_nodes ->", count_nodes(lista_vazia), "(deve ser 0)")
lista_de_um = LinkedList()
lista_de_um.append("Pikachu")
print("\nLista com um elemento ...:", end=" ")
lista_de_um.print_list()
print(" count_nodes ->", count_nodes(lista_de_um), "(deve ser 1)")O campo data não impõe tipo algum — cada nó pode guardar coisas diferentes.
Repare na saída: o último
Noneé o marcador de fim de lista, mas o penúltimo é um dado de verdade armazenado em um nó. Visualmente ficam iguais, e ainda assimcount_nodesconta 5 nós corretamente — porque ela olha as referências, não os dados.
lista_mista = LinkedList()
for valor in ["texto", 42, 3.14, True, None]:
lista_mista.append(valor)
lista_mista.print_list()
print("count_nodes ->", count_nodes(lista_mista), "(deve ser 5)")for tamanho in [10, 100, 1000, 5000]:
lista_grande = LinkedList()
for numero in range(tamanho):
lista_grande.append(numero)
print(f" {tamanho:>5} elementos adicionados -> count_nodes = {count_nodes(lista_grande)}")verificacoes = {
"lista vazia devolve 0": count_nodes(LinkedList()) == 0,
"lista com 1 elemento devolve 1": count_nodes(lista_de_um) == 1,
"lista com 5 elementos devolve 5": count_nodes(lista) == 5,
"lista com tipos variados devolve 5": count_nodes(lista_mista) == 5,
"count_nodes nao altera a lista": (count_nodes(lista), count_nodes(lista)) == (5, 5),
"head da lista vazia e None": LinkedList().head is None,
"append mantem a ordem de insercao": [no.data for no in [lista.head, lista.head.next]] == [10, 20],
}
for descricao, resultado in verificacoes.items():
print(f" [{'OK ' if resultado else 'FALHOU'}] {descricao}")
print("\nTodos os testes passaram!" if all(verificacoes.values()) else "\nHa testes falhando.")| Operação | Custo | Por quê |
|---|---|---|
Node.__init__ | O(1) | duas atribuições |
append | O(n) | percorre até o último nó |
print_list | O(n) | visita todos os nós |
count_nodes | O(n) | visita todos os nós, gastando O(1) de memória |
Consequência prática: construir uma lista com n chamadas de append custa
1 + 2 + ... + n = O(n²), e não O(n). Isso é visível no teste abaixo — dobrar o tamanho
multiplica o tempo de construção por cerca de 4, enquanto o tempo de count_nodes apenas
dobra.
print(f"{'n':>7} {'construcao (ms)':>18} {'count_nodes (ms)':>18}")
print("-" * 45)
for tamanho in [1000, 2000, 4000, 8000]:
inicio = time.perf_counter()
lista_medida = LinkedList()
for numero in range(tamanho):
lista_medida.append(numero)
tempo_construcao = (time.perf_counter() - inicio) * 1000
inicio = time.perf_counter()
total = count_nodes(lista_medida)
tempo_contagem = (time.perf_counter() - inicio) * 1000
print(f"{tamanho:>7} {tempo_construcao:>18.2f} {tempo_contagem:>18.2f}")As três variações abaixo não fazem parte da entrega — servem para comparar abordagens e entender melhor as escolhas da implementação principal.
append em O(1) guardando a caudaSe a lista também guardar uma referência para o último nó (tail), o append deixa de
percorrer a lista e passa a custar O(1). Construir a lista vira O(n) em vez de O(n²).
class LinkedListComCauda(LinkedList):
'''Variacao que guarda tambem o ultimo no, tornando o append O(1).'''
def __init__(self):
super().__init__()
self.tail = None # referencia para o ULTIMO no
def append(self, data):
novo_no = Node(data)
if self.head is None:
self.head = novo_no
else:
self.tail.next = novo_no # liga direto, sem percorrer nada
self.tail = novo_no
inicio = time.perf_counter()
lista_com_cauda = LinkedListComCauda()
for numero in range(8000):
lista_com_cauda.append(numero)
tempo_com_cauda = (time.perf_counter() - inicio) * 1000
print(f"Construcao de 8000 nos com cauda: {tempo_com_cauda:.2f} ms")
print(f"count_nodes -> {count_nodes(lista_com_cauda)}")
print("(compare com o tempo de construcao da versao O(n) na tabela anterior)")Elegante, mas pior na prática: gasta O(n) de memória na pilha de chamadas e estoura o limite
de recursão do Python (padrão: 1000) em listas grandes. A versão iterativa do enunciado gasta O(1).
def count_nodes_recursivo(no):
'''Conta os nos recursivamente a partir de um no. Custo O(n) de tempo E de memoria.'''
if no is None:
return 0
return 1 + count_nodes_recursivo(no.next)
print("Recursivo ->", count_nodes_recursivo(lista.head))
print("Iterativo ->", count_nodes(lista))
try:
lista_profunda = LinkedList()
for numero in range(5000):
lista_profunda.append(numero)
count_nodes_recursivo(lista_profunda.head)
except RecursionError:
print("\nRecursionError com 5000 nos - a versao iterativa lida com isso sem problema:",
count_nodes(lista_profunda))size?Bastaria incrementar um contador dentro do append para que o tamanho fosse conhecido em O(1) —
é o que faz o list do Python. Mas isso anularia o objetivo da atividade, que é justamente
exercitar o percurso da estrutura com um loop.
Vale saber que existe o trade-off: O(1) na consulta, ao custo de manter o contador sincronizado
em toda inserção e remoção — e é aí que aparecem os bugs quando a estrutura cresce.
prepend(data) — insira um nó no início da lista. Por que essa operação é O(1),
enquanto o append é O(n)?search(data) — devolva True se o valor existir na lista. Qual o custo no pior caso?delete(data) — remova o primeiro nó com aquele valor (cuidado com o caso em que o nó
removido é o head).reverse() — inverta a lista alterando apenas os ponteiros next, sem criar nós novos.next do último nó apontar para um nó anterior, count_nodes entra
em loop infinito. Implemente uma versão que detecte isso (algoritmo de Floyd).# Espaco para os exercicios
# Exercicio 1 - prepend
@adicionar_metodo(LinkedList)
def prepend(self, data):
'''Insere um no no INICIO da lista. Custo O(1).'''
# Insira aqui seu codigo
pass
lista_exercicio = LinkedList()
for valor in [20, 30]:
lista_exercicio.append(valor)
lista_exercicio.prepend(10)
lista_exercicio.print_list()
print("Esperado: 10 -> 20 -> 30 -> None | count_nodes =", count_nodes(lista_exercicio))A atividade foi cumprida com as classes Node e LinkedList (com os métodos append e
print_list) e a função independente count_nodes, que percorre a lista com um loop while
enquanto incrementa um contador e devolve o total ao final.
O ponto conceitual: em uma lista encadeada o tamanho não é armazenado — os nós só conhecem seus
vizinhos imediatos. Descobrir quantos são exige caminhar do head até o None final, o que torna
a contagem O(n) de tempo e O(1) de memória.
Execução em Python roda no seu navegador via Pyodide (WebAssembly). Pacotes como numpy, pandas e matplotlib são suportados; outros exigem wheels puro-Python via micropip.