Sunteți pe pagina 1din 60

Universidade Regional do Noroeste do RS

Departamento de Tecnologia

Algoritmos
e
Estruturas de Dados

Parte II  Estruturas Básicas

Marcos César C. Carrard


Apresentação

Este trabalho é a continuidade daquele com o mesmo nome, que tratava


dos fundamentos da área de algoritmos e estruturas de dados. O objetivo aqui é
introduzir o estudos das estruturas básicas, sequenciais e encadeadas, sob a ótica
dos seus algoritmos e sua eficiência de trabalho.
Para isto serão apresentadas as estruturas pilha, fila, fila circular e listas
encadeadas e, para todas elas, além da discussão da sua definição, discutiremos
as formas de implementação, algoritmos e análise destes algoritmos. O objetivo é
não só propor a estrutura, mas sim entendê-la de forma a se obter um grau de
liberdade mais amplo no uso das mesmas.
Como estruturas de dados se caracterizarão como proposições, na
maioria das vezes lógicas, para uso dos dados, existe um objetivo não tão claro
presente que é a idéia de que as pessoas possam não só utilizar estas estruturas de
forma correta como possam modificá-las e até apresentar estruturas novas para os
seus problemas específicos. Isto só é possível após um bom e perfeito
entendimento deste item.
Finalmente e mais uma vez, por ser um material em constante atenção e
experimentação, caso o leitor localize algum incorreção, melhoria ou tenha
qualquer comentário, entre em contato comigo pelo e-mail abaixo.

Marcos Carrard
carrard@detec.unijui.tche.br

2
Índice

Capítulo 1 − Fundamentos
1.1 Introdução 4
1.2 Estruturas de Dados 5
1.3 Formas de Organização 7
1.4 Exercícios 8

Capítulo 2 − Estruturas de Dados Sequenciais


2.1 Caracterização 9
2.2 Pilha 9
2.2.1 Apresentação 9
2.2.2 Implementação 11
2.2.3 Algoritmos 12
2.2.4 Análise 15
2.3 Fila 16
2.3.1 Apresentação 16
2.3.2 Implementação 17
2.3.3 Algoritmos 18
2.3.4 Análise 20
2.4 Fila Circular 22
2.4.1 Apresentação 22
2.4.2 Implementação 23
2.4.3 Algoritmos 23
2.4.4 Análise 26
2.5 Exercícios 26

Capítulo 3 − Estruturas de Dados Encadeadas


3.1 Caracterização 29
3.2 Listas Encadeadas 30
3.2.1 Apresentação 30
3.2.2 Implementação 33
3.2.3 Algoritmos 35
3.2.3.1 Lista unicamente encadeada 36
3.2.3.2 Lista duplamente encadeada 46
3.2.4 Análise 55
3.3 Exercícios 57
Bibliografia 59

3
1
Fundamentos

1.1 Introdução

Na primeira parte deste trabalho ([Car98]) foi abordada e trabalhada a


importância dos temas “algoritmos” e “análise de algoritmos” para uma boa
compreensão no estudo de estruturas de dados. Por outro lado, isto é realmente
necessário? Isto é fundamental!
Note que aquele material trabalha algoritmos sob um ponto de vista
muito próximo do teórico. Já estruturas de dados necessariamente são orientadas
para uma dada aplicação, ou seja, são eminentemente práticas. Onde então eles se
encontram?
Vamos considerar um situação prática hipotética. Suponha que, para um
dado problema existem várias estruturas de dados capazes, através de diferentes
caminhos, de chegar até a resposta correta. Destas estruturas sairão vários
algoritmos que as manipulam. Assim sendo, qual destes algoritmos será o
escolhido? Para chegarmos até esta resposta é imprescindível que, além de
entender os algoritmos, precisamos analisá-los de maneira a chegar até uma
medida quantitativa e qualitativa que permita a comparação dos mesmos entre si
e assim, ao mesmo tempo, estaremos comparando as estruturas de dados.
Se não bastasse isto, ao trabalhar uma estrutura, ela convergirá para
algoritmos de manipulação, ou seja, algoritmos que descrevem as operações
básicas de uso da mesma. Será então muito importante extrair destes algoritmos
características que permitam conhecer não só o funcionamento da estrutura, mas
também seus aspectos positivos e negativos. Isto é perfeitamente possível e
factível quando aplicamos técnicas de análise de algoritmos como aquelas vistas.

4
Finalmente, vale salientar que está é uma interpretação própria ao
assunto. A experiência tem mostrado que não basta apresentar as estruturas e seus
algoritmos, enfatizando o seu aspecto funcional. Devemos entender as estruturas
estudando o seu comportamento em situações variadas para que ela se
contextualize com as formas e alternativas para organização da informação. Se
isto for possível, poderemos então desenvolver novas soluções para casos
específicos.
O leque de estruturas presentes na bibliografia é vasto. Entretanto aqui
serão apresentadas apenas uma parcelas das mesmas. Isto é devido ao
entendimento que estas estruturas cobrem a parcela mais significativa do
horizonte de uso, além de permitirem que, com o seu domínio, a extensão para
outros casos seja facilitada.

1.2 Estruturas de Dados

Em várias oportunidades neste trabalho estará presente o tema


“estruturas de dados”, mas o que na realidade é isto? Em que situação elas se
aplicam? Vamos tentar encontrar respostas para estas questões.
Todos que trabalham com o processamento de informações, de qualquer
natureza, tem uma idéia razoavelmente boa sobre o tema. Entretanto esta idéia,
muitas vezes, precisa ser melhor trabalhada. Façamos o seguinte: escreva em uma
folha de papel a sua própria definição de estrutura de dados e vá acompanhando e
comparando-a com aquela que este texto irá desenvolver.
Em primeiro lugar, vamos definir o que significam cada um dos
elementos presentes no nome, neste caso, estrutura e dados. Segundo [Fra87]:
• Estrutura: “disposição e ordem das partes num todo; um todo,
considerada a forma por que se dispõem as suas partes”;
• Dados: “elemento ou quantidade conhecida que serve de base à
resolução de um problema”.
Para a área de informática, os termos dados e informações, por mais que
diferentes, representam os elementos básicos de qualquer trabalho. Não há
dúvidas quanto a isto. Já o termo estrutura merece algumas reflexões.
Quando falamos no verbo “estruturar”, de onde deriva a definição acima,
estamos pensando em algum tipo de organização. Esta pode ser uma boa
indicação de caminho.

5
Considere dois elementos adicionais: o que manipulamos ou desejamos
que o computador manipule, são informações; e, o computador armazena suas
informações em algum tipo de memória. Bem, como compatibilizar as duas
situações? Precisamos encontrar uma forma condizente de representação das
informações que permita a manipulação (por nós ou por uma linguagem) e que
seja passível de armazenamento na memória escolhida. Neste momento vamos
nos preocupar somente com o armazenamento em memória principal, deixando
para mais tarde os outros casos.
Juntando estes conceitos, é óbvio que estrutura de dados tem um relação
muito próxima com a organização da partes de informação em um todo coerente.
É preciso fazê-lo de acordo com critérios muito bem estabelecidos e que atendam
à finalidade que motivou a proposição. Afinal, para que iremos organizar a
informação? Para utilizá-la na solução dos nossos problemas computacionais.
Assim:

Estrutura de dados é a forma de organização dada às


informações de maneira a facilitar o acesso a elas por um
algoritmo ou programa durante as operações de
manipulação que ocorrem na execução de uma tarefa.

Veja que a definição dada acima ressalta algumas palavras que são
chaves para o bom entendimento de estruturas de dados. Em primeiro lugar fala-
se de organização. Uma vez que desejamos fazer algum uso destas informações,
precisamos dar à elas uma organicidade para que este acesso seja o mais eficiente
possível.
Se esta afirmação é válida, estamos lançando uma idéia que devemos
cultivar: ao pensarmos uma estruturação para os nossos dados, devemos
considerar sempre o problema a ser resolvido, a forma e tipo de uso que
desejamos fazer deles e com será a sua manipulação através de algoritmos ou
programas.
Sendo tudo isso considerado como parte natural da estrutura de dados ela
é muito mais ampla do que a simples ordem dada aos dados, ela é um grande
conjunto de regras para o armazenamento e uso destes dados de forma coerente.
Uma vez que isto esteja claro, pode-se afirmar que estrutura de dados é,
muitas vezes, criação própria e podemos ampliar os conceitos aprendidos de
forma a criar novas soluções para os nossos novos problemas. Ao criarmos estas
novas soluções, definimos novas regras, e, consequentemente, novas estruturas de
dados.

6
1.3 Formas de Organização

É ainda necessário fazer uma distinção fundamental em relação a


organização das informações que irá reforçar mais os conceitos anteriores. Esta
distinção é quanto a organicidade dos dados, que pode ser física ou lógica.
Vamos clarear isto através de um exemplo. Todo o programador é capaz
de imaginar um vetor de 10 elementos tipo caracter sem maiores dificuldades.
Estes elementos, por estarem armazenados em um vetor, estão fisicamente lado a
lado. Ou seja, o elemento da quinta posição é, e sempre será, precedido por
aquele da quarta posição e sucedido pelo da sexta. Esta é a organização física dos
dados ou elementos.
Independente da organização física acima, eu posso utilizar estes dados
na ordem que bem desejar. Por exemplo, primeiro eu uso o dado da primeira
posição; após o oitavo; depois o terceiro e assim por diante. É fácil perceber que
eu posso determinar 10! possíveis combinações para o uso dos elementos do
vetor. Estas são as possíveis organizações lógicas dos dados. Veja:

Organização física:

Î
Organização lógica 1:

Î
Ordem 5, 7, 9, 2, 6, 4, 10, 1, 3, 8
Resultado A, B, C, E, M, N, O, P, R, U

Î
Organização lógica 2:

Î
Ordem 1, 3, 5, 7, 9, 2, 4, 6, 8, 10
Resultado P, R, A, B, C, E, N, M, U, O

Dando uma definição mais formal, organização física é a forma ou


ordem como os dados estão fisicamente armazenados. Já a organização lógica é a
forma ou ordem na qual os dados são acessados ou utilizados.
Estrutura de dados atua nas duas frentes. Ao mesmo tempo que
estabelece formas de organização lógica para as informações, respeita as
restrições impostas pelas organizações físicas. Entretanto vale lembrar que a
liberdade de ação está mais próxima da organização lógica que da física. Esta
última é altamente condicionada as possibilidades de representação das

7
linguagens. Por exemplo, vetores, matrizes, arquivos, alocação dinâmica de
memória, dentre outras possibilidades. Para a organização lógica, desde que
fisicamente seja possível, qualquer possibilidade é válida (desde que atenda as
suas necessidades).

1.4 Exercícios

1. Faça uma comparação fundamentada e crítica da sua própria definição de


estrutura de dados com aquela presente no texto.
2. Busque definições alternativas para estrutura de dados na bibliografia e
compare-as.
3. Quais são as alternativas de organização física dos dados nas linguagens que
você conhece? Busque também informações sobre C, Pascal e para aquela
utilizada para descrever os algoritmos deste texto (veja em [Car98] ).
4. Defina algumas regras para a organização lógica de um vetor de n valores do
tipo inteiro.

8
2
Estruturas de Dados Sequenciais

2.1 Caracterização

É possível caracterizar as estruturas de dados em grupos, de acordo com


a maneira pela qual elas se comportam frente as organizações mencionadas no
capítulo anterior.
O primeiro grupo a ser estudado é formado pelas estruturas de dados
sequenciais. Estas estruturas são caracterizadas pelo fato da organização lógica
dos dados coincidir com a organização física dos mesmos.
Desta forma, se a informação guardada na posição i for precedida por
aquela da posição i-1 e sucedida pela da posição i+1, caracterizando a sua
organização física, a sua utilização será idêntica, ou seja, após utilizarmos o
elemento da posição i, somente será possível a utilização dos elementos das
posições i-1 ou i+1, sem nenhuma outra alternativa.

2.2 Pilha

2.2.1 Apresentação
A primeira estrutura que veremos chama-se pilha. Antes de definí-la,
vamos caracterizá-la por analogia com uma situação muito comum no nosso dia-
a-dia.

