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 6: Código completo
// artigo_linked.c
/*
* Enzo Ferber : < Slackware_10 >
*
* Programa demonstrativo para artigo VOL
*
* C - Listas Duplamente Encadeadas
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MALLOC(a) (a *) malloc ( sizeof(a) )
struct no
{
int info;
struct no *prox;
struct no *ant;
};
// ponteiros de referência
struct no *inicio;
struct no *fim;
/*
* inserir - insere um novo dado na lista
*
* @ info - dado a ser inserido
*/
void inserir ( int info )
{
struct no *novo = MALLOC ( struct no );
struct no *atual;
if ( !novo )
{
perror ( "Malloc: " );
return ;
}
// atribuição do novo valor...
novo->info = info;
// cria lista
if ( !inicio )
{
novo->prox = NULL;
novo->ant = NULL;
inicio = novo;
fim = novo;
return ;
}
// se não for o primeiro elemento da lista...
atual = inicio;
while ( atual )
{
if ( atual->info < info )
atual = atual->prox;
else
{
// elemento intermediário - caso 2
if ( atual->ant )
{
novo->prox = atual;
novo->ant = atual->ant;
atual->ant->prox = novo;
atual->ant = novo;
return ;
}
// novo primeiro elemento - caso 1
novo->prox = atual;
novo->ant = NULL;
atual->ant = novo;
inicio = novo;
return ;
}
}
// novo último elemento - caso 3
fim->prox = novo;
novo->prox = NULL;
novo->ant = fim;
fim = novo;
return ;
}
/*
* imprimeLista - imprime todos os dados da lista
*/
void imprimeLista ( void )
{
struct no *atual = inicio;
while ( atual )
{
printf ( "Info: %.2d\n", atual->info );
atual = atual->prox;
}
return ;
}
/*
* procurar - procura um elemento na lista
*
* @ info - dado a ser pesquisado
*
* Retorno: ponteiro para o dado encontrado ou NULL caso não encontre
*/
struct no *procurar ( int info )
{
struct no *atual = inicio;
while ( atual )
{
if ( atual->info == info)
return atual;
else
atual = atual->prox;
}
return NULL;
}
/*
* remover - remove um nó da lista
*
* @ dado - endereço do nó a ser removido
*/
void remover ( struct no *dado )
{
if ( !dado ) return ;
// item intermediário
if ( dado->prox && dado->ant )
{
dado->ant->prox = dado->prox;
dado->prox->ant = dado->ant;
free ( dado );
return ;
}
// primeiro item
if ( dado == inicio )
{
inicio = dado->prox;
inicio->ant = NULL;
free ( dado );
return ;
}
// último elemento
if ( dado == fim )
{
fim = dado->ant;
fim->prox = NULL;
free ( dado );
return ;
}
}
// main...
int main ( void )
{
register int i;
inicio = fim = NULL;
for ( i = 1; i <= 10; i++ )
inserir (i); // caso 3 - inserção no final...
imprimeLista(); puts ("");
inserir (0); // caso 1
inserir (12); // caso 3
inserir (11); // caso 2
imprimeLista (); puts ("");
// teste da função de procura
printf ("Procurar( 3 ): %.2d\n\n", procurar(3)->info );
remover ( procurar ( 0) ); // caso 1
remover ( procurar ( 5) ); // caso 2
remover ( procurar (12) ); // caso 3
imprimeLista(); puts ("");
return 0;
}
/*
* Enzo Ferber : < Slackware_10 >
*
* Programa demonstrativo para artigo VOL
*
* C - Listas Duplamente Encadeadas
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MALLOC(a) (a *) malloc ( sizeof(a) )
struct no
{
int info;
struct no *prox;
struct no *ant;
};
// ponteiros de referência
struct no *inicio;
struct no *fim;
/*
* inserir - insere um novo dado na lista
*
* @ info - dado a ser inserido
*/
void inserir ( int info )
{
struct no *novo = MALLOC ( struct no );
struct no *atual;
if ( !novo )
{
perror ( "Malloc: " );
return ;
}
// atribuição do novo valor...
novo->info = info;
// cria lista
if ( !inicio )
{
novo->prox = NULL;
novo->ant = NULL;
inicio = novo;
fim = novo;
return ;
}
// se não for o primeiro elemento da lista...
atual = inicio;
while ( atual )
{
if ( atual->info < info )
atual = atual->prox;
else
{
// elemento intermediário - caso 2
if ( atual->ant )
{
novo->prox = atual;
novo->ant = atual->ant;
atual->ant->prox = novo;
atual->ant = novo;
return ;
}
// novo primeiro elemento - caso 1
novo->prox = atual;
novo->ant = NULL;
atual->ant = novo;
inicio = novo;
return ;
}
}
// novo último elemento - caso 3
fim->prox = novo;
novo->prox = NULL;
novo->ant = fim;
fim = novo;
return ;
}
/*
* imprimeLista - imprime todos os dados da lista
*/
void imprimeLista ( void )
{
struct no *atual = inicio;
while ( atual )
{
printf ( "Info: %.2d\n", atual->info );
atual = atual->prox;
}
return ;
}
/*
* procurar - procura um elemento na lista
*
* @ info - dado a ser pesquisado
*
* Retorno: ponteiro para o dado encontrado ou NULL caso não encontre
*/
struct no *procurar ( int info )
{
struct no *atual = inicio;
while ( atual )
{
if ( atual->info == info)
return atual;
else
atual = atual->prox;
}
return NULL;
}
/*
* remover - remove um nó da lista
*
* @ dado - endereço do nó a ser removido
*/
void remover ( struct no *dado )
{
if ( !dado ) return ;
// item intermediário
if ( dado->prox && dado->ant )
{
dado->ant->prox = dado->prox;
dado->prox->ant = dado->ant;
free ( dado );
return ;
}
// primeiro item
if ( dado == inicio )
{
inicio = dado->prox;
inicio->ant = NULL;
free ( dado );
return ;
}
// último elemento
if ( dado == fim )
{
fim = dado->ant;
fim->prox = NULL;
free ( dado );
return ;
}
}
// main...
int main ( void )
{
register int i;
inicio = fim = NULL;
for ( i = 1; i <= 10; i++ )
inserir (i); // caso 3 - inserção no final...
imprimeLista(); puts ("");
inserir (0); // caso 1
inserir (12); // caso 3
inserir (11); // caso 2
imprimeLista (); puts ("");
// teste da função de procura
printf ("Procurar( 3 ): %.2d\n\n", procurar(3)->info );
remover ( procurar ( 0) ); // caso 1
remover ( procurar ( 5) ); // caso 2
remover ( procurar (12) ); // caso 3
imprimeLista(); puts ("");
return 0;
}
Não construí nenhum tipo de menu para este programa, apenas coloquei na função main todos os tipos de inserção e remoção estudados no artigo.
Uma dica, se você não conseguir visualizar a lista em sua mente, é colocar vários printf's nas funções de inserção e remoção, um printf depois de cada linha. Isso facilitará muito seu entendimento...
novo->prox = atual;
printf ("novo->prox (%d) = atual (%d)", novo->prox->info, atual->info);
Também não construí uma página no artigo para a rotina de impressão da lista inteira, pois acho que é desnecessário, uma vez que tudo o que ela faz é percorrer a lista, só que não executa nenhuma outra operação, apenas percorre e imprime. Então acho que o entendimento é bem simples.
{
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.