Você está na página 1de 18

Listas Ligadas O Guia dos Mestres

Listas Ligadas
O Guia dos Mestres
1. Introduo
1.1. Objetivo
O objetivo deste artigo explicar o que so listas ligadas, apresentar os
algortmos envolvidos e algumas formas de implementao em C e C++.
1.2. Estruturas de Dados I
No nvel mais baixo, as informaes manipuladas por um computador consistem
em conjuntos de itens elementares. As maneiras como estes item elementares
se relacionam logicamente so as estruturas de dados. As formas como estas
estruturas lgicas so implementadas fisicamente na memria de um
computador so as estruturas de armazenamento.
Os itens elementares (ou estruturas primitivas de dados) so aqueles
manipulados diretamente pelo computador ou linguagem de programao, tais
como:

nmeros inteiros
nmeros reais (ponto fixo ou flutuante)
caracteres
valores lgicos
ponteiros

Obs.: Na maioria dos computadores atuais, os itens elementares so


armazenados na memria como sequncias de bytes. A distino entre
eles consequncia das instrues utilizadas para manipul-los e no do
contedo dos bytes. Por exemplo, um byte contendo o valar hexadecimal
0x30 pode ser interpretado como o nmero decimal 48 ou o caracter 0,
dependendo do cdigo que o manipula. A linguagem C segue esta
filosofia, permitindo misturar e re-interpretar (via cast) os tipos quase que
livremente. Se por um lado isto d uma imensa flexibiidade, por outro
potencializa erros catastrficos.

(C) 2004, Daniel Quadros

Pgina 1 de 18

Listas Ligadas O Guia dos Mestres

1.3. Ponteiros
Do ponto de vista terico, um ponteiro uma referncia a uma estrutura de
dados. Do ponto de vista prtico, um ponteiro uma varivel cujo contedo o
endereo de uma outra varivel.
Vamos considerar a seguinte sequncia de cdigo C:
int a, b;
int *p;
a = 1;
p = &a;
b = *p;
*p = 5;

Nas duas primeiras linhas declaramos 3 variveis (dois inteiros e um ponteiro


para inteiro). O compilador ir reservar posies na memria para estas
variveis. Suponto que tanto um inteiro como um ponteiro ocupem 4 bytes na
memria, podemos ter algo como:
Endereo
2000
2004
2008

Contedo
valor de a
valor de b
valor de p

A instruo a = 1 coloca o valor 1 no endereo 2000. Quando escrevemos &a


obtemos o endereo de a, portanto p = &a coloca o valor 2000 no endereo
2008. A notao *p no lado direito de uma atribuio significa:

pegue o valor da variavel p (2000 no nosso caso)


considere este valor como um endereo e pegue o valor contido neste
endereo (1 no nosso caso)

portanto, b = *p coloca o valor 1 no endereo 2004.


Por ltimo, *p no lado esquerdo de uma atribuio significa:

pegue o valor da variavel p (2000 no nosso caso)


considere este valor como um endereo e coloque o resultado do lado
direito neste endereo (5 no nosso caso)

portanto, *p = 5 coloca o valor 5 no endereo 2000.

(C) 2004, Daniel Quadros

Pgina 2 de 18

Listas Ligadas O Guia dos Mestres

1.4. Estruturas de Dados II


As estruturas de dados primitivas podem ser combinadas para gerar estruturas
mais complexas de dados. Vejamos algumas delas:
Estruturas
O termo estrutura usado para indicar uma estrutura no-primitiva composta
por uma conjunto estruturado de dados primitivos. Por exemplo, podemos
representar um ponto na tela por uma sequencia de dois inteiros contendo as
duas coordenadas do ponto. Em C:
struct ponto
{
int x;
int y;
};

Neste exemplo, x e y so os campos da estrutura ponto. Na linguagem C


utiliza-se a notao a.b para indicar o campo b da varivel a. Por exemplo:
struct ponto pt;
pt.x = 10;
pt.y = 20;

Um campo de uma estrutura pode ser uma outra estrutura, criando um


conjunto hierarquizado. Por exemplo, vamos definir uma estrutura para
representar um crculo, composta por um ponto (centro) e um valor inteiro
(raio):
struct circulo
{
struct ponto centro;
int raio;
};
struct circulo c;
c.centro.x = 20;