9
Todos nós já tivemos a oportunidade de nos depararmos com um grupo
de objetos quaisquer empilhados. Por exemplo, uma pilha de latas em um
supermercado. Neste caso, sempre que desejamos colocar mais uma lata na pilha,
existe somente um local possível: a parte mais alta ou o topo da pilha. Se, por
outro lado, desejamos retirar um objetos desta pilha, o faremos também na parte
mais alta ou topo, ou correremos o risco de derrubar tudo.
Esta analogia pode ser transposta sem maiores dificuldades para o trato
de informações, basta utilizar estas em substituição às latas. Considere um grupo
de informações onde existe uma delas considerada a “mais alta”. Se desejamos
colocar uma nova informação (empilhar), isto irá ocorrer após esta “mais alta”.
Se o que desejamos é retirar uma informação, retiraremos esta e atualizaremos o
indicador de “mais alta” para a informação anterior a ela. Esta é a pilha, veja:

Pilha é uma estrutura de dados onde as operações de


manipulação dos seus elementos (inserção e retirada)
acontecem em um mesmo local denominado de topo da
pilha.

Note que esta definição é uma regra de uso para os elementos


armazenados, ou seja, é uma organização lógica. De acordo com a caracterização
dada anteriormente, esta organização lógica coincide com a organização física,
pois elementos serão inseridos e/ou retirados da pilha em posições subsequentes.

Figura 2.1 – Caracterização de um estrutura tipo pilha.


Vamos olhar esta definição de forma gráfica na figura 2.1. Nela está
descrito todo o processo de manipulação da pilha e qual é o critério que a rege.
Este critério é conhecido como LIFO  Last In First Out, ou seja, o último
elemento que entrou na estrutura será o primeiro a sair dela. Toda e qualquer
10
aplicação que utilizar este critério na solução do seu problema poderá ser
trabalhada utilizando uma pilha.
São variados os usos para a estrutura pilha, entre eles podemos destacar
a sua aplicação no controle de chamadas de procedimentos na maioria das
linguagens de programação, conversão e solução de expressões matemáticas na
análise sintática, léxica e semântica de alguns interpretadores, controle de
algoritmos iterativos na implementação de uma solução recursiva, dentre outros.
No primeiro exemplo, sempre que a linguagem (módulo de execução)
encontrar a chamada a um procedimento ou função, ela empilha o status atual
(valores de variáveis, endereço de retorno, etc...) em uma pilha. Caso encontre
novos procedimentos pelo caminha, realiza a mesma tarefa. Quando o
procedimento em execução terminar, a linguagem desempilha um status (que é da
última chamada à procedimento), recupera os dados de acordo com este e segue o
seu trabalho.
Para a conversão de expressões matemáticas, a pilha permite descrevê-
las utilizando um sistema de notação conhecido por poloneza (original ou
inversa), o que facilita o processo de solução das mesmas (veja em [TA86] e
[SM94] para maiores detalhes). Durante a conversão, podem ser verificadas as
condições formais da expressão e detectados erros.
No último exemplo citado, a pilha pode ser utilizada para dar suporte à
um algoritmo iterativo durante a implementação de uma definição naturalmente
recursiva. Na prática, ela substitui a pilha implementada pela linguagem para o
controle das chamadas aos procedimentos, de forma que o algoritmo consiga
controlá-los internamente apenas.

2.2.2 Implementação
Como foi mencionado anteriormente, a definição de uma pilha
estabelece “apenas” uma organização lógica para as informações. A única
menção à organização física está no fato desta necessitar coincidir com a
primeira.
Entretanto, precisamos definir formas de armazenamento para uma
pilha, pois estas formas, que ditam a organização física, influenciarão de maneira
decisiva a implementação da estrutura.
Na verdade, qualquer organização física de dados que estabeleça
“vizinhança” entre elementos e este critério de vizinhança seja permanente (duas
posições vizinhas nunca mudarão de posição), é capaz de suportar a organização
lógica da pilha. Para isto basta fazer com que as operações de inserção e retirada
aconteçam em um mesmo e único local chamado de topo.

11
Assim sendo, estruturas físicas como vetores, arquivos, alocação
dinâmica de memória (ponteiros), dentre outras, podem fazê-lo. Por uma questão
de praticidade, vamos apresentar a versão da pilha implementada em um vetor.
Os outros casos são análogos a este, pelo menos na lógica de manipulação da
estrutura.
Note, por outro lado, que independentemente da natureza dos dados
armazenados na pilha (inteiros, reais, caracteres, etc...), o indicador de topo está
vinculado ao tipo de estrutura física escolhida. Ou seja, se escolhemos um vetor,
o topo deve armazenar uma posição deste vetor. Para isto, ele pode ser uma
variável inteira. Se a opção for pelo uso de alocação dinâmica de memória, o topo
necessitará ser um ponteiro para o tipo de estrutura dada à cada nó, e assim por
diante.
Ainda quanto a isto, uma vez que o topo pode ser de vários tipos de
dados, o seu avanço (incremento) ou retração (decremento) nas operações de
inserção e retirada respectivamente, será também condicionado ao seu tipo. Veja,
se ele for um número inteiro, bastará somar ou subtrair uma unidade do seu valor
atual, mas isto não irá funcionar com um ponteiro.

2.2.3 Algoritmos
Considerando então a existência de uma pilha a ser implementada em
um vetor de n elementos do tipo caracter, vamos discutir os algoritmos de
manipulação da mesma.
Antes de qualquer coisa precisamos decidir quais serão estes algoritmos.
Dois deles são óbvios: a inserção de um novo elemento e a retirada de um
elemento existente. Além destes existe mais algum algoritmo? Em geral,
escreveremos algoritmos para as operações básicas permitidas na estrutura. Para o
caso de pilha é possível fazer mais alguma coisa do que a inserção e a retirada?
Enquanto operação de manipulação, não há mais nenhuma. Então serão somente
estes dois?
Neste caso, existe ainda uma pequena tarefa a ser realizada que é
independente e diferente da inserção e retirada e, em razão disto, terá o seu
algoritmo específico. Na verdade existem algumas indicações ou recomendações
para desenvolver uma “boa programação”. Uma delas é para que jamais sejam
misturadas em um mesmo algoritmo tarefas distintas. Isto, além de tornar mais
claro o algoritmo, facilita muito a busca e correção de erros.
Já que estamos tratando da pilha em um vetor, qual é a garantia que
temos de que este vetor e o indicador de topo estarão em condições de serem
utilizados pelos procedimentos de manipulação quando for necessário? Muito
poucas, pois as linguagens, em geral, não se preocupam com isso.

12
Então, o procedimento ou algoritmo que está faltando deve trabalhar
para dar esta garantia desejada. Ele é o algoritmo de inicialização da estrutura, ou
seja, ele inicializa todos os elementos necessários para o bom funcionamento
dentro dos parâmetros conhecidos. Vamos começar por ele.

procedimento inicializar_pilha( var topo: inteiro )


início
topo := 0;
fim
Algoritmo 2.1 – Inicialização da estrutura pilha.

Este algoritmos de inicialização está descrito no algoritmo 2.1.


Dois comentários sobre ele são importantes. Em primeiro lugar, o procedimento
ou algoritmo recebe como parâmetro somente o indicador de topo da pilha e não
esta. Isto acontece porque é no local indicado pelo topo que as operações
ocorrem, independentemente do que há dentro do vetor. Na inclusão, um novo
elemento será inserido na posição após aquela indicada pelo topo. Na retirada, o
elemento atualmente apontado (que foi inserido como descrito antes), sairá.
Então, topo jamais deixará que qualquer elemento impróprio dentro do vetor
interfira no processo.
A segunda questão diz respeito à razão de colocar o valor zero no topo e
não outro valor qualquer como -1 ou 1. Se inicializarmos o topo com –1, ele
necessitará ser incrementado em 2 unidades para alcançar a primeira posição
válida do vetor, que começa em 1. Nas demais vezes o incremento será de apenas
1 unidade. Escolhendo o valor 0 (ou 1) para inicialização, este incremento será
sempre uniforme.
O valor 1 para a inicialização também não parece ser o ideal, pois ele já
está indicando uma posição válida e esta não tem nada. Durante a inclusão,
vamos inserir o novo elemento na posição indicada e depois incrementar o topo.
Na retirada ocorre o inverso, pois precisamos primeiro decrementar o topo para
chegar ao elemento a ser retirado. Isto não é um grande problema, mas por
questão de clareza e correção, é preferível apontar sempre para posições que são
válidas. Desta forma, o topo deve ser inicializado com o valor imediatamente
anterior a primeira posição válida do vetor.
O nosso próximo algoritmo é o que faz a inclusão de um novo elemento
na estrutura. Ele é o algoritmo 2.2 e merece uma série de pequenos comentários,
nem todos relacionados ao processo de inclusão, mas também às normas de uma
boa programação.
Em primeiro lugar, observe que, dentro do algoritmo existem dois
momentos distintos. No primeiro deles verificamos se existe espaço dentro do
13
vetor para um novo elemento. Se a variável topo alcançar o tamanho do vetor,
significa que todas as posições já foram ocupadas. Somente após isso é que se
realiza a inserção do elemento, primeiro incrementando o topo (pois ele foi
inicializado com zero) e depois guardando o elemento desejado.

procedimento inserir_pilha( var pilha: vetor[1..N] de caracter;


var topo: inteiro; elemento: caracter ): booleano;
início
se topo = N então
retornar falso; /* A pilha está cheia */
topo := topo + 1;
pilha[ topo ] := elemento;
retornar verdade; /* Inserção Ok */
fim
Algoritmo 2.2 – Inserção de um novo elemento em uma pilha.

Outro ponto importante é a forma de montagem do procedimento. Uma


vez que ele executa uma dada tarefa, algum outro procedimento ou programa o
chamou e necessita saber o resultado desta tarefa, ou seja, conhecer se a inserção
foi ou não realizada. Assim, a solução adotada foi a de manter um parâmetro de
retorno do procedimento, do tipo booleano, que indica se a execução foi correta
(verdade) ou errada (falso).

procedimento retirar_pilha( var pilha: vetor[1..N] de caracter;


var topo: inteiro; var elemento: caracter ): booleano;
início
se topo = 0 então
retornar falso; /* A pilha está vazia */
elemento := pilha[ topo ];
topo := topo - 1;
retornar verdade; /* Inserção Ok */
fim
Algoritmo 2.3 – Retirada de um novo elemento de uma pilha.

O último algoritmo proposto faz a retirada de um elemento da pilha e é


apresentado no algoritmo 2.3. Analogamente à inserção, antes de retirarmos
alguém é necessário verificar se existe algum elemento presente na estrutura.
Sempre que o indicador de topo for zero (que é o valor de inicialização), a pilha
está vazia.

14
Após isto, estamos em condições de proceder a retirada. Esta é realizada
com o armazenamento do elemento indicado pelo topo na variável elemento e o
posterior decremento daquele para apontar para a localização do novo topo
estrutura. Uma vez que, ainda de forma análoga a inserção, este procedimento
retorna um indicativo de execução correta ou não, é necessário o uso de um
parâmetro adicional, também passado “por referência” (veja livros de
programação), para retornar o elemento retirado. Já que a estrutura não faz a
consulta aos seus elementos, o ato de retirada pressupõem o uso de elemento que
está saindo e ele não pode ser simplesmente ignorado.

2.2.4 Análise
A análise assintótica dos algoritmos de manipulação da pilha é óbvia,
uma vez que qualquer uma das operações mencionadas e implementadas é
realizada em tempo constante. Esta análise considerou como melhor caso a
situação onde os algoritmos de inserção e retirada acusam pilha cheia ou vazia e
como pior caso o trabalho completo do mesmo. Veja:

Algoritmo Melhor Caso Pior Caso


+- := se [] +- := se []
inicializar_pilha  1    1  
inserir_pilha  1 1  1 3 1 1
retirar_pilha  1 1  1 3 1 1

Já que estes algoritmos fazem a implementação de uma pilha em um


vetor de valores tipo caracter, vale perguntar se o desempenho seria o mesmo em
outros casos? Na verdade sim, este desempenho permanece proporcionalmente o
mesmo pois, em uma pilha, as operações de inserção e retirada acontecem sempre
em posições pré-determinadas. Nestes casos não há a necessidade de nenhuma
tarefa adicional, apenas verificamos a possibilidade da operação e, se houver, a
realizamos.
Desta maneira, independentemente da estrutura física que abriga a pilha,
estas operações são realizadas em tempo assintótico constante e proporcional à
O(1). O mesmo vale para o processo de inicialização.
É necessário ainda fazer um comentário sobre o tipo de operação
realizada. Por quê fizemos apenas a inclusão e a retirada de elementos? E se
desejarmos olhar ou consultar o que há armazenado na pilha? É possível?

