Linguagem C - Listas Duplamente Encadeadas
Neste artigo explicarei o que são listas duplamente encadeadas, como construí-las e suas vantagens e desvantagens. O pré-requisito para compreender bem o artigo é uma boa noção de ponteiros.
Parte 2: Listas encadeadas
As listas encadeadas podem ser singularmente ou duplamente encadeadas. As singulares apresentam, em sua estrutura, um elo apenas para o próximo elemento. As duplas apresentam elos para o item antecessor e sucessor. Dito isso, podemos, novamente, declarar nossa estrutura para construir nossa lista.
Existem duas formas básicas de controle de listas duplamente encadeadas.
1. Dois ponteiros globais ou não, que apontam respectivamente, para o início e o fim da lista. Se for esta a implementação, o ponteiro "ant" do primeiro nó será NULO, assim como o ponteiro "prox" do último nó. Este artigo vai usar este tipo de lista, com ponteiros globais, por ser a implementação mais fácil de ser compreendida.
2. Uma lista duplamente encadeada circular. Neste tipo o ponteiro "ant" do primeiro nó aponta para o ultimo nó, e o ponteiro "prox" do último nó aponta para o primeiro elemento. Neste tipo de lista você precisa de ao menos um ponteiro de controle, para apontar, para o primeiro elemento, ou para o último, ou para o último elemento alterado (escolha o tipo que melhor se encaixe no seu programa - ou você pode ter os três ponteiros).
Definido que vamos trabalhar com o primeiro tipo de lista, com variáveis globais, podemos já imaginar como nossa lista ficará após a inserção de alguns dados:
Deve-se notar que a vantagem da lista duplamente encadeada sobre a singular é o segundo ponteiro (o ponteiro de retrocesso). Este ponteiro tem duas funcionalidades. Em bancos de dados extensos, você pode varrer a lista em busca de dados nos dois sentidos ao mesmo tempo, reduzindo o tempo da busca pela metade (no pior dos casos). Outro uso do ponteiro só ocorre se por algum motivo houver uma falha com o ponteiro de inicio da lista, e se este for o caso, você pode usar o ponteiro final para reconstruir a lista e redefinir o ponteiro. Agora vamos às funções!
struct no
{
int info;
struct no *prox;
struct no *ant;
};
{
int info;
struct no *prox;
struct no *ant;
};
- "nó" é como chamamos cada elemento da lista;
- "prox" apontará para o nó posterior ao nó atual;
- "ant" apontará para o nó anterior ao nó atual.
Existem duas formas básicas de controle de listas duplamente encadeadas.
1. Dois ponteiros globais ou não, que apontam respectivamente, para o início e o fim da lista. Se for esta a implementação, o ponteiro "ant" do primeiro nó será NULO, assim como o ponteiro "prox" do último nó. Este artigo vai usar este tipo de lista, com ponteiros globais, por ser a implementação mais fácil de ser compreendida.
// declaração dos ponteiros de controle
struct no *inicio;
struct no *fim;
struct no *inicio;
struct no *fim;
2. Uma lista duplamente encadeada circular. Neste tipo o ponteiro "ant" do primeiro nó aponta para o ultimo nó, e o ponteiro "prox" do último nó aponta para o primeiro elemento. Neste tipo de lista você precisa de ao menos um ponteiro de controle, para apontar, para o primeiro elemento, ou para o último, ou para o último elemento alterado (escolha o tipo que melhor se encaixe no seu programa - ou você pode ter os três ponteiros).
Definido que vamos trabalhar com o primeiro tipo de lista, com variáveis globais, podemos já imaginar como nossa lista ficará após a inserção de alguns dados:
{
int info;
struct LDE *prox;
struct LDE *ante;
};
Não ocupa 12 bytes?
Você está enganado!
Toda a vez que for criado um elemento desta estrutura, ela ocupará UM espaço para um inteiro e DOIS espaços para ponteiros.
Se considerar o GCC 32 bits, ambos, inteiro e ponteiro, tem 4 bytes. Logo, NESTE CASO, Gcc32 bits, a estrutura ocupará SIM 12 bytes de tamanho.
Para 10 elementos de vetores de inteiros se ocupará exatos 40 bytes (em archs e compiladores 32 bits).
Se for uma lista encadeada dos mesmos 10 elementos, se ocupará 120 bytes (10x o tamanho de uma estrutura e cada estrutura ocupa 4 bytes para o info, 4 bytes para o ponteiro prox e 4 bytes para o ponteiro ante).
As vantagens/desvantagens de uma lista encadeada não tem a ver com economia de espaço, mas sim com a flexibilidade. Uma lista encadeada pode crescer até o limite de memória disponível e sempre eu posso "alocar mais um". Um vetor eu preciso previamente decidir o tamanho. Se 10, serão 10 e ponto! Precisei de 11? Não dá para aumentar o vetor em tempo de execução (ops, até dá com realloc, mas ai e outro tiro no pé).
Outra vantagem das listas encadeadas é a possibilidade de inserir ordenado ou remover no meio. Imagine um vetor de 15000 posições, 10mil delas ocupadas (de 0 a 9999). Ele está ORDENADO e preciso inserir o valor X que pela ordenanação ele deve ser inserido em vet[15]. Como é vetor, eu preciso:
(a) mover [9999] para [10000], [998] para [999], ..., [15] para [16] para inserir o novo elemento em [15].
(b) ou inserir no final [10000] e executar algum algoritmo de ordenamento como o quick sort
Se for lista encadeada, basta inserir na posição e ajustar os ponteiros ante e prox corretamente.
Listas encadeadas e suas derivações (duplamente, circular, com header, etc) são estrutura de dados muito úties, resolvem um monte de problemas, mas em comparação com vetores simples são mais complicados de manipular e consomem mais memória por elemento SIM.