Repare no exemplo a notao c.centro.x para indicar o campo x do campo


centro da varivel c.

(C) 2004, Daniel Quadros

Pgina 3 de 18

Listas Ligadas O Guia dos Mestres

A linguagem C permite o uso de ponteiros com estruturas. Por exemplo:


struct circulo *pc;
struct circulo c;
int r, xis;
pc = &c;
r = (*pc).raio;
xis = (*pc).centro.x;

Para simplificar a codificao, a linguagem C permite substituir a notao


(*p). por p-> :
r = pc->raio;
xis = pc->centro.x;

Ao definir estruturas mais complexas (como as listas ligadas), pode surgir a


necessidade de colocar dentro da estrutura um ponteiro para uma estrutura
do mesmo tipo.Em C:
struct circulo
{
struct ponto centro;
int raio;
struct circulo *pc;
};

importante entender que nessa declarao no estamos colocando uma


estrutura dentro dela mesma (o que geraria uma recurso infinita). A linha
struct circulo *pc; acima simplesmente acrescenta estrutura circulo uma
varivel que conter o endereo de uma outra varivel do tipo crculo.
Vetores e Matrizes
Matrizes so conjuntos ordenados com um nmero fixo de elementos, no
possvel acrescentar (inserir) ou retirar (remover) elementos. Um elemento
de uma matriz referenciado atravs de um conjunto de nmeros inteiros
sequenciais, os ndices. A quantidade de ndices usados para acessar uma
matriz a sua dimenso, uma matriz com dimenso um um vetor.
Esta definio formal fica mais simples se examinarmos alguns exemplos.
Comecemos examinando um vetor com 10 inteiros. Na linguagem C,
podemos declar-lo da seguinte forma:
int v[10];

(C) 2004, Daniel Quadros

Pgina 4 de 18

Listas Ligadas O Guia dos Mestres

O vetor v um conjunto com 10 elementos. Os elementos individuais so


acessados atravs dos nmeros 0, 1, 2, 3, 4, 5, 6, 7, 8 e 9, usando a
seguinte notao:
v[5] = 3;

Este comando coloca o valor 3 no elemento de ndice 5, que o sexto


elemento do vetor v.
Obs.: Na linguagem C os ndices comeam sempre de 0. Desta forma, um
vetor com n elementos acessado por ndices de 0 a n-1. O
elemento de ndice i o elemento (i+)-simo do vetor.
Uma forma de visualizar um vetor como uma sequncia de elementos
consecutivos:
v[0]

v[1]

v[2]

v[3]

v[4]

v[5]

v[6]

v[7]

v[8]

v[9]

Uma matriz bi-dimensional pode ser visualizada como uma tabela. Por
exemplo, considerando uma matriz de inteiros cujo primeiro ndice varia de 0
a 2 e o segundo de 0 a 3:
int mat[3][4];
mat[0][0] = 3;
mat[2][3] = 2 * mat[0][0];
mat[0][0]
mat[1][0]
mat[2][0]

mat[0][1]
mat[1][1]
mat[2][1]

mat[0][2]
mat[1][2]
mat[2][2]

mat[0][3]
mat[1][3]
mat[2][3]

Obs.: A linguagem C trata de forma muito semelhante vetores e ponteiros:


as matrizes so armazenadas em posies sequnciais de
memria.

O nome de uma vriavel do tipo matriz representa o endereo do


primeiro elemento:
int vet[10];
int *p;
p = &vet[0];
p = vet;

// p recebe o endereo do primeiro


//
elemento de vet
// idem

(C) 2004, Daniel Quadros

Pgina 5 de 18

Listas Ligadas O Guia dos Mestres

Somar ou subtrair n a um ponteiro equivale a avanar ou recuar


n elementos:
p = vet+2;

// p aponta para o terceiro elemento

p = vet;
*(p+1) = 2;

// p aponta para o primeiro elemento


// (p+1) aponta para o 2o elemento
//
ou seja, equivale a vet[1] = 2

Expandindo o exemplo acima, o C considera p[n] equivalente a


*(p+n). Ou seja, a ltima linha acima poderia ser escrita como
p[1] = 2;