15
Note que, por definição, a estrutura de dados pilha permite que
sejam realizadas apenas operações no local apontado pela variável topo. Estas
operações, em razão disto, se restringem a colocar um novo elemento ou retirar
um elemento existente, mas ambas nesta posição indicada. Então, não há como
vasculhar a estrutura em busca de um dado elemento.
Isto é ainda mais problemático, pois se acessarmos qualquer outro
elemento que não aquele indicado pelo topo, estaremos infringindo a regra de
definição da pilha e, em razão disto, deixando de ter esta estrutura. Ou seja, se
permitirmos acessos ou operações em outro local da estrutura que não seja o topo,
não estaremos trabalhando com uma pilha.

2.3 Fila

2.3.1 Apresentação
Da mesma forma que fizemos com a pilha, vamos utilizar a analogia
para caracterizar a forma inicial da estrutura de dados fila e depois converteremos
este estudo para uma representação adequada ao nosso caso.
Considere um caso comum e corriqueiro que enfrentamos diariamente:
entrar em uma fila, seja em um banco, supermercado, restaurante, etc... Observe
que neste caso, todo o novo integrante da fila ingressa na mesma em um único
local: o final dela (a menos, é claro, que esteja disposto a arrumar confusão). Por
sua vez, o atendimento às pessoas na fila se dá em outro local: o começo desta.
Esta é a caracterização do funcionamento de uma fila em nosso “mundo
real”. Basta agora transcrever esta idéia ao tratamento de informações
armazenadas em um computador. Para isso, considere um grupo delas
“enfileiradas”, onde a inserção de um novo elemento só pode acontecer no final
desta fileira e a retirada no outro extremo, o começo da mesma. Este é a fila. Sua
definição formal é:

Fila é uma estrutura de dados onde a operação de inserção


de um novo elemento acontece em uma extremidade
denominada final (término, fim,...) e a retirada em outra
extremidade, diferente da primeira, denominada começo
(início, frente,...).

Mais uma vez, estamos definindo uma organização lógica para o uso de
elementos armazenados em alguma estrutura física. Ainda aqui existe a
necessidade de que as duas organizações sejam equivalentes, pois elementos
16
“vizinhos” na estrutura física serão utilizados logicamente em sequência, ou seja,
serão vizinhos lógicos também.
A estrutura fila também tem um critério de regência, este chama-se FIFO
 First In First Out, ou seja, o primeiro elemento a ser inserido na estrutura, será
o primeiro a ser utilizado ou retirado. Graficamente este critério é apresentado
como na figura 2.2.

Figura 2.2 – Caracterização de uma fila.

Esta estrutura tem um uso muito frequente em situações onde se faz


necessário respeitar um critério de enfileiramento das informações. Dentre muitos
outros casos, podemos citar: o controle de arquivos enviados para impressão,
onde não pode haver mistura nem intercalação dos mesmos e respeita-se o ordem
de chegada; o controle de processos que utilizarão o processador em um ambiente
multiprogramado; simulações dos mais variados tipos; compiladores e linguagens
de programação; e outros mais.

2.3.2 Implementação
Para discutirmos a implementação de uma fila, precisamos antes definir
qual será a estrutura física que a abrigará. Da mesma forma que a pilha, qualquer
estrutura física que permita estabelecer este critério de “vizinhança” física e
lógica, necessária ao caso, pode ser utilizada.
Neste caso, as opções também são idênticas àquelas da pilha, pois
podemos implementar a fila em vetores (forma matricial), estruturas com
alocação dinâmica de memória, arquivos e outros.
Desta vez, note que são necessárias duas varáveis de controle: o
indicador de começo e o de final da fila. Estas duas variáveis executam a mesma
função em qualquer uma das estruturas físicas escolhida, mas tem natureza ou
tipo diferente em cada uma delas. Ou seja, para uma implementação matricial,
elas podem ser do tipo inteiro; para estruturas e alocação dinâmica de memória,
elas são ponteiros; e assim por diante.
17
Da mesma forma que antes e por entender que não há nenhum prejuízo
em apresentar a fila desta forma, faremos a escolha da estrutura física para
implementação como sendo matricial, na forma de um vetor com as informações.
Isto torna mais simples e óbvios os algoritmos de manipulação.

2.3.3 Algoritmos
Já sabemos que estrutura física irá abrigar a nossa fila: será um vetor
com n elementos do tipo caracter. Precisamos então determinar quais serão os
algoritmos que iremos implementar.
Um destes algoritmos é óbvio e natural, pois necessitamos inicializar os
controladores da estrutura para que ela funcione. De forma semelhante, não há
aplicabilidade para uma estrutura que não permita inserir e retirar seus elementos.
Este também é o caso de uma fila e escreveremos também estes dois algoritmos.
Como esta estrutura define claramente os locais onde acontecerão as
operações de inserção e retirada, que serão no começo e final, não há liberdade
para realizarmos outras, pois estaríamos rompendo a regra que define uma fila.
Desta maneira, a semelhança da pilha, implementaremos somente três algoritmos:
inicialização, inserção e retirada de elementos.
O primeiro algoritmo faz a inicialização da estrutura e está no algoritmo
2.4. Esta tarefa é resumida com a atribuição de valores às variáveis de controle
começo e final. Para elas foram destinados os valores 1 e 0, respectivamente. Por
que estes valores e não outros, como por exemplo, todos zerados? As razões já
foram expressadas no estudo da pilha. É recomendável que exista coerência entre
os apontadores, seu modo de incremento e a posição apontada. Se inicializarmos
o começo com o valor 0, ele estará sempre apontando para uma posição inválida,
que é antes do primeiro elemento válido para a retirada. Isto não acontece quando
ele é inicializado com o valor 1, pois após a primeira inclusão, tanto o começo
como o final indicarão sempre um elemento válido. Como é no final que
realizamos a inclusão, a sua inicialização com o valor 0 (zero) tem a mesma
justificativa da operação análoga realizada no topo da pilha.

procedimento inicializar_fila( var começo,final: inteiro )


início
começo := 1;
final := 0;
fim
Algoritmo 2.4 – Inicialização da estrutura fila.

18
O próximo algoritmo, descrito no algoritmo 2.5, fará a inclusão de um
novo elemento na fila. O primeiro passo do mesmo é descobrir se existe espaço
disponível no vetor para mais um elemento. Como a variável final indica a
posição da última inserção (final da fila), se ela já apontar para a última posição
do vetor, não há mais onde colocar este novo elemento. Note que para esta tarefa,
bem como para a inclusão propriamente dita, não há a necessidade da utilização
do começo. Portanto, esta variável de controle não é passada como parâmetro ao
procedimento.

procedimento inserir_fila( var fila:vetor[1..N] de caracter;


var final: inteiro; elemento: caracter ): booleano;
início
se final = N então
retornar falso; /* A fila está cheia */
final := final + 1;
fila[ final ] := elemento;
retornar verdade; /* A inserção foi realizada */
fim
Algoritmo 2.5 – Inserção de um elemento na fila.

Após a verificação da existência de espaço, o novo elemento pode ser


incluído na estrutura. Como isto acontece no final da fila e esta variável (final)
indica o último elemento incluído, é necessário incrementá-la para a nova posição
antes da efetiva inserção.
Como vai se tornando hábito (espero!), os procedimentos informam a
que os chamou como ocorreu a operação. Neste caso, se a fila já estava cheia ou
foi possível inserir mais um elemento.

procedimento retirar_fila( var fila:vetor[1..N] de caracter;


var começo,final: inteiro; var elemento: caracter ): booleano;
início
se começo > final então
retornar falso; /* A fila está vazia */
elemento := fila[ começo ];
começo := começo + 1;
retornar verdade; /* A retirada foi realizada */
fim
Algoritmo 2.6 – Retirada de um elemento da fila.

19
Finalmente podemos efetuar a retirada de um elemento da nossa fila.
Acompanhe pelo algoritmo 2.6. Neste algoritmo, uma vez que desejamos fazer
uma retirada, precisamos garantir que existe elementos dentro da estrutura, no
mínimo um, para validar a operação. Isto é feito nas duas primeiras linhas válidas
do mesmo, quando testamos as variáveis começo e final. Assim, como na
inicialização, onde começo := 1 e final := 0, sempre que a variável começo
ultrapassar (for maior) do que o final, a estrutura não tem nenhum elemento
armazenado. Caso tenha alguma dúvida quanto isso, faça uma simulação e um
teste de mesa com os algoritmos.
Quando tivermos certeza da pertinência da operação, podemos executá-
la. Para isso é necessário lembrar que, à semelhança da variável final, o começo
sempre aponta para aquele que é o “primeiro” da fila, ou seja, não podemos
incrementá-lo sem antes armazenar o elemento desta posição. Desta forma,
guardamos o resultado na variável elemento e, logo após, incrementamos o
começo. Com isso consumamos a retirada.

2.3.4 Análise
Considerando os três algoritmos apresentados antes, valos proceder a
análise da estrutura fila. Em primeiro lugar, veja a análise dos procedimentos de
inicialização, inserção e retirada:

Algoritmo Melhor Caso Pior Caso


+- := se [] +- := se []
inicializar_fila  2    2  
inserir_fila  1 1  1 3 1 1
retirar_fila  1 1  1 3 1 1

Para realizar este quadro, foi considerado como melhor caso para os
algoritmos a situação onde a fila está cheia ou vazia e ambos terminam sua
execução no primeiro dos testes. O pior caso é tomado pela continuidade do
trabalho.
Mais do que o valor quantitativo das colunas da análise, perceba que eles
são constantes. Esta observação pode ser estendida para qualquer tipo de
implementação de uma fila. Isto acontece, mais uma vez, porque a fila define a
priori os locais e a formas da operações permitidas, ou seja, não existe nenhum
trabalho extenso adicional, muito menos que seja proporcional ao tamanho da
estrutura que abriga a fila.
20
Necessitamos ainda observar que a fila, assim como a pilha, não prevê
(ou não permite) nenhuma outra operação além daquelas já vistas. Se desejarmos
realizar algum trabalho com uma fila, temos que fazê-lo através de inclusões e
retiradas de elementos, nada mais.
Por outro lado, nem tudo é perfeito e eficiente com a fila. Analise a
seguinte situação prática: implementamos uma fila em um vetor de n posições (de
qualquer tipo). Durante a operação, realizamos a inclusão, em sequência, de n
valores nesta fila. Logo após, retiraremos n-2 destes valores e tentaremos incluir
mais um deles. O que irá acontecer? A fila irá acusar “fila cheia”, mas há n-2
espaços não utilizados na estrutura, veja:

a) Incluir n valores na fila:

V1 V2 V3 V4 ........ Vn-2 Vn-1 Vn


1 2 3 4 n-2 n-1 n
⇑ ⇑
começo final

b) Retirar n-2 valores da fila

    ........  Vn-1 Vn
1 2 3 4 n-2 n-1 n
⇑ ⇑
começo final
Mais uma vez, se tentarmos incluir um novo elemento, não será possível,
pois a fila está “cheia”, mesmo havendo espaço disponível. Como isto pode ser
corrigido?
Antes disto, vamos situar o problema. Por que ele acontece? Isto é
devido a regra que define a fila? Não! O causador deste problema é a estrutura
física escolhida para a implementação: o vetor. Este possui tamanho fixo e pré-
definido, além de ser linearmente alocado. Assim, um elemento que foi retirado
não cede a sua posição para um novo uso. Resumindo, não há, pelo menos por
enquanto, como resolver este problema. Na próxima seção vamos apresentar uma
proposta de solução.
Se, entretanto, representarmos a fila com uma organização física mais
dinâmica, este problema não ocorrerá. Por exemplo, utilizando estruturas e
alocação dinâmica de memória. Neste caso, o processo de retirada não é um
simples incremento de apontadores. Ocorre uma efetiva liberação do espaço
alocado e sua consequente disponibilização para um novo uso. Assim, a fila
21
somente estará “cheia” quando toda a memória destinada aos dados estiver
ocupada.

2.4 Fila Circular

