Introdução
Quando falamos sobre estruturas de dados, Listas Encadeadas (Linked List) é uma das primeiras que devemos aprender. Isso porque ela serve como base para várias outras estruturas, como pilhas (stacks), filas (queues) e árvores binárias. Além disso, dominar as Listas Encadeadas ajudará você a desenvolver um raciocínio lógico mais sólido e a resolver problemas de forma mais eficiente.
Diferente dos arrays, onde os elementos são armazenados em posições contínuas na memória, uma Lista Encadeada é composta por nós que se conectam entre si por referências. Por esse motivo, sua estrutura permite maior flexibilidade, facilitando a inserção e remoção de elementos sem a necessidade de deslocar outros itens.
Outro motivo importante para aprender Listas Encadeadas é que elas aparecem com frequência em entrevistas de emprego. Em muitos testes técnicos, candidatos são desafiados a resolver problemas clássicos, como reverter uma Lista Encadeada ou detectar ciclos.
- Neste artigo, você aprenderá:
O que é uma Lista Encadeada e como ela funciona. - Como implementá-la em Java, do zero.
- As operações essenciais: inserção, busca e remoção.
- A análise de complexidade de cada operação.
Ao final, você terá um conhecimento sólido sobre quando e por que usar Listas Encadeadas. Bora começar!
Estrutura da Lista Encadeada: Como os Nós se Conectam
Diferente de um array, onde os elementos ocupam posições contínuas na memória, uma Lista Encadeada é composta por nós (nodes) que se conectam entre si por meio de referências.
Como os nós estão organizados?
Cada nó de uma Lista Encadeada contém dois elementos principais:
- O valor armazenado: Representa a informação contida no nó.
- A referência para o próximo nó: Um ponteiro indicando a posição do próximo elemento da lista.
Esses nós se conectam formando uma cadeia de referências. O primeiro nó da lista é chamado de cabeça (head cell), enquanto o último nó aponta para null, indicando o final da estrutura.
Para visualizar melhor, veja a seguinte sequência armazenada em uma Lista Encadeada:
Criando um Nó para a Lista Encadeada em Java
Agora que entendemos a estrutura de uma Lista Encadeada, é hora de começar a implementação. O primeiro passo é criar a classe responsável por representar cada nó.
Implementação do nó em Java
A seguir, implementamos a classe No, onde cada instância terá um valor e uma referência para o próximo nó:
class No {
int valor;
No proximo;
No(int valor) {
this.valor = valor;
this.proximo = null;
}
}
Explicação do código
int data→ Armazena o valor do nó.Node next→ Guarda a referência para o próximo nó da lista.- Construtor
Node(int data)→ Define o valor do nó e inicializanextcomonull, pois ao ser criado, ele ainda não aponta para outro elemento.
Complexidade da criação de um nó
Criar um nó é uma operação O(1), pois envolve apenas alocação de memória e atribuição de valores. No entanto, a forma como manipulamos esses nós ao longo da implementação da Lista Encadeada influenciará a eficiência de outras operações, como inserção e remoção.
Implementando uma Lista Encadeada em Java
Agora que criamos a classe No, podemos estruturar a Lista Encadeada e implementar suas operações básicas.
Criando a classe ListaEncadeada
Para representar uma Lista Encadeada, precisamos de uma classe que contenha um nó inicial (primeiro) e métodos para realizar operações como inserção, busca e remoção.
Aqui está a implementação básica da classe ListaEncadeada:
public class LinkedList {
private No primeiro;
private No ultimo;
private int tamanho;
public void inserir(int valor) {
No no = new No(valor);
if (estaVazia()) {
primeiro = no;
} else {
ultimo.proximo = no;
}
ultimo = no;
tamanho++;
}
public boolean estaVazia() {
return tamanho == 0;
}
public void imprimirLista() {
No atual = primeiro;
while (atual != null) {
System.out.print(atual.valor + " -> ");
atual = atual.proximo;
}
}
}
Explicação dos principais elementos
primeiroeultimo→ Referenciam, respectivamente, o primeiro e o último nó da lista. Isso permite inserções eficientes no final sem percorrer toda a estrutura.tamanho→ Mantém o número total de elementos na lista, tornando operações comoestaVazia()mais rápidas.inserir(int valor)→ Adiciona um novo nó no final da lista:
-
- Se a lista está vazia, o novo nó se torna o primeiro.
- Caso contrário, ele é ligado ao último nó atual.
- O ponteiro
ultimoé atualizado, garantindo acesso rápido ao final da lista.
Adicionando e Removendo Elementos no Início e no Final da Lista Encadeada
Agora vamos expandir nossa Lista Encadeada para incluir operações de inserção e remoção no início e no final da lista.
Inserindo no Início da Lista
Inserir um nó no início da Lista Encadeada é uma operação rápida e eficiente, pois basta atualizar o ponteiro primeiro para apontar para o novo nó.
public void inserirNoInicio(int valor) {
No no = new No(valor);
if (estaVazia()) {
primeiro = no;
ultimo = no;
} else {
no.proximo = primeiro;
primeiro = no;
}
tamanho++;
}
Explicação do método:
- Se a lista está vazia, o novo nó se torna tanto o primeiro quanto o último elemento.
- Caso contrário, ele é ligado ao antigo primeiro nó, e
primeiroé atualizado. - A complexidade é O(1), pois não há necessidade de percorrer a lista.
Removendo Elementos da Lista Encadeada
Agora que podemos inserir elementos, precisamos também de métodos para removê-los. Vamos implementar:
removerPrimeiro()→ Remove o primeiro elemento da lista.removerUltimo()→ Remove o último elemento da lista.
A remoção no início é uma operação eficiente (O(1)), mas a remoção no final exige percorrer a lista para encontrar o penúltimo nó (O(n)).
Removendo o Primeiro Elemento
Para remover o primeiro elemento, basta atualizar primeiro para apontar para o segundo nó.
public void removerPrimeiro() {
if (!estaVazia()) {
primeiro = primeiro.proximo;
size--;
// Se a lista ficou vazia, resetamos o último nó também
if (estaVazia()) {
ultimo = null;
}
}
} Explicação do método:
- Se a lista não estiver vazia, movemos
primeiroparaprimeiro.proximo. - Se a lista ficar vazia após a remoção,
ultimotambém precisa ser resetado paranull.
Removendo o Último Elemento
Agora vamos implementar o método para remover o último elemento da Lista Encadeada. Como não temos um ponteiro direto para o penúltimo nó, precisamos percorrer a lista para encontrá-lo antes de atualizar ultimo.
public void removerNoFim() {
if (!estaVazia()) {
if (primeiro == ultimo) {
removerPrimeiro(); // Reutiliza a lógica da remoção do primeiro nó
} else {
No atual = primeiro;
while (atual.proximo != ultimo) {
atual = atual.proximo;
}
ultimo = atual;
ultimo.proximo = null;
size--;
}
}
}
Explicação do método
- Apenas um elemento na lista
- Se
primeiro == ultimo, chamamosremoverPrimeiro(), já que a lógica de remoção do primeiro nó cobre esse cenário.
- Se
- Vários elementos na lista
- Percorremos a lista até encontrar o penúltimo nó.
- Atualizamos
ultimopara que ele passe a seratual. - Definimos
ultimo.proximo = nullpara remover a referência ao último nó anterior. - Reduzimos
size--para manter a contagem correta de elementos.
Complexidade da Operação
O(n) → Pois precisamos percorrer a lista para encontrar o penúltimo nó.
Buscando um Elemento na Lista Encadeada
Diferente de um array, onde podemos acessar um elemento diretamente pelo índice, a Lista Encadeada não possui índices. Isso significa que, para encontrar um elemento, precisamos percorrer a lista nó a nó, verificando se algum deles contém o valor desejado.
A busca tem complexidade O(n), pois no pior caso precisamos percorrer toda a lista.
Implementação do método buscar
public No buscar(int valor) {
No atual = primeiro;
while (atual != null) {
if (atual.valor == valor) {
return atual; // Retorna o nó encontrado
}
atual = atual.proximo; // Atualiza o ponteiro para o próximo nó
}
return null; // Retorna null se o valor não for encontrado
}
Explicação do Código:
- Inicia a busca a partir do primeiro nó (
primeiro). - Percorre a lista nó a nó, comparando
atual.valorcom o valor buscado. - Se o valor for encontrado, retornamos a referência do nó correspondente.
- Se o loop termina sem encontrar o valor, retornamos
null.
Conforme mencionado, diferente de um array, onde podemos acessar array[i] em O(1), aqui precisamos percorrer toda a lista, o que pode levar O(n) no pior caso.
Removendo um Elemento Específico da Lista Encadeada
Agora que conseguimos buscar elementos, vamos implementar a remoção de um nó com um valor específico. Diferente da remoção do primeiro ou do último elemento, aqui precisamos:
- Percorrer a lista até encontrar o nó que contém o valor desejado.
- Atualizar a referência do nó anterior para ignorar o nó a ser removido.
- Tratar os casos especiais, como remover o primeiro ou o último nó.
Implementação do método remover
public void remover(int valor) {
if (estaVazia()) {
return; // Lista vazia, nada a remover
}
// remover o primeiro nó
if (primeiro.valor == valor) {
removerPrimeiro();
return;
}
No atual = primeiro;
No anterior = null;
// Percorre a lista para encontrar o nó a ser removido
while (atual != null && atual.valor != valor) {
anterior = atual;
atual = atual.proximo;
}
// Se o valor não foi encontrado, encerra
if (atual == null) {
return;
}
// Caso especial: remover o último nó
if (atual == ultimo) {
removerUltimo();
} else {
// Nó encontrado no meio da lista, ajustamos as referências
anterior.proximo = atual.proximo;
size--;
}
}
Explicação do Método
- Caso especial: Se a lista estiver vazia, não há nada para remover.
- Se o valor estiver no primeiro nó, chamamos
removerPrimeiro()e encerramos. - Percorremos a lista até encontrar o nó com o valor desejado.
- Se o nó a ser removido for o último, chamamos
removerUltimo(). - Caso contrário, ajustamos a referência do nó anterior para ignorar o nó removido.
Complexidade da Operação
O(n) → No pior caso, precisamos percorrer toda a lista para encontrar o nó a ser removido.
Conclusão
Neste artigo, exploramos Listas Encadeadas em Java, cobrindo desde a estrutura básica até a implementação de operações essenciais, como inserção, remoção, busca e verificação de posição.
Aprendemos que, diferente dos arrays, uma Lista Encadeada não possui índices diretos, exigindo um percurso nó a nó para acessar os elementos. Embora essa estrutura tenha vantagens, como inserções eficientes no início e no fim, operações como busca e remoção podem ter complexidade O(n) no pior caso.
Agora que você domina o básico, chegou o momento de avançar para um desafio real!?
📌 Em detecção de ciclos, precisamos verificar se há um loop na lista. Esse é um problema clássico em algoritmos e aparece com frequência em entrevistas de emprego. Já escrevi um artigo explicando isso passo a passo.
👉 Clique aqui para conferir: Detecção de Ciclos em Listas Encadeadas
Se gostou do artigo ou tem dúvidas, deixe um comentário! 🚀
Quer praticar esse tipo de problema do jeito que cai em entrevistas?
Estou construindo o DevPrep, uma plataforma em português focada em raciocínio e padrões de algoritmos.
👉 Acesso por beta fechado: https://devprep.dev/
Referências
Cracking the Coding Interview’ é um best-seller renomado e um favorito pessoal – um livro indispensável na preparação para entrevistas técnicas!
Designing Data-Intensive Applications: é uma referência para criar aplicações escaláveis, robustas e de alto desempenho!