Portanto, usa-se a mesma notao para acessar elementos


apontados por um ponteiro (p) e elementos de um vetor (vet).

importante lembrar que int vet[10]; cria 10 variveis do tipo


inteiro, enquanto que int *p; cria apenas uma varivel do tipo
ponteiro para inteiro. A varivel p s poder ser utilizada para
acessar elementos depois de ter sido inicializada.

Listas
D uma forma genrica poderamos dizer que uma lista um conjunto
ordenado de um nmero varivel de elementos, para a qual esto definidas
funes de insero e remoo de elementos.
1.5. Alocao Dinmica de Memria
As declaraes de variveis em C que apresentamos at agora so o que
chamamos de alocao esttica: as posies de memria para armazenar as
variveis so definidas no momento da sua declarao e no se alteram mais.
A alocao esttica muito prtica, segura e eficiente, porm impem que o
tamanho alocado ser fixo durante toda a execuo do programa. No caso de
vetores, matrizes e listas isto pode no ser apropriado.
Existem duas funes bsicas em C para alocar e liberar memria
dinamicamente: malloc e free. A rotina malloc reserva uma determinada
quantidade de bytes e retorna o ponteiro para a primeira posio. A rotina free
libera uma regio de memria, deixando-a disponvel para alocaes futuras.
Uma vez que a rotina malloc enxerga a memria simplemente como uma
sequncia de bytes, cabe ao programador determinar quantos bytes so
necessrios para armazenar a informao e usar um cast para informar para
que tipo de dado o ponteiro ir apontar.
Comeando com um exemplo simples, a sequncia abaixo aloca dinamicamente
uma varivel do tipo inteiro e armazena zero nela:

(C) 2004, Daniel Quadros

Pgina 6 de 18

Listas Ligadas O Guia dos Mestres

int *p;
p = (int *) malloc (sizeof (int));
*p = 0;

Reparar o uso de sizeof para determinar o tamanho em bytes da varivel, o cast


(int *) para informar que o ponteiro retornado por malloc ser usado para apontar
para um inteiro e *p para acessar a varivel apontada por p.
Uma sequencia semelhante pode ser usada para alocar uma estrutura:
struct circulo *pc;
pc = (struct circulo *) malloc (sizeof (struct circulo));
pc->raio = 10;

A alocao de um vetor de 10 inteiros tambm semelhante:


int *pv;
pv = (int *) malloc (10 * sizeof (int));
*pv = 0;
pv[9] = 1;
// ultimo elemento do vetor alocado

Por ltimo, um exemplo de alocao de um vetor de estruturas


struct circulo *pvc;
pvc = (struct circulo *) malloc (7 * sizeof (struct circulo));
(*(pvc+2)).raio = 10;
// raio do 3o elemento
(pvc+2)->raio = 10;
// idem
pvc[2].raio = 10;
// idem

Note, mais uma vez, como a semelhana de notao para ponteiros e vetores
na ltima linha do exemplo.

(C) 2004, Daniel Quadros

Pgina 7 de 18

Listas Ligadas O Guia dos Mestres

2. Listas Lineares
2.1. Definio
Neste artigo vamos tratar apenas de listas lineares, que so listas que
apresentam somente uma relao de adjacncia. Em uma lista linear existe um
primeiro elemento e um ltimo elemento, todos os demais elementos possuem
um elemento anterior e um elemento seguinte.
2.2. Operaes Bsicas
As operaes bsicas que vamos examinar so:

Inserir um elemento em uma lista


remover um elemento de uma lista
percorrer os elementos de uma lista

2.3. Listas Lineares Especializadas


Existem dois tipos especializados de listas lineares que so encontrados com
frequncia:

Uma pilha uma lista onde todas as inseres e remoes so sempre


feitas na mesma ponta. Desta forma, o item removido sempre o que foi
inserido mais recentemente. Por isso, as pilhas so vezes chamadas de
LIFO (last in first out, ltimo a entrar primeiro a sair). Voc pode visualizar
este tipo de lista como uma pilha de pratos:

Pilha Inicial

Insero

Remoo