2.4.1 Apresentação
Como foi mencionado na final da última seção, existe um problema de
desperdício de espaço quando implementamos uma fila na forma matricial. Isto
ocorre por que o vetor é uma organização com características pré-definidas e de
difícil alteração, como o tamanho e a linearidade de alocação entre os elementos.
Na tentativa de solucionar isto será apresentada uma proposta
alternativa. Vale salientar que ela pode ser aplicada em outras situações que
apresentem problemas e características semelhantes. Esta proposta é baseada na
estrutura “fila circular”.
Decompondo a denominação dada, percebemos que ela é também um
fila e, portanto, tem as mesmas características e restrições desta. A novidade está
quanto ao “circular”. Este termo está ligado a forma de tratarmos a organização
física que abriga os dados e tem a sua caracterização dizendo que a última
posição da organização física é seguida, necessariamente, da primeira.
Em outras palavras, se, ao chegarmos até o final do vetor, necessitarmos
incrementar algum dos apontadores, este não indicará uma posição inválida, mas
sim a primeira posição do vetor.
Formalmente a fila circular fica assim definida:

Fila circular é uma fila, portanto tem as inserções em uma


extremidade (final) e as retiradas em outra (começo), na qual
a organização física escolhida deve, necessariamente,
propiciar que ocorra linearidade, mesmo que lógica, entre a
última posição disponível e a primeira delas.

Uma vez que aqui a única diferença reside na forma de tratar a


organização física da fila, ela tem as mesmas aplicações e considerações desta.
Veja graficamente, na figura 2.3, como se comporta uma estrutura tipo fila
circular.

22
Figura 2.3 – Caracterização de uma fila circular

2.4.2 Implementação
As condições para a implementação de uma fila circular são exatamente
as mesmas da fila normal. Aqui, entretanto, é reforçada a implementação que
utiliza como organização física um vetor de n elementos. Isto acontece porque é
em razão desta organização que acontece o problema de desperdício de espaço da
fila comum que objetivamos resolver com a circularidade.
Note ainda que o princípio da fila circular pode ser adotado em todas as
situações onde uma fila tem utilidade e mais, pode utilizar qualquer organização
física que a fila utilize. Claro que o uso de uma estrutura fixa como o vetor
salienta o problema que estamos resolvendo.

2.4.3 Algoritmos
A fila circular, em razão de ser uma fila, terá os mesmos algoritmos
desta. Por outro lado, a forma interna dos procedimentos deverá mudar. Para ela
também utilizaremos um vetor de n elementos do tipo caracter para abrigar as
informações.
A primeira questão relevante diz respeito a forma de controlarmos “fila
cheia” e “fila vazia”. No caso da fila, testamos fila cheia verificando se o
indicador de final chegou ao limite do vetor. Isto obviamente não pode mais ser
realizado, pois não existe mais um fim lógico do vetor, uma vez que após a
última posição vem a primeira delas.
Para o teste de fila vazia, o problema é ainda mais dramático. O teste
proposto à fila, confrontando o começo e o final também não é mais válido.
Facilmente poderemos encontrar situações que invalidam testes de igualdade e/ou
maioridade entre estes elementos, sejam porque a fila circular está cheia ou vazia.

23
A solução mais simples a ser adotada é a utilização de um contador de
elementos presentes na estrutura. Assim, sempre que um novo elemento é
inserido, este contador será incrementado. Quando procedermos uma retirada, o
contador será diminuído de uma unidade.
Esta nova variável de controle, o contador, necessita ser inicializada
junto com o começo e o final. Naturalmente, no início, como a fila circular está
vazia, seu valor deve ser 0 (zero). Com isso, já podemos desenvolver o algoritmo
de inicialização da estrutura, que é apresentado no algoritmo 2.7.
Os valores utilizados neste algoritmo 2.7, para a inicialização do começo
e final da fila circular, são os mesmos da fila e mais, são motivados exatamente
pelas mesmas razões. Lembre-se que a fila circular é, antes de mais nada, uma
fila.

procedimento inicializar_filac( var começo,final,cont: inteiro )


início
começo := 1;
final := 0;
cont := 0;
fim
Algoritmo 2.7 – Inicialização da estrutura fila circular.

Uma segunda questão a ser comentada e que estará presente nos


algoritmos de inserção e retirada, é a forma que usaremos para implementar a
circularidade da estrutura. Note que este circularidade é lógica e não física, pois o
vetor continua sendo o mesmo da outra fila.
Neste caso, o nosso problema é muito bem localizado: temos que fazer
com que os indicadores de posicionamento da fila circular (começo e final)
retornem ao início do vetor sempre que tentarmos ultrapassar o final deste. Como
estas variáveis de controle tem incremento linear, ou seja, são acrescidas sempre
de uma unidade, sabemos que vamos passar o fim do vetor sempre que somarmos
1 (um) ao controlador que aponte para a posição n deste. Para contornarmos isso,
basta acrescentar um pequeno teste ao código e garantir a circularidade. Veja:
variável := variável + 1;
se variável > N então variável := 1;
Com estas linhas de programação garantimos que a variável será
incrementada linearmente enquanto não chegar ao limite do vetor. Uma vez
ultrapassado este limite, ela retorna à primeira posição fazendo a circularidade
necessária à estrutura.

24
A partir deste comentário é possível delinear os algoritmos de inserção
(algoritmo 2.8) e de retirada (algoritmo 2.9) da fila circular. A lógica dentro
destes algoritmos é a mesma da fila. Antes de procedermos a inserção de um
novo elemento, verificamos se a fila ainda tem espaço. Na retirada, verificamos
se existe algum elemento lá dentro. Isto é realizado através do contador de
elementos. Se este contador é zero, significa que nenhum elemento está presente;
se ele for igual ao tamanho do vetor, todas as posições deste, independente de
onde estiverem o começo e o final, já foram ocupadas.

procedimento inserir_filac( var filac:vetor[1..N] de caracter;


var final,cont: inteiro; elemento: caracter ): booleano;
início
se cont = N então
retornar falso; /* A fila está cheia */
final := final + 1;
se final > N então
final := 1;
filac[ final ] := elemento;
cont := cont + 1;
retornar verdade; /* A inserção foi realizada */
fim
Algoritmo 2.8 – Inserção de um elemento na fila circular.

procedimento retirar_filac( var fila:vetor[1..N] de caracter;


var começo,cont: inteiro; var elemento: caracter ): booleano;
início
se cont = 0 então
retornar falso; /* A fila está vazia */
elemento := filac[ começo ];
começo := começo + 1;
se começo > N então
começo := 1;
cont := cont –1;
retornar verdade; /* A retirada foi realizada */
fim
Algoritmo 2.9 – Retirada de um elemento da fila circular.

Após esta verificação, procedem-se as operações pedidas. Note que, em


ambos os algoritmos, o incremento do começo e final é realizado de forma
circular. Além disto, após a operação, atualiza-se o contador de elementos,
somando-se uma unidade na inclusão e retirando-se uma unidade na retirada de
um elemento.
25
Um última consideração quanto aos algoritmos dia respeito a retirada de
elementos. Neste algoritmo não é mais necessário passar como parâmetro o
apontador de final da fila, como no algoritmo 2.6. Aqui testamos o caso de fila
vazia através do contador de elementos, sem haver necessidade de conhecer o
final dela para isso. Esta era a única razão para que esta variável estivesse
presente no algoritmo retirar_fila.

2.4.4 Análise
Veja no seguinte quadro a análise dos algoritmos da manipulação de
uma fila circular mencionados na seção anterior:

Algoritmo Melhor Caso Pior Caso


+- := se [] +- := se []
inicializar_filac  3    3  
inserir_filac  1 1  2 5 2 1
retirar_filac  1 1  2 5 2 1

Nesta análise, como nas anteriores, foi considerado como melhor caso a
situação da estrutura estar cheia ou vazia e sair no primeiro teste dos algoritmos
de inserção e retirada. O pior caso é o trabalho completo no momento em que
acontece a circularidade, quando há uma atribuição adicional ao normal.
Note que os valores do quadro acima continuam constantes e,
principalmente, equivalentes assintoticamente àqueles da fila. Isto mostra, mais
uma vez, que estamos falando de estruturas de dados equivalentes e com
comportamento semelhante.
As demais considerações sobre o desempenho da fila circular são
análogas às da fila, com exceção do desperdício de espaço que aqui não ocorre.

2.5 Exercícios

1. Escreva um algoritmo que duplique o conteúdo de uma pilha. A pilha resultado


e a original devem apresentar os elementos na mesma ordem e esta deve idêntica
àquela do começo das operações.
26
2. Escreva uma algoritmo que concatene duas pilhas A e B gerando um terceira
onde devem estar, em primeiro lugar, os elementos da pilha A e depois os da
pilha B, na ordem original.
3. Considere a existência de duas pilhas A e B onde os elementos estão ordenados
da seguinte forma: o maior deles está no topo e o menor na base. Escreva um
algoritmo que cria uma terceira pilha com os elementos das pilhas A e B também
ordenados segundo o mesmo formato.
4. Escreva um procedimento que inverta o conteúdo de um pilha.
5. É possível implementar duas pilhas A e B em um único vetor de n elementos
tal que não ocorra “pilha cheia” para nenhuma delas sem que todas as posições do
vetor estejam ocupadas? Justifique a sua resposta e, caso seja afirmativa, descreva
os algoritmos de manipulação.
6. Escreva um programa (escolha a linguagem) que faça a manipulação de uma
ou mais pilhas em um computador através de um pequeno menu de opções.
7. Escreva um algoritmo que duplique o conteúdo de uma fila. A fila resultado e a
original devem apresentar os elementos na mesma ordem e esta deve ser a mesma
do começo das operações.
8. Escreva uma algoritmo que concatene duas filas A e B gerando um terceira
onde devem estar, em primeiro lugar, os elementos da fila A e depois os da fila B,
na ordem original.
9. Escreva um programa (escolha a linguagem) que faça a manipulação de uma
ou mais filas em um computador através de um pequeno menu de opções.
10. Considere a existência de duas filas A e B onde os elementos estão ordenados
da seguinte forma: o maior deles está no começo da fila e o menor no final.
Escreva um algoritmo que crie uma fila circular com todos os elementos das filas
A e B também ordenados pelo mesmo critério.
11. Considere a existência de uma fila F em um vetor de n elementos
completamente cheia e uma pilha P, também em um vetor de n elementos, vazia.
Utilizando apenas uma variável auxiliar e os algoritmos apresentados neste
capítulo, inverta a ordem dos elementos da fila (obs: pode utilizar a pilha para
isto).
12. Escreva um programa (escolha a linguagem) que faça a manipulação de uma
ou mais filas circulares em um computador através de um pequeno menu de
opções.
13. Encontre 5 aplicações práticas para uma pilha e uma fila.
14. Compara, segundo critérios de comportamento e desempenho, as estruturas
de dados pilha, fila e fila circular.

27
15. Faça uma pequena pesquisa bibliográfica e proponha um algoritmo capaz de
converter uma expressão matemática da notação usual (húngara) para a notação
poloneza inversa.
16. Na mesma pesquisa solicitada acima, descubra e apresente um algoritmo que
soluciona uma expressão em notação poloneza inversa uma vez dados os valores
das variáveis envolvidas.

28
3
Estruturas de Dados Encadeadas

3.1 Caracterização

No capítulo anterior foram apresentadas as estruturas de dados de


alocação sequencial onde a organização física era coincidente com a organização
lógica dos dados. Um novo grupo de estruturas é formado por aquelas onde isto
não precisa acontecer, ou seja, onde a organização física pode ser diferente da
organização lógica das informações armazenadas. Estas são as estruturas de
dados de alocação encadeada.
Assim, mesmo que tenhamos “vizinhança” física entre os dados, isto não
implica na necessidade de utilizarmos estes dados na mesma sequência. As
estruturas encadeadas podem propor uma sequência lógica alternativa de uso dos
dados, sem prejuízo da sua proposição, coisa que nenhuma das estruturas vistas
podia fazer.
Para fazermos isto, vamos necessitar de uma liberdade maior quanto a
maneira de guardarmos os dados, ou seja, a sua organização física. Qualquer
estrutura que permita indicar uma sequência lógica diferente da sequência física
de armazenamento poderá ser utilizada neste caso. Isso pressupondo que esta
estrutura permita endereçar ou referenciar diretamente cada uma das unidades de
informação armazenada.
Existe, enquanto “estruturas básicas”, apenas um representante deste
grupo, são as listas lineares encadeadas, onde o encadeamento define a
“sequência lógica” de acesso aos dados.

