Projeto de Algoritmos

Listas encadeadas

Uma lista encadeada é uma representação de uma seqüência de objetos na memória do computador. Cada elemento da seqüência é armazenado em uma célula da lista: o primeiro elemento na primeira célula, o segundo na segunda e assim por diante.

Veja o verbete Linked list na Wikipedia.

Estrutura de uma lista encadeada

Uma lista encadeada  (= linked list = lista ligada)  é uma seqüência de células; cada célula contém um objeto de algum tipo e o endereço da célula seguinte.   Suporemos nesta página que os objetos armazenados nas células são do tipo int.  A estrutura de cada célula de uma tal lista pode ser definida assim:
    struct cel {
       int         conteudo;
       struct cel *prox;
    } ;                                    
 
   
conteudo prox

É conveniente tratar as células como um novo tipo-de-dados e atribuir um nome a esse novo tipo:

   typedef struct cel celula;

Uma célula  c  e um ponteiro  p  para uma célula podem ser declarados assim:

   celula  c;
   celula *p;

Se c é uma célula então  c.conteudo  é o conteúdo da célula e  c.prox  é o endereço da próxima célula.  Se  p  é o endereço de uma célula, então  p->conteudo  é o conteúdo da célula e  p->prox  é o endereço da próxima célula.  Se  p  aponta a última célula da lista então 

p->prox  vale  NULL.

lista encadeada

Endereço de uma lista encadeada

O endereço de uma lista encadeada é o endereço de sua primeira célula.  Se p é o endereço de uma lista, convém, às vezes, dizer simplesmente

"p é uma lista".

Listas são animais eminentemente recursivos. Para tornar isso evidente, basta fazer a seguinte observação:  se p é uma lista então vale uma das seguintes alternativas:

Listas com cabeça e sem cabeça

Uma lista encadeada pode ser organizada de duas maneiras diferentes, um óbvia e outra menos óbvia.

Suporemos no que segue que nossas listas têm cabeça. O caso de listas sem cabeça será tratado nos exercícios. Eu prefiro listas sem cabeça (porque são mais "puras"), mas a vida do programador fica mais fácil quanto a lista tem cabeça.

Exemplos

Eis como se imprime o conteúdo de uma lista encadeada com cabeça:

// Imprime o conteúdo de uma lista encadeada
// com cabeça. O endereço da primeira célula é ini.

void imprima (celula *ini)
{
   celula *p;
   for (p = ini->prox; p != NULL; p = p->prox) 
      printf ("%d\n", p->conteudo);
}

Eis a correspondente função para lista sem cabeça:

// Imprime o conteúdo de uma lista encadeada ini.
// A lista não tem cabeça.

void imprima (celula *ini)
{
   celula *p;
   for (p = ini; p != NULL; p = p->prox) 
      printf ("%d\n", p->conteudo);
}

Busca em uma lista encadeada

Veja como é fácil verificar se um inteiro x pertence a uma lista encadeada, ou seja, se é igual ao conteúdo de alguma célula da lista:

// Esta função recebe um inteiro x e uma lista
// encadeada de inteiros. O endereço da lista é
// ini e ela tem uma celula-cabeça. A função
// devolve o endereço de uma celula que contém x.
// Se tal celula não existe, a função devolve NULL.

celula *busca (int x, celula *ini)
{
   celula *p;
   p = ini->prox;
   while (p != NULL && p->conteudo != x) 
      p = p->prox; 
   return p; 
}

Que beleza! Nada de variáveis booleanas! A função se comporta bem até mesmo quando a lista está vazia.

Eis uma versão recursiva da mesma função:

celula *busca2 (int x, celula *ini)
{
   if (ini->prox == NULL) 
      return NULL;
   if (ini->prox->conteudo == x) 
      return ini->prox;
   return busca2 (x, ini->prox);
}

Exercícios

  1. Critique a função abaixo. Ao receber uma lista encadeada com cabeça e um inteiro x, ela promete devolver o endereço de uma célula com conteúdo x. Se tal célula não existe, promete devolver NULL.
    celula *busca (int x, celula *ini) {
       int achou;
       celula *p;
       achou = 0;
       p = ini->prox;
       while (p != NULL && !achou) {
          if (p->conteudo == x) achou = 1;
          p = p->prox; }
       if (achou) return p;
       else return NULL; }
    
  2. Escreva uma versão da função busca para listas sem cabeça.
  3. Escreva uma função que faça um busca em uma lista crescente. Faça versões para listas com e sem cabeça. Faça versões recursiva e iterativa.
  4. [Bom!]  Escreva uma função que receba uma lista encadeada e devolve o endereço de um nó que esteja o mais próximo possível do meio da lista. Faça isso sem contar explicitamente o número de nós da lista.

Inserção em uma lista

