Documente Academic
Documente Profesional
Documente Cultură
Departamento de Tecnologia
Algoritmos
e
Estruturas de Dados
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
3
1
Fundamentos
1.1 Introdução
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.
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:
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
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
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
8
2
Estruturas de Dados Sequenciais
2.1 Caracterização
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:
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.
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:
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 é:
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.
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.
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.
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:
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:
........ 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.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:
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.
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.
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:
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
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
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
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:
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:
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;
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:
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
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;
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.
se pnd = 0 então
retornar falso; /* A lista está cheia */
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.
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.
se começo = 0 então
retornar falso; /* A lista está vazia */
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.
retornar posição;
fim
Algoritmo 3.4 – Pesquisa a localização de um elemento na lista unicamente
encadeada na forma matricial.
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;
43
Procedimento incluir_lue( var começo,posição: ^Tipo_LUE;
elemento:caracter ): booleano ;
var
livre: ^Tipo_LUE;
início
fim
Algoritmo 3.6 – Inclusão de um elemento em uma lista unicamente encadeada em
alocação dinâmica de memória.
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 */
posição := começo;
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.
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.
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.
Î
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.
50
começo: inteiro; elemento:caracter ): inteiro;
var
posição: inteiro;
início
posição := começo;
retornar posição;
fim
Algoritmo 3.12 – Pesquisa a localização de um elemento na lista duplamente
encadeada na forma matricial.
51
Algoritmo 3.13 – Inicialização da lista duplamente encadeada em alocação
dinâmica de memória.
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.
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.
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 é:
56
3.3 Exercícios
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