29
3.2 Listas Encadeadas

3.2.1 Apresentação
Para apresentar e definir uma lista linear encadeada, vamos utilizar o
exemplo a seguir:

Código Descrição
1 027 Feijão
2 013 Batata
3 033 Tomate
4 103 Milho
5 017 Arroz
6 042 Macarrão
7 021 Espinafre
8 001 Pimentão
9 098 Cenoura
10 069 Ervilha

Neste pequeno exemplo agrário, encontraremos uma certa quantidade de


informações sobre produtos alimentícios, com um código qualquer de referência,
armazenados em uma estrutura matricial. Na visão mais simples de todas,
podemos utilizar estes dados na forma e ordem na qual estão guardados, ou seja,
pelas posições 1, 2, 3, 4,...., 9, 10.
Entretanto poderemos desejar percorrer estes dados por qualquer outra
ordem. Por exemplo, em ordem alfabética crescente de nome. Neste caso, a
sequência lógica de uso, de acordo com o índice da posição, será:
5 - 2 - 9 - 10 - 7 - 1 - 6 - 4 - 8 - 3
Já se a ordem desejada for alfabética decrescente da coluna nome, teremos:
3 - 8 - 4 - 6 - 1 - 7 - 10 - 9 - 2 - 5
Podemos ainda propor o mesmo para o campo código. Na forma numérica
crescente deste campo, a ordem será:
8 - 2 - 5 - 7 - 1 - 3 - 6 - 10 - 9 - 4
Observe que as sequências dadas acima são possíveis organizações
lógicas para os dados e mais, elas são independentes e diferentes da organização
física dos mesmos.

30
A noção de encadeamento é muito semelhante a isto, pois ela estabelece
uma sequência lógica de uso dos dados. O primeiro raciocínio da fazermos
encadeamento será o de transformarmos ou armazenarmos as sequências acima
em vetores, por exemplo. Esta não parece uma solução eficiente, por mais que
possa ser eficaz, pois a cada inclusão ou exclusão poderemos ter, no pior caso,
uma pesquisa sequencial (para achar a posição ou o elemento) e um deslocamento
inversamente proporcional a esta (para criar espaço à inclusão ou reorganizar os
elementos na retirada). Este é um tempo muito grande e precisamos buscar outras
alternativas.
A solução vem da observação que uma das principais razões deste
trabalho adicional é a disposição diferenciada dos elementos na estrutura. Por
exemplo, o primeiro elemento de informação útil não tem relação direta com o
primeiro elemento da sequência (a menos que seja o mesmo). Este descompasso
faz com que as operações aconteçam em locais (posições) diferentes nas
estruturas, obrigando a realização de, no mínimo, duas pesquisas e realocação de
elementos.
A proposta da lista encadeada vem de encontro com a solução deste
problema. Nela, cada unidade de informação indica a próxima em uma ordem
lógica qualquer e pré-definida. Formalmente, a definição seria:

Lista linear encadeada é uma estrutura onde cada unidade de


informação ou nó traz junto um apontador que indica o
próximo elemento de acordo com uma sequência pré-
determinada.

Assim, em lugar de guardarmos a sequência lógica, faremos com que os


nós “mostrem-na” dentro de cada um deles. Veja o exemplo:

Código Descrição A B C
1 027 Feijão 6 7 3
2 013 Batata 9 5 5
3 033 Tomate  8 6
4 103 Milho 8 6 
5 017 Arroz 2  7
6 042 Macarrão 4 1 10
7 021 Espinafre 1 10 1
8 001 Pimentão 3 4 2
9 098 Cenoura 10 2 4
10 069 Ervilha 7 9 9

31
Sendo que cada uma das colunas tem o seu próprio início, definido por:

Início A Î 5 Início B Î 3 Início C Î 8

Nesta nova lista, cada uma das colunas A, B e C representa uma das
sequências dadas antes. Por exemplo, para a coluna A, que dá a ordem alfabética
crescente do campo nome, o início é na posição 5, ou seja, o primeiro elemento
nesta ordem é aquele armazenado fisicamente na quinta posição. Após, vem a
informação da posição 2; isto está dito na posição da coluna A equivalente ao
elemento trabalhado, no caso arroz. Depois vem a posição 9, obtida da mesma
forma, e assim sucessivamente até chegarmos a posição 3 que será a última da
sequência por não ter continuidade prevista na respectiva coluna. Para as outras
colunas, o raciocínio é idêntico.
Em uma lista encadeada, cada unidade de informação ou nó tem a
seguinte característica:

Área de Área de
Informações Apontadores ou
Elos

Assim, ele é dividido em duas áreas distintas: uma delas, a área de informações,
guarda todos os dados que compõem a razão de ser da estrutura; na outra, estão
os apontadores, também conhecidos como elos, que formam as sequências
lógicas de acesso ou encadeamentos.
Sempre que formos utilizar uma lista destas, devemos trabalhar toda a
linha horizontal onde estão as informações e os apontadores. Não faz sentido
separarmos ambos e trabalhar entendendo que não há relação entre eles. Basta
trocar um elemento de posição para verificar que todo o sequenciamento lógico é
quebrado.
Em relação ao tipo de encadeamento utilizado, uma lista pode apresentar
dois tipos deles. Estes tipos fazem referência ao critério escolhido para a
sequência lógica. Observe que, para a existência de uma destas listas, o critério
escolhido deve ser capaz de estabelecer de forma inequívoca uma relação de
sucessão entre os elementos envolvidos. Em cima disto, os tipos possíveis de lista
são:
a) Encadeamento simples ou único:
Este encadeamento ocorre quando, em relação a um dado critério, a lista traz
somente um dos sentidos possíveis. Aqui, cada unidade de informação indica

32
o seu sucessor ou antecessor naquela ordem solicitada, mas somente um
deles. Assim, a aparência da lista é:

b) Encadeamento duplo:
Esta lista ocorre quando, em relação ao critério escolhido, cada unidade de
informação indica sempre o seu antecessor e o seu sucessor. Sua aparência é:

Observe entretanto que, em uma mesma lista podem estar presentes mais
de um tipo de encadeamento. Veja o exemplo dado antes, onde as colunas A e B
juntas estabelecem um duplo encadeamento pela ordenação alfabética do campo
nome. Já a coluna C é um encadeamento simples ou único pela ordenação
numérica do campo código no sentido crescente.

3.2.2 Implementação
Uma vez claro o conceito da lista encadeada, precisamos discutir formas
de implementá-la. Como foi caracterizado antes, o encadeamento estabelece a
organização lógica que foi escolhida. Desta forma, necessitamos então escolher
uma organização física para armazenar os elementos.
A principal característica da lista reside no fato de cada unidade de
informação indicar a próxima unidade a ser utilizada. Para que isto ocorra, esta
“próxima” unidade deve ser acessível de forma direta, ou seja, deve haver uma
forma de encontrarmos e utilizarmos diretamente esta informação, a partir do seu
endereço definido. Desta maneira, qualquer organização física que permita esta
tipo de endereçamento pode abrigar uma lista encadeada.
Algumas organizações podem atender a estas necessidades e, em
especial, representações matricial e através de alocação dinâmica de memória.
Estas, além de atenderem à quase totalidade dos casos práticos, serão capazes de
estabelecer uma lógica de uso que permitirá a adequação aos casos novos.
Por outro lado, é necessário estabelecer algumas diferenças entre os
casos. A representação matricial, que tem como vantagem a facilidade de
implementação e entendimento para a maioria das pessoas, tem duas
desvantagens potenciais. A primeira delas está no fato de uma matriz ser uma
estrutura uniforme, ou seja, todos os elementos são de um mesmo tipo. Isto não
33
acontece na realidade e teremos informações de tipo variado na estrutura (veja o
exemplo anterior). Se isto acontecer basta, em lugar de uma matriz uniforme,
utilizar um conjunto de vetores, todos do mesmo tamanho, um para cada campo
da lista, ou declarar uma estrutura que represente cada nó e criar uma matriz não
uniforme com ela. Veja os dois casos para o exemplo dado antes:
codigo: vetor[1..N] de inteiro;
nome: vetor[1..N] de caracter;
eloA, eloB, eloC: vetor[1..N] de inteiro;
ou
estrutura Tipo_Lista
codigo: inteiro;
nome: caracter;
eloA, eloB, eloC: inteiro;
fim
lista: vetor[1..N] de Tipo_Lista;

O segundo problema é muito mais sério. Ele ocorre devido ao fato da


matriz (em qualquer uma das formas citadas) ter tamanho pré-definido, ou seja,
necessitamos saber, antes de trabalhar a estrutura, qual será o número de
elementos presentes na lista. Isto, além de ser difícil, tende a gerar desperdício de
memória, pois iremos, na maioria das vezes, superdimensionar o espaço para que
não faltem posições.
A alternativa de implementação com alocação dinâmica de memória cria
uma estrutura semelhante aquela mostrada acima (por exemplo, veja aquela
utilizada no item b da sub-secção 3.2.3.1). Sempre que necessitamos de um novo
elemento, solicitamos mais memória ao sistema operacional. Assim, não
precisamos dimensionar nada a priori e não desperdiçaremos espaço. O mesmo
acontece com a retirada. Entretanto, além de ser de entendimento e
implementação mais difícil para alguns, esta implementação ocupa mais espaço,
pois os elos dos encadeamentos serão ponteiros e estes, geralmente, são maiores
(bem maiores) do que números inteiros utilizados na representação matricial.
Como o objetivo deste material é didático, vamos apresentar e discutir
ambas as formas de implementação, matricial e por alocação dinâmica de
memória. Em qualquer um dos casos, teremos uma variável de controle adicional
para cada um dos encadeamentos utilizados. Esta será o começo ou início da
sequência lógica. No caso de encadeamento duplo, haverão duas variáveis, o
começo e o final daquela sequência, sendo que o final pode ser visto como o
indicador do primeiro elemento de cada uma das sequências invertidas.

34
3.2.3 Algoritmos
Conforme mencionado antes, discutiremos dois tipos de implementação,
a matricial e com alocação dinâmica de memória. Para cada um destes casos
discutiremos um conjunto de algoritmos de manipulação (inicialização, inclusão,
exclusão e consulta) para a lista unicamente encadeada e a lista duplamente
encadeada. Isto é necessário em razão de algumas particularidades existentes em
cada caso.
Vamos também trabalhar com listas que tenham um só dos tipos de
encadeamento. Casos mais complexos, como o exemplo dados antes, podem ser
codificados com uma combinação dos casos tratados aqui.
Antes de apresentarmos os algoritmos, precisamos discutir um problema
adicional. Quando utilizamos alocação dinâmica de memória, o sistema
operacional gerencia o espaço utilizado para nós. E para o caso matricial, quem
faz e como isto é feito? Já que a matriz é criada dentro do programa, pelo
programador, este necessita definir e implementar formas de realizar este
controle.
O mecanismo a ser proposto deve garantir o uso de todo o espaço
disponível, inclusive àqueles nós que foram liberados durante o trabalho de
exclusão. Para realizar isto vamos utilizar uma estrutura auxiliar que irá gerenciar
o espaço livre. Está será conhecida por Pilha de Nós Disponíveis  PND.
Na PND serão empilhados todas as posições livres da estrutura (pelo
menos no começo dos trabalhos será assim) e sempre que quisermos espaço para
um novo nó, desempilharemos uma posição da PND. Na retirada, após concluído
do processo, a posição liberada será empilhada. Note que, como a localização
física das informações é irrelevante para uma lista, não precisamos nos preocupar
com o local da posição que será desempilhada, basta tomá-la para o uso.
Veremos um pequeno exemplo da PND, em uma matriz de 10 elementos
e com apenas 4 em uso, na forma de uma lista unicamente encadeada:

Info Elo PND


1 10
2 A 4 9
3 C 6 8 Começo 2
4 B 3 7
5 6 1
6 D 0 5 5 Topo PND 6
7 4 7
8 3 8
9 2 9
10 1 10

35
Esta técnica de gerenciamento é eficaz, mas pode ter a sua eficiência
ampliada. Nela estaremos acrescentando mais uma estrutura, com tamanho pré-
definido e gastaremos mais espaço de armazenamento. Vamos discutir uma
solução para isto sem perder a eficácia da proposta.
A solução vem da seguinte observação: a lista e a PND são
complementares em número de elementos. Sempre que somarmos estas
quantidades teremos um total equivalente ao tamanho da lista. Se isto é verdade,
estamos afirmando que, dentro da lista, sempre há espaço suficiente disponível
para armazenar a PND. Basta apenas discutir como isto será feito.
A solução é considerar a PND como uma outra lista “lógica” dentro da
mesma estrutura física da lista encadeada, que congrega apenas os elementos
vazios. O seu começo será dado pela variável PND. Veja o exemplo:

