Capitulo02 Listas
Capitulo02 Listas
Listas Lineares
Edmilson Marmo Moreira
Universidade Federal de Itajubá - UNIFEI
Instituto de Engenharia de Sistemas e Tecnologia da Informação - IESTI
“Os preguiçosos estão sempre a falar do que tencionam fazer, do que hão de realizar;
aqueles que verdadeiramente fazem alguma coisa não têm tempo de falar nem sequer do
que fazem”.
Goethe
1 Considerações Iniciais
Dentre as estruturas de dados não primitivas, as listas lineares são as de manipulação mais
simples. Uma lista linear agrupa informações referentes a um conjunto de elementos que, de
alguma forma, se relacionam entre si. Ela pode se constituir, por exemplo, de informações sobre
os funcionários de uma empresa, sobre notas de compras, itens de estoque, notas de alunos,
etc. Na realiade, são inúmeros os tipos de dados que podem ser descritos por listas lineares
(SZWARCFITER; MARKENZON, 1994).
Uma lista linear é um conjunto de n ≥ 0 nós L[1], L[2], . . ., L[n] tais que suas propriedades
estruturais decorrem, unicamente, da posição relativa dos nós dentro da sequência linear. Tem-
se:
1
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 2
Além disso, estas respostas podem ser realizadas em tempo constante, sendo esta a maior
vantagem deste tipo de estrutura. Entretanto, as operações de inserção e remoção exigirão mais
cuidados.
A declaração a seguir especifica uma estrutura para representar uma lista estática sequencial:
Basicamente, há necessidade de um vetor para armazenar os elementos da lista que, neste
caso, são do tipo No. Este tipo, em verdade, apenas sinaliza uma estrutura com as informações
que serão utilizadas pela aplicação. O campo Fim da estrutura ListaSequencial representa a
posição do último elemento válido na lista dentro do vetor.
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 3
É preciso destacar o fato de que a lista deve ser inicializada. Neste caso, basta atribuir o
valor -1 ao campo Fim, sinalizando que a lista está vazia.
int i = 0;
while ((i <= [Link]) && ([Link][i].Chave != [Link]))
i++;
if (i > [Link])
return false;
N = [Link][i];
while (i < [Link]) {
[Link][i] = [Link][i+1];
i++;
}
[Link]--;
return true;
}
Para resolver o segundo problema, pode-se utilizar um mecanismo conhecido como encade-
amento dos nós. A figura 2 ilustra esta estrutura. Cada elemento da lista no vetor será um
registro com dois campos: o campo Info (informação), que será utilizado para armazenar os
valores na lista, e o campo Lig (ligação), utilizado para propiciar o encadeamento na lista, ou
seja, este campo irá indicar a posição do próximo nó na lista.
Existem duas listas nesta estrutura. A lista de disponı́veis, indicada pela variável Dispo, e
a lista propriamente dita, indicada pela variável Com. O valor -1 indica o fim das duas listas
mantidas nesta estrutura.
A próxima declaração especifica uma estrutura para representar uma lista estática encadeada:
struct ListaEstaticaEncadeada {
int Com;
int Dispo;
No Dados[TAM];
};
Nesta nova representação, o número de nós na lista continua sendo limitado, uma vez que a
lista está implementada utilizando vetor, no entando os processos de inserção e remoção dos nós
serão simplificados pelo acerto de apontadores (campo de ligação). A figura 3 ilustra o acerto
dos apontadores após a inserção de um nó e a figura 4 ilustra o procedimento de remoção.
• Se a lista estiver vazia. Neste caso, passará a ter um único nó e a lista começará neste nó.
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 6
• Inserção de um nó como primeiro da lista (menor critério de ordem). Aqui, o novo nó será
o começo da lista.
• Se a inserção do novo nó ocorrer no meio ou no fim da lista. Neste caso, será feito um
acerto de apontadores como descrito anteriormente.
} else {
[Link][Aux].Lig = [Link][Ant].Lig;
[Link][Ant].Lig = Aux;
}
}
return true;
}
}
• Remoção do primeiro nó da lista. Neste caso, deve-se mudar o apontador de começo da
lista para o segundo nó.
• O nó a ser removido está no meio da lista. Aqui, as ligações devem ser refeitas conforme
discutido anteriormente.
• A lista está vazia ou o nó a ser removido não se encontra na lista. Neste caso, a função
deve retornar false indicando que a remoção não pode ser realizada.
É importante considerar ainda que a remoção de um nó implica na inserção deste nó na lista
de disponı́veis, ou seja, após a atualização das ligações da lista principal, o espaço ocupado pelo
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 8
nó deve voltar para a lista de disponı́veis. Portanto, também será preciso atualizar as ligações
da lista de disponı́veis.
A função seguinte apresenta a codificação do método de remoção:
4 Lista Dinâmica
Foi possı́vel, através do encadeamento, resolver o problema do desempenho dos algoritmos
de inserção e remoção da lista sequêncial. Para solucionar o problema do limite de elementos,
provocado pela estrutura de vetor, é possı́vel a utilização de ponteiros, ou seja, alocação dinâmica
de memória.
Para utilizar alocação dinâmica de memória será preciso alterar a estrutura No. Este tipo
de estrutura deve ser autorreferente, isto é, deve conter um membro ponteiro que armazena o
endereço de outra estrutura do mesmo tipo. Por exemplo, o trecho de código abaixo define um
tipo No que é uma estrutura autorreferente:
struct No {
char Info;
No * Lig;
};
Portanto, não há mais necessidade de passar o vetor que representa a lista encadeada e nem o
apontador de disponı́veis, pois toda a memória está disponı́vel para a alocação. Neste contexto,
também não é preciso se preocupar com o controle dos nós disponı́veis.
A função seguinte apresenta o código, semelhante ao da estrutura estática, para a lista
encadeada dinâmica:
Para facilitar a manipulação do ponteiro de inı́cio da lista, foi definido um novo tipo para
um ponteiro de nó (typedef No * PtrNo). Além disso, a função InsereLista não possui valor
de retorno (void), pois não há necessidade de testar se a lista está cheia.
{
NoPtr Ant = NULL;
NoPtr Aux = L;
while ((Aux != NULL) && (Aux->Info != N)) {
Ant = Aux;
Aux = Aux->Lig;
}
if (Aux == NULL)
return false;
if (Aux == L)
L = L->Lig;
else
Ant->Lig = Aux->Lig;
delete Aux;
return true;
}
Finalmente, é preciso considerar que não há necessidade de uma função para iniciar a lista
dinâmica, basta fazer com que o ponteiro de inı́cio da lista comece valendo NULL.
Devido à semelhança entre as funções de inserção e remoção das listas simplesmente e du-
plamente encadeada será apresentado apenas o algoritmo de inserção, ficando a primitiva de
remoção como sugestão de exercı́cio.
struct No {
char Info;
No * Ant;
No * Pos;
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 11
};
typedef No * NoPtr;
1. Quando o novo nó é o primeiro da lista. Neste caso, há a necessidade de acertar o ponteiro
de inı́cio da lista (ponteiro L).
Na lista duplamente encadeada o último nó não pode ser trabalhado de forma idêntica aos
nós internos da lista. Por conseguinte, três situações foram tratadas na primitiva:
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 12
2. Quando o novo nó fará parte dos nós internos da lista, ou seja, não será nem o primeiro
nem o último.
struct NoDescritor {
No * Com;
int Nro;
No * Fim;
};
typedef NoDescritor * DescritorPtr;
Usando esta nova estrutura de lista, passa a ser necessária a existência de uma nova operação:
criar uma lista vazia. Esta operação consiste em alocar um nó do tipo descritor, tornar as suas
referências nulas, e fazer a variável que indica o inı́cio da lista apontar para o nó. A rotina
CriarDescritor, a seguir, implementa esta operação:
A seguir, são apresentados outras funções para manipulação de listas encadeadas com des-
critor. A primeira, InsereEsquerda, implementa a operação de inserção de um nó com o dado
valor à esquerda da lista cujo descritor é apontado pela variável D.
Para remover o nó da esquerda de uma lista, pode-se utilizar a função RemoveEsquerda, a
seguir. Esta rotina remove o primeiro nó da lista, se houver, e retorna o dado que o nó removido
continha através do parâmetro Valor.
NoPtr P = D->Com;
Valor = P->Info;
D->Com = P->Lig;
D->Nro--;
if (D->Nro == 0)
D->Fim = NULL;
}
}
A operação de remoção do nó à direita da lista com descritor envolve o mesmo problema
apresentado para o caso de listas sem descritor: a necessidade de percorrer todos os nós da lista,
sequencialmente, a partir do primeiro (da esquerda), até atingir o nó da direita. Este problema
pode ser evitado com o uso de listas duplamente encadeadas.
Entretanto, esta estrutura apresenta um problema sutil. No caso do ponteiro Aux ser NULL,
o comando pode ser inválido, pois Aux->Info não existe. Se o compilador não realiza o segundo
teste no caso do primeiro falhar, não haveria problema. Em nı́vel de algoritmo, é melhor resolver
o problema evitando a possibilidade desse tipo de erro ocorrer, ao invés de usar soluções que
dependem do compilador (PIMENTEL; OLIVEIRA, 2006).
Uma possibilidade de solução para o problema exposto seria um código semelhante ao descrito
a seguir:
Acabou = false;
while ((Aux != NULL) && (!Acabou))
if (Aux->Info == Valor)
Acabou = true;
else
Aux = Aux->Lig;
Apesar de resolver o problema do ponteiro nulo, este código adiciona novos testes o que pode
diminuir a eficiência do método de busca.
Um elemento sentinela pode ser utilizado na busca em listas encadeadas. Basta, para isso,
usar um registro no final da lista. Neste contexto, uma lista vazia não será aquela cujo apontador
é NULL, mas aquela cujo ponteiro tem o endereço do nó sentinela.
A função a seguir ilustra o procedimento de inicialização de uma lista com sentinela:
O código seguinte implementa uma busca por um elemento Valor na lista apontada por L
não ordenada. Esta função retorna NULL caso o elemento não seja encontrado; caso contrário,
a função retorna o endereço do registro que contém o elemento.
8 Listas Circulares
Em algumas situações é necessário o uso de uma lista circular, na qual os nós formam um
anel: a lista é finita e cada nó tem um sucessor. Um exemplo de tal situação é quando diversos
processos de um Sistema Operacional estão usando os mesmos recursos ao mesmo tempo, sendo
necessário assegurar que nenhum processo acesse o recurso antes de todos os outros processos.
Em consequência, todos os processos são colocados em uma lista circular, acessı́veis através do
ponteiro de inı́cio da lista, conforme ilustra a figura 7. Depois que um nó da lista é acessado e o
número do processo é recuperado do nó para ativar esse processo, o ponteiro de inı́cio se move
para o próximo nó de modo que o próximo processo possa ser ativado a seguir (DROZDEK,
2002).
O conceito de lista circular pode ser aplicado às listas simplemente encadeadas, bem como
às listas duplamente encadeadas.
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 16
O código a seguir apresenta um exemplo de declaração para a estrutura nó de uma lista
generalizada. Destaca-se o uso do construtor union das linguagens C/C++, que permite imple-
mentar o conceito de campo variável. Neste caso, o espaço ocupado pelo campo Cab será capaz
de armazenar um ponteiro ou um átomo. Como, no exemplo, o átomo é uma variável do tipo
char, o compilador reservará o espaço para um ponteiro, pois este ocupa um tamanho maior
que apenas um caracter.
struct No;
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 17
typedef No * NoPtr;
union Cabeca {
char Atomo;
NoPtr Ponteiro;
};
struct No {
bool Tag;
Cabeca Cab;
NoPtr Cauda;
};
nó aponta para o nó duas posições à frente, cada quarto nó aponta para o nó quatro posições à
frente e assim por diante, como mostrado na figura 9a. Isso é realizado com diferentes números
de ponteiros em nós na lista: metade dos nós tem somente um ponteiro, um quarto dos nós tem
dois ponteiros, um oitavo dos nós tem três ponteiros e assim por diante. O número de ponteiros
indica o nı́vel de cada nó, e o número de nı́veis é maxLevel = blgnc + 1.
A busca pelo elemento el consiste em seguir os ponteiros no nı́vel mais alto até que um
elemento seja encontrado, o que finaliza a busca com sucesso. No caso de se atingir o final da
lista ou de encontrar um elemento chave que seja maior do que el, a busca é reiniciada a partir
do nó que precede aquele contendo a chave, mas agora fixando um ponteiro num nı́vel mais
baixo do que antes. A busca continua até que el seja encontrado ou que os ponteiros do primeiro
nı́vel atinjam o final da lista, ou encontrem um elemento maior do que el.
Por exemplo, se for necessário encontrar o número 16 na lista da figura 9b, o nı́vel quatro
é tentado primeiro, o que não tem sucesso porque o primeiro nó nesse nı́vel tem valor 28. A
seguir, tenta-se a sublista do terceiro nı́vel, começando a partir da raiz: isso leva a 8 e, então,
a 17. Por isso, tenta-se a sublista de segundo nı́vel que se origina no nó contendo 8: isso leva a
10 e então novamente a 17. A última tentativa é iniciar a sublista do primeiro nı́vel que começa
no nó 10; esse primeiro nó da sublista tem 12, o próximo número é 17 e, desde que não haja um
nı́vel mais baixo, a busca é declarada como sem sucesso.
O código para o procedimento de busca é dado a seguir:
if (Chave == Aux->Info)
return Aux;
else if (Chave < Aux->Info) {
if (nvl == 0)
return NULL;
else if (Aux == Raiz[nvl])
Aux = Raiz[--nvl];
else Aux = *(Ant->Lig + --nvl);
} else {
Ant = Aux;
if (*(Aux->Lig + nvl) != NULL)
Aux = *(Aux->Lig + nvl);
else {
for (nvl--; nvl >= 0 && *(Aux->Lig + nvl)==NULL; nvl--);
if (nvl >= 0)
Aux = *(Aux->Lig + nvl);
else
return NULL;
}
}
}
}
A busca parece ser eficiente. No entanto, o projeto de listas de salto pode levar a proce-
dimentos de inserção e remoção ineficientes. Para inserir um novo elemento, todos os nós que
seguem o nó inserido têm que ser re-estruturados; o número de ponteiros e os valores dos pon-
teiros têm que ser mudados. De modo a reter algumas das vantagens que as listas de salto
oferecem com relação à busca e evitar problemas em re-estruturar as listas ao inserir e remover
nós, a exigência sobre as posições dos nós de diferentes nı́veis é agora abandonada e somente a
exigência sobre o número de nós de diferentes nı́veis é mantida. Por exemplo, a lista na figura
9a se torna a lista da figura 9b: ambas têm seis nós com somente um ponteiro, três nós com dois
ponteiros, dois nós com três ponteiros e um nó com quatro ponteiros. A nova lista é pesquisada
exatamente do mesmo modo que a lista original. A inserção não exige re-estruturação da lista, e
os nós são gerados de modo que a distribuição deles em diferentes nı́veis seja mantida adequada
(DROZDEK, 2002).
Por exemplo, assumindo que maxN ivel = 4. Para 15 elementos, o número exigido de nós de
um ponteiro é oito, nós de dois ponteiros é quatro, nós de três ponteiros é dois e nós de quatro
ponteiros é um. Cada vez que um nó é inserido, um número não-sequencial r entre 1 e 15 é
gerado e, se r < 9, um nó de nı́vel um é inserido. Se r < 13, um nó de segundo nı́vel é inserido;
se r < 15, é um nó do terceiro nı́vel; se r = 15, um nó de nı́vel quatro é gerado e inserido. Se
maxN ivel = 5, então, para 31 elementos, a correspondência entre o valor de r e o nı́vel de nó é
esta:
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 20
Para determinar tal correspondência entre r e o nı́vel do nó para qualquer maxN ivel, a
função DefinirLimites inicializa a matriz Limites[], colocando limites inferiores para cada
intervalo. Por exemplo, para maxN ivel = 4 a matriz é [1, 9, 13, 15] e para maxN ivel = 5,
[1, 17, 25, 29, 31]. A função EscolherNivel usa Limites[] para determinar o nı́vel do nó a ser
inserido. A seguir os códigos para DefinirLimites e EscolherNivel:
É importante considerar ainda que o modo mais simples de implementar cada nó é fazer cada
nó ter maxN ivel ponteiros, mas isso é um desperdı́cio. O ideal é utilizar somente a quantidade
de ponteiros que o nı́vel do nó exige. Para realizar isso, o membro Lig de cada nó não é um
ponteiro para o próximo nó, mas para uma matriz de ponteiro(s) ao(s) próximo(s) nó(s). O
tamanho dessa matriz é determinada pelo nı́vel do nó. Assim, a lista na figura 9b é realmente
uma lista cujos quatro primeiros nós são mostrados na figura 9c. O procedimento de inserção
para este caso é apresentado a seguir:
void InsereListaDeSalto (NoPtr Raiz[], int Limites[], int maxNivel, char Chave)
{
NoPtr Aux[maxNivel], Ant[maxNivel], P;
int nvl, i;
Aux[maxNivel-1] = Raiz[maxNivel-1];
Ant[maxNivel-1] = 0;
for (nvl = maxNivel-1; nvl >= 0; nvl--) {
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 21
struct No {
char Info;
No ** Lig;
};
0 0 0 0 6 0
0 3 0 0 0 0
0 0 0 0 4 0
5 0 1 0 0 0
0 0 0 0 0 0
É necessário encontrar outra representação que evite o armazenamento de tantas posições nu-
las. Uma das soluções para este tipo de situação é a utilização de listas cruzadas (PIMENTEL;
OLIVEIRA, 2006).
Para não representar os valores nulos, será necessário criar listas de linhas e listas de
colunas tais que o elemento Mi,j diferente de zero pertence à lista dos elementos da linha i e
à lista dos elementos da coluna j. Então, se a matriz possui nl linhas e nc colunas, tem-se nl
listas de linhas e nc listas de colunas. Graficamente esta estrutura pode ser representada pela
figura 10.
#define NL 10;
#define NC 10;
struct No {
int Lin;
int Col;
int Valor;
No * PL;
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 23
No * PC;
};
struct ListasCruzadas {
No * Linha[NL];
No * Coluna[NC];
};
• 5 × n espaços para armazenar os membros de cada nó (um para cada campo do registro:
Lin, Col, Valor, PL e PC);
Portanto, o espaço total ocupado por essa estrutura seria 5n+nl +nc. Como a representação
matricial ocuparia um total de nl×nc, conclui-se que, em termos de espaço ocupado, há vantagem
em utilizar-se a representação de listas cruzadas quando:
Algoritmos e Estruturas de Dados - Notas de Aula - Capı́tulo 02 - 24
Como (nl −1)×(nc−1) é aproximadamente o tamanho da matriz, pode-se dizer, de uma ma-
neira geral que, há ganho em termos de espaço, quando um número inferior a 1/5 dos elementos
da matriz não forem nulos.
A função seguinte apresenta um código para somar uma constante (K) aos elementos da
coluna J de uma determinada matriz, representada pela variável LC:
10 Exercı́cios
1. Implemente as primitivas de consulta e alteração de um nó em uma lista estática sequencial.
2. Desenvolver uma função para retornar a quantidade de elementos de uma lista estática
encadeada.
3. Desenvolva uma função para percorrer uma lista estática encadeada apresentando os seus
elementos.
4. Faça uma função para retornar o último elemento de uma lista estática encadeada.
7. Polinômios podem ser representados por meio de listas, cujos nós são registros com 3
campos: coeficiente, expoente e referência ao seguinte. Usando esta representação, descreva
procedimentos para
9. Escreva uma função para inverter as referências de uma lista simplesmente encadeada.
10. Escreva uma função para verificar se duas listas encadeadas possuem o mesmo conteúdo.
11. Faça uma função para anexar uma lista simplesmente encadaeada ao final de outra lista
do mesmo tipo.
12. Seja L uma lista simplesmente encadeada, composta dos números l1 , l2 , · · · , ln , respectiva-
mente, segundo a ordem de armazenamento. Escreva uma função que, percorrendo L uma
única vez, constrói uma outra lista L0 , formada dos elementos seguintes:
(a) l2 , l3 , · · · , ln , l1 .
(b) ln , ln−1 , · · · , l1 .
(c) l1 + ln , l2 + ln−1 , · · · , ln/2 + ln/2+1 , onde n é par.
13. As frações de Farey de nı́vel um (1) são definidas como a sequência ( 10 , 11 ). Essa sequência
é estendida no nı́vel dois para formar a sequência ( 10 , 12 , 11 ), a sequência ( 01 , 31 , 12 , 23 , 11 ) no
nı́vel três e a sequência ( 01 , 41 , 13 , 21 , 23 , 43 , 11 ) no nı́vel quatro, de modo que a cada nı́vel n
(a+b) a b
uma nova fração (c+d) é inserida entre duas frações vizinhas c e d somente se c + d ≤ n.
Pede-se:
(a) A declaração do nó para uma lista que represente as frações de Farey.
(b) Uma função que recebe um ponteiro para a lista descrita e um número n, e crie a
lista estendendo-a constantemente até se alcançar a lista de frações no nı́vel n.
15. Seja a função Esvazie() tal que, recebendo uma lista como entrada, esvazie a lista descar-
tando todos os seus elementos. Escreva a função Esvazie() para as seguintes estruturas:
17. Faça uma função para apresentar a representação literal de uma lista generalizada.
18. Dê um exemplo de lista generalizada a qual contém, no total, pelo menos 25 átomos e 10
cabeças em, pelo menos, quatro nı́veis. Escreva sua lista em notação de parênteses (LISP).
19. Desenvolva uma função para apresentar todos os elementos de uma lista de salto.
Referências
DROZDEK, A. Estrutura de Dados e Algoritmos em C++. São Paulo: Pioneira Thomson
Learning, 2002.
VELOSO, P. et al. Estrutura de Dados. 2a. ed. Rio de Janeiro: Editora Campus, 1986.