You are on page 1of 8

MC202 Estruturas de Dados

Aulas 47: Listas Ligadas


Felipe P.G. Bergo

Listas Generalizadas

E muitas vezes desejvel armazenar uma seqncia de elementos em alguma ordem espec a ue ca, em algum tipo de lista. Operaes naturais sobre listas so: co a Inserir um novo elemento (no m, no in cio, em algum ponto espec co). Buscar por um determinado valor. Obter o n-simo elemento da lista. e Remover um elemento da lista. A partir de um elemento, obter o prximo elemento, ou o elemento anterior. o Apagar toda a lista. Obter o primeiro ou o ultimo elemento da lista. Obter o nmero de elementos da lista. u Percorrer todos os elementos da lista, do primeiro para o ultimo ou vice-versa. Embora seja poss realizar todas estas operaes sobre um vetor, algumas delas tornam-se inevel co cientes devido ` necessidade de manter os elementos em um espao cont a c guo de memria. Listas o ligadas so uma soluo em geral mais eciente para a implementao de listas. a ca ca

Listas Ligadas

Uma lista ligada uma coleo de ns que podem ser alocados em regies no cont e ca o o a guas de memria. o Cada n mantem uma ou mais referncias (ponteiros) que indicam onde esto localizados os ns o e a o relacionados (prximo n, n anterior). o o o Podemos nos referenciar a uma lista ligada atravs de seu primeiro n. Entretanto, toda vez que e o realizarmos uma insero no in da lista teremos que garantir que todas as partes do programa ca cio que referenciam a lista tenha suas referncias atualizadas. Para evitar este problema, comum e e usar um n especial, chamado n-cabea, um ponteiro para o primeiro elemento da lista. o o c e 1

2.1

Lista Simplesmente Ligada

Nesta seo comentamos a implementao em C de uma lista simplesmente ligada com n cabea. ca ca o c Precisamos de dois tipos de dados: um tipo N, que representa cada elemento da lista ligada, e um o tipo Lista que contem um ponteiro para o n cabea: o c typedef struct Noh { int dado; // o dado poderia ser de qualquer tipo struct Noh *prox; // ponteiro para proximo elemento } Noh; typedef struct Lista { Noh *cabeca; } Lista; As operaes bsicas sobre a estrutura so a criao de listas e de ns. Ambas as operaes levam co a a ca o co tempo constante, O(1): Lista * lista_cria() { Lista *L; L = (Lista *) malloc(sizeof(Lista)); L->cabeca = NULL; return L; } Noh * noh_cria(int dado) { Noh *N; N = (Noh *) malloc(sizeof(Noh)); N->dado = dado; N->prox = NULL; return N; } Ao contrrio dos vetores, onde acessamos qualquer elemento usando um a ndice numrico, com uma e lista temos apenas acesso seqncial: para chegar no dcimo elemento temos que passar por todos ue e os nove elementos anteriores. Para percorrer uma lista, precisamos obter seu primeiro elemento e ento obtemos os elementos subseqentes seguindo os ponteiros de prximo elemento em cada n. a u o o Estas duas operaes tambm levam tempo constante. co e Noh *lista_primeiro(Lista *L) { return(L->cabeca); } Noh *lista_proximo(Noh *N) { return(N->prox); } 2

Andar para trs em uma lista simplesmente ligada no to simples, requer que a lista seja a a e a percorrida desde o in cio vericando se, para cada elemento x, proximo(x) o elemento do qual e procuramos o elemento anterior. A operao leva tempo O(n) em uma lista com n elementos. ca Noh *lista_anterior(Lista *L,Noh *N) { Noh *p; if (N == L->cabeca) return NULL; p = lista_primeiro(L); while(lista_proximo(p)!=NULL) { if (lista_proximo(p) == N) return p; p = lista_proximo(p); } return NULL; } Obter o ultimo elemento tambm requer que toda a lista seja percorrida: e Noh *lista_ultimo(Lista *L) { Noh *p; p = lista_primeiro(L); if (p==NULL) return NULL; while(lista_proximo(p)!=NULL) p = lista_proximo(p); return p; } Podemos querer 3 tipos de inseres em uma lista: no in co cio, no m ou em uma posio arbitrria ca a no meio. Na lista simplesmente ligada a insero no in trivial: Fazemos o prximo do novo ca cio e o elemento apontar para o primeiro elemento da lista e fazemos com que o novo elemento seja o primeiro elemento (cabea) da lista. A insero em posio arbitrria pode ser feita em tempo c ca ca a constante se tivermos um ponteiro para o elemento que ser o elemento anterior ao novo elemento: a void lista_insere_apos(Noh *novo, Noh *anterior) { novo->prox = anterior->prox; anterior->prox = novo; } A insero no nal da lista exige que encontremos seu ultimo elemento, o que leva tempo O(n). A ca remoo de um elemento arbitrrio requer que encontremos o elemento anterior ao removido, o que ca a leva tempo O(n). A operao de remoo ca assim: ca ca void lista_remove(Lista *L, Noh *N) { 3

Noh *p; if (N == L->cabeca) { L->cabeca = N->prox; } else { p = lista_anterior(L,N); if (p!=NULL) q->prox = p->prox; } noh_destroi(N); // desaloca o no removido } Para desalocar ns e listas temos as 3 operaes abaixo. o co void noh_destroi(Noh *N) { free(N); } void noh_destroi_recursivo(Noh *N) { if (N->prox != NULL) noh_destroi_recursivo(N->prox); noh_destroi(N); } void lista_destroi(Lista *L) { if (L->cabeca != NULL) noh_destroi_recursivo(L->cabeca); free(L); } A busca de um dado dentro de uma lista ligada leva tempo O(n) (pode ser necessrio percorrer a todos os elementos), e a obteno do x-simo elemento leva tempo (x) ( necessrio percorrer a ca e e a lista desde o in at o elemento desejado). cio e

2.2

Listas Circulares Duplamente Ligadas

A lista simplesmente ligada ineciente quando precisamos acessar o nal da lista ou obter o e elemento anterior. Estes dois problemas podem ser resolvidos pela lista duplamente ligada circular. Nesta lista, cada n tem um apontador para o prximo elemento e para o elemento anterior. Alm o o e disso, o primeiro elemento aponta para o ultimo como seu anterior, e o ultimo elemento aponta para o primeiro como seu prximo. Sabemos onde a lista comea atravs do apontador cabea, o c e c que indica o primeiro elemento. As denies de tipos para a Lista Duplamente Ligada Circular co (LDLC) cam assim: typedef struct LdlNoh { int dado; struct LdlNoh *ant, *prox; 4

} LdlNoh; typedef struct { LdlNoh *cabeca; } LDLC; As operaes para obter o elemento anterior e o ultimo elemento cam triviais, O(1), bastando co seguir o ponteiro ant e pegar o elemento anterior ao primeiro, respectivamente. A lista duplamente ligada circular exige, entretanto, alguns cuidados especiais: como o ultimo elemento tem o ponteiro de prximo elemento apontando para o primeiro, preciso tomar cuidado para no percorrer a lista o e a em c rculos. Abaixo mostramos um exemplo de como percorrer uma LDLC, implementando uma operao de busca: ca LdlNoh * ldlc_busca(LDLC *L, int dado) { LdlNoh *p; p = ldlc_primeiro(L); // p = L->cabeca; if (p==NULL) return NULL; do { if (p->dado == dado) return p; p = ldlc_proximo(p); } while(p!=ldlc_primeiro(L)); return NULL; } Ao realizar inseres e remoes tambm preciso tomar cuidado para costurar os ponteiros co co e e afetados de forma adequada. A funo C abaixo implementa a insero aps um n arbitrrio: ca ca o o a void ldlc_insere_apos(LDLC *L, LdlNoh *novo, LdlNoh *onde) { novo->ant = onde; novo->prox = onde->prox; novo->ant->prox = novo; novo->prox->ant = novo; } Com a LDLC conseguimos realizar todas as operaes, exceto busca e acesso aleatrio, em tempo co o constante. O preo pago por esta performance o gasto de memria com o ponteiro adicional para c e o o elemento anterior. Em um PC comum, se utilizarmos uma LDLC para armazenar apenas um inteiro em cada n de lista, estamos usando 12 bytes por n, 4 para o dado e 8 para os ponteiros. o o Isto signica que 66% da memria ser usada apenas para manter a estrutura, e no para os dados o a a em si.

Performance

Na tabela abaixo comparamos a performance assinttica de tempo para operaes sobre listas em o co trs estruturas de dados: vetores, lista simplesmente ligada (LSL) e LDLC. e Operao ca Criao ca Destruio ca Obter x-simo Elemento e Obter Primeiro Elemento Obter Ultimo Elemento Obter Prximo Elemento o Obter Elemento Anterior Busca Insero (In ca cio) Insero (Meio) ca Insero (Fim) ca Remoo ca Vetor O(1) O(1) O(1) O(1) O(1) O(1) O(1) O(N ) O(N ) O(N ) O(1) O(N ) LSL O(1) O(N ) O(N ) O(1) O(N ) O(1) O(N ) O(N ) O(1) O(N ) O(N ) O(N ) LDLC O(1) O(N ) O(N ) O(1) O(1) O(1) O(1) O(N ) O(1) O(1) O(1) O(1)

Listas ligadas gastam um pouco mais de memria que vetores, mas permitem inseres e remoes o co co mais ecientes. Em conjuntos dinmicos de dados, em que so realizadas muitas inseres e a a co remoes, listas ligadas so mais vantajosas que vetores. Em algumas arquiteturas de computadores co a a fragmentao de uma grande lista como vrios ns no cont ca a o a guos na memria principal pode ser o uma vantagem. A grande vantagem de vetores sobre listas ligadas o acesso aleatrio (obter o e o x-simo elemento) em tempo constante. e Para melhorar a performance da busca necessrio manter os dados ordenados dentro da estrutura. e a Em um vetor ordenado podemos realizar buscas em tempo O(log N ). A busca binria exige acesso a aleatrio, e no diretamente aplicvel a listas ligadas. Para realizar buscas rpidas em estruturas o a e a a ligada utilizaremos rvores de busca, que sero apresentadas em aulas futuras. a a

Exemplo: Representao de Polinmios ca o

Listas ligadas so uma forma comum para a representao de polinmios. Podemos usar uma LDLC a ca o para representar um polinmio mantendo cada termo em um n da lista. Neste caso, cada n ter o o o a duas informaes: o coeciente e e o expoente. Uma opo que melhora a performance na soma co ca de termos e facilita a impresso do polinmio manter os ns em ordem crescente de expoente. a o e o Temos, ento, as estruturas da lista denidas da seguinte forma: a typedef struct Termo { float coef, expo; struct Termo *prox, *ant; } Termo;

typedef struct { Termo *cabeca; } Poly; A operao essencial a adio de um termo a um polinmio existente: procuramos um termo ca e ca o com o mesmo expoente. Se houver, apenas somamos os coecientes. Seno, adicionamos um novo a termo na posio adequada da lista. ca void poly_soma_termo(Poly *P, float coef, float expo) { Termo *t,*u,*s; t = poly_primeiro(P); if (t==NULL || t->expo > expo) { u = termo_cria(coef,expo); poly_insere_inicio(P,u); } else { while(t->expo < expo) { t = poly_proximo(t); if (t == poly_primeiro(P)) break; } if (t->expo == expo) { t->coef = t->coef + coef; } else { u = termo_cria(coef,expo); s = poly_ultimo(P); if (expo > s->expo) { poly_insere_fim(P,u); } else { t = poly_anterior(t); poly_insere_apos(P,u,t); // insere u apos t } } } } E podemos usar esta operao para realizar a soma de polinmios. Para realizar P1 = P1 + P2 ca o basta percorrer P2 e somar cada um de seus termos a P1 : void poly_soma(Poly *P1, Poly *P2) { Termo *t; t = poly_primeiro(P2); if (t==NULL) return; do { poly_soma_termo(P1,t->coef,t->expo); t = poly_proximo(t); } while(t!=poly_primeiro(P2)); } 7

A multiplicao de dois polinmios pode ser realizada com a seguinte funo: ca o ca Poly *poly_mult(Poly *P1, Poly *P2) { Termo *t1, *t2; Poly *P3; P3 = poly_cria(); if (poly_primeiro(P1)==NULL || poly_primeiro(P2)==NULL) return P3; t1 = poly_primeiro(P1); do { t2 = poly_primeiro(P2); do { poly_soma_termo(P3,(t1->coef)*(t2->coef),(t1->expo)+(t2->expo)); t2 = poly_proximo(t2); } while(t2 != poly_primeiro(P2)); t1 = poly_proximo(t1); } while(t1 != poly_primeiro(P1)); return P3; }