Info Elo
1 5
2 A 4
3 C 6 Começo 2
4 B 3
5 7
6 D 0 PND 1
7 8
8 9
9 10
10 0

Neste caso, o primeiro elemento vazio é aquele da posição 1, por


indicação da PND, e ele indica a próxima posição nesta condição no seu elo. Isto
acontece desta forma por todos aqueles que estão vazios. Para mantermos o
princípio da pilha devemos apenas inserir e retirar elementos a partir da indicação
da variável de controle PND, que trabalhará neste caso, como o topo da pilha
anterior.

3.2.3.1 Lista unicamente encadeada


O primeiro grupo de algoritmos será apresentado e discutido para as
listas unicamente encadeadas, assim vamos definir, antes de mais nada, o tipo de
dado que irá abrigar a lista. À semelhança das estruturas do capítulo anterior,
vamos armazenar um dado do tipo caracter apenas. Isto simplifica o trabalho de
manipulação. Por analogia, o caso pode ser ampliado para situações mais amplas
e complexas.

36
Vamos dividir, conforme dito, os algoritmos em dois grupos:
implementação matricial e com alocação dinâmica de memória. Para cada um
destes grupos veremos quatro algoritmos: inicialização da estrutura, inserção de
um novo elemento, remoção de um elemento existente e a busca ou localização
da posição de um dado valor.

a) Representação matricial:
Para implementar a lista descrita acima na forma matricial, vamos
representar a mesma como um estrutura e um vetor de n elementos do tipo
definido por ela. Veja:

estrutura Tipo_LUE
info: caracter;
elo: inteiro;
fim
lista: vetor[1..N] de Tipo_LUE;
começo, pnd: inteiro;

Utilizando esta proposta, vamos realizar a inicialização da estrutura. Para


isto, precisamos definir quais serão as tarefas a serem realizadas. Serão duas: a
atribuição de um valor inicial à variável começo e a construção da lista PND que
inicialmente estará cheia. Estas tarefas estão postas no algoritmo 3.1. Neste
algoritmo é dado zero para o começo, indicando que a lista está vazia e construído
o conjunto de elos que colocam todas as posições da lista como vagas ou
disponíveis na PND.

procedimento inicializar_lue( var lista:vetor[1..N] de Tipo_LUE;


var começo,pnd: inteiro );
var
i: inteiro;
início
começo := 0; /* A lista está vazia */
para i := 2 até N faça /* Preenche a lista de disponíveis */
lista[i-1].elo := i;
lista[N].elo := 0;
pnd := 1; /* Primeiro disponível */
fim
Algoritmo 3.1 – Inicialização da lista unicamente encadeada na forma matricial.

37
O resultado do processo de inicialização é mostrado na figura 3.1. Note
que, na inicialização, a lista de nós disponíveis é linear e sequencial. A quebra
desta sequência se dará com a realização das operações de manipulação da
estrutura.

Figura 3.1 – Resultado da inicialização da lista unicamente encadeada

Figura 3.2 – Casos especiais de inclusão de um elemento na lista unicamente


encadeada.
38
Feita a inicialização da estrutura, que é sempre o primeiro passo a ser
realizado, podemos fazer as demais operações. O algoritmo 3.2 apresenta a
inclusão de um novo elemento na lista. Note que ele traz um novo parâmetro
posição que indica o local após o qual será realizada a inclusão. Desta forma, ele
presume que o processo de localização desta posição é externo a ele e
independente da sua tarefa.

procedimento incluir_lue( var lista:vetor[1..N] de Tipo_LUE;


var começo,pnd,posição: inteiro; elemento:caracter ): booleano;
var
livre: inteiro;
início

se pnd = 0 então
retornar falso; /* A lista está cheia */

/* Toma a próxima posição livre da pnd e a atualiza */


livre := pnd;
pnd := lista[livre].elo;

/* Insere o novo elemento */


lista[livre].info := elemento;

/* Atualiza os apontadores de acordo com o caso da inclusão */


se posição = 0 então /* A inserção é no começo */
lista[livre].elo := começo;
começo := livre;
senão /* A inserção é no meio ou final */
lista[livre].elo := lista[posição].elo;
lista[posição].elo := livre;
fim

retornar verdade; /* Inserção Ok. */


fim
Algoritmo 3.2 – Inclusão de um elemento em uma lista unicamente encadeada na
forma matricial.

Quanto a inclusão propriamente dita, é importante notar que existem três


casos diferentes de inserção de um novo elemento na lista: antes do começo (este
novo elemento será o novo começo da lista); após o final da lista; e, no meio
desta. Cada um destes casos altera parâmetros diferentes da estrutura. Veja isto na
figura 3.2.

39
O algoritmo de inclusão generaliza os casos de inclusão no meio e final,
fazendo as mesmas operações para ambos. Experimente implementá-las em
separado e veja que o resultado é o mesmo.
Não podemos esquecer ainda que é necessário atualizar a pnd após a
inclusão. Neste algoritmo isto é feito no momento da retirada do nó disponível.
Se não houver nenhum nó a ser retirado da PND (pnd = 0), a lista está cheia e não
é possível realizar a inclusão.
Lembre-se então que este processo é realizado em etapas: a primeira
verifica e obtém o espaço para o armazenamento; após isto, o novo elemento é
colocado no nó disponível; finalmente, são atualizados os elos, dependendo do
caso, para que este novo nó participe da sequência lógica, ou seja, efetivamente
entre na estrutura.

Figura 3.3 – Retirada de um elemento de uma lista unicamente encadeada.

Veremos agora o procedimento de retirada. Para este caso, de uma lista


unicamente encadeada, este é um procedimento muito “problemático”. A razão
disto é bastante simples e é espelhada na figura 3.3. Para retirar um elemento,
precisamos fazer com que o elo do nó anterior a ele aponte para aquele nó que ele
originalmente aponta. O problema é que, posicionados no nó a ser excluído, não
sabemos quem é o seu antecessor na sequência lógica, apenas o sucessor.

40
A verdade é que uma lista unicamente encadeada não é uma boa
estrutura para aqueles casos onde ocorrem muitas retiradas. Para isso recomenda-
se o uso de outras estruturas, como a lista duplamente encadeada. Caso se insista
em realizar a operação, existem duas soluções possíveis para este problema.
Nenhuma desta soluções é “muito limpa”, ou seja, necessitaremos fazer algum
arranjo que facilite a operação. Este arranjo será sempre no sentido de conhecer
quem é o antecessor lógico do nó. As duas soluções mencionadas são passar
como parâmetro a posição do nó a ser excluído e a posição do seu antecessor; ou
passar somente a posição do antecessor já que, a partir dele, podemos facilmente
chegar ao nó a ser retirado.
O algoritmo 3.3 apresenta uma solução baseada na primeira das
propostas. Neste algoritmo observaremos uma mudança em relação aos
anteriores: não salvaremos o elemento excluído para o retorno. A razão disto é
que já fizemos uma busca pela posição do elemento a ser retirado e se chegamos
até esta operação é porque ele existe e já dispomos do mesmo para uso. Mesmo o
teste inicial feito no algoritmo é supérfulo neste caso. Já quanto a posição,
infelizmente não há como garantir se ela é válida ou não sem uma pesquisa
anterior muito custosa sob o ponto de vista computacional.

procedimento retirar_lue( var lista:vetor[1..N] de Tipo_LUE;


var começo,pnd,posição,anterior: inteiro ): booleano;
início

se começo = 0 então
retornar falso; /* A lista está vazia */

/* Retira o elemento, atualizando os ponteiros de acordo com o caso */


se posição = começo então /* Está retirando o primeiro elemento */
começo := lista[posição].elo;
senão /* Está retirando no meio ou final */
lista[anterior].elo := lista[posição].elo;
fim

/* Retorna a posição liberada para a PND */


lista[posição].elo := pnd;
pnd := posição;

retornar verdade; /* Retirada Ok. */


fim
Algoritmo 3.3 – Retirada de um elemento de uma lista unicamente encadeada na
forma matricial.

41
Quanto a retirada propriamente dita, teremos também três casos de ajuste
de apontadores a serem tratados: a retirada do primeiro elemento da sequência
lógica; a retirada do último elemento desta sequência; e, a retirada de um
elemento intermediário. Mais uma vez, o algoritmo implementa os dois últimos
casos de forma idêntica sem prejuízo da tarefa realizada. Veja novamente a figura
3.3 para detalhes.
Após a retirada do elemento, necessitamos devolver a sua posição à
PND, para que volte a ser utilizado futuramente. Isto é realizado colocando-o
novamente na sequência lógica de disponíveis.
Finalmente podemos realizar a consulta ou pesquisa na lista unicamente
encadeada, tentando localizar a posição física de uma dada informação. O
algoritmo 3.4 apresenta esta operação. Ele retorna a posição da localização ou 0
(zero) se o elemento não existir na lista. Observe somente que não é possível
percorrer a lista com laços de incremento linear, como para, pois a organização
lógica pode ser diferente da física. Desta forma, só podemos fazer a varredura
através dos elos. Outro detalhe é que o único método de pesquisa admissível é a
pesquisa sequencial. Métodos como a pesquisa binária (veja em [AU92]) não
funcionam porque não é possível localizar o “meio lógico” da lista necessário à
realização da partição.

procedimento pesquisar_lue( lista:vetor[1..N] de Tipo_LUE;


começo: inteiro; elemento:caracter ): inteiro;
var
posição: inteiro;
início
posição := começo;

enquanto posição <> 0 e lista[posição].info <> elemento faça


posição := lista[posição].elo;
fim

retornar posição;
fim
Algoritmo 3.4 – Pesquisa a localização de um elemento na lista unicamente
encadeada na forma matricial.

b) Alocação dinâmica de memória


Vamos apresentar agora a segunda possibilidade de implementação da
lista unicamente encadeada, utilizando a alocação dinâmica de memória. Para

42
isso vamos definir como será a estrutura básica de trabalho que armazenará cada
um dos nós. Veja:

estrutura Tipo_LDE
info: caracter;
elo: ^Tipo_LDE;
fim
começo: ^Tipo_LDE;

Neste caso não há necessidade de termos a PND, pois o sistema


operacional se encarrega da tarefa de gestão do espaço. Outra observação
importante é quanto ao elo e ao começo, pois eles agora são apontadores para
endereços de memória, ou seja, ponteiros e não mais inteiros como no caso
matricial.

procedimento inicializar_lue( var começo: ^Tipo_LUE );


início
começo := NULO; /* A lista está vazia */
fim
Algoritmo 3.5 – Inicialização da lista unicamente encadeada em alocação
dinâmica de memória.

O algoritmo de inicialização é muito mais simples para esta situação já


que não existe mais a PND. Agora, apenas necessitamos inicializar o começo,
que é feito nulo quando não há ninguém na estrutura. Por nulo, que é um valor
especial, se entende aqui o ponteiro que não aponta para nenhuma região de
memória. Veja-o no algoritmo 3.5.
Para realizarmos a inclusão de um novo elemento, o procedimento é
muito semelhante ao caso anterior (algoritmo 3.2) pois a lógica da estrutura
continua a mesma, mudou apenas a alocação física dela. Este algoritmo está
descrito no algoritmo 3.6. Nele, os casos especiais da inclusão continuam os
mesmos, inclusive a variável posição, com a mesma função de antes. Outro
detalhe de extrema importância é a “ausência” da lista como parâmetro. Como ela
é residente em memória, basta conhecer o começo para alcançar qualquer outra
posição.
A retirada apresenta os mesmos problemas já citados e adotaremos a
mesma alternativa ou arranjo de solução do caso matricial. Aqui, entretanto, não
temos o problema do espaço e precisamos apenas informar ao sistema
operacional que aquele espaço não é mais útil. O resto é por conta dele. Este é o
algoritmo 3.7.

43
Procedimento incluir_lue( var começo,posição: ^Tipo_LUE;
elemento:caracter ): booleano ;
var
livre: ^Tipo_LUE;

início

/* Solicita espaço para o novo elemento */