Quero inserir (= insert) uma nova célula com conteúdo x entre a posição apontada por  p  e a posição seguinte [por que seguinte e não anterior?] em uma lista encadeada.  É claro que isso só faz sentido se p é diferente de NULL.

// Esta função insere uma nova celula em uma 
// lista encadeada. A nova celula tem conteudo
// x e é inserida entre a celula apontada por 
// p e a seguinte. Supõe-se de p != NULL.

void insere (int x, celula *p)
{
   celula *nova;
   nova = mallocX (sizeof (celula));
   nova->conteudo = x;
   nova->prox = p->prox;
   p->prox = nova;
}

Veja que maravilha! Não é preciso movimentar células para "criar espaço" para um nova célula, como fizemos para inserir um elemento de um vetor. Basta mudar os valores de alguns ponteiros.

Observe também que a função se comporta corretamente mesmo quando quero inserir no fim da lista, isto é, quando p->prox == NULL.  Se a lista tem cabeça, a função pode ser usada para inserir no início da lista: basta que p aponte para a célula-cabeça.  Infelizmente, a função não é capaz de inserir antes da primeira célula de uma lista sem cabeça.

O tempo que a função consome não depende do ponto da lista onde quero fazer a inserção: tanto faz inserir uma nova célula na parte inicial da lista quanto na parte final.  Isso é bem diferente do que ocorre com a inserção em um vetor.

Exercícios

  1. Por que a seguinte versão de insere não funciona?
    void insere (int x, celula *p) {
       celula nova;
       nova.conteudo = x;
       nova.prox = p->prox;
       p->prox = &nova; }
    
  2. Escreva uma função que insira um novo elemento em uma lista encadeada sem cabeça. Será preciso tomar algumas decisões de projeto antes de começar a programar.

Remoção em uma lista

Suponha que quero remover (= to remove = to delete) uma certa célula da lista. Como posso especificar a célula em questão? A idéia mais óbvia é apontar para a célula que quero remover. Mas é fácil perceber que essa idéia não é boa. É melhor apontar para a célula anterior à que quero remover. Infelizmente, isso traz uma nova dificuldade: não há como pedir a remoção da primeira célula. Portanto, vamos nos limitar às listas com cabeça.

Vamos supor que p é o endereço de uma célula de uma lista com cabeça e que desejo remover a célula apontada por p->prox.  (Note que a função de remoção não precisa saber onde a lista começa.)

// Esta função recebe o endereço p de uma celula 
// de uma lista encadeada. A função remove da
// lista a celula p->prox. A função supõe que
// p != NULL e p->prox != NULL.

void remove (celula *p)
{
   celula *morta;
   morta = p->prox;
   p->prox = morta->prox;
   free (morta);
}

Veja que maravilha! Não é preciso copiar informações de um lugar para outro, como fizemos para remover um elemento de um vetor: basta mudar o valor de um ponteiro. A função consome sempre o mesmo tempo, quer a célula a ser removida esteja perto do início da lista quer esteja perto do fim.

Exercícios

  1. Critique a seguinte versão da função remove:
    void remove (celula *p, celula *ini) {
       celula *morta;
       morta = p->prox;
       if (morta->prox == NULL)  p->prox = NULL;
       else  p->prox = morta->prox;
       free (morta); }
    
  2. Invente um jeito de remover uma célula de uma lista encadeada sem cabeça. (Será preciso tomar algumas decisões de projeto antes de começar a programar.) 
  3. Escreva uma função recursiva que remova de uma lista encadeada uma célula cujo conteúdo é mínimo. Documente sua função.

Mais exercícios

  1. Escreva uma função que copie um vetor para uma lista encadeada. Faça duas versões: uma iterativa e uma recursiva.
  2. Escreva uma função que copie uma lista encadeada para um vetor. Faça duas versões: uma iterativa e uma recursiva. 
  3. Escreva uma função que faça uma cópia de uma lista dada.
  4. Escreva uma função que concatena duas listas encadeadas (isto é, "amarra" a segunda no fim da primeira).
  5. Escreva uma função que conta o número de células de uma lista encadeada.
  6. Escreva uma função que remove a k-ésima célula de uma lista encadeada sem cabeça. Escreva uma função que insere na lista uma nova célula com conteúdo x entre a k-ésima e a k+1-ésima células.
  7. Escreva uma função que verifica se duas listas dadas são iguais, ou melhor, se têm o mesmo conteúdo. Faça duas versões: uma iterativa e uma recursiva. 
  8. Escreva uma função que desaloca (função free) todos os nós de uma lista encadeada.  Estamos supondo, é clarom que cada nó da lista foi originalmente alocado por malloc
  9. Escreva uma função que inverte a ordem das células de uma lista encadeada (o primeiro passa a ser o último, o segundo passa a ser o penúltimo etc.). Faça isso sem usar espaço auxiliar: somente altere os ponteiros. Dê duas soluções: uma iterativa e uma recursiva. 
  10. Projeto de Programação.  Digamos que um texto é um vetor de caracteres contendo apenas letras, espaços em branco e sinais de pontuação. Digamos que uma palavra é um segmento maximal de texto que consiste apenas de letras. Escreva uma função que recebe um texto e imprime uma relação de todas as palavras que ocorrem no texto juntamente com o número de ocorrências de cada palavra.