Uma fila uma lista onde todas as inseres so sempre feitas em uma
ponta e as remoes so sempre feitas na outra ponta. Desta forma, o
item removido sempre o que foi inserido a mais tempo. Por isso, as filas
so vezes chamadas de FIFO (first in first out, primeiro a entrar primeiro
a sair). Voc pode visualizar este tipo de lista como uma fila de pessoas:
Fila inicial
Insero
Remoo
(C) 2004, Daniel Quadros

Pgina 8 de 18

Listas Ligadas O Guia dos Mestres

Neste artigo no vamos examinar algortmos otimizados para estes tipos


especializados.
2.4. Alocao Sequencial
A maneira mais simples e bvia de implementar uma lista mantendo os
elementos sequencialmente na memria. Podemos fazer isto tanto usando um
vetor alocado estticamente como uma rea alocada dinamicamente.
Um exemplo de alocao sequencial esttica:
int lista [10];
int tamlista;

// rea para guardar a lista


//
lista pode ter at 10 elementos
// nmero de elementos atualmente na lista

// inicia lista
void IniciaLista ()
{
tamlista = 0;
// lista est vazia
}
// Insere um elemento na lista na posio pos
// (as posies so numeradas de 0 a tamlista-1)
int Insere (int pos, int elemento)
{
int i;
if (tamlista == (sizeof(lista)/sizeof(int)))
return FALSE; // lista cheia
if ((pos < 0) || (pos > tamlista))
return FALSE; // posicao invalida
if (pos == tamlista)
{
// inserir no final
lista[tamlista++] = elemento;
}
else
{
// insero no meio, mover os elementos seguintes
for (i = tamlista; i > pos; i--)
lista [i] = lista [i-1];
lista[pos] = elemento;
tamlista++;
}
return TRUE;
}

(C) 2004, Daniel Quadros

Pgina 9 de 18

Listas Ligadas O Guia dos Mestres

A rotina de insero acima mostra uma desvantagem da alocao sequencial:


preciso mover os elementos na memria quando feita uma insero ou
remoo.
Obs: no caso de filas e pilhas possvel evitar a movimentao dos elementos.
Por outro lado, o acesso aleatrio as elementos muito simples. A rotina abaixo
calcula a soma de todos os elementos da lista:
int Soma ()
{
int i, total;
for (i = total = 0; i < tamlista; i++)
total += lista[i];
return total;
}

(C) 2004, Daniel Quadros

Pgina 10 de 18

Listas Ligadas O Guia dos Mestres

3. Listas Ligadas
As listas ligadas so uma alternativa s listas sequenciais. Como os elementos
no esto em posies sequenciais necessrio acrescentar aos elementos
informaes que permitam percorrer a lista sequencialmente. Desta forma os
elementos so ligados uns aos outros.
Obs.: A forma mais comum de fazer as ligaes atravs de ponteiros.
Entretanto, nada impede de implementar uma lista ligada sobre um vetor
alocado estaticamente, usando como link o ndice do elemento.
3.1. Listas Ligadas Simples
Na sua forma mais simples, uma lista ligada implementada atravs de:

Um ponteiro para o primeiro elemento da lista


Um ponteiro em cada elemento da lista apontando para o prximo
elemento

Um valor especial (em C, tipicamente NULL e mais raramente 1) utilizado


para indicar o fim da lista.
As listas ligadas so normalmente representadas graficamente como retngulos
(os elementos ou ns), interligados atravs de setas:
Primeiro

Item 1

Item 2

Item 3

A direo da seta neste diagrama importante, a origem da seta um ponteiro


que armazena o endereo do elemento na outra ponta. Portanto, possvel
percorrer o link apenas no sentido da seta.
O smbolo no final da seta que sai do item 3 (smbolo de aterramento em
circuitos eltricos) indica o fim da lista. Em outras palavras, o ponteiro possui o
valor especial de fim de lista.
Uma lista vazia simplesmente
Primeiro

(C) 2004, Daniel Quadros

Pgina 11 de 18

Listas Ligadas O Guia dos Mestres

A insero de um novo item consiste apenas em alterar ponteiros:


Primeiro

Item 1

Item 2

Item 3

Item 1,5

Idem para a remoo de um item:


Primeiro

Item 1

Item 2

Item 3

O cdigo abaixo mostra uma implementao simples de lista ligada em C:


