Sumrio
1 Listas Lineares 3
1.1 Alocao Esttica versus Dinmica 4
1.2 Alocao Seqencial 4
1.3 Alocao Encadeada 5
2 Pilhas 6
2.1 Declarando e inicializando uma pilha 7
2.2 Verificando limites da pilha 7
2.3 Inserindo e removendo elementos na pilha 7
2.4 Verificando o elemento do topo da pilha 8
3 Filas 9
3.1 Declarando e inicializando uma fila 10
3.2 Verificando limites da fila 10
3.3 Inserindo e removendo elementos da fila 10
3.4 Problemas na implementao seqencial de filas 11
3.5 Implementao circular para filas 11
3.6 Inserindo e removendo elementos da fila circular 12
9 Listas Ordenadas 51
9.1 Implementao Encadeada para Listas Ordenadas 51
9.1.1 A Operao de Insero 52
9.1.2 A operao de Remoo 54
9.1.3 A Operao de Pesquisa 55
9.1.4 Imprimindo uma Lista Ordenada 56
9.1.5 Destruindo uma Lista Ordenada 57
9.2 Tcnicas de Encadeamento Nodos cabea e sentinela 57
9.3 Encadeamento circular 59
9.4 Encadeamento Duplo 61
Bibliografia 62
Lista de exerccios 63
3
1 LISTAS LINEARES
Uma das formas mais comumente usadas para se manter dados agrupados a lista. Afinal, quem nunca
organizou uma lista de compras antes de ir ao mercado, ou ento uma lista dos amigos que participaro
da festa? As listas tm-se mostrado um recurso bastante til e eficiente no dia-a-dia das pessoas. Em
computao, no tem sido diferente: a lista uma das estruturas de dados mais empregadas no
desenvolvimento de programas.
Uma lista linear uma coleo L: [a1, a2, ... an ], n .>= 0, cuja propriedade estrutural baseia-se apenas
na posio relativa dos elementos, que so dispostos linearmente. Se n = 0 dizemos que a lista vazia;
caso contrrio, so vlidas as seguintes propriedades:
a1 o primeiro elemento de L;
an o ltimo elemento de L;
ak , 1 < k < n, precedido pelo elemento ak - 1 e seguido pelo elemento ak + 1 em L.
Em outras palavras, a caracterstica fundamental de uma lista linear o sentido de ordem
unidimensional dos elementos que a compem. Uma ordem que nos permite dizer com preciso onde a
coleo inicia-se e onde termina, sem possibilidade de dvida. Entre as diversas operaes que
podemos realizar sobre listas, temos:
acessar um elemento qualquer da lista;
inserir um elemento numa posio especfica da lista;
remover um elemento de uma posio especfica da lista;
combinar duas listas em uma nica;
particionar uma lista em duas;
obter cpias de uma lista;
determinar o total de elementos na lista;
ordenar os elementos na lista
procurar um determinado elemento na lista;
apagar uma lista;
outras ...
Considerando-se somente as operaes de acesso, insero e remoo, restritas aos extremos da lista,
temos casos especiais que aparecem muito freqentemente na modelagem de problemas a serem
resolvidos por computador. Tais casos especiais recebem tambm nomes especiais:
Pilha: lista linear onde todas as inseres, remoes e acessos so realizados em um nico
extremo. Listas com esta caracterstica so tambm denominadas lista LIFO (Last-In/Firs-Out)
ou em portugus: UEPS (ltimo que Entra/Primeiro que Sai).
Fila: lista linear onde todas as inseres so feitas num certo extremo e todas as remoes e
acessos so realizados no outro extremo. Filas so tambm denominadas listas FIFO (First-
In/First-Out) ou em portugus: PEPS (Primeiro que Entra/Primeiro que Sai).
Fila Dupla: lista linear onde as inseres, remoes ou acessos so realizados em qualquer
extremo. Filas Duplas so tambm denominadas DEQUE (Double-Ended QUEue), ou em
portugus: fila de extremidade dupla. Uma Fila Dupla pode ainda gerar dois casos especiais:
- Fila Dupla de Entrada Restrita (se a entrada for restrita a um nico extremo) e
- Fila Dupla de Sada Restrita (se a remoo for restrita a um nico extremo).
Ao desenvolver uma implementao para listas lineares, o primeiro problema que surge : como
podemos armazenar os elementos da lista, dentro do computador? Sabemos que antes de executar um
programa, o computador precisa carregar seu cdigo executvel para a memria. Da rea de memria
que reservada para o programa, uma parte usada para armazenar as instrues a serem executadas e
a outra destinada ao armazenamento dos dados. Quem determina quanto de memria ser usado para
4
Observando a figura, com a memria no momento da execuo, verificamos que mais uma varivel foi
criada na posio de memria 1FFA. Esta uma varivel alocada dinamicamente (s vezes
denominada varivel annima, pois no conhecemos seu nome, temos apenas seu endereo!).
Os atributos seqencial e encadeada s tm sentido se formos armazenar uma coleo de objetos,
como o caso da memria necessria para armazenar uma lista linear ou um vetor.
Endereo(ai ) = E ;
5
A maior vantagem no uso de uma rea seqencial de memria para armazenar uma lista linear que,
dado o endereo inicial B da rea alocada e o ndice i de um elemento qualquer da lista, podemos
acess-lo imediatamente, com um simples e rpido clculo. O ponto fraco desta forma de
armazenamento aparece quando precisamos inserir ou suprimir elementos do meio da lista, quando
ento um certo esforo ser necessrio para movimentar os elementos, de modo a abrir espao para
insero, ou de modo a ocupar o espao liberado por um elemento que foi removido.
2 PILHAS
A pilha uma das estrutura de dados mais teis em computao. Uma infinidade de problemas
clssicos da rea podem ser resolvidos com o uso delas. Uma pilha um tipo especial de lista linear
em que todas as operaes de insero e remoo so realizadas numa mesma extremidade,
denominada topo. Cada vez que um novo elemento deve ser inserido na pilha, ele colocado no seu
topo; e em qualquer momento, apenas aquele posicionado no topo da pilha pode ser removido.
Devido a esta disciplina de acesso, os elementos so sempre removidos numa ordem inversa quela
em que foram inseridos, de modo que o ltimo elemento que entra exatamente o primeiro que sai.
Da o fato destas listas serem tambm denominadas listas LIFO (Last-In/First-Out) ou em portugus:
UEPS (ltimo que Entra/Primeiro que Sai).
Uma pilha ou lista LIFO uma coleo que pode aumentar ou diminuir durante a sua existncia. O
nome pilha advm justamente da semelhana entre o modo como as listas LIFO crescem e diminuem e
o modo como as pilhas, no mundo real, funcionam. O exemplo mais comum do quotidiano uma pilha
de pratos, onde o ltimo prato colocado o primeiro a ser usado (removido da pilha).
Uma pilha suporta 3 operaes bsicas, tradicionalmente denominadas como:
Top (Visualizar o Topo): acessa o elemento posicionado no topo da pilha;
Push (Inserir): insere um novo elemento no topo da pilha;
Pop (Retirar): remove um elemento do topo da pilha.
Sendo P uma pilha e x um elemento qualquer, a operao Push ( P, x ) aumenta o tamanho da pilha P,
acrescentando o elemento x no seu topo. A operao Pop ( P ) faz com que a pilha diminua, removendo
e retornando o elemento existente em seu topo. A operao Top no altera o estado da pilha; apenas
retorna uma cpia do elemento existente no topo da pilha sem remov-lo.
Exerccio: Dadas as operaes sobre uma pilha P, preencha o quadro a seguir com o estado da pilha e
o resultado de cada operao:
8
x: integer;
Begin
topo := 0; // inicializao da pilha
3 FILAS
Uma fila um tipo especial de lista linear em que as inseres so realizadas em um extremo e as
remoes so feitas no outro. Cada vez que uma operao de insero executada, um novo elemento
colocado no final da fila. Na remoo, retornado o elemento que aguarda h mais tempo na fila,
posicionado no comeo. A ordem de sada corresponde ordem de entrada dos elementos na fila, de
modo que os primeiros elementos que entram so sempre os primeiros a sair. Por este motivo, as filas
so denominadas listas FIFO (First-In/First-Out) ou PEPS (Primeiro que Entra/Primeiro que Sai).
Um exemplo bastante comum de filas verifica-se num balco de atendimento, onde pessoas formam
uma fila para aguardar at serem atendidas. Naturalmente, devemos desconsiderar os casos de pessoas
que .furam. a fila ou que desistem de aguardar! Diferentemente das filas no mundo real, o tipo abstrato
de dados no suporta insero nem remoo no meio da lista.
A palavra inglesa queue significa fila. As operaes bsicas que uma fila suporta so:
Enqueue (Inserir): insere um novo elemento no final da fila;
Dequeue (Retirar): remove um elemento do comeo da fila.
Sendo F uma fila e x um elemento qualquer, a operao Enqueue ( F, x ) aumenta o tamanho da fila
F , acrescentando o elemento x no seu final. A operao Dequeue ( F ) faz com que a fila diminua,
removendo e retornando o elemento existente em seu comeo.
Exerccio: Dadas as operaes sobre uma fila F, preencha o quadro a seguir com o estado da fila e o
resultado de cada operao:
Graficamente, representamos uma fila como uma coleo de objetos que cresce da esquerda para a
direita, com dois extremos bem definidos: comeo e final:
11
else begin
writeln(O valor retirado : , V[comeco]);
comeco := comeco + 1;
end;
Para verificar se a fila est cheia, basta testar se a varivel contadora igual ao seu tamanho
mximo. Para verificar se a fila est vazia, basta testar se o comeo igual ao final. Estes testes so
mostrados no exemplo abaixo.
if qtd = MAX
then writeln(Fila cheia!);
if qtd = 0
then writeln(Fila vazia!);
aux := V[j];
i := j - 1;
while ((i > 0) and (V[i] > aux))
do begin
V[i+1] := V[i];
i := i 1;
end;
V[i+1] := aux;
end;
end;
Podemos tambm usar uma outra abordagem para estimar a complexidade do mtodo.
Examinaremos o seu desempenho para os casos de haver nenhuma, uma, duas, trs, etc... chaves fora
do seu local definitivo. Depois, generalizaremos para um certo valor mdio de chaves fora do lugar.
Para cada caso, consideramos dois tipos de comparaes: as necessrias para descobrir a posio
correta das chaves que esto fora do seu lugar, e as necessrias para descobrir que as demais chaves j
esto no seu local correto. Iremos supor que as chaves fora do local esto a uma distncia mdia de n/2
posies, j que a distncia mnima 1 e a mxima n-1.
A quantidade de chaves fora do local poder variar de nenhuma at n-1, j que, mesmo que o vetor
se apresente na ordem inversa quela desejada, podemos considerar que pelo menos uma chave se
encontra na sua posio correta, enquanto as n-1 demais se encontram deslocadas.
A tabela a seguir mostra a quantidade de comparaes necessrias em cada caso. A coluna A indica a
quantidade de chaves fora do seu local. A coluna B mostra o nmero de comparaes efetuadas para
descobrir o local correto de cada chave que est fora do seu lugar. J a coluna C exibe o nmero de
comparaes para descobrir que as demais chaves esto no seu local correto.
A B C
0 0 n-1
1 n/2 n-2
2 2 * n/2 n-3
3 3 * n/2 n-4
... ... ...
k=n-1 k * n/2 n (k-1)
O total de comparaes para um certo 0 <= i <= k , pois, a soma das comparaes indicadas nas
colunas B e C, ou seja: (i * n/2) + (n - (i+1)). De fato, para i = 0 e para i = k, temos, respectivamente,
(n 1) e ((n - n) / 2) comparaes, as quais correspondem ao melhor e ao pior caso do mtodo,
tambm respectivamente, na anlise efetuada anteriormente. Considerando, como antes, que o caso
mdio corresponde mdia aritmtica entre o melhor e o pior caso, temos:
nmero mdio de chaves fora do local = ( 0 + (n - 1)) / 2 = (n - 1) / 2
nmero mdio de comparaes = (n-1)/2 * (n/2) + (n- ((n-1)/2-1)) = (n + n-2)/4 o que
corresponde exatamente ao resultado encontrado na anlise anterior.
log2 k. No nosso caso, esta operao ser repetida para valores de k variando de 1 at n-1. Restam as
operaes de deslocamento que, como j sabemos, so realizadas em quantidades proporcionais ao
quadrado do nmero de chaves. Sugerimos, como exerccio, que demonstre que o nmero mdio de
deslocamentos necessrios para inserir as n-1 chaves (n - n)/4.
Vemos, assim, que a alternativa de usar a pesquisa binria para rapidamente encontrar a posio de
insero no produz grandes ganhos no desempenho do mtodo, pois aps a localizao, ainda
necessrio promover os deslocamentos das chaves direita da posio de insero, que uma
operao de desempenho O(n). Caso, no entanto, possamos dispor de uma arquitetura que execute
deslocamentos em blocos, poderemos obter vantagens no tempo despendido com a operao.
Deixamos ainda a sugesto de implementar o mtodo de classificao por insero com busca
binria, comparando-o com o de busca seqencial.
4.3.2 Implementao
A implementao do mtodo dos incrementos decrescentes uma adaptao da implementao do
mtodo de insero direta. Ao invs de classificar um vetor cujos elementos se encontram contguos
uns aos outros, classifica apenas os elementos do vetor que pertencem a um certo segmento: os
elementos que iniciam numa dada clula f e esto espaados entre si a uma distncia h.
{ --- declarao do vetor, anterior --- }
const MAX = 10;
Var V: Array[1..MAX] of integer;
respeito, observa-se que os valores dos incrementos no podem ser mltiplos uns dos outros. Isto evita
que se combine, em uma iterao, dois segmentos entre os quais no havia nenhuma iterao prvia.
Esta iterao desejvel e deve ser a maior possvel, de acordo com o teorema a seguir:
Se a uma seqncia, previamente ordenada, de distncia k, for em seguida aplicada uma ordenao
de distncia i, ento essa seqncia permanece ordenada de distncia k. - D.Knuth
O autor do teorema mostra, por meio de simulaes, que os melhores resultados obtidos foram com
as seguintes seqncias de incrementos:
2k +1,...,9,5,3,1; k = ...3,2,1
2k - l,...,15,7,3,l; k = ...3,2,1
(2k - (-1) k) / 3,...,11,5,3,1; k = ...,3,2
(3k - 1) / 2, ...,40,13,4,1; k = ...,2,1
Observao: na primeira srie o valor 1 foi inserido. Quanto ao valor inicial do incremento,
recomendado que seja aquele valor que divida o vetor de chaves no maior nmero possvel de
segmentos de mais de um elemento.
20
Os mtodos de classificao por trocas caracterizam-se por efetuarem a classificao por comparao
entre pares de chaves, trocando-as de posio caso estejam fora de ordem no par.
A denominao deste mtodo resulta da associao das chaves com bolhas dentro de um fluido.
Cada bolha teria um dimetro proporcional ao valor de uma chave. Assim, as bolhas maiores subiriam
mai depressa, o que faria com que, aps um certo tempo, elas se arranjassem em ordem de tamanho.
5.1.1 Implementao
A implementao do bubblesort bastante simples. O programa controlado por um comando while,
executado enquanto ocorrerem trocas durante uma varredura, condio esta indicada pela varivel
booleana troca: seu valor ser verdadeiro caso haja trocas durante uma varredura, e falso caso
contrrio. A varivel k indica a posio onde ocorreu a ltima troca. Assim, a varredura seguinte, se
necessria, ser feita somente at k.
A implementao apresentada abaixo para o mesmo vetor V (MAX = 10) no leva em considerao
estes dois pontos citados: (1) parar a iterao se no houve troca e (2) s fazer as comparaes at o
ltimo item trocado. Faa estas alteraes no mtodo como exerccio. Coloque tambm dois
contadores: um para a quantidade de iteraes e outro para a de trocas.
procedure bubbleSort;
Var aux, i, j: integer;
Begin
for i := (MAX 1) downto 1
do begin
for j := 1 to i
do begin
if V[j] > V[j+1]
then begin
aux := V[j];
V[j] := V[j + 1];
V[j + 1] := aux;
end;
end;
end;
end;
O total de comparaes ser, pois, a soma da progresso aritmtica cujo primeiro termo n - 1 e o
ltimo 1, com n - 1 termos, ou seja, ((n-1+1)/2 ) x (n-1) = (n2 - n)/2.
22
Podemos supor que, na prtica, os casos que ocorram correspondam a situaes intermedirias entre
os extremos acima. Assim, se tomarmos a mdia aritmtica dos valores encontrados, provavelmente
estaremos fazendo uma boa estimativa do nmero mdio de comparaes que sero efetuadas.
Cmdio = (Cpior + Cmelhor) /2 = ((n - n)/2)+ (n-1))/2 = (n + n-2)/4
Na verdade, o nmero mdio de comparaes provavelmente ser pouco menor do que isso, pois no
estamos considerando, nesta anlise, os possveis blocos de chaves j ordenadas. De qualquer maneira,
sendo n2 a parcela dominante, o mtodo de complexidade quadrtica: O(n2).
As sucessivas redues dos saltos so anlogas ao ato de pentear cabelos longos e embaraados,
inicialmente apenas com os dedos e depois usando pentes com espaos entre os dentes cada vez
menores. Da a denominao do mtodo dada pelos autores.
Suponhamos, para exemplo, o mesmo vetor de chaves usado para exemplificar o mtodo bubblesort.
Como o vetor possui 5 chaves, o salto inicial igual a 3 (= 5/1.3). Como vemos, a classificao do
vetor, embora tambm tenha consumido 9 iteraes, demandou apenas trs trocas, enquanto o
bubblesort, para ordenar o mesmo vetor, efetuou 8 trocas. justamente neste ponto que reside a
vantagem do combsort em relao ao bubblesort, uma vez que a operao de troca, por envolver vrios
acessos memria, a que vai determinar a velocidade de classificao.
procedure combSort;
Var aux, i, salto: integer;
Begin
salto := MAX;
while (salto >= 1)
do begin
calcSalto(salto);
for i := 1 to (MAX salto)
do begin
if (i + salto) <= MAX)
then begin
if V[i] > V[i + salto]
then begin
aux := V[i];
V[i] := V[i + salto];
V[i + salto] := aux;
end;
end
else break;
end;
end;
end;
O mtodo apresentado a seguir foi proposto por Hoare. Seu desempenho to melhor, que seu inventor
denominou-o quicksort (ordenao rpida). De fato o que apresenta, em mdia, o menor tempo de
classificao. Isto porque, embora tenha um desempenho logartmico como muitos outros, o que
apresenta menor nmero de operaes por iterao. Isto significa que, mesmo que tenha que efetuar
uma quantidade de iteraes proporcional a N(log2 N), cada uma delas ser mais rpida.
Seja o vetor de chaves C[1..n] a ser ordenado. Numa etapa inicial, esse vetor particionado em trs
segmentos S1, S2, S3, da seguinte forma:
S2 ter comprimento 1 e conter uma chave denominada particionadora;
S1 ter comprimento >=0 e conter todas as chaves cujos valores forem menores ou iguais ao da
chave particionadora. Esse segmento posicionado esquerda de S2;
S3 tambm ter comprimento >=0 e conter todas as chaves cujos valores forem maiores do que
o da particionadora. Esse segmento posicionado direita de S2.
Esquematicamente esse particionamento mostrado nas figuras a seguir. vetor inicial: 1
Suponhamos o vetor: 9 25 10 18 5 7 15 3
Escolhemos a chave 9 como particionadora e a guardamos em uma varivel temporria cp. A
posio ocupada por ela se torna disponvel para ser ocupada por outra chave, situao indicada por x.
Marcamos tambm o incio e o fim do vetor por dois apontadores: i (de incio) e f (de fim). A
expresso esquerda escrita ao lado do vetor indica que a posio apontada pelo ponteiro i (o da
esquerda) est disponvel e pode ser ocupada por outra chave. Nos passos seguintes, a expresso
direita indica o contrrio.
i f
1. x 25 10 18 5 7 15 3 esquerda cp=9
A seguir, comparamos a chave que est apontada por f com a particionadora. Como aquela menor
do que esta, a deslocamos para o lado esquerdo do vetor (que o lado onde ficam as chaves menores
ou iguais particionadora), ao mesmo tempo em que avanamos o ponteiro i para indicar que a chave
recm-movida j se encontra no segmento correto. A nova posio vaga passa a ser a apontada por f:
i f
2. 3 25 10 18 5 7 15 x direita cp=9
Agora comparamos a chave 25 (pois a nova posio vaga a da direita) com a particionadora. Como
25 > 9, deslocamos 25 para x, e recuamos o ponteiro uma posio para a esquerda, indicando assim
que a chave 25 j se encontra no seu segmento correto. Agora a posio vaga a da esquerda:
i f
3. 3 x 10 18 5 7 15 25 esquerda cp=9
O processo prossegue comparando a chave 15. Neste caso, por ela ser maior do que a particionadora,
no deve ser trocada de posio, pois j se encontra no segmento correto. Apenas o ponteiro f
deslocado para a esquerda. A posio vaga permanece sendo a da esquerda:
i f
4. 3 x 10 18 5 7 15 25 esquerda cp=9
5. 3 7 10 18 5 x 15 25 direita cp=9
6. 3 7 x 18 5 10 15 25 esquerda cp=9
i f
26
7. 3 7 5 18 x 10 15 25 direita cp=9
8. 3 7 5 x=9 18 10 15 25 cp=9
5.4.2 Implementao
A implementao do mtodo exigir, portanto, a definio de dois procedimentos: um para executar
uma ordenao (a que mostramos no exemplo) e outro para controlar os particionamentos. O primeiro
procedimento, denominado ordenao a seguir apresentado.
end
else inicio := inicio + 1;
end;
end;
V[inicio] := cp; // retorna a varivel inicio
end;
Persistindo a situao ideal ao longo dos demais particionamentos, os segmentos que sero
sucessivamente gerados tero os seguintes comprimentos:
E assim por diante, at que (n-k)/(k+1) <=1. Cada um desses segmentos vai requerer, para ser
particionado, tantas comparaes quantos so seus elementos menos uma unidade.
Assim, temos:
1. Para o primeiro segmento: n-1 comparaes;
2. Para os 2 seguintes: (((n-1)/2)-1) x 2 = n-3 comparaes;
3. Para os 4 seguintes: (((n-3)/4)-1) x 4 = n-7 comparaes;
4. Para os 8 seguintes: (((n-7)/8)-1) x 8 = n-15 comparaes;
e assim sucessivamente, enquanto restarem segmentos de mais de um elemento. Esta condio deixar
de ocorrer aps executados log2 n passos de particionamento. Logo, o total das comparaes a serem
efetuadas igual soma do nmero de comparaes efetuadas em cada passo, ou seja:
Este seria, pois, o melhor caso para o mtodo. Naturalmente, no devemos esperar que isto ocorra
em situaes normais. Supondo que as chaves sejam nicas, a probabilidade de escolhermos ao acaso
exatamente a particionadora mediana 1/n. No entanto, segundo Wirth, quando escolhemos a chave
particionadora aleatoriamente, o desempenho mdio do quicksort se torna inferior ao do caso timo de
um fator da ordem de apenas 2 ln 2.
29
H, ainda, a considerar o pior caso: quando a chave particionadora escolhida a menor (ou a maior)
de todas. Nesse caso, e persistindo a situao em todos os demais particionamentos, o mtodo se torna
extremamente lento, com desempenho quadrtico. De fato, se a chave escolhida for sempre a menor de
todas, os particionamentos produziro os segmentos S1 vazios e os S3 com n-1 elementos, conforme o
esquema mostrado adiante. Sendo necessrios, pois, n-1 passos, cada um deles exigindo um nmero de
comparaes igual ao nmero de elementos menos um, ou seja:
(n-1) + (n-2) + (n-3) + ... + 1 = ((n-1) + 1)/2 x (n-1) = (n2 - n)/2
Esse caso ocorrer, por exemplo, se o vetor j estiver ordenado e sempre escolhermos a primeira
chave como particionadora. Assim, quando o vetor estiver parcialmente ordenado, conveniente usar
uma chave que ocupe uma posio mais central do mesmo.
30
Os mtodos que formam a famlia de classificao por seleo caracterizam-se por procurarem, a cada
iterao, a chave de menor (ou maior) valor do vetor e coloc-la na sua posio definitiva correta, qual
seja, no incio (ou no final) do vetor, por permutao com a que ocupa aquela posio. O vetor a ser
classificado fica, desta maneira, reduzido de um elemento. O mesmo processo repetido para a parte
restante do vetor, at que este fique reduzido a um nico elemento, quando ento a classificao estar
concluda. Os mtodos de classificao por seleo diferenciam-se uns dos outros, pelo critrio
utilizado para selecionar, a cada iterao, a chave de menor (ou maior) valor.
Observe que a ltima iterao (7) encerra a classificao, pois se as n-1 menores chaves do vetor
esto em suas posies definitivas corretas, ento a maior (e ltima) automaticamente tambm estar.
6.1.1 Implementao
A implementao do mtodo direta. O procedimento a seguir toma a primeira chave da parte ainda
no ordenada do vetor como sendo, em princpio, a menor de todas, comparando-a seqencialmente
com todas as demais. Sempre que encontrar uma de valor inferior, passa a considerar esta como a
menor. Ao final de cada iterao, a menor chave encontrada inserida na primeira posio da parte no
classificada, que assim fica reduzida de um elemento.
procedure SelecaoDireta;
Var i, j, indX, menor: integer;
Begin
for i := 1 to (MAX -1)
do begin
menor := V[i];
indX := i;
for j := (i + 1) to MAX
do begin
if (V[j]) < menor)
then begin
menor := V[j];
31
indX := j;
end;
end;
V[indX] := V[i];
V[i] := menor;
end;
end;
A concluso a que chegamos sobre o mtodo da seleo direta torna o seu uso muito restrito, pois
infelizmente sempre sero necessrias n-1 comparaes para decidir qual a menor dentre n chaves.
Por outro lado, essa condio s absolutamente necessria na primeira varredura. Ao final desta,
32
podemos reter outras informaes alm da identificao da menor chave, que permitam acelerar a
busca das outras menores que sucedem a primeira.
Por exemplo, se dividirmos o vetor de chaves em dois segmentos de igual tamanho, poderemos, na
primeira varredura, identificar a menor chave de cada segmento. A seguir, selecionaramos a menor
dentre estas duas, colocando-a na sua posio definitiva correta. Na segunda varredura ser necessrio
operar apenas com o segmento que forneceu a menor chave, ou seja, trabalharemos com n/2 chaves ao
invs de n-1, o que j representa um ganho significativo. Uma vez encontrada a segunda menor chave
deste segmento, a comparamos com a do outro para selecionar a menor chave desta segunda iterao.
Este princpio pode ser estendido de modo a subdividir o vetor de chaves em tantos segmentos
quantos forem possveis, de tal forma a minimizar a quantidade de comparaes a ser efetuada em
cada iterao para encontrar a chave de menor valor. O quadro a seguir exemplifica a aplicao deste
princpio, mostrando um vetor de chaves sucessivamente dividido ao meio, sendo a menor chave de
cada segmento identificada em negrito.
A identificao da menor chave em cada um dos sucessivos segmentos nos quais o vetor foi
subdividido pode ser representada pela seguinte estrutura de rvore:
Agora podemos selecionar a menor de todas as chaves e remov-la do sistema, deixando vagas as
posies que ela ocupava. Essas vagas sero tomadas, por sua vez, pelas chaves sucessoras (na ordem
crescente), de cada subrvore, permanecendo livre apenas a ltima posio anteriormente ocupada pela
chave removida. Na primeira figura vemos a rvore com a chave 09 removida e, na seguinte vemos as
vagas por ela deixadas ocupadas pelas chaves sucessoras.
33
Promoo da sucessora
Desse modo, a segunda menor chave aparecer na raiz da rvore e poder ser removida, seguindo-se
a promoo da terceira menor chave, mostrada na prxima figura. Esse processo se repete at que
todas as n chaves tenham sido removidas.
Observe que, a cada nvel da rvore que se desce, o problema fica reduzido pela metade, pois apenas
uma subrvore afetada pela remoo da chave. Com isso, a cada iterao, evitamos trabalhar com a
totalidade das chaves restantes, o que ir acelerar bastante a seleo da menor chave em cada iterao.
Este , pois, o princpio bsico do mtodo de seleo em rvore.
6.2.1 Exemplo
A implementao do mtodo, proposta por J. Williams, permite simplificar ainda mais a estrutura de
rvore que mantm a hierarquia das chaves. A simplificao elimina a redundncia de ocorrncias de
chaves na rvore, fazendo com que ela possua exatamente n nodos, ao invs dos 2n-1 at agora usados,
o que simplifica a manipulao da estrutura. Dado ento um vetor de chaves C1, C2, ... , Cn,
consideramos este vetor como sendo a representao de uma rvore binria, usando a seguinte
interpretao dos ndices das chaves:
C1 raiz da rvore e C2i = subrvore da esquerda de Ci, C2i+1 = subrvore da direita de Ci para
i=1, n div 2.
Exemplificando: dado o vetor C1..7, e utilizando a interpretao anterior, podemos v-lo como
sendo a representao da rvore mostrada na figura:
34
O passo seguinte consiste em trocar as chaves de posio dentro do vetor, de tal forma que estas
passem a formar uma hierarquia, na qual todas as razes das subrvores sejam maiores ou iguais a
qualquer uma das suas sucessoras, ou seja, cada raiz deve satisfazer as seguintes condies:
Ci >= C2i e Ci >= C2i+1 para i= 1, n div 2
Quando todas as razes das subrvores satisfazem essas condies, dizemos que a rvore forma um
heap. Da a denominao do mtodo.
Essas condies, na verdade, estabelecem uma relao de ordem entre as subrvores, de tal modo
que, para qualquer heap, podemos afirmar que C1 >= Ci, i=2, n, ou seja, podemos afirmar que a maior
chave dentre as n que formam o vetor aquela que est na raiz da rvore, isto , a chave C1.
O problema agora , ento, efetuar as trocas de posies das chaves no vetor, de tal forma que a
rvore representada passe a ser um heap. Esse processo pode ser feito testando cada uma das
subrvores para verificar se elas satisfazem, separadamente, a condio de heap. Apenas as subrvores
que possuem pelo menos um sucessor devem ser testadas. A maneira mais simples de sistematizar esse
teste inici-lo pela ltima subrvore, qual seja, aquela cuja raiz est na posio n div 2 do vetor de
chaves, prosseguindo, a partir da, para as subrvores que antecedem esta, at testar a raiz da rvore.
Desse modo, a primeira subrvore da figura a ser testada aquela cuja raiz C3, seguindo-se o teste
pela subrvore de raiz C2 e finalizando com a de raiz C1. Sempre que for encontrada uma subrvore
que no forme um heap, seus componentes devem ser rearranjados de modo a formar o heap. Por
exemplo, se for encontrada uma subrvore como a da figura (a), seus componentes devem ser
rearranjados na forma da figura (b).
Pode ocorrer tambm que, ao rearranjarmos uma subrvore, isto venha a afetar outra, do nvel
imediatamente inferior, fazendo com que esta deixe de formar um heap. Essa possibilidade obriga a
verificar, sempre que for rearranjada uma subrvore, se a sucessora do nvel abaixo no teve a sua
condio de heap desfeita. Para exemplificar todo o processo, suponhamos o seguinte vetor de chaves:
1 2 3 4 5 6 7
12 09 13 25 18 10 22
cuja interpretao sob a forma de rvore :
35
A transformao dessa rvore em heap inicia pela subrvore cuja raiz 13, j que seu ndice, no
vetor, 7 div 2 = 3. O rearranjo dos componentes desta subrvore produz a configurao da rvore e
do vetor correspondente mostrada na figura:
Nesse caso ocorreu a possibilidade mencionada, na qual a transformao de uma subrvore afeta
outra de um nvel abaixo. A subrvore afetada deve ser rearranjada, conforme mostra a figura abaixo.
Uma vez que todas as subrvores formam heaps, a rvore toda tambm um heap. At aqui se
conseguiu apenas selecionar a maior chave que aparece na raiz da rvore. Entretanto, a partir deste
ponto, em vista da configurao tomada pela rvore, a seleo das chaves seguintes ser simplificada.
Se a chave que est na raiz a maior de todas, ento sua posio definitiva correta na ordem
crescente na ltima posio do vetor, onde ela colocada, por troca com a chave que ocupa aquela
posio. Com a maior chave ocupando a sua posio definitiva podemos, a partir de agora, considerar
o vetor como tendo um elemento a menos, o qual ser mostrado no vetor (rea sombreada), mas no
aparecer na rvore correspondente, conforme ilustrado na figura a seguir.
Para selecionar a prxima chave, deve-se fazer com que a rvore volte a ser um heap. Como a nica
subrvore que sofreu alteraes foi a da raiz, basta fazer com que esta subrvore volte a formar um
heap, rearranjando-se os seus componentes, e verificando-se, tambm, se este rearranjo no interferiu
em uma das subrvores do nvel imediatamente abaixo.
Novamente a maior chave dentre as restantes aparece na raiz (perceba a pequena quantidade de
comparaes necessrias para efetuar a seleo desta chave). A figura abaixo mostra a segunda maior
chave sendo colocada em sua posio definitiva correta, de forma idntica ao do caso anterior.
Na figura seguir a rvore novamente rearranjada para formar um heap, o que permitir selecionar a
prxima chave.
Os demais passos seguem o mesmo princpio e so mostrados na figura a seguir. Da mesma forma
que no caso da seleo direta, a classificao estar encerrada aps a iterao n-1, pois teremos
selecionado as n-1 maiores chaves, restando apenas a menor de todas, que estar na posio inicial do
vetor, o que encerra a classificao.
38
6.2.2 Implementao
O algoritmo que implementa esse mtodo segue exatamente a descrio anterior. Para simplificar a
manipulao do vetor, foi criada uma funo auxiliar keyval, que retorna o valor da chave que est na
posio i do vetor, caso i <= n. Em caso contrrio, o valor da funo ser igual a minkey, que
corresponde ao menor valor de chave possvel de ser representado. Por exemplo, se as chaves forem do
tipo inteiro de 16 bits, ento minkey ser igual a -215 = -32768. O uso dessa funo nos permitir no
fazer distino entre nodos razes e folhas, assegurando o trmino natural do processamento.
/* baseado em algoritmo de [Aze 96], Pg 59 */
function keyval(n, i: integer): integer;
begin
if (i > n)
then return := MINKEY
else return := V[i];
end;
Com auxlio de keyval, escrevemos o procedimento heap, cuja finalidade transformar em heap a
subrvore cuja raiz apontada por r. Para cada subrvore transformada, verificado se a subrvore do
nvel inferior que esteve envolvida na transformao no foi afetada. Em caso afirmativo, a verificao
tambm se estende a ela. O processo se encerra quando no houver mais trocas, ou quando for atingido
o nvel das folhas da rvore.
/* baseado em algoritmo de [Aze 96], Pg 60 */
procedure heap(n, r: integer);
var i, h, r, aux: integer;
troca: boolean;
begin
i := r;
troca := true;
while troca
do begin
// procura maior subrvore
if (keyVal(n, 2*i) > keyVal(n, 2*i+1))
then h := 2 * i // subrvore da esquerda
else h := 2 * i + 1; // subrvore da direita
if (V[i] < keyVal(n, h)) // compara raiz com o maior sucessor
then begin
aux := V[i]; // troca
V[i] := V[h];
V[h] := aux;
i := h; // desce para a raiz da subrvore afetada pela troca
end
else troca := false;
end;
end;
Resta agora o procedimento final, que denominaremos heapsort. Este ser constitudo de duas partes.
A primeira far a formao do heap inicial: preparar a rvore para a seleo da maior chave. A
segunda parte, que denominaremos manuteno do heap, rearranjar a rvore aps cada seleo da
maior chave, para que ela retome a forma de heap, permitindo assim a seleo da prxima chave. Essa
segunda parte demanda n-1 iteraes, e corresponde fase de classificao propriamente dita.
A seqncia de quadros das figuras ilustram esse processo, partindo do heap j formado. O valor
minkey est representado pelo smbolo (*). Os arcos representam as amarras. A seta apontando para
cima sobre a raiz indica que esta ser removida. Abaixo de cada rvore mostrado o vetor de chaves
(acima) e o vetor auxiliar (abaixo).
A anlise da fase de manuteno neste caso mais simples, uma vez que todas as chaves, com
exceo daquela que j se encontra na raiz, devero subir desde o nvel onde esto at o nvel 1. Dessa
forma, teremos 2 chaves subindo 1 nvel, 4 chaves subindo 2, 8 subindo 3 e assim por diante. Esse
valor corresponde quantidade total de nveis que as chaves subiram.
Obviamente esse total superior s trocas efetuadas pelo mtodo heapsort em sua fase de
manuteno, as quais correspondem a aproximadamente 3/4 do valor que obtivemos. Entretanto, no
caso do mtodo threadedheapsort, no se trata de operaes de trocas, mas simples transposies
(atribuies) de chaves de um vetor para outro. Devemos ento, para comparar as duas alternativas,
computar as atribuies efetuadas em cada caso. Cada troca do heapsort corresponde a trs atribuies,
enquanto cada operao de subida de nvel do threadedheapsort corresponde a somente uma atribuio.
Assim, o total de atribuies do heapsort expresso por (9/4)n * log2(n/4)+3n enquanto as atribuies
do threadedheapsort correspondem a n * log2 (n/4)+n, o que claramente corresponde a uma vantagem
para esta alternativa.
Alm disso, a quantidade de comparaes efetuadas tambm menor, pois enquanto o heapsort
efetua duas comparaes para cada troca, o threadedheapsort efetua apenas uma para cada promoo
de nvel. O quadro a seguir mostra as quantidades de operaes efetuadas em cada um dos dois
mtodos na fase de manuteno do heap. A desvantagem do uso dessa alternativa a necessidade de
uma rea extra para o vetor ordenado.
Este mtodo antecede aos computadores digitais. uma adaptao do processo de classificao
mecnica de cartes perfurados, usados desde a primeira metade do sculo XX como dispositivo para
armazenamento de dados. O modelo mais comum representava at 80 caracteres, um por coluna,
formando, cada carto, um registro de arquivo. Caracteres contguos podiam representar um dado
(campo). Desta forma, o carto era formado de um ou mais campos de dados, conforme a aplicao.
Antes do advento do computador, o carto perfurado era usado em processos mecanizados, que
envolviam basicamente operaes de tabulao, contagem e totalizao de dados. Com a chegada do
computador digital, o carto passou a ser usado tambm como meio de entrada de dados. Em ambos os
casos, freqentemente surgia a necessidade de ordenar fisicamente os cartes segundo os valores de
um determinado campo de dados, para que pudessem ser processados naquela ordem.
A operao de classificao dos cartes era feita por um equipamento que lia a informao
perfurada, em uma dada coluna de cada vez, e remetia o carto para um escaninho correspondente ao
valor lido. Dessa forma, todos os cartes que possuam o dgito 0 na coluna selecionada eram
empilhados no escaninho 0. Os que possuam o dgito 1, no escaninho 1, e assim por diante. Para obter
os cartes ordenados pelo dgito da coluna selecionada, bastava recolh-los dos escaninhos na ordem
crescente, ou seja, primeiro os do escaninho 0, depois os do escaninho 1 e assim at o 9.
Quando o dado a ser classificado ocupava um campo com mais de uma coluna (possua mais de um
dgito), era necessrio efetuar tantos passos de ordenaes quantos fossem os dgitos, iniciando pelo de
menor ordem (das unidades) e prosseguindo at o de mais alta ordem. Aps classificar cada coluna, as
pilhas formadas em cada escaninho eram colocadas umas sobre as outras, na ordem crescente, sendo
repetido o processo para as colunas seguintes, sucessivamente, at a ltima.
Por exemplo, suponhamos que um arquivo formado por 20 cartes tivesse, em um certo campo, os
seguintes valores perfurados:
523, 018, 125, 937, 628, 431, 243, 705, 891, 362, 429, 005, 540, 353, 115, 427, 910, 580, 174, 456
A classificao pelo dgito da unidade produziria a seguinte distribuio pelos escaninhos:
115
580 353 005
910 891 243 705 427 628
540 431 362 523 174 125 456 937 018 429
0 1 2 3 4 5 6 7 8 9
Recolhendo-os em ordem:
705, 005, 910, 115, 018, 523, 125, 427, 628, 429, 431, 937, 540, 243, 353, 456, 362, 174, 580, 891
44
005, 018, 115, 125, 174, 243, 353, 362, 427, 429, 431, 456, 523,540,580,628, 705, 891,910,937.
O mesmo processo podia ser aplicado em campos com dados alfanumricos, exigindo no entanto
que cada passo fosse desdobrado em duas etapas, j que os caracteres alfabticos eram representados
por meio de duplas perfuraes em cada coluna do carto.
A utilizao deste princpio para classificar dados usando um computador parece desvantajosa, pois o
processo requer tantos passos de classificao quantos so os dgitos dos dados. Entretanto, como em
cada passo somente um dgito do dado considerado, podemos utiliz-lo como um ndice que vai
remeter o dado todo para o escaninho correspondente. Ou seja, no ser necessrio efetuar
comparaes entre os dados para estabelecer a sua ordem relativa. Resta-nos, assim, construir um
mecanismo que simule de maneira eficiente os escaninhos da classificadora de cartes, considerando
que no sabemos, a priori, qual a capacidade de empilhamento que deve ter cada um deles.
Para isto, temos duas alternativas. A primeira construir uma lista encadeada onde os dados so
inseridos medida que so distribudos segundo os valores de seus dgitos. Para simplificar a
administrao da lista como um todo, pode-se manter uma lista (pilha) separada para representar cada
escaninho, juntamente com um vetor de cabeas que apontam para o topo de cada uma. O valor inicial
dos ponteiros null, indicando escaninhos vazios. Para que possamos manter a informao que indica
a base de cada pilha, podemos organizar as listas sob forma circular, de modo que o nodo do topo
aponte para o nodo da base. A manuteno dessa informao necessria para que possamos, ao final
de cada passo, unir as listas formadas, e evita, ao mesmo tempo, a necessidade de manter outro vetor
de ponteiros que apontem para as bases das pilhas.
medida que os dados vo sendo distribudos, os ponteiros correspondentes vo sendo atualizados,
indicando o topo de cada pilha. Aps a distribuio do ltimo dado, as pilhas devem ser concatenadas,
formando a lista ordenada pelo dgito em questo. Deve haver um apontador cabea para indicar o
incio da lista. Dessa forma, a distribuio das chaves pelos escaninhos pode ser feita utilizando-se o
dgito selecionado para enderear o vetor de ponteiros, inserindo-se a chave na posio apontada pelo
ponteiro correspondente, o que corresponde a distribuir a chave no topo do seu escaninho.
Visando minimizar as operaes de alocao e desalocao dinmica de nodos da lista, os mesmos
podem ser reutilizados de um passo para outro, bastando efetuar o ajuste adequado dos ponteiros
envolvidos, o que evita o uso de uma rea extra de memria. Para tornar os passos homogneos,
conveniente dispor inicialmente as chaves sob a forma de lista encadeada.
A segunda alternativa para simular os escaninhos da mquina classificadora calcular previamente
a quantidade de dados que conter cada um deles. Com isto, podemos utilizar uma nica estrutura de
vetor para representar os escaninhos, j que a posio inicial de cada um deles pode ser calculada. Os
ndices que apontam estas posies iniciais so obtidos calculando-se as freqncias acumuladas dos
dgitos, e apontam para a primeira posio livre (topo) de cada escaninho. medida que as chaves vo
sendo distribudas pelos escaninhos, os ndices vo sendo incrementados, passando a indicar as
prximas posies livres.
Para executar o primeiro passo do exemplo, obtemos as seguintes freqncias de ocorrncia de cada
dgito, as quais nos permitem calcular as freqncias acumuladas, que por sua vez indicam a posio
inicial de cada escaninho:
45
Dgito 0 1 2 3 4 5 6 7 8 9
Freqncia 3 2 1 3 1 4 1 2 2 1
Freqncia acumulada 0 3 5 6 9 10 14 15 17 19
Freqncias acumuladas por escaninho
Esta segunda alternativa possui a vantagem de dispensar o uso de ponteiros (usa ndices). Por outro
lado, exige uma varredura prvia das chaves para contabilizar as freqncias de ocorrncia dos dgitos.
Qualquer que seja a alternativa adotada, o mtodo tem a vantagem de adaptar a mquina
classificadora para as chaves que vo ser ordenadas, caso estas possuam alguma caracterstica especial
que possa ser explorada para obter alguma vantagem no processo como um todo.
Considere, por exemplo, a classificao de dados que representam os anos de nascimento de um
grupo de pessoas. Se considerarmos as chaves como um nmero comum de 4 dgitos cada, teremos de
fazer a classificao em 4 passos. Entretanto, podemos observar que apenas no 1 e 2 passo podem
ocorrer todos os dgitos de 0 a 9. No 3 passo os dgitos possveis so 8, 9 e 0, enquanto que no 4
apenas o 1 e o 2 so possveis. De posse desta informao, podemos dividir as chaves de forma
diferente. Agrupando os dgitos da centena e do milhar, efetuaremos a classificao em trs passos ao
invs de quatro, sendo o 1 e 2 normalmente processados, enquanto o 3 considerar os agrupamentos
18, 19 e 20. Isto possvel, j que podemos construir a mquina classificadora com a quantidade de
escaninhos que quisermos, cada um deles com o endereo que tambm quisermos. Nesse caso, para o
terceiro passo teramos apenas trs escaninhos para receber as chaves que tiverem os valores 18, 19 e
20 na parte considerada. Certamente isto contribui para melhorar a eficincia do mtodo.
Naturalmente, esse mtodo no recomendado para chaves muito longas, como por exemplo nomes
de pessoas, pois alm de exigir muitos passos para decidir a ordem, cada parte da chave possui muitas
alternativas (as letras do alfabeto, com acentos, maisculas e minsculas). Nesses casos, recomenda-se
o uso de mtodos baseados em comparaes entre as chaves, pois muitas vezes apenas a 1 letra do
prenome suficiente para decidir a ordem relativa de dois nomes.
7.1 Implementao
O procedimento distribuio, apresentado adiante, implementa a alternativa que usa listas
encadeadas para representar os escaninhos, e efetua apenas um dos passos do processo de
classificao, fazendo a distribuio da chaves segundo a isima parte de cada uma delas. Supe-se
que os dados j se encontram sob a forma de uma lista encadeada, cujo primeiro elemento apontado
pela varivel incio. Cada nodo da lista um registro de dois componentes: a chave e o ponteiro para o
nodo sucessor. Esses componentes so acessados pelas funes key (p) e next (p), respectivamente. O
vetor top, de b elementos, indexados de 0 a b-1, contm os ponteiros para os topos das pilhas que
representam os escaninhos. A funo parte (i, k) extrai a parte i da chave k. A operao setnext (p, q)
atribui o valor q ao componente next do nodo apontado por p. A execuo do procedimento redistribui
os nodos da lista de modo que estes se apresentem ordenados segundo a parte i das chaves. A varivel
incio aponta para o incio da nova lista. As figuras seguintes mostram a esquematizao da insero de
um nodo em um escaninho vazio e em um no vazio.
A implementao da alternativa que utiliza um vetor no lugar da lista encadeada mais simples, pois
no lida com ponteiros. Entretanto ser necessrio utilizar um vetor de chaves auxiliar, uma vez que
47
no podemos deslocar as posies dos seus elementos como fizemos no caso da lista encadeada. A
implementao, que deixamos a cargo do aluno, est esquematizada a seguir:
procedure distribuicao;
begin
inicializao;
clculo das freqncias;
clculo das freqncias acumuladas;
distribuio das chaves;
end;
Da mesma forma que na alternativa anterior, esse procedimento dever ser repetido para cada parte
na qual as chaves foram divididas, atentando para o fato de que o resultado de um passo de
classificao sempre aparecer no vetor auxiliar.
7.2 Desempenho
Este mtodo apresenta a caracterstica de possuir um desempenho determinstico, ou seja, sempre ser
executada a mesma quantidade de operaes para uma dada quantidade n de chaves,
independentemente da ordem com que estas se apresentem no incio. Como cada passo demanda
exatamente n operaes de distribuio, ento a quantidade total de operaes ser n * np, sendo np o
nmero de partes pelas quais as chaves esto divididas. Trata-se, pois, de um mtodo de desempenho
linear, ou seja, O (n).
Infelizmente, como j mencionado anteriormente, o mtodo no recomendado para chaves longas
(np grande). Outra deficincia do mtodo que ele no pode ser aplicado, na forma como foi
apresentado, em chaves numricas que possuam valores negativos e positivos misturados. Deixamos
para o leitor a discusso deste caso e a proposta de solues.
48
Uma alternativa iniciar com segmentos de comprimento 1, os quais j esto obviamente ordenados.
Dessa forma, consideramos, ento, o vetor de n elementos como sendo formado de n segmentos de 1
elemento cada, e aplicamos o processo reiterado de intercalao a partir da, sem a necessidade de
outro mtodo de classificao para iniciar o processo. A tabela abaixo mostra um exemplo deste
princpio sendo aplicado a um vetor de 8 chaves. Cada linha, a partir da segunda, corresponde ao
resultado da intercalao de todos os pares de segmentos consecutivos da linha anterior.
23 17 8 15 9 12 19 7
17 23 8 15 9 12 7 19
8 15 17 23 7 9 12 19
7 8 9 12 15 17 19 23
Quando o tamanho do vetor no for uma potncia inteira de 2, sempre ocorrer, em pelo menos uma
iterao, um segmento sem um par para ser mesclado. Neste caso, esse segmento, que sempre ser o
ltimo do vetor, simplesmente transcrito para a iterao subseqente, como no caso abaixo.
23 17 8 15 9 12 19 7 14 10
17 23 8 15 9 12 7 19 10 14
8 15 17 23 7 9 12 19 10 14
7 8 9 12 15 17 19 23 10 14
7 8 9 10 12 14 15 17 19 23
8.1 Implementao
A implementao deste mtodo ser feita em trs nveis. No primeiro, denominado simplemerge,
definida a operao de intercalao de um par de segmentos ordenados, resultando um terceiro
segmento tambm ordenado. O segundo nvel, mergepass, far a intercalao de todos os pares de
49
segmentos ordenados nos quais o vetor est dividido. O terceiro e ltimo nvel, mergesort, efetuar
toda a seqncia de intercalaes necessria para que resulte apenas um segmento intercalado.
Esquematicamente, a implementao da operao simplemerge est representada na figura abaixo.
Os dois segmentos a serem intercalados ocupam posies contguas no vetor C (VA na implementao)
. O primeiro segmento inicia na posio isl e termina na fsl. O segundo ocupa as posies de is2 at
fs2. O resultado da intercalao aparece no vetor C (VB na implementao) a partir da posio r (=
is1).
A operao mergepass, por sua vez, efetuar a intercalao de todos os pares consecutivos de
segmentos de comprimento L do vetor C.
then begin
for i = ib2 to fa2
do begin
VB[k] := VA[i];
k := k + 1;
end;
end
else begin
for i = ib1 to fa1
do begin
VB[k] := VA[i];
k := k + 1;
end;
end;
end;
Observe que o parmetro fa2 de chamada da procedure simpleMerge que indica a posio final do 2
segmento prev a possibilidade de que o ltimo deles tenha um comprimento menor do que L.
O nvel final da implementao dever efetuar todas as seqncias de intercalaes, desde as dos
segmentos de comprimento 1, at obter um segmento de comprimento n, dobrando o comprimento dos
segmentos aps cada execuo da operao mergepass. Devido ao fato de a intercalao sempre ser
feita em um vetor auxiliar, a operao mergepass dever alternar sucessivamente os vetores origem e
destino. Assim, a primeira intercalao ser feita do vetor VA para o VB. A segunda de VB para VA e
assim at o final. Ao concluir a ltima seqncia de intercalaes, deve ser verificado em qual dos dois
vetores est o resultado final. Isto pode ser feito examinando-se a quantidade de iteraes executadas.
Se estiver em VB, este copiado para VA, uma vez que VB no visvel o usurio.
/* pseudo-cdigo baseado em algoritmo de [Aze 96], Pg 89 */
procedure mergeSort(VA: vetor; n: integer);
// L: comprimento dos segmentos
var ib1, ib2: integer; // ndices de incio dos segmentos no vetor VB
L, i, n, nInt: integer; // nInt: nmero de intercalaes
begin
L := 1; // comprimento do primeiro segmento
nInt := 1;
while (L < n)
do begin
if odd(nInt) // nInt mpar
then mergePass(VA, VB, n, L); // de VA --> VB
51
O nmero esperado de comparaes necessrias para intercalar dois segmentos de n elementos cada
dado, ento, pela relao entre os dois totais da tabela anterior. Para expressar essa relao sob a
forma de uma funo recorreremos definio de combinaes de r elementos tomados k de cada
vez, e a trs propriedades das combinaes.
53
9 LISTAS ORDENADAS
Este captulo apresenta uma estrutura de dados auto-ajustvel, no sentido de sempre se manter
ordenada aps cada insero ou remoo. Devido a este comportamento altamente dinmico, esta
estrutura perfeita para mostrar como a alocao dinmica encadeada pode ser utilizada na
implementao de um tipo de dados abstrato. Uma lista ordenada L:[a 1, a 2, ..., a n ] uma lista linear
tal que, sendo n > 1, temos:
a 1 <= a k para qualquer 1 < k <= n;
a k <= a n para qualquer 1 <= k < n;
a k-1 <= a k <= a k+1 , para qualquer 1 < k < n.
Se L uma lista ordenada, podemos garantir que nenhum elemento em L inferior a a1 ou superior
a an. Alm disso, tomando um elemento qualquer no meio da lista, nenhum elemento sua esquerda o
supera e nenhum elemento sua direita inferior a ele.
Entre as diversas operaes que podem ser realizadas com uma lista ordenada, podemos destacar:
Ins (Inserir): insere um novo elemento na lista ordenada;
Rem (Remover): remove um elemento da lista ordenada;
Find (Encontrar): procura um elemento na lista ordenada.
Sendo L uma lista ordenada e x um elemento qualquer, a operao Ins ( L, x ) aumenta o tamanho
da lista L , acrescentando o elemento x na sua respectiva posio. A operao Rem ( L ) faz com que
a lista diminua, se o elemento estiver na mesma, removendo o elemento e retornando verdadeiro. A
operao Find ( L, x ) retornar a posio do elemento x na lista.
Exerccio: Dadas as operaes sobre uma lista ordenada L, preencha o quadro a seguir com estado
da lista e o resultado de cada operao:
Como nenhum espao de memria ser alocado at que um elemento seja inserido na lista ordenada,
para defini-la, precisamos apenas especificar o formato dos nodos e o tipo de ponteiro que ser usado
para criar a varivel L, que aponta o primeiro dos nodos encadeados:
Type
elem: char; // define tipo elem baseado em char
LstOrd = ^nodo; // define tipo LstOrd: ponteiro para record nodo
Nodo = record // define estrutura nodo
Obj: elem;
Prox: LstOrd;
End;
No nosso exemplo:
Type
Pessoa = Record
Nome : String[30];
Sexo : Char;
Idade : Integer;
Altura: Real;
End;
ponteiro = ^Pessoas; // este o ponteiro para o nodo
Pessoas = Record // este o nosso nodo
Valor : Pessoa;
Prox : Ponteiro;
End;
Var p, prim : Ponteiro;
Obviamente, uma lista vazia representada por um ponteiro cujo valor nulo, nil. A inicializao e
o teste de lista vazia so triviais nesta implementao, uma vez definido que um ponteiro com valor
nulo representar uma lista vazia. Para criar uma lista ordenada vazia, vamos usar a procedure Criar e
para verificar se uma lista est vazia, usaremos a function Anular:
procedure Criar;
begin
LstOrd := nil;
end;
function Anular: boolean;
begin
if (LstOrd := nil
then result := true
else result := false;
end;
X existe na lista. Em seguida, o endereo do nodo apontado por N deve ser copiado para a varivel L,
tal que ambos os ponteiros apontem o mesmo nodo.
If (L = nil)
Then begin
New(N);
N^.Obj := X;
N^.Prox := nil;
L := N;
end;
If (X <= L^.Obj)
Then begin
New(N);
N^.Obj := X;
N^.Prox := L;
L := N;
end;
Analisando os casos descritos acima, observamos uma grande semelhana entre os algoritmos
apresentados para os dois primeiros casos. Tambm os dois ltimos casos podem ser descritos por um
nico algoritmo. Logo, o algoritmo completo para insero pode ser codificado como a seguir:
No nosso exemplo:
procedure TformPonteiroA.btnMostreClick(Sender: TObject);
begin
memo1.Visible := true;
memo1.Lines.clear;
p := prim;
While p <> nil
do Begin
memo1.Lines.add(p^.valor.Nome + intToStr(p^.valor.Idade) + p^.valor.Sexo
+ floatToStr(p^.valor.Altura));
p := p^.prox;
End;
end;
Inserir(d);
Inserir(c);
Inserir(a);
Mostrar;
End;
Durante a execuo da rotina, ser criada urna lista encadeada contendo quatro nodos, sendo o
endereo do nodo inicial aquele armazenado na varivel P. Como P uma varivel local, ela ser
destruda ao trmino da execuo da rotina Principal. Entretanto, os nodos pertencentes cadeia, sendo
variveis dinmicas, somente sero liberados atravs de um comando explcito (Dispose). Desta forma,
aps a execuo da rotina, teremos quatro nodos perdidos na memria. Para resolver o problema,
vamos desenvolver uma rotina que, dada uma lista encadeada L, libera todos os seus nodos.
Devemos lembrar que no possvel liberar diretamente o primeiro nodo, pois perderamos o acesso
ao resto da cadeia. Assim, vamos precisar de uma varivel auxiliar P para guardar o endereo de um
nodo at que ele possa ser liberado.
procedure Destruir;
Var P: ^nodo; // ou : LstOrd = mesmo tipo de L e N
begin
while (L <> nil)
do begin
P := L;
L := L^.prox;
Dispose(P);
end;
end;
Com esta nova operao, podemos agora liberar todos os nodos de uma lista encadeada, assim que
ela no for mais necessria em uma certa aplicao. Vale lembrar que vetores so automaticamente
liberados quando saem do escopo em que foram definidos, isto torna desnecessrio definir operaes
de destruio para estruturas implementadas com alocao esttica. Entretanto, toda implementao
baseada em alocao dinmica deve oferecer uma operao de destruio, que ser chamada
explicitamente toda vez que uma varivel criada no for mais necessria na aplicao.
De qualquer forma, aps a execuo total de um programa, o sistema operacional recupera
automaticamente qualquer espao de memria que tenha sido alocado por ele e que no foi liberado
explicitamente. O problema dos nodos perdidos s ocorre durante a execuo do programa, que pode
ser interrompida devido insuficincia de memria.
Vejamos como esta alterao torna mais simples os procedimentos de insero e remoo.
Comecemos pela inicializao: ela no se resume mais em anular o ponteiro inicial da cadeia:
procedure Criar;
begin
60
new(L);
L^.prox := nil;
end;
Como toda lista ter sempre um nodo cabea, que nunca pode ser removido, todas as operaes de
insero e remoo sero realizadas em nodos a partir da segunda posio.
Procedure Inserir(X: elem); // X: char
Var N: ^nodo; // ou : LstOrd = mesmo tipo de L e N
begin
New(N); // cria o novo nodo
N^.Obj := X; // X vai sempre para o nodo criado
P = L^.prox;
while ((X > P^.obj) and (P^.prox <> nil)) // ? (X > P^.prox^.obj) ?
do P = P^.prox;
N^.Prox := P^.prox;
P^.prox := N;
End;
Compare com a procedure anterior. Observe que a insero fica reduzida a um nico caso: o
elemento ser inserido no meio ou no fim da lista. Assim, basta procurar a posio correta para
insero (com o lao while) e atualizar as ligaes. Tambm a remoo ser reduzida a procurar o
elemento no resto da lista e atualizar as ligaes relevantes.
function Remover(X: elem): boolean; // X: char
var N, P: ^nodo; // ou : LstOrd = mesmo tipo de L e N
begin
result := false;
P = L^.prox;
while ((X > P^.obj) and (P^.prox <> nil)) // ? (X > P^.prox^.obj) ?
do P = P^.prox;
if ((P <> nil) and (X = P^.obj)) // ? (P^.prox <> nil) e (X = P^.prox^.obj) ?
then begin
N := P^.prox;
P^.Prox := N^.prox;
Dispose(N);
Result := true;
End;
End;
O nodo cabea pode ser usado para guardar informaes gerais a respeito da lista encadeada. Um
caso bastante comum armazenar nele o nmero de elementos da lista. Naturalmente, cada vez que
um nodo inserido ou removido, o campo de dados do nodo cabea deve ser atualizado.
Um nodo sentinela tambm um nodo extra na ltima posio da lista e armazena um valor especial
chamado high value (HV): o valor mximo entre todos os que podem fazer parte da lista ordenada. Por
exemplo, em uma lista cujos elementos so caracteres, o valor HV seria o cdigo do ltimo caractere
da tabela ASCII: unsigned char = 255. Nodos cabea e sentinela, em conjunto, tornam os algoritmos
mais simples:
Para inicializar a lista, precisamos alocar e encadear os dois nodos iniciais da estrutura:
procedure Criar;
var X: char;
begin
new(L); // cria primeiro nodo
new(N); // cria ltimo nodo
61
A rotina a seguir implementa a operao de insero em listas ordenadas com nodos cabea e
sentinela. Observe que o nodo sentinela permite eliminar o teste que verifica se o final da lista j foi
atingido. Como HV mximo, certamente X nunca ser inserido no fim da lista encadeada.
Procedure Inserir(X: elem); // X: char
Var N: ^nodo; // ou : LstOrd = mesmo tipo de L e N
begin
New(N); // cria o novo nodo
N^.Obj := X; // X vai sempre para o nodo criado
P = L^.prox;
while ((X > P^.obj) // X nunca vai ser > o ltimo
do P = P^.prox;
N^.Prox := P^.prox;
P^.prox := N;
End;
Nada impede que um elemento inserido na lista tenha o valor HV. Entretanto, ao remover um nodo
da cadeia, devemos nos certificar de que ele no seja o sentinela. Para isto que serve o teste P^.prox
<> nil; qualquer nodo pode armazenar o valor de HV, mas somente o sentinela tem um ponteiro nulo!
function Remover(X: elem): boolean; // X: char
var N, P: ^nodo; // ou : LstOrd = mesmo tipo de L e N
begin
result := false;
P = L^.prox;
while (X > P^.obj) // ? (X > P^.prox^.obj) ?
do P = P^.prox;
if (P^.prox <> nil) and (X = P^.obj)
then begin // ? (P^.prox^.prox <> nil) e (X = P^.prox^.obj) ?
N := P^.prox;
P^.Prox := N^.prox;
Dispose(N);
Result := true;
end;
end;
Durante a operao de pesquisa, precisamos tambm ter certeza de que o nodo encontrado no o
nodo sentinela, j que ele no faz parte dos elementos da lista.
Como o endereo do primeiro nodo pode ser obtido atravs do campo de ligao do ltimo nodo, a
varivel ponteiro para uma lista circularmente encadeada guarda no o endereo do primeiro, mas sim
do ltimo nodo da cadeia. A grande vantagem em se usar encadeamento circular que podemos
acessar rapidamente tanto o primeiro quanto o ltimo nodo da lista. Na implementao no circular,
acessar o ltimo elemento requer a passagem por todos os elementos da cadeia, um a um, at atingir o
ltimo. Por exemplo, se resolvemos implementar filas com listas encadeadas, a operao de remoo
62
seria rpida (primeiro elemento), entretanto a operao de insero seria bastante lenta (ltimo
elemento). Veja a seguir, a implementao de filas usando listas circularmente encadeadas.
Type
elem: char; // define tipo elem baseado em char
Fila = ^nodo; // define um ponteiro para record nodo
Nodo = record // define estrutura nodo
Obj: elem;
Prox: Fila;
End;
A inicializao e teste de fila vazia so triviais na implementao com listas circulares. Note que no
h necessidade de teste de fila cheia, j que estamos usando alocao dinmica de memria:
Ao ser inserido na fila, um novo nodo dever sempre armazenar o endereo do primeiro elemento da
lista encadeada. Se a fila estava vazia antes da insero, o novo nodo ter que apontar para si mesmo.
procedure Inserir(var F: fila; X: elem); // X: char
var N: fila; // ou ^nodo: ponteiro para nodo
begin
New(N); // cria o novo nodo
N^.Obj := X; // X vai sempre para o nodo criado
if filaVazia(F)
then begin
F = N;
N^.prox := N;
end
else begin
N^.prox := F^.prox;
F^.prox := N;
F = N;
end;
end;
Na remoo, devemos atentar para o caso de a fila ter somente o elemento a ser removido, pois,
neste caso, a fila ficar vazia. Analise o cdigo apresentado e veja como o encadeamento circular torna
a implementao muito mais eficiente do que para uma lista encadeada comum.
function Retirar(F: Fila): boolean;
Var P: fila; // ou ^nodo
begin
if (not FilaVazia(F))
then begin
if (F = F^.prox) // fila s tem um elemento
then begin
retirar(F^.obj);
dispose(F);
F := nil
end
else begin // fila tem mais de um elemento
63
P := F^.prox;
F^.prox := P^.prox;
Retirar(P^.obj);
Dispose(P);
end;
result := true;
end
else result := false; // fila vazia
end;
Sendo cada nodo da forma (esq, obj, dir), dado um ponteiro P que armazena o endereo de um nodo
qualquer no meio da lista, vale a seguinte igualdade: P = P^.esq^.dir = P^.dir^.esq.
Na figura anterior, por exemplo, se partimos do ponteiro P e vamos para a esquerda e depois para a
direita, retornamos ao mesmo nodo de partida. Assim, esta igualdade caracteriza a principal
propriedade de uma lista duplamente encadeada, que a facilidade de retroceder ou avanar na cadeia,
a partir de qualquer nodo, com a mesma eficincia. Entretanto, a igualdade no se verifica se P estiver
apontando um nodo extremo da lista. Por exemplo, se P aponta o primeiro nodo da lista, ento no
possvel ir para a esquerda e depois voltar direita. Por este motivo, listas duplamente encadeadas
normalmente so implementadas de forma circular; neste caso, an precede a1 e a1 sucede an. A
existncia de um nodo sentinela garante a validade da igualdade mesmo estando a lista vazia e,
portanto, torna a estrutura ainda mais uniforme.
Ento em uma lista ordenada com a estrutura apresentada na figura anterior, podemos acessar os
seus elementos em ordem crescente ou decrescente. Se o encadeamento duplo tem a desvantagem de
gastar mais memria, por outro lado, tem a vantagem de tornar as operaes bsicas mais simples.
Sendo P um ponteiro para um nodo qualquer no meio de uma lista duplamente encadeada, e N um
ponteiro para um novo nodo a ser inserido antes de P, podemos usar as seguintes instrues:
N^.esq = P^.esq;
N^.dir = P;
P^.esq^.dir = N;
P^.esq = N;
Para remover o nodo apontado por P, fazemos:
P^.esq^.dir = P^.dir ;
P^.dir^.esq = P^.esq;
dispose (P);
64
Bibliografia
[Aze 96] Azeredo, Paulo A. . Mtodo de Classificao de dados e anlise de suas complexidades.
Editora Campus. Rio de Janeiro. 1996.
[Per 96] Pereira, Silvio Lago. Estrutura de dados Fundamentais: conceitos e aplicaes. Editora
rica. So Paulo. 1996
[Szw 94] Szwarcfiter, Jaime Luiz, Markenzon, Lilian. Estruturas de dados e seus algoritmos.
Editora Livros Tecnicos e Cientficos. Rio de Janeiro. 1994.
[Ten 95] Tenenbaum, Aaron M.. Estruturas de dados usando C. Editora Makron Books, So
Paulo.1995.
[Vel 83] Veloso, Paulo, Santos, Clesio dos, Azeredo, Paulo, Furtado, Antonio. Estrutura de Dados.
Editora Campus, Rio de Janeiro. 1998.
[Buc 02] Bucknall, Julian. Algoritmos e estruturas de dados com Delphi. Editora Berkeley, So
Paulo. 2002.
65
LISTA DE EXERCCIOS
1. Implementar uma pilha com um vetor de n elementos do tipo record (nome[30], idade, sexo e
altura) e as seguintes operaes:
1.1. inserir um elemento: no topo, no meio e no incio. Nas inseres no meio e no incio, andar
com a pilha para a direita: colocar esta operao em uma funo ou procedure separada.
Chamar uma funo para testar se pilha est cheia.
1.2. retirar um elemento: do topo, do meio, do incio. Nas retiradas do meio e do incio, andar com a
pilha para a esquerda: colocar esta operao em uma funo ou procedure separada. Chamar
uma funo para testar se pilha est vazia. Usar a pesquisa do item 1.9 para informar se
elemento est na pilha.
1.3. informar qual o tamanho da pilha,
1.4. gravar a pilha: do vetor para um arquivo,
1.5. ler a pilha: do arquivo para o vetor,
1.6. ordenar a pilha, usando o mtodo da bolha (bubble sort),
1.7. listar a pilha em um memo,
1.8. esvaziar a pilha,
1.9. com a pilha ordenada, programar uma pesquisa: dado um elemento x, informar a posio em
que se encontra na pilha (ou no est na pilha)
o incrementar uma pesquisa seqencial
o incrementar uma pesquisa binria.
2. Implementar uma pilha com uma lista encadeada com ns do tipo record (nome[30], idade, sexo e
altura) e as seguintes operaes:
2.1. inserir um elemento: no topo, no meio e no incio,
2.2. retirar um elemento: do topo, do meio, do incio,
2.3. informar qual o tamanho da pilha,
2.4. gravar a pilha: do vetor para um arquivo,
2.5. ler a pilha: do arquivo para o vetor,
2.6. ordenar a pilha, usando o mtodo da bolha (bubble sort),
2.7. listar a pilha em um memo,
2.8. esvaziar a pilha,
2.9. pesquisar um elemento x, informar a posio em que est (ou no est) na pilha.
3. Implementar uma fila no circular com um vetor de n elementos do tipo record (nome[30], idade,
sexo e altura) e as seguintes operaes:
3.1. inserir elemento no topo,
3.2. retirar um elemento do incio, sem andar com a fila,
3.3. retirar um elemento do meio ou incio, andando com a fila
3.4. informar qual o tamanho da fila e listar a fila,
3.5. pesquisar um elemento x, informar a posio em que est (ou no est) na fila.
4. Implementar uma fila circular com um vetor de n elementos do tipo record (nome[30], idade, sexo
e altura) e as seguintes operaes:
4.1. inserir elemento no topo,
4.2. retirar um elemento do incio, sem andar com a fila,
4.3. retirar um elemento do meio ou incio, andando com a fila
4.4. informar qual o tamanho da fila e listar a fila,
4.5. pesquisar um elemento x, informar a posio em que est (ou no est) na pilha.
5. Implementar uma fila com uma lista encadeada com ns do tipo record (nome[30], idade, sexo e
altura) e as seguintes operaes:
66
6. Implementar uma lista duplamente encadeada com ns do tipo record (nome[30], idade, sexo e
altura) e as seguintes operaes:
6.1 inserir um elemento: no topo, no meio e no incio,
6.2 retirar um elemento: do topo, do meio, do incio: dar mensagem se no estiver na lista,
6.3 informar qual o tamanho da lista,
6.4 listar a lista num memo: do incio para o fim e do fim para o incio,
6.5 esvaziar a pilha,
6.6 programar uma pesquisa seqencial: dado um elemento x, informar a posio em que se
encontra na pilha (ou no est na pilha)