livre := novo Tipo_LUE;

se livre = NULO então


retornar falso; /* A lista está cheia, pois não há mais memória */

/* Insere o novo elemento no espaço obtido*/


livre.info := elemento;

/* Atualiza os ponteiros de acordo com o caso da inclusão */


se começo = NULO ou posição = NULO então /* Inserir no começo */
livre.elo := começo;
começo := livre;
senão /* A inserção é no meio ou final */
livre.elo := posição.elo;
posição.elo := livre;
fim

retornar verdade; /* Inserção Ok. */

fim
Algoritmo 3.6 – Inclusão de um elemento em uma lista unicamente encadeada em
alocação dinâmica de memória.

Finalmente o algoritmo de pesquisa. Aquele apresentado no algoritmo


3.4 é absolutamente análogo ao formato agora utilizado para armazenamento da
lista. Veja-o no algoritmo 3.8. Ao final, a variável posição conterá nulo se não for
encontrado o elemento que desejamos. Caso contrário, ela apontará para o
endereço de memória onde este elemento está.

44
procedimento retirar_lue( var começo,posição,anterior: ^Tipo_LUE ):
booleano;
início
se começo = NULO ou posição = NULO então
retornar falso; /* A lista está vazia ou posição é inválida */

/* Retira o elemento, atualizando os ponteiros de acordo com o caso */


se posição = começo então /* Está retirando o primeiro elemento */
começo := posição.elo;
senão /* Está retirando no meio ou final */
anterior.elo := posição.elo;
fim

/* Libera a memória utilizada */


liberar posição;

retornar verdade; /* Retirada Ok. */


fim
Algoritmo 3.7 – Retirada de um elemento de uma lista unicamente encadeada em
alocação dinâmica de memória.

Procedimento pesquisar_lue(começo: ^Tipo_LUE; elemento:caracter ):


^Tipo_LUE;
var
posição: ^Tipo_LUE;
início

posição := começo;

enquanto posição <> NULO e posição.info <> elemento faça


posição := posição.elo;
fim

retornar posição;
fim
Algoritmo 3.8 – Pesquisa a localização de um elemento na lista unicamente
encadeada em alocação dinâmica de memória.

3.2.3.2 Lista duplamente encadeada


Para implementarmos a lista de forma a utilizar um duplo encadeamento,
manteremos analogia ao que foi proposto para o caso mais simples, ou seja,
45
armazenaremos apenas uma informação do tipo caracter. A única modificação
será o acréscimo de um novo elo. Este, com aquele que já existia, serão
denominados de eloa e elop, ou seja, elo para o elemento anterior (antecessor) e
elo para o elemento posterior (sucessor), respectivamente.
Os algoritmos apresentados e as formas de apresentação serão as
mesmas de antes, ou seja, faremos a inicialização, inclusão, retirada e pesquisa na
lista duplamente encadeada sob a forma matricial e com alocação dinâmica de
memória.

a) Representação matricial
Para implementar a lista duplamente encadeada na forma matricial,
necessitamos definir a matriz que a abrigará. Esta será:

estrutura Tipo_LDE
info: caracter;
eloa, elop: inteiro;
fim
lista: vetor[1..N] de Tipo_LDE;
começo, final, pnd: inteiro;

Note que aqui também existe a lista auxiliar de nós disponíveis PND.
Isto é necessário pois estamos novamente trabalhando com uma estrutura de
tamanho fixo e pré-definido. Como agora temos dois elos e a lista PND é de
encadeamento único ou simples, precisamos escolher um deles para definir a sua
sequência. Vamos utilizar o elop para esta tarefa.
Outra coisa a observar é a existência de um controlador para o final da
lista. Ele é necessário porque indica o “início” da encadeamento através do elo
anterior.
Baseado então nestas definições, o algoritmo 3.9 descreve o
procedimento de inicialização desta estrutura. A única novidade é a atribuição de
zero para o final, indicando que a lista está vazia.
O procedimento de inclusão de um novo elemento, no algoritmo 3.10,
tem a mesma lógica da lista unicamente encadeada, mas aqui as três situações
diferentes de inclusão, começo, meio e final da lista, devem ser implementadas
em separado pois existe a variável final para ser ajustada na última da situações.
Veja a figura 3.4 para o gráfico explicativo dos casos e suas respectivas ações.

procedimento inicializar_lde( var lista:vetor[1..N] de Tipo_LDE;


var começo,final,pnd: inteiro );
46
var
i: inteiro;
início
começo := 0; /* A lista está vazia */
final := 0;
para i := 2 até N faça /* Preenche a lista de disponíveis */
lista[i-1].elop := i;
lista[N].elop := 0;
pnd := 1; /* Primeiro disponível */
fim
Algoritmo 3.9 – Inicialização da lista duplamente encadeada na forma matricial.

Figura 3.4 – Inserção de um novo elemento em uma lista duplamente encadeada

procedimento incluir_lde( var lista:vetor[1..N] de Tipo_LDE;


var começo,final,pnd,posição: inteiro; elemento:caracter ):
booleano;
var
livre, posterior: inteiro;
47
início

/* Verifica se a lista está cheia */


se pnd = 0 então
retornar falso; /* A lista está cheia */

/* Toma a próxima posição livre da pnd e a atualiza */


livre := pnd;
pnd := lista[livre].elop;

/* Insere o novo elemento */


lista[livre].info := elemento;

/* Atualiza os apontadores de acordo com o caso da inclusão */


se começo = 0 então /* A lista está vazia */
lista[livre].eloa := 0;
lista[livre].elop := 0;
começo := livre;
final := livre;
retornar verdade; /* Inserção Ok */
fim

se posição = 0 então /* Inserção no começo da lista */


lista[livre].eloa := 0;
lista[livre].elop := começo;
lista[começo].eloa := livre;
começo := livre;
retornar verdade; /* Inserção Ok */
fim

se posição = final então /* Inserção no final da lista */


lista[livre].eloa := final;
lista[livre].elop := 0;
lista[final].elop := livre;
final := livre;
retornar verdade; /* Inserção Ok */
fim

/* A inserção é no meio da lista */


posterior := lista[posição].elop;
lista[livre].eloa := posição;
lista[livre].elop := posterior;
lista[posição].elop := livre;

48
lista[posterior].eloa := livre;
retornar verdade; /* Inserção Ok. */

fim
Algoritmo 3.10 – Inclusão de um elemento em uma lista duplamente encadeada
na forma matricial.

Figura 3.5 – Retirada de um elemento de uma lista duplamente encadeada.

Para a retirada de um elemento, também vão ocorrer três casos, descritos


na figura 3.5, que devem ser tratados em separado pelo algoritmo. A principal
diferença é que agora não precisamos informar quem é o nó anterior aquele a ser
retirado. O seu campo eloa informa este nó, bem como o campo elop informa
quem é o próximo nó da sequência lógica. Este processo é descrito no algoritmo
3.11.

procedimento retirar_lde( var lista:vetor[1..N] de Tipo_LDE;


var começo,final,pnd,posição: inteiro ): booleano;
var
anterior, posterior: inteiro;
início
49
se começo = 0 ou posição = 0 então
retornar falso; /* A lista está vazia ou a posição é inválida */

/* Retira o elemento, atualizando os ponteiros de acordo com o caso */


se posição = começo então /* Esta retirando o primeiro da lista */
posterior := lista[começo].elop;
se posterior = 0 então /* Só tem um elemento na lista */
final := 0;
começo := 0;
senão
lista[posterior].eloa := 0;
começo := posterior;
fim
senão
anterior := lista[posição].eloa;
posterior := lista[posição].elop;
se posição = final então /* O elemento é o último da lista */
lista[anterior].elop := 0;
final := anterior;
senão /* É um elemento do meio da lista */
lista[anterior].elop := posterior;
lista[posterior].eloa := anterior;
fim
fim

/* Retorna a posição liberada para a PND */


lista[posição].elop := pnd;
pnd := posição;

retornar verdade; /* Retirada Ok. */


fim
Algoritmo 3.11 – Retirada de um elemento de uma lista duplamente encadeada na
forma matricial.
Finalmente, no algoritmo 3.12, está descrita a pesquisa pela posição de
uma dado elemento na lista duplamente encadeada. Observe que foi

Î
implementada a pesquisa através do campo elop, ou seja, sequencialmente no
sentido começo final da lista. Se desejar, pode fazê-lo pelo campo eloa,
começando pela posição final e terminando no começo da lista. Por outro lado,
esta alternativa não muda em nada o desempenho do algoritmo, pois o trabalho
realizado por ele continuará o mesmo.

procedimento pesquisar_lde( lista:vetor[1..N] de Tipo_LDE;

50
começo: inteiro; elemento:caracter ): inteiro;
var
posição: inteiro;
início
posição := começo;

enquanto posição <> 0 e lista[posição].info <> elemento faça


posição := lista[posição].elop;
fim

retornar posição;
fim
Algoritmo 3.12 – Pesquisa a localização de um elemento na lista duplamente
encadeada na forma matricial.

b) Alocação dinâmica de memória


Em primeiro lugar vamos apresentar a estrutura que será utilizada para
definir a unidade de informação ou nó:
estrutura Tipo_LDE
info: caracter;
eloa, elop: ^Tipo_LDE;
fim
começo, final: ^Tipo_LDE;

De posse desta estrutura, podemos compor o algoritmo de inicialização


da lista duplamente encadeada. Este é o algoritmo 3.13. Como estamos utilizando
alocação dinâmica de memória, não existe a PND e precisamos apenas atribuir
um valor inicial às variáveis de controle começo e final. Ambas serão feitas nulas
para indicar que a lista está vazia e, neste caso, elas não apontam para nenhuma
região de memória.

procedimento inicializar_lue( var começo, final: ^Tipo_LDE );


início
começo := NULO; /* A lista está vazia */
final := NULO;
fim

51
Algoritmo 3.13 – Inicialização da lista duplamente encadeada em alocação
dinâmica de memória.

procedimento incluir_lde( var começo,final,posição: ^Tipo_LDE;


elemento:caracter ): booleano;
var
livre, posterior: ^Tipo_LDE;
início
/* Solicita memória para o novo elemento e testa de há espaço */
livre := novo Tipo_LDE;
se livre = NULO então
retornar falso; /* A lista está cheia */

/* Insere o novo elemento */


livre.info := elemento;

/* Atualiza os apontadores de acordo com o caso da inclusão */


se começo = NULO então /* A lista está vazia */
livre.eloa := NULO;
livre.elop := NULO;
começo := livre;
final := livre;
retornar verdade; /* Inserção Ok */
fim

se posição = NULO então /* Inserção no começo da lista */


livre.eloa := NULO;
livre.elop := começo;
começo.eloa := livre;
começo := livre;
retornar verdade; /* Inserção Ok */
fim

se posição = final então /* Inserção no final da lista */


livre.eloa := final;
livre.elop := NULO;
final.elop := livre;
final := livre;
retornar verdade; /* Inserção Ok */
fim

posterior := posição.elop; /* A inserção é no meio da lista */


livre.eloa := posição;

52
livre.elop := posterior;
posição.elop := livre;
posterior.eloa := livre;
retornar verdade; /* Inserção Ok. */
fim
Algoritmo 3.14 – Inclusão de um elemento em uma lista duplamente encadeada
em alocação dinâmica de memória.

procedimento retirar_lde( var começo, final, posição: ^Tipo_LDE ): booleano;


var
anterior, posterior: ^Tipo_LDE;
início
se começo = NULO ou posição = NULO então
retornar falso; /* A lista está vazia ou a posição é inválida */

/* Retira o elemento, atualizando os ponteiros de acordo com o caso */


se posição = começo então /* Esta retirando o primeiro da lista */
posterior := começo.elop;
se posterior = NULO então /* Só tem um elemento na lista */
final := NULO;
começo := NULO;
senão
posterior.eloa := NULO;
começo := posterior;
fim
senão
anterior := posição.eloa;
posterior := posição.elop;
se posição = final então /* O elemento é o último da lista */
anterior.elop := NULO;
final := anterior;
senão /* É um elemento do meio da lista */
anterior.elop := posterior;
posterior.eloa := anterior;
fim
fim

/* Liberar o espaço ocupado ao sistema operacional */


liberar posição;

retornar verdade; /* Retirada Ok. */


fim
53
Algoritmo 3.15 – Retirada de um elemento de uma lista duplamente encadeada
em alocação dinâmica de memória.