// estrutura de um elemento (n) da lista
typedef struct no_lista
{
int
valor; // dado do elemento
struct no_lista *prox; // ponteiro para o elemento seguinte
} NO;
// variavel que aponta para o incio da lista
NO *inicio;
// Inicia a lista
void IniciaLista ()
{
inicio = NULL;
}
// Insere um valor na lista na posio pos
int Insere (int pos, int valor)
{
NO *pno, *p;
NO **pant;
int i;
// procura o ponto a inserir
for (i = 0, pant = &inicio, p = inicio;
(i < pos) && (p != NULL); i++)
{
pant = &p->prox;
p = p->prox;
}
if (i != pos)
return FALSE; // posicao invalida
// Insere
(C) 2004, Daniel Quadros

Pgina 12 de 18

Listas Ligadas O Guia dos Mestres

pno = malloc (sizeof (NO));


if (pno == NULL)
return FALSE; // faltou memoria
pno->valor = valor;
pno->prox = p;
*pant = pno;
return TRUE;
// sucesso
}
// Remove elemento na posio pos
int Remove (int pos)
{
NO *p;
NO **pant;
int i;
// procura o elemento a remover
for (i = 0, pant = &inicio, p = inicio;
(i < pos) && (p != NULL); i++)
{
pant = &p->prox;
p = p->prox;
}
if ((i != pos) || (p == NULL))
return FALSE; // posicao invalida
// Remove
*pant = p->prox;
free (p);
return TRUE;
// sucesso
}
// Soma os elementos da lista
int Soma ()
{
int total;
NO *p;
for (p = inicio, total = 0; p != NULL; p = p->prox)
total += p->valor;
return total;
}

As rotinas de insero e remoo utilizam um ponteiro (pant) para guardar o


endereo da varivel que contm o endereo do n atual. Isto dispensa tratar de
forma diferente o caso em que o ponteiro a ser atualizado o ponteiro para o
inicio da lista. Uma forma de melhorar a legibilidade, desperdiando um pouco
de memria, usar um n para guardar o ponteiro de incio:

(C) 2004, Daniel Quadros

Pgina 13 de 18

Listas Ligadas O Guia dos Mestres

NO inicio;

// inicio.prox aponta para o primeiro elemento

// Insere um valor na lista na posio pos


int Insere (int pos, int valor)
{
NO *pno, *p;
NO *pant;
int i;
// procura o ponto a inserir
for (i = 0, pant = &inicio, p = inicio.prox;
(i < pos) && (p != NULL); i++)
{
pant = p;
p = p->prox;
}
if (i != pos)
return FALSE; // posicao invalida
// Insere
pno = malloc (sizeof (NO));
if (pno == NULL)
return FALSE; // faltou memoria
pno->valor = valor;
pno->prox = p;
pant->prox = pno;
return TRUE;
// sucesso
}

Comparando uma lista sequencial a uma lista ligada, temos:

A lista ligada consome memria adicional para os links;


fcil e rpido inserir e remover elementos, pois basta alterar ponteiros;
O acesso a um elemento qualquer da lista (acesso randmico) mais
trabalhoso e lento na lista ligada, pois preciso percorrer os links;
O acesso sequencial para frente tem velocidade bastante semelhante;
fcil quebrar e juntar listas, pois basta alterar ponteiros;
A estrutura de links da lista ligada leva facilmente a estruturas mais
complexas, como uma lista onde os elementos so listas.

Portanto uma lista ligada apropriada quando so esperadas vrias inseres


e/ou remoes e o acesso essencialmente sequencial. Um exemplo quando
se deseja manter ordenada uma lista que sofre seguidas inseres e
pesquisada por busca sequencial.

(C) 2004, Daniel Quadros

Pgina 14 de 18

Listas Ligadas O Guia dos Mestres

3.2. Listas Duplamente Ligadas