Outros tipos de listas

A partir de agora, tudo é festa: você pode inventar uma grande variedade de tipos de listas encadeadas. Por exemplo, você pode fazer uma lista encadeada circular:  a última célula aponta para a primeira e não para NULL. A lista pode ou não ter uma célula-cabeça (você decide). Para especificar uma lista circular, basta fornecer um endereço (por exemplo, o endereço da última célula).

Outro tipo útil é a lista duplamente encadeada:  cada célula contém o endereço da célula anterior e o da célula seguinte. A lista pode ou não ter uma célula-cabeça (você decide). A lista pode até ter uma célula-rabo se você achar isso útil! Uma tal lista pode ser especificada por dois endereços: o da primeira célula e o da última.

Pense nas seguintes questões, apropriadas para qualquer tipo de lista encadeada. Em que condições a lista está vazia? Como remover a célula apontada por p? Idem para a célula seguinte à apontada por p? Idem para a célula anterior à apontada por p? Como inserir uma nova célula entre o elemento apontado por p e o seu antecessor? Idem entre p e seu sucessor?

Exercícios

  1. Descreva, em linguagem C, a estrutura de uma das célula de uma lista duplamente encadeada.
  2. Escreva uma função que remove de uma lista duplamente encadeada a célula apontada por p. (Que dados sua função recebe? Que coisa devolve?)
  3. Escreva uma função que insira em uma lista duplamente encadeada, logo após a célula apontada por p, uma nova célula com conteúdo y. (Que dados sua função recebe? Que coisa devolve?)
  4. Problema de Josephus.  Imagine que temos uma n pessoas dispostas "em roda". Suponha que as pessoas estão numeradas 1 a n no sentido horário. Repita o seguinte processo enquanto a roda tiver duas ou mais pessoas:   andando sempre no sentido horário, elimine cada m-ésima pessoa.   Qual o número do sobrevivente? 

Busca-e-remoção

Suponha que ini é o endereço de uma lista encadeada com cabeça.  Nosso problema:  Dado um inteiro y, remover da lista a primeira célula que contém y (se tal célula não existe, não é preciso fazer nada).

// Esta função recebe uma lista encadeada ini,
// com cabeça, e remove da lista a primeira
// celula que contiver y, se tal celula existir.

void buscaEremove (int y, celula *ini)
{
   celula *p, *q;
   p = ini;
   q = ini->prox;
   while (q != NULL && q->conteudo != y) {
      p = q;
      q = q->prox;
   }
   if (q != NULL) {
      p->prox = q->prox;
      free (q);
   }
}

Invariante: no início de cada iteração (imediatamente antes da comparação de q com NULL), temos

q == p->prox ,

ou seja,  q  está sempre um passo à frente de  p.

Busca-e-inserção

Mais uma vez, suponha que tenho uma lista encadeada ini, com cabeça. (É óbvio que ini é diferente de NULL.)  Nosso problema:  Inserir na lista uma nova célula com conteúdo x imediatamente antes da primeira célula que tiver conteúdo y ;  se tal célula não existe, inserir x no fim da lista.

// Esta função recebe uma lista encadeada ini,
// com cabeça, e insere na lista uma nova celula
// imediatamente antes da primeira que contiver y.
// Se nenhuma celula contém y, insere a nova
// celula no fim da lista. O conteudo da nova
// celula é x.

void buscaEinsere (int x, int y, celula *ini)
{
   celula *p, *q, *nova;
   nova = mallocX (sizeof (celula));
   nova->conteudo = x;
   p = ini;
   q = ini->prox;
   while (q != NULL && q->conteudo != y) {
      p = q;
      q = q->prox;
   }
   nova->prox = q;
   p->prox = nova;
}

Exercícios

  1. Escreva funções busca-e-remove e busca-e-insere para listas encadeadas sem cabeça (só pra ver que dor de cabeça isso dá).
  2. Escreva uma função para remover de uma lista encadeada todos os elementos que contêm y.

 

 


URL of this site: www.ime.usp.br/~pf/algoritmos/
1998 | Last modified: Tue Oct 17 07:57:41 BRST 2006
Paulo Feofiloff
IME-USP

Valid HTML 4.0 Transitional     Valid CSS!