procedimento pesquisar_lde( começo: ^Tipo_LDE; elemento:caracter ):


^Tipo_LDE;
var
posição: ^Tipo_LDE;
início
posição := começo;

enquanto posição <> NULO e posição.info <> elemento faça


posição := posição.elop;
fim

retornar posição;
fim
Algoritmo 3.16 – Pesquisa a localização de um elemento na lista duplamente
encadeada em alocação dinâmica de memória.

Para a lista duplamente encadeada armazenada com alocação dinâmica


de memória os processos são análogos aqueles já vistos. Estes estão descritos nos
algoritmos 3.14 (inclusão), 3.15 (retirada) e 3.16 (pesquisa). Se ainda assim
houver alguma dúvida, entenda bem os algoritmos anteriores, faça um teste de
mesa com estes apresentados agora e analise/observe todos os detalhes do seu
funcionamento.
Outra observação bastante importante, antes tarde do que nunca, é
recomendar à todos uma revisão/atualização forte no uso e tratamento de
ponteiros pelas linguagens. Para isso, uma vez tendo escolhido a sua linguagem
de programação, recorra a manuais ou livros sobre ela e verifique como são
manipulados ponteiros e alocação dinâmica de memória. Faça todos os exercícios
propostos nestes livros e se capacite no uso desta ferramenta de extrema
importância para o uso de estruturas de dados.

3.2.4 Análise
Vamos agora realizar a análise da complexidade dos algoritmos de
manipulação das listas encadeadas vistos nesta seção. Veja, para isso, o somatório
das operações realizadas em cada um dos casos de trabalho (melhor e pior) dados

54
na tabela a seguir. Como existe algoritmos com o mesmo nome nesta seção, eles
trazem o seu número utilizado como referência neste texto. A tabela é:

Algoritmo Melhor Caso Pior Caso


+- := se [] +- := se []
Inicializar_Lue (3.1) 2N-2 2N+2 N N 2N-2 2N+2 N N
Incluir_Lue (3.2)  1 1   6 2 5
Retirar_Lue (3.3)  1 1   4 2 3
Pesquisar_Lue (3.4)  2 1   N+2 2N+1 2N
Inicializar_Lue (3.5)  1    1  
Incluir_Lue (3.6)  2 1   5 3 
Retirar_Lue (3.7)  1 1   2 3 
Pesquisar_Lue (3.8)  2 1   N+2 2N+1 
Inicializar_Lde (3.9) 2N-2 2N+3 N N 2N-2 2N+3 N N
Incluir_Lde (3.10)  1 1   9 4 7
Retirar_Lde (3.11)  1 1   7 4 5
Pesquisar_Lde  2 1   N+2 2N+1 2N
(3.12)
Inicializar_Lde  2    2  
(3.13)
Incluir_Lde (3.14)  2 1   8 4 
Retirar_Lde (3.15)  1 1   5 4 
Pesquisar_Lde  2 1   N+2 2N+1 2N
(3.16)

É possível notar, a partir das quantificações dadas na tabela acima, que


as tarefas de inclusão e retirada de elementos de uma lista encadeada, de qualquer
tipo, são sempre constantes. A razão disto é que estas operações realizam apenas
ajustes na sequência lógica da estrutura (apontadores) de forma a fazer com que o
nó em questão entre ou saia da lista. A tarefa anterior a estas, de localização da
posição de inserção ou retirada é externa aos procedimentos e, portanto, não
computada aqui.
55
Os únicos processos que são proporcionais ao tamanho da lista são a
inicialização para a representação matricial e a pesquisa em qualquer caso. A
primeira delas é motivada pelo ajuste dos elos para a PND, que visita todos os
nós da lista. O segundo algoritmo tem este tempo para o pior caso, quando o
elemento pesquisado não está presente na lista e toda a estrutura necessita ser
percorrida para dar tal garantia.
Quanto aos casos verificados, a inicialização realiza a mesma tarefa em
qualquer um deles. Para a inclusão, foi considerado como melhor caso a lista
estar cheia, pois o algoritmo termina no primeiro testes, e como pior caso, a
inserção no meio da lista, pois vai até o final do algoritmo.
Na retirada, o melhor caso ocorre quando a lista está vazia, pois
novamente saímos no primeiro dos testes, e o pior caso quando o nó a ser retirado
está no meio da estrutura. Já na pesquisa, como foi dito, o melhor caso é quando
o elemento solicitado se encontra no começo da lista, na primeira posição lógica
dela, e o pior caso quando ele não está presente e percorremos toda a estrutura.
Ainda quanto a isto, os testes consideraram o comando retornar como
uma atribuição e os comandos de alocação de memória, como novo e liberar não
foram computados. Outra observação importante deve ser feita para os testes
compostos, com duas condições ligadas por um operador lógico e ou ou. Nestes,
a semelhança das linguagens de programação usuais, quando a primeira parte do
teste valida todo ele, o restante não é realizado. Isto foi utilizado nos melhores
casos dos algoritmos.
Em todos os casos as listas se mostram eficientes no seu trabalho,
necessitamos apenas fazer duas considerações sobre isto. A primeira delas é
quanto a retirada de elementos de uma lista unicamente encadeada. Em razão do
arranjo feito para conhecer o elemento anterior ao excluído, a solução não se
tornou muito “limpa” do ponto de vista computacional. Isto funciona, mas mostra
que esta estrutura não é orientada para esta operação. Se desejamos fazer muitas
retiradas com o mínimo de incômodo, devemos considerar a possibilidade de uso
de uma lista duplamente encadeada para o caso.
A segunda consideração, também já mencionada no texto, é relacionada
com a pesquisa. Como não há relação da posição ou alocação física do nó com a
sequência lógica de uso, é impossível encontrar o “meio” desta lista e, portanto,
impossível de realizarmos uma pesquisa mais eficiente do que a sequencial.
Naturalmente esta pesquisa é conhecida como pouco eficiente, pois não explora
nenhuma característica dos dados armazenados para agilizar a busca.
Infelizmente não há como procedermos de forma diferente neste caso. Resta-nos
saber que existem, e veremos mais tarde, outras estruturas construídas para
permitir a pesquisa das informações com muita eficiência.

56
3.3 Exercícios

1. Escreva um procedimento que concatena duas listas unicamente encadeadas


armazenadas na forma matricial.
2. Escreva um procedimento que concatena duas listas duplamente encadeadas
armazenadas em alocação dinâmica de memória.
3. Dadas duas listas unicamente encadeadas, armazenadas na forma matricial,
onde os elementos (números inteiros) estão ordenados de forma crescente,
escreva um procedimento que intercala estas listas e produz uma terceira,
ordenada no mesmo formato.
4. Dadas duas listas duplamente encadeadas, armazenadas em alocação dinâmica
de memória, onde os elementos (números inteiros) estão ordenados de forma
crescente, escreva um procedimento que intercala estas listas e produz uma
terceira, ordenada no mesmo formato.
5. Escreva um procedimento que ordena uma lista duplamente encadeada pelo
método de bolha (ver [Aze96]).
6. Considere a existência de dois conjuntos numéricos, em ordem crescente,
armazenados em listas duplamente encadeadas, sem repetição de elementos.
Escreva os procedimentos para a realização das operações básicas com estes
conjuntos. Algumas delas são: união, interseção, diferença, etc...
7. Considere um polinômio no seguinte formato:
P(x) = A0 + A1x1 + A2x2 + A3x3 + ..... + Anxn
Sabendo que este polinômio está armazenado em uma lista duplamente
encadeada, onde os nós tem o seguinte formato:

EloA i Ai EloP
escreva os seguinte procedimentos:
a) Solucione o polinômio para um dado valor de x.
b) Multiplique um polinômio por uma constante k.
c) Multiplique dois polinômios entre si.
d) Divida um polinômio por uma constante k.
e) Divida dois polinômio entre si.

57
8. Dadas duas listas unicamente encadeadas A e B, na forma matricial, ambas
com n elementos, escreva um procedimento que informa o primeiro elemento da
lista A que não está presente na lista B.
9. Considere uma lista duplamente encadeada com n elementos em alocação
dinâmica de memória, cuja a informação armazenada é um número inteiro.
Escreva um ou mais procedimentos que realizem a seguinte sequência:
a) Encontre os dois menores valores na lista (em uma única varredura);
b) Retire os nós onde estão estes valores;
c) Crie um novo nó onde a informação armazenada é a soma das
informações dos nós retirados;
d) Insira este novo nó no fim da lista;
e) Repita o processo enquanto a lista tiver mais do que 1 nó.
10. Escreva um procedimento que conta a quantidade de nós válidos em uma lista
unicamente encadeada armazenada nos dois formatos possíveis.
11. Escreva um procedimento que inverte os apontadores de uma lista unicamente
encadeada em alocação dinâmica de memória.
12. Faça o mesmo que o exercício 11 pede para uma lista duplamente encadeada.
13. Dadas duas listas unicamente encadeadas A e B, ambas com n elementos
numéricos ordenados crescentemente, escreva um procedimento que recebe um
valor k como parâmetro e informa quem é o k-ésimo maior elemento dentre as
duas listas.
14. Faça a análise assintótica de todos os algoritmos escritos nestes exercícios.
15. Escreva os procedimentos destes exercícios em alguma linguagem de
programação e execute-os em um computador.

58
Bibliografia

[AHU74] Alfred Aho, John Hopcroft and Jeffrey Ullman. The Design and
Analysis of Computer Algortihms. Addison-Wesley, 1974.
[AHU83] Alfred Aho, John Hopcroft and Jeffrey Ullman. Data Strucutres and
Algorithms. Addison-Wesley, 1983.
[Amm88] Leendert Ammeraal. Programs and Data Structures in C. John Wiley
& Sons, 1988.
[AU92] Alfred Aho and Jeffrey Ullman. Foundations of Computer Science.
Computer Science Press, 1992.
[Aze96] Paulo Azeredo. Métodos de Classificação de Dados e Análise de suas
Complexidades. Ed. Campus, 1996.
[Car98] Marcos Carrard. Algoritmos e Estruturas de Dados, Parte I 
Fundamentos. Cadernos da Unijuí, Série Informática, número 4.
Editora Unijuí, 1998.
[CLR91] Thomas Cormer, Charles Leiserson and Ronald Rivest. Introduction to
Algorithms. McGraw-Hill, 1991.
[EW__] Jeffrey Esakov and Tom Weiss. Data Structures: an Advanced
Approach Using C. Prentice-Hall, ____.
[Fra87] Ana Helena Fragomeni. Dicionário Enciclopédico de Informática.
Campus, 87.
[HS82] Ellis Horowitz and Sartaj Sahni. Fundamentals of Data Structures.
Computer Science Press, 1982.
[Knu73] Donald Knuth. The Art of Computer Programming: Fundamentals
Algorithms. Addison-Wesley, 1973.
[Man89] Udi Mamber. Introduction to Algorithms: A Creative Approach.
Addison-Wesley, 1989.
[Sam90] Hanan Samet. The Design and Analysis of Spatial Data Structures.
Addison-Wesley, 1990.
[Sch90] Herbert Schildt. C - The Complete Reference. McGraw-Hill, 1990.
[Sed88] Robert Sedgewick. Algorithms, 2nd edition. Addison-Wesley, 1988.

59
[SM94] Jayme Szwarcfiter and Lilian Markenzon. Estruturas de Dados e seus
Algoritmos. LTC, 1994.
[Swa91] Joffre dan Swait Jr. Fundamentos Computacionais: Algoritmos e
Estruturas de Dados. Makron Books, 1991.
[TA86] Aaron Tenenbaun and Moshe Augenstein. Data Structures Using
Pascal, 2nd edition. Prentice-Hall, 1986.
[Ter91] Routo Terada. Desenvolvimento de Algoritmos e Estruturas de Dados.
Makron Books, 1991.
[TS84] Jean-Paul Tremblay and Paul Sorenson. An Introduction to Data
Structures with Applications, 2nd edition. McGraw-Hill, 1984.
[VF*93] Marcos Villas, Andréa Ferreira, Patrick Leroy, Cláudio Miranda and
Christine Bockman. Estruturas de Dados: Conceitos e Técnicas de
Implementação. Campus, 1993.
[Ziv93] Nivio Ziviani. Projeto de Algoritmos com Implementações em Pascal e
C. Pioneira, 1993.

60

S-ar putea să vă placă și