Uma desvantagem da lista ligada simples que podemos seguir os links apenas
em uma direo. Por exemplo, nas rotinas de insero tivemos que usar duas
variveis, uma para guardar o endereo do n atual e outra para guardar o
endereo do n anterior.
Na lista duplamente ligada armazenamos em cada n dois ponteiros, um para o
prximo n e outro para o anterior. Ficamos com algo assim:
// estrutura de um elemento (n) da lista
typedef struct no_lista
{
int
valor; // dado do elemento
no_lista *prox; // ponteiro para o elemento seguinte
no_lista *ant;
// ponteiro para o elemento anterior
} NO;
// variavel que aponta para o incio da lista
NO inicio;
// inicio.prox aponta para o primeiro elemento
// Insere um valor na lista na posio pos
int Insere (int pos, int valor)
{
NO *pno, *p;
int i;
// procura o ponto a inserir
for (i = 0, p = &inicio;
(i < pos) && (p->prox != NULL); i++)
{
p = p->prox;
}
if (i != pos)
return FALSE; // posicao invalida
// Insere
pno = malloc (sizeof (NO));
if (pno == NULL)
return FALSE; // faltou memoria
pno->valor = valor;
pno->prox = p->prox;
pno->ant = p;
if (p->prox != NULL)
p->prox->ant = pno;
p->prox = pno;
return TRUE;

// sucesso

(C) 2004, Daniel Quadros

Pgina 15 de 18

Listas Ligadas O Guia dos Mestres

3.3. Listas Ligadas em C++


O cdigo apresentado anteriormente pode, claro, ser utilizado diretamente em
um programa C++. Entretanto, mais elegante criar uma classe que encapsule
as estruturas e rotinas. Por exemplo, o destrutor da classe pode percorrer a lista
liberando a memria alocada para os ns. As situaes de erro, indicadas
atravs de um retorno FALSE nas rotinas anteriores, podem ser sinalizadas
atravs de excees.
Uma desvantagem do cdigo apresentado anteriormente que ele especfico
para um determinado tipo de elemento (nos exemplos a lista armazenava
sempre inteiros). Se precisarmos, por exemplo, de uma lista para inteiros e uma
lista para nmeros de ponto flutuante, ser preciso duplicar a maior parte do
cdigo e das estruturas de dados.
Uma soluo, que pode tambm ser usada em C, armazenar no n um
ponteiro para a informao ao invs da prpria informao. Se este ponteiro for
do tipo void *, ele poder apontar para qualquer tipo de informao. Isto permite,
inclusive, guardar em uma lista elementos de tipo diferente. A desvantagem
desta soluo que ela pouco robusta quanto ao controle dos tipos. Alm
disso, a alocao e liberao das reas que contm as informaes precisa ser
feita por uma camada externa de controle da lista (j que a lista desconhece o
formato e tamanho da rea de informaes).
A linguagem C++ possui um recurso justamente para os casos em que se
deseja definir uma funo ou classe de forma independente dos tipos dos dados
a serem manipulados: os templates.
Um template uma espcie de macro cujo parmetro um tipo ou uma classe.
Por exemplo, vamos definir um template de uma funo para somar dois
nmeros:
template <class T> T Soma (T a, T b)
{
return a+b;
}

Podemos usar este template com qualquer classe que suporte o operador +, o
C++ verificar se os parmetros so do mesmo tipo:
int a,b,c;
double x,y,z;
c = Soma (a,b);
z = Soma (x,y);
z = Soma (a,y);

// soma dois inteiros


// soma dois doubles
// erro, tipos diferentes

(C) 2004, Daniel Quadros

Pgina 16 de 18

Listas Ligadas O Guia dos Mestres

Podemos usar templates tambm na definio de classes:


template <class T> class NO
{
T valor;
// dado do elemento
NO *prox;
// ponteiro para o elemento seguinte
// ...
};

Portanto podemos criar um template capaz de criar uma classe de lista para
cada tipo que precisarmos.
Melhor ainda, isto j foi feito. O Visual C++ possui dois templates para criao
de listas duplamente ligadas:

CList, que faz parte da MFC; e


list, que faz parte da STL (Standard Template Library).

(C) 2004, Daniel Quadros

Pgina 17 de 18

Listas Ligadas O Guia dos Mestres

4. Referncias

The Art of Computer Programming, Volume 1 Fundamental Algorithms


Donald E. Knuth
An Introduction To Data Structures With Applications Jean-Paul Tremblay &
Paul G. Sorenson
The C Programming Language Brian W. Kernighan & Dennis M. Ritchie.

(C) 2004, Daniel Quadros

Pgina 18 de 18