Apontamentos Data Strutures

Fazer download em pdf ou txt
Fazer download em pdf ou txt
Você está na página 1de 110

UNIVERSIDADE POLITÉCNICA

APOLITÉCNICA
Escola Superior de Gestão, Ciência e Tecnologias
Notas de Algoritmos e Estrutura de Dados

Capítulo I
Listas

Uma lista, como array, pode conter uma sequência ordenada de registos
(records) com elementos disponíveis de forma consecutiva -Lista Estática
Sequencial- ou não consecutiva -Lista Estática Encadeada.

Algumas linguagens como o Pascal, permitem construir estruturas de dados


avançadas -Listas Dinâmicas-, mais versáteis utilizando ponteiros e
variáveis dinâmicas.

Um ponteiro é uma variável que contém o endereço de memória de uma


outra variável ou estrutura de dados. Uma variável declarada como ponteiro
é alocada durante a execução real de um programa.

Por exemplo, uma variável dinâmica é a única estrutura de dados do Turbo


Pascal que tem de estar identificada numa declaração Var antes de ser
utilizada num programa. O Turbo Pascal armazena as variáveis dinâmicas
numa área especial da memória chamada Heap (um tipo de pilha, não
confundir com stack). Um programa pode criar qualquer número de
variáveis dinâmicas, enquanto existir espaço disponível no Heap.

1.1 Lista Estática Sequencial

Uma lista estática sequencial é um arranjo de registos onde estão


estabelecidas regras de precedência entre seus elementos ou é uma colecção
ordenada de componentes do mesmo tipo. O sucessor de um elemento
ocupa posição física subsequente.
Ex: lista telefônica, lista de alunos

A implementação de operações pode ser feita utilizando matriz (vector) e


record (regisro), onde o vector associa o elemento a(i) com o índice i
(mapeamento sequencial).

1.1.1 Características de Lista Estática Sequencial

 elementos na lista estão ordenados;


 armazenados fisicamente em posições consecutivas;
 inserção de um elemento na posição a(i) causa o deslocamento a
direita do elemento de a(i) ao último;
 eliminação do elemento a(i) requer o deslocamento à esquerda do
a(i+1) ao último;

Mas, absolutamente, uma lista estática sequencial ou é vazia ou pode ser


escrita como ( a(1), a(2), a(3), ... a(n) ) onde a(i) são átomos de um mesmo
conjunto S.

Além disso, a(1) é o primeiro elemento, a(i) precede a(i+1), e a(n) é o último
elemento.

Assim as propriedades estruturadas da lista permitem responder a questões


como:

 qual é o primeiro elemento da lista


 qual é o último elemento da lista
 quais elementos sucedem um determinado elemento
 quantos elementos existem na lista
 inserir um elemento na lista
 eliminar um elemento da lista

Consequência: As quatros primeiras operações são feitas frequentemente.


Mas, as operações de inserção e remoção requererão mais cuidados.

2
Vantagem:

 acesso directo indexado a qualquer elemento da lista;


 tempo constante para aceder o elemento i - depfimerá somente do
índice.

Desvantagem:

 movimentação quando eliminado/inserido elemento;


 tamanho máximo pré-estimado.

Quando usar:

 listas pequenas;
 inserção/remoção no fim da lista;
 tamanho máximo bem definido.

1) Acesso a um elemento

Escrever(A[i]);

2) Actualização

A[i] 'Valor'

3) Tamanho da Lista

início
tamanho  n;
Fim;

4) Inserção de um elemento na posição i


Requer o deslocamento à direita dos elementos a(i+1)...a(n)

3
{ considerando que a posição foi obtida em um procedimento de consulta
executado préviamente }

{ também é necessário verificar se a lista não está cheia }

Procedure Inserir()

início
Para j de n+1 até i+1 passo –1 faça
A[j]  A[j-1];
Fim-para
A[i] 'novo valor';
n  n+1;
Fim;

5) Remoção do i-ésimo elemento


Requer o deslocamento à esquerda dos elementos a(i+1)...a(n)

Procedure Remover()

Inicio
Para j de i até n-1 passo 1 faça
A[j]A[j+1];
Fim-para
nn-1;
Fim;

1.1.2 Exercícios

Dada uma lista sequencial ordenada L1, escreva procedimentos que:

 verifique se a lista L1 está ordenada ou não (a ordem pode ser


crescente ou decrescente)
 faça uma cópia da lista L1 numa outra lista L2;
 faça uma cópia da Lista L1 para L2, eliminando elementos repetidos;
 inverta L1 colocando o resultado em L2;
 inverta L1 colocando o resultado na própria L1;
 intercale L1 com a lista L2, gerando a lista L3. considere que L1, L2 e
L3 são ordenadas.

4
 gere uma lista L2 onde cada registo contém dois campos de
informação: elem contém um elemento de L1, e count contém quantas
vezes este elemento apareceu em L1.
 elimine de L1 todas as ocorrências de um elemento dado, L1
ordenada.
 assumindo que os elementos da lista L1 são inteiros positivos, forneça
os elementos que aparecem o maior e o menor número de vezes
(forneça os elementos e o número de vezes correspondente)

1.2 Lista Estática Encadeada

Os elementos da lista são registos com um dos componentes destinado a


guardar o endereço do registo sucessor.

Ex: L = anta, cabra, macaco, pato, rato

Cada registo é:

ou

Há duas alternativas para implementação de operações de listas


encadeadas: utilizando matrizes ou variáveis dinâmicas.

Encadeamento em matrizes

Eliminando o elemento "cobra" teremos:

5
O registo 2 tornou-se disponível para as próximas inserções...

Implementação utilizando matriz

Após sucessivas inserções e eliminações como descobrir quais registos


estão disponíveis?

Juntá-los numa lista DISPO. Assim, os registos 6, 7, ... m estariam


inicialmente na lista DISPO.

Como deverá ser Dispo ? Sequencial?


Ela deve ser capaz de anexar os registos eliminados da lista principal L.
Suponha que queremos inserir algum elemento.

Isso implica que :

 a eliminação de um elemento da lista principal causa a inserção de um


registo na lista Dispo

 a inserção de um elemento na lista principal causa a utilização de um


dos registos da Dispo

No exemplo dado, ao eliminar "cobra" anexamos esse registo à dispo.

Vantagens:

 não requer mais a movimentação de elementos na inserção e


eliminação (como na lista sequencial);

6
 apenas os ponteiros são alterados (lembre que cada registo pode
conter elementos muito complexos);

Desvantagens:

 necessário prever espaco máximo da lista;


 necessário gerir a Dispo.
 o acesso é não indexado, para aceder a(i) temos que percorrer a(1) ...
a(i-1) pois o endereço de a(i) está disponível apenas em a(i-1);
 aumento do tempo de execução, o qual é gasto obtenção de espaço de
memória;
 reserva de espaço para a Dispo;
 tamanho máximo pré-definido.

Quando usar:

 quando for possível fazer uma boa previsão do espaço utilizado (lista
principal + Dispo) e quando o ganho dos movimentos sobre a perda
do acesso directo a cada elemento for compensador.

1.2.1 Implementação de Operações

Supondo apenas um campo de informação do tipo T:

Constante N=_____; {número máximo de elementos}


Tipo endereço= 0..N; { elementos armazenados nas
posições de
1 a N da matriz; o valor 0
indica fim da lista }
Tipo Rec = registo
info: T

7
lig: endereço;
fim;

Lista = registo
A: matriz[1..N] de Rec;
Prim, Dispo: fimereco;
fim;

declarar L: lista;
Function Primeiro (L): T;
Início
Se L[prim] <> 0 então
Primeiro  L[info];
Fim-se
Fim;

2) Qual é o último ?

Function Ultimo(L): T;

Declarar p: fimereco;

Se L[prim] = 0 então
{Retorna lista vazia }
Senão

p L[prim];
Enquanto Link[p] <> 0 faça
p  Link[p]; {L[info] é o último elemento}
fim-enquanto
Fim-se
Ultimo  L[info];
Fim;

3) Quantos elementos tem a lista ?

Function No_elementos(L): inteiro;


Declarar Nelem: inteiro;
p: fimereco;

8
Se L(Prim) = 0 então
Nelem  0 {lista vazia}
Senão

p  L(Prim);
Nelem  1;
Enquanto Link[p] <> 0 faça

Nelem  Nelem + 1; {Nelem contém o Número


de elementos}
p  Link[p];
Fim-enquanto;
Fim-se;
No_elementos  Nelem;
Fim;

Algoritmos para inserção e eliminação de elementos

Os algoritmos requerem a mudança de vários ponteiros: do antecessor do


registo na lista, do ponteiro da lista Dispo, e do registo a ser
inserido/eliminado.

Temos ObterNo(j) e DevolverNo(j), as quais permitem obter um registo de


índice j da Dispo, ou devolver o registo de indice j, respectivamente.

Na inserção podemos contar com o registo j como sfimo disponível, e na


eliminação podemos contar com o registo j como a ser deixado disponível.

Para inserir ou eliminar um elemento da lista, temos que saber do endereço


do predecessor (lembre que a busca é guiada pelo conteúdo do registo, no
caso de listas ordenadas). Este predecessor é quem contém o endereço
daquele que será o sucessor do elemento inserido/eliminado.

Inserção

Eliminação

9
1) Inserção após o registo de endereço k

Procedure Insere(L; k, valor);


Declarar j: fimereco;

Inicio
Se L(Dispo) <> 0 então {se a Dispo está vazia, nao
pode inserir!}

ObterNo(j);
L[info]  valor;
Link[j] Link[k];
Link[k]  j;

Senão
Escrever(`não pode inserir registo`)

Fim;

2) Eliminação do registo de endereço j precedido por k

Procedure Remover(L,k,j);
Inicio
Link[k]  Link[j];
DevolverNo(j);
Fim;

3) Casos especiais

Inserção antes do primeiro elemento

10
Procedure Insere_prim(L, valor);
Declarar j: fimereco;
Inicio

Se L(Dispo) <> 0 então

ObterNo(j);
L[info]  valor;
Link[j]  Link(Prim);
Link(Prim)  j;

Senão
Escrever(“não pode inserir”)
Fim-se;
Fim;

OBS: No caso da lista estar vazia, Prim passa a apontar o único elemento da
lista.

Inserção em lista vazia

Procedure Insere_ListaVazia(L, valor);


Declarar j: fimereco;

Inicio
Se L(Dispo) <> 0 então { lista vazia ou prim =
0 }

Obter_No(j);
L[info] valor;
Link[j]  0; {null}
L(Prim) j;
Fim-se;
Fim;

Eliminação do primeiro elemento

11
Procedure Remove_Primeiro(L);
Declarar j: fimereco;
Início
j  Link(Prim);
Link(Prim)  Link(link[Prim]);
DevolverNo(j);
Fim;

4) Acesso ao i-ésimo elemento A[i].info

Function Acessa_iesimo(L, dado): booleana;


Declarar p,j: endereço;
Inicio
p  L(Prim); { começa do primeiro elemento }
j  1;
Enquanto (j < i) e (p <> 0) faça

p  Link[p]; { fim  (p=0) }


j  j + 1; { achou  (j=i) }
Fim-enquanto;
Se p = 0 então

Acessa_iesimo false;{ lista não possui i


elementos }
dado 0;

Senão

Acessa_iesimo Verdadeiro;
dadoL[info]; { L[info] é o elemento
procurado}
Fim-se;
Fim;

12
5) Eliminar o registo de conteúdo dado pela variável valor

Procedure Remove_elem(L, valor);


Declarar pa, p, fimereco;
acabou: booleana;
Inicio
pa  0; { ponteiro anterior }
p  L(Prim); { busca a partir do primeiro }
acabou  (p=0); { termina quando fim da lista
]
Enquanto not (acabou) faça

Se L[info] <> valor então

pa  p;
p  Link[p];
acabou  (p=0);

Senão
acabou  Verdadeiro;
Fim-se;

Se p=0 então
Escrever(“não existe o conteúdo
`valor' na lista”)
Senão

Se pa = 0 então
Prim  Link(Prim); { eliminar
o primeiro elemento }
Senão
Link[pa]  Link[p];
DevolverNo(p);
Fim-se
Fim-se
Fim-enquanto

Fim;

13
1.2.2 Alocação de Nó em Lista Estática Encadeada

Inicialização da Lista

Inicialmente todas as posições do vector A estão disponíveis, portanto fazem


parte de "Dispo".

Procedure Init(L);
Inicio
L(Dispo) 1; {primeiro elemento}
L(Prim) 0; {lista principal está vazia}
Para i de 1 até n-1 passo 1 faça
Link[i] i+1;
Link[n] 0; {receber 0 corresponde ao
nil}
Fim-para;
Fim;

Obter_No(j): obtém um registo de índice j da Dispo. Dispo contém pelo


menos nil.

Procedure Obter_No(j);
Inicio
j Dispo; { A dispo passa a apontar para quem
ela apontava }
Dispo Link[Dispo];
Fim;

OBS: A procedure Obter_No oferece uma "posição vazia", devemos


portanto, determinar os campos do registo.

14
Devolver_No(j): devolve um registo de índice j à Dispo.

Procedure Devolver_No(j);
Inicio
Link[j]  Dispo;
Dispo  j;
Fim;

OBS:

 Dispo está vazia quando a lista está cheia

 Dispo está cheia quando a lista está vazia

1.2.3 Exercícios

1) Dada uma lista encadeada ordenada L1, escreva procedimentos que:

 verSeique se L1 está ordenada ou não (a ordem pode ser crescente ou


decrescente)
 faça uma cópia da lista L1 em uma outra lista L2;
 faça uma cópia da Lista L1 em L2, eliminando elementos repetidos;
 inverta L1 colocando o resultado em L2;
 inverta L1 colocando o resultado na própria L1;
 intercale L1 com a lista L2, gerando a lista L3. considere que L1, L2 e
L3 são ordenadas.
 gere uma lista L2 onde cada registo contém dois campos de
informação: elem contém um elemento de L1, e count contém quantas
vezes este elemento apareceu em L1.
 elimine de L1 todas as ocorrências de um elemento dado, L1
ordenada.
 assumindo que os elementos da lista L1 são inteiros positivos, forneça
os elementos que aparecem o maior e o menor número de vezes
(forneça os elementos e o número de vezes correspondente)

15
2) Escreva os seguintes algoritmos que implementem Listas Encadeadas
Estáticas (em array) com sentinela:

 criação de lista;
 busca em lista ordenada e não ordenada
 inserção e eliminação de elementos

1.3 Lista Dinâmica

As linguagens de programação modernas tornaram possível explicitar não


apenas o acesso aos dados, mas também aos endereços desses dados.

Isso significaca que ficou possível a utilização de ponteiros explicitamente


implicando que uma distinção notacional deve existir entre os dados e as
referências (endereços) desses dados.

A notação introduzida por Wirth, com a introdução de Pascal, é:

Type Tp = ^T

Essa declaração expressa que valores do tipo Tp são ponteiros para dados do
tipo T.
Portanto, lemos o simbolo ^ como sfimo ponteiro para... e na declaração
acima lemos Tp é um ponteiro para variáveis do tipo T.

O facto de que o tipo dos elementos apontados ser evidente na declaração do


ponteiro tem importância fundamental. Isso distingue ponteiros de
linguagens de alto nível de endereços em Assembly.

Valores para ponteiros são gerados quando dados correspondentes a seus


tipos são alocados/desalocados dinamicamente. Em Pascal, os
procedimentos new e dispose existem com esse propósito.

Portanto, deixa-se a cargo do programa (via linguagem de programação), e


não do programador, prover e devolver espaço para inserções e eliminações
em tempo de execução.

Custo: Tempo de execução comprometido.

16
Idéia: O programador declara apenas o tipo de registo que contém um
elemento da lista, e avisa que a alocação será dinâmica. Sempre que
requisitar um registo novo para a inserção, ou liberar um registo a ser
eliminado, o programador lança mão de duas rotinas pré-definidas para este
fim.

Um ponteiro do tipo Lista pode assumir o conjunto de valores que


correspondem a endereços reais de memória. Por exemplo, sfimo declarar p:
Lista podemos ter:

onde o conteúdo de p corresponderia ao endereço do objecto. Esses


endereços serão as ligações das listas encadeadas dinâmicas.

Sfimo o registo:

O acesso ao registo depfime do tipo da alocação. Se compararmos alocação


em array com alocação dinâmica com ponteiros, temos que para aceder o
conteúdo de uma variável fazemos:

alocação alocação
array L.A[p] dinâmica p^

Para designar ponteiro, objecto e campos, a notação utilizada é:

ponteiro: p
objecto: p^
campos: p^(info)
link(p)

1) Endereço nulo (terra)

Algumas linguagens como o Pascal provêm uma constante pré-definida para


denotar o endereço nulo nil. Podemos utilizá-la para atribuições e testes,
como nos exemplos abaixo:

17
1: L  nil;
2: Se (p = nil) então ...

Ponteiro x Objecto Apontado

Nos exemplos abaixo, é ilustrada a diferença entre as operações de


atribuição entre ponteiros (por exemplo, p  q ) e a atribuição entre o
conteúdo dos registos apontados pelos ponteiros (isto é: p^  q^).

Declarar p,q: Lista;

Dada a situação abaixo, chamada de (a):

Dada a situação (a), após a atribuição p  q temos a representação abaixo


(b). Esta atribuição só é válida para ponteiros do mesmo tipo e é a única
operação entre ponteiros.

Dada a situação (a), após a atribuição p^ q^ temos a representação abaixo


(c), onde o conteúdo é atribuido (lembre que p^ equivale a L.A[p]).

18
1.3.1 Manipulação de Registos

1) Declaração de Variável

Registo do tipo rec

Declarar p: ^rec;

2) Criação de um registo

Substitui o procedimento ObterNo(j) dada uma variável ponteiro do tipo


^rec.

new(p);

a) efectivamente aloca uma variável do tipo rec


b) gera um ponteiro do ^rec apontando para aquela variável
c) atribui o ponteiro à variável p

A partir daí:

a) o ponteiro pode ser referenciado como p


b) variável referenciada por p é denotada por p^

3) Atribuição de conteúdo ao registo:


p^(info)  valor;

4) Liberação de um registo

Substitui o DevolverNo(j) - dispose(p)

a) operação libera o espaço apontado por p


b) p passa a ter valor indefinido

19
1.3.2 Definição da ED
Tipo prec = ^rec;
Lista = prec;
rec = registo
info: T; {seu tipo preferido}
lig: Lista
Fim;

Var p: Lista; {ponteiro para qualquer elemento da


lista}
L: Lista; {ponteiro para o primeiro elemento da
lista }

Operações sobre Listas Dinâmicas

1.3.3 Implementação das Operações

1) Criação da lista vazia

Procedure Create (Var L : Lista);


Inicio
L nil;
Fim;

20
Procedure Insere_Prim(L, valor);
Declarar p: Lista;
Inicio
new(p);
p^(info)  valor;
Link(p)  nil;
L  p;
Fim;

3) Inserção no início de uma lista

Procedure Insere_Inic(L, valor);


Declarar p: Lista;
Inicio

p^(info)  valor;
link(p)  L;
L  p;
fim;

21
4) Acesso ao primeiro elemento da lista

Function Prim(L): T;
Inicio
Prim L^(info);
Fim;

5) Acesso ao último elemento da lista

Function Ultimo(L): T;
Inicio
Se L = nil então
Escrever(“lista vazia”)
Senão

p  L;
Enquanto link(p) <> nil faça
p  link(p);
Fim-enquanto
Ultimo  p^(info); {p^(info) é último
elemento}
Fim-se; {p^ é o último registo}
Fim;

6) Qual o número de elementos da lista ?

Function Nelem(L): inteiro;


Inicio
Se L = nil então
Nelem  0
Senão

p  L;
Nelem  1;
Enquanto link(p) <> nil faça

Nelem  Nelem + 1;

22
p  link(p);
Fim-enquanto;
Fim-se;
fim;

7) Inserção do valor v depois do elemento apontado por k

Procedure Insere_depois(L, v, k);


Declarar j: Lista;
Inicio
new(j);
j^(info)  v;
link(j)  link(k);
link(k)  j;
fim;

OBS: Funciona para inserção após último elemento.

8) Remoção do elemento apontado por j, que segue k

Procedure Elimina_depois(L, k, j);


Inicio
Link(k) link(j);
dispose(j);
fim;

23
9) Remoção do primeiro elemento

Procedure Remove_Prim(L);
Inicio
p  L;
L link(L);
dispose(p)
fim;

OBS: funciona no caso de remoção em lista com um único elemento.

10) Eliminar um valor v de uma lista ordenada L

Procedure Elimina(v, L);


declarar
p, pa: Lista;
acabou: booleana;

inicio
pa  nil;
p  L;
acabou  (p = nil);
Enquanto (not acabou) faça
Se p^(info) < v então

pa  p;
p  link(p);
acabou  (p = nil);

Senão
acabou  true;
{Enquanto acaba aqui!}
Fim-enquanto

24
{ p = nil ou p^.info = v ou p^.info > v }
Se (p = nil) então
Escrever("lista não contém v")
Senão
Se p^(info) > v então
Escrever("lista não contém v")
Senão

Se pa = nil então
L  link(p)
Senão
Link(pa)  link(p);
Fim-se
Fim-se
Fim-se
dispose(p);
fim;

11) Inserção do valor v antes do elemento apontado por p

Uma alternativa para evitarmos percorrer a lista para encontrar o antecessor


de p é inserirmos o valor dado, de facto, após o elemento apontado por p,
após realizar uma cópia de conteúdo entre ponteiros....

Procedure Insere_antes(L, v, p);


Declarar q: Lista;
inicio
new(q);
q^  p^;
p^(info)  v;
link(p)  q;
fim;

12) Criação de uma lista L com registos numerados de 1 .. n

Procedure Cria_Lista_num(L);
Declarar L, q: Lista;

25
Inicio
L  nil; { começa com lista vazia }
Enquanto n > 0 faça

new(q);
link(q)  L;
q^(info)  n;
L  q;
n  n-1;
Fim-enquanto;
fim;

13) Remoção do registo sucessor de p e inserção do mesmo como cabeça


em uma lista apontada por q

Procedure Remove_Insere(q, p);


inicio
r  link(link(p)); {r aponta o sucessor de p^}
link(p)  link(r); {r^ sai da lista}
link(r)  q; {r^ passa a apontar q^ }
q  r; {r^ passa a ser o primeiro elemento da
lista
apontada por q}
fim;

14) Impressão recursiva de uma lista

Procedure Printlist(p);
inicio
Se (p <> nil) então

Escrever(p^(info));
Printlist(link(p));
Fim-se;
Fim;

26
1.3.4 Busca em Lista Dinâmica

1) Problema com nil ?

Se estivermos buscando um elemento x em uma lista encadeada, podemos


ter algo similar a:

Enquanto (p <> nil) e (p^(info) <> x) faça


p  link(p);

Que problema temos no comando acima ?

No caso de p = nil o comando pode ser inválido pois p^.info não existe!

OBS.: Se o compilador não realiza o segundo teste no caso do primeiro


falhar, não haveria problema. Em nível de algoritmo, é melhor resolver o
problema evitando a possibilidade desse tipo de erro ocorrer, ao invés de
usar soluções que depfimem do compilador.

O que fizemos nos nossos algoritmos foi algo similar a:

acabou  false;
Enquanto (p <> nil) e (not acabou) faça
Se p^(info) = x então
acabou  true
Senão
p  link(p);

2) Uso de sentinela na busca em lista encadeada

Um elemento sentinela pode ser utilizado na busca em lista linear


encadeada: usamos para isso um registo no final da lista.

Supondo que inicializamos uma lista apontada por L com a lista vazia,
faremos:

Procedure Create(L);
Declarar sentinela: Lista;
Inicio
new(sentinela);
L  sentinela;

27
Fim;

O algoritmo seguinte implementa uma busca por um elemento x na lista


apontada por L não ordenada. Retorna p = nil se o elemento não for
encontrado; caso contrário p aponta o registo que contém o elemento.

Procedure busca(x, L, p);

Inicio
p  L;
sentinela^(info)  x;
Enquanto p^(info) <> x faça
p  link(p);
fim-enquanto
Se (p = sentinela) então
p  nil; {elemento não encontrado}
Fim-se
Fim;

3) Busca com sentinela e inserção na cabeça da lista não ordenada

Caso o elemento não seja encontrado e quisermos inseri-lo no início da lista,


teremos o procedimento abaixo:

Procedure busca_insere(x, L, p);


Declarar p: Lista;

Inicio
p  L;
sentinela^(info)  x;
Enquanto p^(info) <> x faça
p  link(p);
Fim-enquanto
Se (p = sentinela) Então
Inicio
{ inserção no começo da lista}
p  L; {aponta primeiro}
new(L); {novo registo é primeiro}
L^(info)  x;
Link(L)  p;

28
Fim-se;
Fim;

4) Sentinela em busca com lista encadeada ordenada

No algoritmo acima, ao realizar uma busca sequencial numa lista encadeada


anulamos a vantagem de utilizar listas encadeadas. Neste caso, esta
implementação só pode ser considerada para listas com um número pequeno
de elementos.

No caso da lista estar ordenada, a busca termina assim que encontrarmos


um elemento maior que aquele que buscamos. A ordenação é obtida ao
inserirmos cada novo elemento na sua posição correcta, ao invés de na
cabeça da lista.

Essa "ordenação" na realidade não tem custo algum, porque fazemos uso das
características da lista encadeada. Isso não ocorre com estruturas estáticas
como o array e os arquivos sequenciais.

O procedimento abaixo elimina um registo que contém o elemento v de uma


lista apontada por L.

O bloco de busca pelo elemento realiza dois testes para cada elemento da
lista. Esse esforço pode ser reduzido introduzindo o recurso de sentinela.

Depois da busca, testes devem ser efetuados para verificar se:

 o elemento foi encontrado ou não

 se encontrado, se ele era o primeiro da lista ou não

Procedure elimina(v, Prim);

Declarar
p, pa: Lista;
acabou: booleana;

Inicio
pa  nil;
p  L;
acabou  (p = nil);

29
Enquanto (not acabou) faça
Se p^(info) < v Então

pa  p;
p  link(p);
acabou  (p = nil);

Senão
acabou  true;
{ p = nil ou p^.info = v ou p^.info > v }
Se (p = nil) Então
Escrever("lista não contém v")
Senão
Se p^(info) > v Então
Escrever("lista não contém v")
Senão

Se pa = nil Então
L  link(p)
Senão
Link(pa) link(p);
Fim-se
Fim-se
Fim-se
Fim-se
dispose(p);
Fim-enquanto;
Fim;

Dada a seguinte inicialização:

new(raiz);
new(sentinela);
link(raiz)  sentinela;

Temos acima uma lista com dois elementos: o elemento apontado por raiz
vai ser sempre um dummy (elemento sem conteúdo), e o sentinela sempre
marca o final da lista.

30
O algoritmo abaixo realiza a busca de um elemento e o insere na posição
correcta caso ele não seja encontrado.

Procedure busca_insere(x, raiz);


Declarar w1, w2, w3: Lista;

Inicio
w2  raiz;
w1  link(w2);
sentinel^(info)  x;
Enquanto w1^(info) < x faça

w2  w1;
w1  link(w1);
fim-enquanto
Se (w1^(info) = x) e (w1 <> sentinela) Então
Escrever(“elemento já existe”)
Senão

new(w3); {insere w3 entre w1 e w2}


w3^(info)  x;
link(w3)  w1;
link(w2)  w3;
Fim-se;
Fim;

1.3.6 Exercício

Reescreva os algoritmos de inserção e eliminação de um elemento em uma


lista duplamente encadeada de forma a incluir uma chamada à Função
Busca_Dup_Ord(x).

1.4 Lista Duplamente Encadeada

Características

 Listas foram percorridas do início ao final.


 Ponteiro "anterior" necessário para muitas operações.

31
 Em alguns casos pode-se desejar percorrer uma lista nos dois sentidos
indiferentemente.
 Nestes casos, o gasto de memória imposto por um novo campo de
ponteiro pode ser justificado pela economia em não reprocessar a lista
toda.

Type tpont = ^ trec;


trec = record
info:Tipoelem;
esq, dir: tpont;
Fim;
Lista = tpont;

Var pont: Lista;

 Como consequência, podemos realizar as operações de inserção e


eliminação à esquerda ou à direita de um campo no interior de uma
lista sem a necessidade de ponteiros "anteriores".

Implementação de lista duplamente encadeada com alocação dinâmica

1) Inserção à direita de pont

Procedure ins_dir (Var pont: lista; x: Tipoelem);


Var j: Lista;
Inicio
new(j);
j^(info)x;
j^(dir)pont^(dir);
j^((dir^)(esq)) j;
j^(esq) pont;
pont^(dir) j;

32
Fim;

2) Inserção à esquerda de pont

Procedure ins_esq (ptlista, x);


Declarar j: Lista;
Inicio
new(j);
j^(info) x;
j^(dir) pont;
j^(esq) pont^(esq);

j^((esq^)(dir)) j;
pont^(esq) j;
Fim;

3) Eliminação à direita de pont

Procedure elim_dir (pont);


Declarar j: Lista;
Inicio
jpont^(dir);
pont^(dir) j^(dir);
j^(dir^(esq)) pont;
dispose(j);
Fim;

4) Eliminação do próprio pont

Procedure elim (pont);


Inicio
pont^((dir^)(esq)) pont^(esq);
pont^(esq^)(dir) pont^(dir);

33
dispose(pont);
Fim;

5) Busca em uma lista circular

Funtion Busca_Dup_Ord(ptlista, x):Lista;

{ Lista Duplamente Encadeada Ordenada com sentinela


apontado por ptlista }

Declarar pont, ultimo: Lista;


Inicio
ultimoptlista^(esq);
Se x<=ultimo^(info) então
pontptlista^(dir);
Enquanto pont^(info) < x do
pontpont^(dir);
Busca_Dup_Ordpont;
Fim
Senão
Busca_Dup_Ordptlista;
Fim
Fim;

1.4.1 Exercício

34
Reescreva os algoritmos de inserção e eliminação de um elemento em uma
lista duplamente encadeada de forma a incluir uma chamada à Função
Busca_Dup_Ord(x).

1.5 Listas Encadeadas Circulares

A implementação de listas circulares significaram uma melhoria na


utilização do espaço liberado pelos elementos que são retirados da fila.

O conceito de anel (circular) pode ser aplicado também às listas comuns,


como por exemplo, listas encadeadas.

Nestas listas, um procedimento comum que devemos fazer é o de busca.


Veremos a seguir como realizar buscas em listas circulares encadeadas. O
conceito de sentinela é também utilizado.

O trecho do algoritmo abaixo é o de busca e inserção em lista encadeada


ordenada que utiliza dois registos auxiliares: o sentinela e o "dummy".

Procedure busc_enc (root, sentinela, x);

{w2 - anterior; w1 - elemento se existir}

Declarar w1, w2: Lista;


Inicio
w2 root; {dummy}
w1 link(w2); { primeiro elemento }
sentinela^(chave)  x;
Enquanto w1^(chave) < x faça

w2w1;
w1link(w1);
Fim-enquanto
Se (w1^(chave) = x) e (w1 <> sentinela) então
Escrever(“elemento já existe”)
Senão
{inserir entre w1 e w2}
Fim;

35
Quando consideramos uma lista circular encadeada, podemos utilizar apenas
um registo auxiliar, como indicado no algoritmo de busca abaixo:

{ Busca em uma lista circular encadeada ordenada


com sentinela.
O sentinela é o primeiro elemento da lista
circular apontado por ptlista }

Procedure busc_circ (ptlista, x);


Declarar ant, pont: Lista;
Inicio
antptlista; {sentinela}
ptlista^(chave)  x;
pontlink(ptlista); {primeiro elemento}
Enquanto pont^(chave) < x faça

antpont;
pontlink(pont);
Fim-enquanto;
Se pont^(chave) = x and (pont<>ptlista) então
{elemento já existe}
Senão
{inserir entre ant e pont}
Fim;

1.5.1 Exercícios

1) Como a lista seria inicializada nesse caso ?

2) Como seriam os algoritmos de inserção e eliminação ?

3) Qual alteração devemos fazer no caso das listas não serem ordenadas ?

36
1.6 Listas Generalizadas

Uma lista generalizada é aquela que pode ter como elemento ou um átomo
ou uma outra lista (sub-lista).
Toda lista pode ser representada usando-se a estrutura de nó onde:

 CABEÇA (CAR) é um atomo ou um ponteiro para uma outra lista

 CAUDA (CDR) ligação para a cauda da lista ('próximo elemento')

 TAG é 0 (CABEÇA é atomo) ou é 1 (CABEÇA é ponteiro)

Exemplo:

L1 = (a, (b, c) )

L2 = (a, b, c)

L3 = ( (a, b), (c, d), e)

37
1.6.1 Implementação de Listas Generalizadas

Utilizam o Recurso do Pascal (record variante)

Type
pont_no = ^no;
no = record
cauda: pont_no;
case tag: boolean of
true: (pont: pont_no);
false: (atomo: tipo_elem);
Fim;
Lista = pont_no;

1.6.2 Variações de Listas Generalizadas

Listas Compartilhadas

Exemplo:

L4 = (L3, L3, ( ) ) onde:

 L3 é a lista do exemplo anterior


 ( ) é lista vazia

Listas Recursivas

Exemplo:

L5 = (a, L5)

38
1.6.3 Exercícios

1) Sobre Listas Generalizadas

 Quais suas vantagens


 Quais suas desvantagens
 Dê exemplos de uso
 Dê um exemplo de uma lista generalizada a qual contém, no total,
pelo menos 25 átomos e 10 cabeças (em vários níveis). Escreva sua
lista em notação de parênteses (LISP).
 Reescreva a sua lista do item acima para incluir listas compartilhadas.
Escreva sua lista em notação de parênteses (LISP).
 Reescreva a sua lista do item acima para incluir listas recursivas.
Escreva sua lista em notação de parênteses (LISP).

2) Implemente a função abaixo, a qual determina se duas listas são iguais,


assumindo que listas são não recursivas e compartilhadas.
Teste o programa incluindo para as listas:

A = B = ( ( a, b), (c, d) )
A = (a, (b, c) ) e B = (a, b, (c))

Function Igual (S,T: Lista) : boolean


Declarar resp:boolean;

Inicio
respfalse;
Se (S=nil ) e (T=nil) Então
resptrue;
Senão
Se (S<>nil) e (T<>nil) Então
Se ((S^(tag) = T^(tag))

Se (S^(tag)=false) Então
resp (S^(atomo), T^(atomo))
Senão
resp igual(S^(pont), T^(pont));
Fim-se
Se resp Então

39
respigual(S^(cauda), T^(cauda));
Fim;
Fim-se
igualresp;
Fim-se
Fim;

Matriz Esparsa

Problema: Representação de matrizes que contém grande parte de seus


elementos nulos.

Por exemplo seja a matriz abaixo, a qual contém 5 linhas e 6 colunas.


Apenas 5 de seus 30 elementos são não nulos.

Precisamos buscar uma representação que evite o armazenamento de tantas


posicões nulas.
Veremos uma solução que utiliza, as listas cruzadas como estruturas de
dados.

Estrutura de Dados de Listas Cruzadas

Numa matriz genérica, para cada elemento temos as informacões de:

 Linha,

40
 Coluna e

 Valor

Para não representarmos os valores nulos, fazemos listas de linhas e listas de


colunas tais que o elemento a(ij) diferente de 0 pertence:

 à lista dos elementos da linha i

 à lista dos elementos da coluna j

Então se a matriz tem nl linhas e nl colunas, temos nl listas de linhas e nc


listas de colunas. Graficamente podemos ter algo como:

Para o exemplo anterior, temos:

41
1.7.1 Operações

1) Acesso ao primeiro elemento não nulo da linha i:

 L[i] ^

2) Acesso ao elemento i,j:

 percorrer a lista L[i] até encontrar registo com campo de coluna = j ou

 percorrer a lista C[j] até encontrar registo com campo de linha = i

Operações com matrizes esparsas

Além de armazenar matrizes esparsas, as aplicações normalmente exigem a


realização de operações sobre essas matrizes, como por exemplo:

 multiplicar uma dada linha ou coluna por uma constante

 somar uma constante a todos os elementos de uma linha ou coluna

 somar duas matrizes esparsas de igual dimensão

42
Como consequência dessas operacões, alguns elementos podem deixar de
ser nulos, enquanto que outros podem se tornar nulos.

Por exemplo, ao se somar -4 a coluna 5 do exemplo temos:

Esse exemplo ilustra que, quando um elemento a(i,j) deve ser eliminado, ele
deve ser eliminado, de fato, de duas listas: L[i] e L[j].

Algoritmo: Somar k aos elementos da coluna j

Procedure Soma (k: TipoElemento);


Declarar p: Lista;
inicio
p C[j];
Para i de 1 até nl passo 1 faça
Se p = nil então insere(i,j,k)
senão

Se p^(linha) <> i então { valor era nulo}


inserir(i,j,k) {insere antes de p}
senão

p^(valor)  p^(valor) + k;
Se p^(valor) = 0 então

43
p  p^(pc);
eliminar(i,j);
fim
senão p  p^(pc);
fim;
fim;
fim;

1.7.2 Usabilidade

Quando a representação de listas cruzadas é vantajosa em relação à


representação convencional (bidimensional) ?

Factor Espaço

Suponhamos o caso de:

 uma matriz esparsa que armazena inteiros


 um ponteiro que ocupa o mesmo espaço que um inteiro

Matriz Esparsa

Espaço ocupado por uma matriz esparsa de nl linhas, nl colunas e n valores


não-nulos:

 5 * n espaços para ponteiros para os registos (um para cada campo do


registo: linha, coluna, valor, PL, PC)
 nl espaços para ponteiros para o vector L
 nc espaços para ponteiros para o vector C
 espaço total de 5n + nl + nc

Representação bidimensional

 o espaço ocupado seria nl * nc

44
Conclusão:

Em termos de espaço ocupado, há vantagem em utilizar-se a representação


de listas cruzadas quando:

5n + nl + nc < nl * nc

ou seja, quando:

n < [(nl - 1) * (nc - 1) -1] / 5

Como (nl - 1) * (nc - 1) é aproximadamente o tamanho da matriz, pode-se


dizer, de uma maneira geral que, há ganho em termos de espaço, quando um
número inferior a 1/5 dos elementos da matriz forem não nulos.

Factor Tempo

As operações sobre listas cruzadas podem ser mais lentas e complicadas que
para o caso bidimensional.
Portanto, para algumas aplicacões, deve ser feita uma reavaliação de tempo-
espaço.

1.7.3 Exercícios

1) Sobre Matrizes Esparsas (ME)

 Quais suas vantagens


 Quais suas desvantagens
 Dê exemplos de uso
 Defina vetor esparso, de tanto sua declaração como sua
funcionalidade
 Quando usar uma ME

2) Faça algorítmos que:

1. leia e imprima um matriz esparsa


2. some os elementos da linha i a uma constante k
3. some duas matrizes esparsas
4. verSeique se uma matriz esparsa é a identidade
5. multiplique um vetor esparso por uma ME

45
6. multiplique duas matrizes esparsas
7. calcule a inversa de uma matriz esparsa

LISTAS COM DISCIPLINAS DE ACESSO

Pode-se criar condições especiais para a execução de operações de


inserção e remoção de elementos em uma lista, sendo que as
estruturas resultantes são conhecidas por listas com disciplinas de
acesso.
As principais listas com disciplinas de acesso são a PILHA e a FILA.
Cada uma delas é descrita em detalhes a seguir.

PILHA

Conceito

É uma lista na qual as operações de inserção e remoção de


elementos são executadas em somente uma das extremidades,
denominada topo da lista.

Desta forma, o último elemento inserido é o primeiro a ser removido.

Esta disciplina de acesso é conhecida por LIFO - "Last In First Out",


ou em sua forma aportuguesada, UEPS - "Último a Entrar Primeiro
a Sair".

No quotidiano existem inúmeros exemplos de aplicações nas quais é


utilizada a disciplina de acesso LIFO, como por exemplo em uma
pilha de pratos ou de livros, um pacote de bolacha água e sal, a
mudança acomodada em um caminhão de mudanças, os anéis em
um dedo da mão, ou ainda um elevador de pessoas gordas, onde
somente depois que o último que entrou sair, será possível aos
outros saírem também.

A representação dos vários momentos de uma pilha é mostrada a


seguir:

46
Figura 1: Representação dos vários momentos de uma
pilha

A figura comprova a característica principal das pilhas, que é a


inserção e remoção efectuadas somente sobre o último elemento
(topo da lista). É interessante notar que não é possível distinguir a
pilha nos instantes 1 e 5 ou 2 e 4, uma vez que nestes instantes, a
pilha contém itens idênticos, empilhados na mesma ordem e com
topos idênticos.

Isto acontece uma vez que a estrutura pilha não mantém nenhum
registo dos elementos que lhe são inseridos ou removidos. No caso
deste registo se fazer necessário, deve ser mantido a parte.

Na realidade, esta visão lateral dos arranjos de uma pilha não é


verdadeira, sendo no entanto a mais didáctica. Para o entendimento
real de como a pilha é tratada pelo computador, ela deve ser olhada
de cima para baixo. Deste ponto de vista, não seria possível, por
exemplo, diferenciar a pilha nos instantes 2 e 7, uma vez que esta
visão somente nos permite visualizar o topo da pilha, isto é, a letra G.

As operações de inserção e remoção de elementos da pilha recebem


nomes especiais: push e pop, respectivamente.

Dada a pilha p e um item i qualquer, define-se a operação push(p,i)


como a adição do item i no topo da pilha p.

De forma similar, a operação pop(p) é definida como a operação que


remove um elemento do topo da pilha p. Por exemplo, x  pop(p),

47
vai remover o elemento constante no topo da pilha p e armazená-lo
na variável x.

Na figura anterior, os 7 momentos seriam representados da seguinte


maneira, utilizando-se das operações push e pop:

2 - push(p,g) 3 - push(p,h) 4 - pop(p)


5 - pop(p) 6 - pop(p) 7 - push(p,g)

Teoricamente, não existe um limite superior para o número de


elementos na pilha, ou seja, seria possível a colocação infinita de
novos elementos em uma pilha.

Por outro lado, se uma pilha p possui somente um elemento, e é


objecto de uma operação de remoção {pop(p)}, a pilha resultante é
uma pilha vazia, que não possui mais nenhum elemento para ser
retirado. Uma maneira de se garantir que uma operação pop(p) não
seja efectuada em uma pilha vazia é definição de uma 3a operação
para a manipulação de pilhas, denominada empty(p), que retorna
um valor booleano TRUE caso a pilha esteja vazia, e FALSE caso
contrário.
Uma 4a operação que pode ser definida para a manipulação de
pilhas é a verificação de qual elemento se encontra no topo da pilha
em um determinado momento. Esta operação poderia ser
denominada TOPO_PILHA, e seria equivalente a:

i  pop(p);
push(p,i);

A operação TOPO_PILHA, assim como POP não pode ser aplicada em uma
pilha vazia. Caso se aplique uma destas 2 operações em uma pilha vazia,
incorre-se em um erro conhecido como underflow (tentativa de se retirar um
elemento de uma pilha vazia).

Outro exemplo de pilha:

Pilha vazia

48
Insere(A)

Insere(B)

Retira(B)

Insre(C)

Retira(C)

Retira(A)

2.2 Implementação de Pilhas

Como lista Sequencial ou Encadeada ?

No caso geral de listas ordenadas, a maior vantagem da alocação encadeada


sobre a sequencial - se a memória não for problema - é a eliminação de
deslocamentos na inserção ou eliminação dos elementos. No caso das pilhas,
essas operações de deslocamento não ocorrem.

Portanto, podemos dizer que a alocação sequencial é mais vantajosa na


maioria das vezes.

Um outro modo de se implementar pode ser feito utilizando pilhas múltiplas.

49
2.3 Exemplo do Uso de Pilhas

Chamadas de procedimentos

Suponha a seguinte situação:

Quando o procedimento A1 é executado, ele efetua uma chamada a A2, que


deve carregar consigo o endereço de retorno e1. Ao término de A2, o
processamento deve retornar ao A1, no devido endereço.
Situação idêntica ocorre em A2 e A3.

Assim, quando um procedimento termina, é o seu endereço de retorno que


deve ser consultado. Portanto, há uma lista implícita de endereços (e0, e1,
e2, e3) que deve ser manipulada como uma pilha pelo sistema, onde e0 é o
endereço de retorno de A1.

No caso de processamento recursivo - por exemplo uma chamada a A2


dentro de A4 - o gerenciamento da lista como uma pilha resolve
automaticamente a obtenção dos endereços de retorno na ordem apropriada
(e0, e1, e2, e3, e4).

2.4 Alocação Sequencial de Pilhas

Operações

50
1. criar (P) - criar uma pilha P vazia

procedure criar (topo);


inicio
topo  0;
fim;

2. inserir (x, P) - insere x no topo de P(empilha): push (x, P).

procedure push (x, P, topo);


inicio
Se topo = maxp então
"PILHA CHEIA"
senão

topo  topo + 1;
P[topo]  x;
Fim-se
fim;

3. vazia (P) - testa se P está vazia

function vazia (topo): booleana;


inicio
vazia  (topo = 0);
fim;

4. topo (P) - acessa o elemento do topo da pilha (sem eliminar)

procedure top (topo_p, P, topo);


inicio
Se topo = 0 então
"PILHA VAZIA"
senão
topo_p  P[topo];
fim-se;
fim;

5. elimina (P) - elimina o elemento do topo de P (desempilha): pop (P)

51
procedure pop (P, topo);
inicio
Se topo = 0 então
"PILHA VAZIA"
senão
topo  topo-1;
fim-se
fim;

6) Devolve elemento eliminado

procedure pop_up (topo_p, P, topo);


inicio
Se topo = 0 então
"PILHA VAZIA"
senão

topo_p  P[topo]; {no caso de acesso ao elemento}


topo  topo-1;
fim-se;
fim;

2.5 Alocação Encadeada de Pilhas

Uma outra maneira de se implementar pilhas pode ser através de


listas ligadas. A vantagem deste método é a flexibilidade que se
consegue por causa da utilização de listas ligadas. Um exemplo disto
é que diminui muito a possibilidade de se cometer um erro de
overflow, pois teoricamente não existe um limite superior para o
armazenamento em lista ligada.

O esquema de implementação da pilha utilizando-se uma estrutura


definida é mostrada abaixo:

52
Operações

1. criar (P) - criar uma pilha P vazia

procedure criar (p);


inicio
p  nil;
fim;

2. inserir (x, P) - insere x no topo de P (empilha): push(x,P)

procedure push (x, p);


Declarar pont: pilha;
inicio
new(pont);
pont^(info)  x;
link(pont)  p;
p  pont;
fim;

3. vazia (P) - testa se P está vazia

53
function vazia (p): boolean;
inicio
vazia  (p = nil);
fim;

4. topo (P) - acede o elemento do topo da pilha (sem eliminar)

function top (p): TipoElem;

Se (p <> nil) então


top  p^(info)
fim;

5. elimina (P) - elimina o elemento do topo de P (desempilha) : pop(P)

procedure pop ( p);

Declarar aux: pilha;

inicio

Se (p <> nil) então

aux  p;

p link(p);

dispose (aux);

fim-se

fim.

2.6 Aplicação de Pilha: Notação Polonesa

Uma representação para expressões aritméticas que seja conveniente do


ponto de vista computacional é assunto de interesse, por exemplo, na área de
compiladores.

54
A notação tradicional é ambígua e, portanto, obriga o pré-estabelecimento de
regras de prioridade.
Isso torna a tarefa computacional menos simples. Outras notacões são
apresentadas a seguir, considerando-se apenas operações binárias (com dois
operandos):

Notação completamente Parentizada: acrescenta-se sempre um parênteses a


cada par de operandos e seu
operador.

Exemplo:
tradicional: A * B - C / D
parentizada: ((A*B)-(C/D))

Notação Polonesa: os operadores aparecem imediatamente antes dos


operandos. Esta notação especSeica quais operadores, e em que ordem,
devem ser calculados. Por esse motivo dispensa o uso de parênteses, sem
ambiguidades.

Exemplo:
tradicional: A * B - C / D
polonesa: - * A B / C D

Notação Polonesa Reversa (ou posfix): é como a polonesa na qual os


operandos aparecem após os operadores.

Exemplo:
tradicional: A * B - C / D
polonesa reversa: A B * C D / -

Avaliação de expressões aritméticas

programa fonte - notação infix: x := A / B + D * E - A


objetivo- notação posfix: x := A B / D E * + A -

Um algoritmo para a avaliação de Expressões PosFix:

empilha operandos até encontrar um operador

55
retira o número de operandos; calcula e empilha o valor resultante
até que chegue ao final da expressão

Exemplo: A B / D E * + A -

function valor ( E): TipoValor;


Declarar x : TipoOperador;
inicio
topo  0;
enquanto not acabou(E) do

x  proxsimb(E);
Se x é operando então push(x,pilha)
senão

remove o número de operandos (dois) para o operador x da pilha;


calcule o resultado da operação;
empilhe resultado;
fim-se;
valor  P[topo];
fim;

56
2.7 Exercícios

1) Seja a função esvazie( ) tal que, recebe uma pilha como entrada, esvazie a
pilha descartando todos os seus elementos.
Escreva a função esvazie( )

2) Escreva um programa que verifique que expressões aritméticas estão com


a parentização correcta. Guarde o resultado numa pilha também. Seu
programa deve checar expressões para ver se cada "abre parênteses" tem um
"fecha parênteses" correspondente.

3) Escreva um algoritmo que converta uma expressão escrita na notação


parentizada no seu equivalente na notação polonesa reversa.

4) Uma palavra é uma palíndrome se a seqüência de letras que a forma é a


mesma seja ela lida da esquerda para a direita ou vice-versa. Exemplos:
arara, rairar, hanah. Escreva a função palíndrome que, dada uma palavra,
retorne true caso a palavra seja uma palíndrome, e false caso contrário.

A utilização da estrutura pilha é recomendada sempre que a


resolução de um problema qualquer necessitar do armazenamento
de valores utilizando-se a disciplina de acesso LIFO.

Fila

Conceito

É uma lista na qual os itens componentes devem ser eliminados


somente em uma extremidade denominada início da fila, e os itens
somente podem ser inseridos na outra extremidade, denominada
final da fila.

57
A disciplina de acesso que rege a inserção e remoção de elementos
em uma fila é denominada FIFO ("First In First Out"), ou em sua
forma aportuguesada, PEPS ("Primeiro a Entrar Primeiro a Sair").

A figura abaixo apresenta uma fila em diversos momentos distintos:

Figura 2: Diversos momentos de uma fila

Assim como nas pilhas, existem 3 operações primitivas principais


que podem ser aplicadas em uma fila: inserção de elementos,
remoção e verificação de fila vazia.

Escalonamento de "Jobs": fila de processos aguardando os recursos do


sistema operacional.

Operações associadas:

1.Criar (F) - criar uma fila F vazia


2.Inserir (x, F) - insere x no fim de F
3.Vazia (F) - testa se F está vazia
4.Primeiro (F) - acessa o elemento do início da fila
5.Elimina (F) - elimina o elemento do início da fila

Implementação de filas como lista

Sequencial ou
Encadeada ? Dinâmica ou Estática ?

Só tem sentido falarmos em fila sequencial ou encadeada dinâmica, uma vez


que não existe
movimentação de elementos. As filas encadeadas são usadas quando não há
previsão do
tamanho máximo da fila.

58
3.2 Implementação Sequencial de Fila

F: fila;
Começo, {posição anterior ao primeiro elemento}
Fim: índice; {posição do último elemento}

Operações com Filas

1. Criar (F) - criar uma fila F vazia

Procedure CriaFila (Começo, Fim);


Inicio
Começo 0;
Fim  0;
Fim;

2. Inserir (x, F) - insere x no fim de F

Procedure Inserir (x, F, Fim);


Inicio
Se Fim < maxfila Então

Fim  Fim + 1;
F[Fim] x;
Fim
Senão
{ OVERFLOW }
Fim;

3. Vazia (F) - testa se F está vazia

Function Vazia (Começo, Fim): Boolean;


Inicio

59
Se Começo = Fim Então
{FILA VAZIA}
Fim;

4. Primeiro (F) - acede o elemento do início da fila

Function Primeiro (F, Começo): TipoElem;


Inicio
x  F[Começo + 1];
Fim;

5. Elimina (F) - elimina o elemento do início da fila

Procedure Eliminar (Começo, Fim);


Inicio
Se (Começo = Fim) Então
{FILA VAZIA}
Senão
Começo  Começo + 1;
Fim;

3.3 Implementação Encadeada de Fila

Operações com Filas

1. Criar (F) - criar uma fila F vazia

Procedure CriaFila (Começo, Fim);


Inicio
Começo  nil;
Fim  nil;
Fim;

2. Inserir (x, F) - insere x no fim de F

60
{só se lista não vazia}

Procedure Inserir (x: TipoElem; Var Fim: fila);


Inicio
new(link(Fim));
Fim link(Fim);
F(info)  x;
link(Fim)  nil;
Fim;

3. Vazia (F) - testa se F está vazia

Function Vazia (Começo, Fim): boolean;


Inicio
Vazia  ( Começo = nil) e (Fim = nil);
Fim;

4. Primeiro (F) - acede o elemento do início da fila

Function Primeiro (Começo): TipoElem;


Inicio
Primeiro  F(info);
Fim;

5. Elimina (F) - elimina o elemento do início da fila

Procedure Elimina ( Começo, Fim);


Declarar p: fila;

Inicio

Se (começo <> nil) então


p  Começo;
Começo  link(Começo); {válido se lista não vazia}
Se ( Começo = Fim) Então

Começo  nil;
Fim  nil;
Fim;

61
dispose(p);

fim
Fim;

3.4 Problema na Implementação com Fila

O que acontece com a fila considerando a seguinte sequência de operações


sobre uma
fila IEIEIEIEIE. (I - inserção e E - eliminação)

A fila terá sempre 1 ou 0 elementos, no entanto num certo instante:

Ou seja, apenas um elemento na fila, o qual ocupa a última posição do array!


Na próxima inserção, teremos uma condição de overflow e a fila está vazia !

Alternativa: no algoritmo de eliminação após a actualização de Começo,


verificar se a fila ficou
vazia, i.e, Começo = Fim; se este for o caso, reinicializar Começo = Fim 
0;

Portanto, ficaria:

Procedure Eliminar (Começo, Fim);


Inicio
Se ( Começo = Fim) Então
{FILA VAZIA}
Senão

Começo  Começo + 1;
Se Começo = Fim Então

Começo  0;

62
Fim  0;
Fim;
Fim;
Fim;

O que aconteceria se a sequência fosse IIEIEIEIEI...

A lista estaria com no máximo dois elementos, mas ainda ocorreria overflow
com a lista quase
vazia.

Alternativa: Forçar Fim a usar o espaço liberado por Começo (Fila Circular)

3.5 Fila Circular

63
Fila Circular Implementada como Anel

1.Índices do array: 0 .. m-1


2.Ponteiros de controle: Começo e Fim
3.Inicialmente Começo = Fim = 0
4.Quando Fim = m-1, o próximo elemento será inserido na posição 0 (se
essa posição
estiver vazia!)
5.Condição de fila vazia Começo = Fim
6.Para inserir:

Procedure Inserir (Fim);


Inicio
Se Fim = m-1 então
Fim  0
Senão
Fim  Fim + 1; {ou Fim  ( Fim + 1 ) mod m}
Fim-se;
Fim;

7.Para eliminar um elemento

Procedure Eliminar (Começo);


Inicio

64
Começo  (Começo + 1) mod m;
Fim;

Problema: Nesta representação, como teremos fila cheia ???

Começo = Fim

Para resolver esse problema, utilizaremos apenas m-1 posições do anel.


Deste modo, existe
sempre um elemento vazio e, portanto, Fim não coincide com Começo.

O algoritmo para inserção em anel fica:

Procedure Inserir (Fim, Começo, F);


Inicio
Se (Fim + 1) mod m = Começo Então
{FILA CHEIA}
Senão

Fim  (Fim + 1) mod m; {um registo fica vazio}


F[Fim] := x;
Fim;
Fim;

O algoritmo para eliminação em anel fica:

Procedure Eliminar (Comeco, Fim);


Inicio
Se Começo = Fim Então
{FILA VAZIA}
Senão
Começo := (Começo + 1) mod m;
Fim;

3.6 Exercício

1. Escreva a função esvazie( ) recebfimo uma fila como entrada,


esvazie a fila descartando
todos os seus elementos.

65
ÁRVORES

1. Introdução
Árvores são estruturas de dados não lineares que caracterizam uma relação de
hierarquia entre os dados (nós) que as compõem, onde um conjunto de dados
pode se apresentar hierarquicamente subordinado a outro conjunto de dados.

Por definição, uma árvore é um conjunto finito A composto de um ou mais nós,


tal que:

 existe um nó denominado raiz da árvore;


 os demais nós formam m >= 0 conjuntos S1, S2,...,Sn, onde cada um
destes conjuntos é uma árvore. Estes conjuntos S1, S2,...,Sn recebem o
nome de subárvores.

Esquematicamente, uma árvore pode ser representada da seguinte maneira:

Os círculos representam os nós da árvore, cuja raiz é o nó A.

2. Terminologia
Por definição, cada nó da árvore é a raiz de uma subárvore. O no de subárvores
de um nó é o grau daquele nó. Um nó de grau 0 é denominado folha ou nó
terminal.

O nível do nó é definido da seguinte maneira: a raiz tem nível 1, e este vai


aumentando, a medida que desce-se para as folhas.

A altura de uma árvore é igual ao seu número de níveis. Assim sendo, a altura
do exemplo colocado acima é igual a 4.

66
A raiz de uma árvore é denominada pai das raízes de suas subárvores. As
raízes das subárvores de um mesmo nó são denominados irmãos, que, por sua
vez, são filhos de seu nó pai.

PAI RAIZ
NÍVEL 1
A

NÍVEL 2 FILHO RAIZ DA


B FILHO C SUBÁRVORE

FOLHA ALTURA
NÍVEL 3
D E F

FOLHA
NÍVEL 4
G H I

FOLHA FOLHA FOLHA


SUBÁRVORE

Uma árvore ordenada é aquela na qual os filhos de cada nó estão ordenados.


Assume-se que esta ordenação se desenvolva da esquerda para a direita. Desta
forma, as árvores da figura abaixo são distintas se consideradas ordenadas,
porém podem se tornar coincidentes mediante reordenação de nós irmãos.

A A

B C C B

D E F D F E

G H I G H I

Duas árvores não ordenadas são consideradas isomorfas quando puderem se


tornar coincidentes através de uma permutação na ordem das subárvores de
seus nós. Desta forma, as árvores da figura acima podem ser consideradas
isomorfas.

67
3. Representações Básicas para Árvores

3.1. Diagrama de Inclusão


A C E
H
B D
G F

3.2. Representação Hierárquica


A

B C

D E F

G H I

3.3. Diagrama de Barras


A
B
C
D
G
H
E
F
I

3.4. Parêntesis Aninhados


(A (B) (C (D (G) (H)) (E) (F (I))))

3.5. Representação Matricial

68
Índice Informação 1a Subárvore 2a Subárvore 3a Subárvore
1 A 2 3 0
2 B 0 0 0
3 C 4 5 6
4 D 7 8 0
5 E 0 0 0
6 F 9 0 0
7 G 0 0 0
8 H 0 0 0
9 I 0 0 0

No caso da representação matricial, o nó raiz da árvore ocupa a primeira linha


da matriz. Quando alguma das subárvores de um nó raiz se apresentar vazia, a
coluna do índice correspondente desta subárvore vale 0. Um nó folha apresenta
todas as colunas de suas subárvores iguais a 0.

4. Árvores Binárias
4.1. Definição

Árvores binárias são estruturas do tipo árvores, onde o grau de cada nó é


obrigatoriamente menor ou igual a 2. Além disso, as subárvores de um nó têm
de estar divididas em subárvores esquerda e direita. A figura abaixo apresenta
uma árvore binária.

4.2. Tipos de Árvores Binárias

Uma árvore estritamente binária é aquela em que cada nó possui exactamente


0 ou 2 nós. O item a) da figura abaixo apresenta uma árvore estritamente
binária.

Uma árvore binária completa é aquela que apresenta a seguinte propriedade:


se v é um nó tal que alguma subárvore de v é vazia, então v se localiza no
penúltimo ou último nível da árvore. O item b) da figura abaixo apresenta uma
árvore binária completa.

69
Uma árvore binária cheia é aquela em que, se v apresenta uma de suas
subárvores vazia então v se encontra obrigatoriamente no último nível da árvore.
Pode-se afirmar que uma árvore binária cheia também é completa e
estritamente binária. O item c) da figura abaixo apresenta uma árvore binária
cheia.

a) b) c)

Árvores binárias ziguezague ou degeneradas são aquelas que apresentam


sempre a altura h máxima possível dados os n nós da árvore. A figura abaixo
apresenta diferentes árvores ziguezague.

a) b) c) d)
A altura h mínima para uma árvore binária de n nós é conseguida sempre que
esta árvore binária for do tipo completa. É possível deduzir a altura mínima para
uma árvore de n nós através da seguinte fórmula:

h min = 1 + log n
onde n = número de nós da árvore.

Ex: para uma árvore binária de 15 nós:

h min = 1 + log 15  h min = 1 + 3.9  h min = 1 + 3  h min = 4

70
Esta dedução pode ser comprovada ao se analisar a árvore do item c) da figura
acima, que por ser do tipo cheia apresenta a altura mínima (igual a 4) dados os
seus 15 nós.

4.2. Caminhamento em Árvore Binária

Uma das mais importantes operações que pode ser aplicada em uma árvore
binária é o caminhamento, isto é, a habilidade de se mover através de todos os
seus nós.

Cada caminhamento impõe uma ordenação sobre os nós da árvore, isto é, a


ordem na qual eles são acedidos. Dos diversos tipos de caminhamentos
definidos, os três principais (central, pré-fixado e pós-fixado) são apresentados
abaixo. O princípio básico para a execução destes caminhamentos é decidir se o
nó raiz vai ser visitado antes de se caminhar para as suas subárvores, durante
este caminhamento, ou depois que todas as subárvores terem sido visitadas.

Os 3 tipos de caminhamento são resumidos a seguir:

a) Caminhamento Pré-Ordem ou Pré-Fixado:


1) Visite a Raiz;
2) Visite a Subárvore Esquerda;
3) Visite a Subárvore Direita;

b) Caminhamento Central:
1) Visite a Subárvore Esquerda;
2) Visite a Raiz;
3) Visite a Subárvore Direita;

c) Caminhamento Pós-Ordem ou Pós-Fixado:


1) Visite a Subárvore Esquerda;
2) Visite a Subárvore Direita;
3) Visite a Raiz;

Ex:

71
A figura abaixo mostra a seqüência em que o caminhamento central (Esquerdo,
Raiz, Direito) é executado em uma árvore binária.
ERD

A
ERD ERD

B C
ERD
4
D
3 2
1

O ponto de partida é a raiz da árvore 1 (nó A). Na árvore 1 devem ser visitadas a
sua subárvore da esquerda, a sua raiz e depois a sua subárvore da direita. A
subárvore esquerda da árvore 1 é a árvore 2, cuja raiz é o nó B.

Na subárvore 2 devem ser visitadas a sua subárvore da esquerda, a sua raiz e


depois a sua subárvore da direita. A subárvore esquerda da árvore 2 é a árvore
3, cuja raiz é o nó D.

Na subárvore 3 devem ser visitadas a sua subárvore da esquerda, a sua raiz e


depois a sua subárvore da direita. Uma vez que não existe subárvore à
esquerda de D, o próximo nó a se visitar é a raiz (nó D) da árvore 3. Uma vez
que também não existe subárvore à direita de D, o caminhamento central na
árvore 3 está terminado.

Encerrado o caminhamento na árvore 3, retoma-se o caminhamento na árvore 2,


que neste instante teve apenas a sua subárvore da esquerda visitada, restando
ainda visitar-se a sua raiz (nó B) e subárvore da direita. Uma vez que não existe
subárvore à direita de B, o caminhamento central na árvore 2 está terminado.
Encerrado o caminhamento na árvore 2, retoma-se o caminhamento na árvore 1,
que neste instante teve apenas a sua subárvore da esquerda visitada, restando
ainda visitar-se a sua raiz (nó A) e subárvore da direita. A subárvore direita da
árvore 1 é a árvore 4, cuja raiz é o nó C.

72
Na subárvore 4 devem ser visitadas a sua subárvore da esquerda, a sua raiz e
depois a sua subárvore da direita. Uma vez que não existe subárvore à
esquerda de C, o próximo nó a se visitar é a raiz (nó C) da árvore 4. Uma vez
que também não existe subárvore à direita de C, o caminhamento central na
árvore 4 está terminado.

Para o exemplo, encerrado o caminhamento central na árvore 4, encerra-se


também o caminhamento central na árvore 1, uma vez que tanto as subárvores
esquerda e direita desta árvore quanto sua raiz já foram visitadas.

5. Aplicações de Árvores Binárias


A estrutura de árvore binária pode ser utilizada em casos onde os dados ou
objectos a serem representados possuem uma relação de hierarquia entre si,
como por exemplo no caso da expressão matemática abaixo:

a * b + c / (d + e)

onde a relação hierárquica aparece na medida em que alguns operadores da


expressão têm maior precedência sobre outros, tornando possível a sua
representação através da seguinte árvore binária:

A representação é feita de modo que a prioridade das operações fique implícita:


o operador de menor prioridade da expressão fica na raiz da árvore. A
subexpressão que forma o operando da esquerda do operador dá origem a
subárvore à esquerda da raiz. Analogamente, o operando da direita dá origem à
subárvore da direita.

O caminhamento central sobre a árvore devolve a expressão original, enquanto


que os caminhamentos pré-ordem e pós-ordem devolvem a expressão na forma
pré-fixada e pós-fixada, respectivamente.

Outra aplicação de árvore binária é a ordenação e pesquisa de conjuntos de


dados. Estas árvores são denominadas árvores binárias de busca ou de
pesquisa/ordenação (ABPO).

6. Árvores de Busca ou de Pesquisa e Ordenação

73
O processo de colocação um elemento do conjunto de dados na árvore obedece
a seguinte regra:

a) se a árvore estiver vazia, o elemento é colocado na raiz da árvore;


b) caso contrário, se o elemento for menor ou igual ao elemento da raiz, ele é
posicionado à esquerda deste. Se o elemento for maior do que o da raiz, ele
é posicionado à direita da mesma.

Este procedimento, caso aplicado ao conjunto de dados apresentado a seguir:

42-51-19-37-42-86-71-10-75-22-31-42

resulta na árvore binária colocada abaixo:

O caminhamento central na árvore devolve os elementos em ordem crescente.

7. Implementação de Árvores Binárias de Busca

Da mesma maneira como implementamos as listas encadeadas em algoritmos


genéricos com o auxílio da alocação dinâmica, também as árvores binárias
podem ser implementadas da mesma maneira.

No caso das listas ligadas, ponteiros serviam para armazenar os endereços dos
elementos vizinhos a um nó qualquer da lista.
No caso das árvores binárias, os ponteiros terão a função de armazenar os
endereços dos filhos de cada nó raiz de uma subárvore. Estes filhos, enquanto
raízes de outras subárvores, também armazenarão o endereço de seus filhos, e
assim por diante. Nas folhas da árvore, os nós não possuem mais filhos, por isso
seus ponteiros vão apontar para NIL, indicando o fim daquela subárvore.

Esquematicamente:

74
ESQ ITEM DIR RAIZ

A
ESQ ITEM DIR ESQ ITEM DIR

B C

ESQ ITEM DIR ESQ ITEM DIR ESQ ITEM DIR


D E F

ESQ ITEM DIR

Por exemplo em Pascal, a estrutura necessária para se representar uma árvore


binária seria:

type ptr = ^nó;


nó = record
item : integer;
esq : ptr;
dir : ptr;
end; {record}

var raiz : ptr;

onde o campo item tem a função de guardar o elemento a ser armazenado no


nó da árvore; o campo esq guarda o endereço do filho à esquerda do nó e o
campo dir guarda o endereço do filho à direita do nó.

Em uma árvore binária de busca existem basicamente 4 operações que podem


ser efetuadas:

1. caminhamento (pré-fixado, central e pós-fixado);


2. inserir um novo elemento na árvore;
3. pesquisar um elemento na árvore
4. remover um elemento da árvore.

7.1. CAMINHAMENTO

Os procedimentos de caminhamento em árvore binária podem ser


implementados de maneira recursiva, o que os torna bastante simples.

7.1.1. Caminhamento Pré-Fixado

75
O procedimento recursivo que implementa o caminhamento pré-fixado pode ser
assim implementado:

procedure PreFix(raiz);
Início
Se raiz <> NIL então

Escrever(raiz^.item);
PreFix(raiz^.esq);
PreFix(raiz^.dir);
Fim-se
Fim; { procedure PreFix }

7.1.2. Caminhamento Central

O procedimento recursivo que implementa o caminhamento central pode ser


assim implementado:

procedure Central(raiz);
Início
Se raiz <> NIL Então

Central(raiz^.esq);
Write(raiz^.item);
Central(raiz^.dir);
Fim-se;
Fim; { procedure Central }

7.1.3. Caminhamento Pós-Fixado

O procedimento recursivo que implementa o caminhamento pós-fixado pode ser


assim implementado:

procedure PosFix(raiz);
Início
Se raiz <> NIL então

PosFix(raiz^.esq);
PosFix(raiz^.dir);
Write(raiz^.item);
Fim-se;
Fim; { procedure PosFix }

7.2. INSERÇÃO

76
No caso da inserção de elementos em uma árvore binária de ordenação e
pesquisa, o esquema seria:

Em linguagem algorítmica, a procedure para inserção seria implementada da


seguinte maneira:

procedure InsArv(raiz, nó_aux);


Início
Se raiz = NIL então
raiz  no_aux
Senão
Se no_aux^.item <= raiz^.item então
InsArv(raiz^.esq, no_aux)
Senão
InsArv(raiz^.dir, no_aux);
Fim-se;
Fim-se;
end; {Procedure InsArv}

A chamada da procedure InsArv, executada, por exemplo, a partir do programa


principal deveria ser assim codificada:
.....
.....
new(no_aux);
Ler(no_aux^.item);
no_aux^.esq  NIL;
no_aux^.dir  NIL;
Se raiz = NIL então
raiz no_aux
Senão
InsArv(raiz, no_aux);
.....
.....

77
7.3. PESQUISA

O esquema genérico para a operação de pesquisa de um elemento qualquer


dentro da árvore de busca seria:

Em linguagem algorítmica, a operação de localização poderia ser implementada


da seguinte maneira:

procedure PesqArv(raiz, chave, posição);


Início
Se raiz = NIL então
posição  NIL
Senão
Se raiz^.item = chave {achou}
posição  raiz
Senão
Se chave <= raiz^.item then {pesquisa à esquerda}
PesqArv(raiz^.esq, chave, posição)
Senão { pesquisa à direita}
PesqArv(raiz^.dir, chave, posição);
Fim-se;
Fim-se;
Fim-se
Fim; {PesqArv}

A variável posição devolve o endereço do elemento encontrado na árvore, ou


então NIL, tanto no caso da árvore estar vazia ou então do elemento não ter sido
encontrado na árvore. A chamada a procedure PesqArv seria:

.....
.....

78
PesqArv(raiz, 20, ponteiro);
Se ponteiro = NIL então
Escrever("O elemento não existe na árvore")
Senão
Escrever("O elemento 20 da árvore: ", ponteiro^.item);
.....
.....

A ordem na qual os dados são inseridos influencia na eficiência da busca dentro


da árvore. O pior caso é aquele onde os dados possuem algum tipo de
ordenação prévia antes da inserção. A figura abaixo ilustra alguns conjuntos de
dados e as árvores de busca resultantes.

a)
b) 1-2-3-4-5-6-7-8-9
c) 5-4-3-2-1-6-7-8-9
d) 9-8-7-6-5-4-3-2-1
e) 5-2-7-1-6-4-3-9-8

1 5 9 5
2 4 6 8 2 7
3 3 7 7 1 4 6 9
4 2 b) 8 6 3 8
1 5 d)
5 9
4 c)
a) 6
3
7
2
8
1
9

Na média a pesquisa será mais eficiente na árvore de item d) pois sua altura é
mínima em relação aos seus 9 nós. A pesquisa será mais ineficiente nas árvores
a) e c), pois tratam-se de árvores degeneradas. A organização física das árvores
da figura acima foi ditada pela ordem em que os dados foram inseridos. As
árvores a) e c), mais ineficientes, originaram-se de um conjunto de dados
previamente ordenado. Já a árvore d), mais eficiente, originou-se de um
conjunto de dados não ordenado.

7.4. REMOÇÃO

Das operações em árvores binárias de busca, a remoção é a mais complexa


para ser implementada, uma vez que pode ser necessário um rearranjo dos nós
da árvore, dependendo da posição do nó a ser removido. Deve-se considerar 2
possibilidades distintas:

79
a) quando o nó a ser removido possui uma ou ambas as subárvores vazias;
b) quando o nó a ser removido possui ambas as subárvores ocupadas.

No primeiro caso, a solução é bastante simples, uma vez que, no caso de


ambas as subárvores estarem vazias, basta remover o nó, fazendo o ponteiro de
seu pai passar a apontar para NIL.

No caso de apenas uma das subárvores do nó a ser removido estar ocupada,


basta fazer o ponteiro de seu pai apontar para seu filho, ao invés de para si
próprio. Esquematicamente:

Para o último caso, onde ambas as subárvores do nó a ser removido estão


ocupadas, uma das soluções é fazer o ponteiro do nó a ser removido apontar
para a sua subárvore da direita, e realocar a sua subárvore da esquerda, de
modo que ela se coloque mais a esquerda possível da subárvore da direita.
Muito embora, a primeira vista possa parecer complicado, a figura a seguir com
certeza ajuda a desvendar o mistério, sendo que o nó removido é o de no 8.

Em linguagem algorítmica, a procedure que realiza a remoção de elementos em


árvores binárias seria implementada mais ou menos assim:

procedure DeletaElementoDaArvore (posição);


Declarar temp : ptr;
Início
Se posição = NIL então
Escrever("Erro, o nó a ser removido não existe")
Senão
Se posição^.dir = NIL então

80
temp  posição;
posição  Link(posição^.esq);
dispose(temp);

Senão
Se posição^.esq = NIL então

temp  posição;
posição  posição^.dir;
dispose(temp);

Senão {ambas as subárvores estão ocupadas}

temp  posição^.dir;
Enquanto temp^.esq <> NIL Faça { procura a posição mais a esquerda }
temp  Link(temp^.esq); { possível na subárvore da direita }
Fim-enquanto
Link(temp^.esq)  posição^.esq; { liga esta posição na subárvore da esquerda }
tempposição;
posição  Link(posição^.dir); { liga o avô com o neto da direita }
dispose(temp);
Fim-se;
Fim-se;
Fim-se;
Fim-se;
Fim; {proc DeletaElementoDaArvore }

B. Grafos

1. Definição

Quando analisamos um conjunto de elementos de dados (não necessariamente


dados computacionais), podemos estar preocupados com o seu conteúdo ou
com as relações existentes entre eles. A Teoria dos Grafos preocupa-se com o
estudo das relações existentes entre diversos objectos de análise, podendo ser
utilizada em qualquer área que necessite de organização de dados: sociologia,
pesquisa operacional, química, etc.
O grafo propriamente dito é uma representação gráfica das relações exitentes entre
elementos de dados. Ele pode ser descrito num espaço euclidiano de n dimensões como
sendo um conjunto V de vértices e um conjunto A de curvas contínuas (arestas).

Um vértice é representado por um círculo e uma curva é representada pela representação


gráfica plana característica, ou seja, um segmento de reta. Quando uma curva possui
indicação de sentido (uma seta), ela é chamada de arco , caso contrário é chamada de
linha .

81
As principais características desta estrutura são:

1. toda curva fechada de A tem apenas um ponto de V ;

2. toda curva aberta de A tem exatamente dois pontos de V

3. as curvas de A não têm pontos comuns exceto os elementos do conjunto V .

 Quando um grafo possui arcos, o mesmo denomina-se grafo dirigido ou dígrafo .

Ex.:

 Quando um elemento do conjunto A (curvas) tem o mesmo elemento do conjunto V (vértices)


como ponto inicial e final, dizemos que este elemento é um laço .

Ex.:

2. Operações Sobre Grafos


Nós podemos aplicar operações sobre um grafo para que ele melhor represente as operações que sofrem os
seus componentes: os dados.

2.1. Operações elementares

São elas:

82
 Inserção de uma curva ou vértice;

 Retirada ou Eliminação de curvas e/ou vértices;

 Rotação ou redesignação dos vértices pela troca entre eles num dos sentidos;

 Fusão ou junção de dois vértices em um;

 Inversão ou troca do sentido dos arcos.

83
2.2. Operações Binárias

São operações como as realizadas na Teoria dos Conjuntos. Seu objectivo é


fazer operações sobre os elementos dos grafos (vértices e arestas).

Sejam os grafos G1(V1, A1), G2(V2, A2) e G3(V3, A3), onde V1, V2 e V3 são os
conjuntos de vértices e A1, A2 e A3 são os conjuntos de arestas dos grafos.

Sobre eles serão realizadas as seguintes operações:

A. União

É a junção de dois grafos através de vértices em comum, se existentes.

G'(V', A') U G"(V", A") = G(V, A), V = V' U V" e A = A' U A".

Ex.:

B. Intersecção

Tem como resultado os elementos em comum entre dois grafos.


G'(V', A') G"(V", A") = G(V, A), V = V' V" e A = A' A".

Ex.: G1 G2: G1 G3: Ø

C. Diferença

É a retirada de elementos comuns do primeiro grafo em relação ao segundo.

84
G'(V', A') - G"(V", A") = G(V, A), V = V' - V" e A = A' - A".

Ex.:

3. Caracterização dos grafos

Para podermos discernir melhor as características dos grafos, temos um


conjunto de conceitos que expressam a peculiaridade de cada grafo.

3.1. Cardinalidade

A cardinalidade de um conjunto de vértices é igual à quantidade de seus


elementos. É também chamada de ORDEM.
Ex.:

C(V) = 4
A cardinalidade de um conjunto de curvas é igual ao número e elementos que o
compõe. Ex.: Para G(V, A) do exemplo anterior: C(A) = 4.

3.2. Vértices Adjacentes

Dois vértices são adjacentes se existir uma curva entre eles. Dado dois nós x e
y, a representação de uma linha é dada por xy e de um arco por xy.

3.3. Vértices Conexos

85
Dois vértices são conexos se são adjacentes ou possuem uma relação de
adjacência, ou seja, dados dois vértices x e y, existe um nodo x1 que é
adjacente a x, outro x2 que é adjacente a x1, e assim sucessivamente.

Ex.: No grafo ao lado, C é conexo a D


3.4. Cadeia

É um conjunto qualquer de linhas de um grafo.


Ex.: Para o exemplo anterior, são cadeias DAB, CB.

3.5. Ciclo

É uma cadeia fechada, onde a extremidade inicial e final coincidem.

Ex.: Os ciclos ABCDA e CBADC foram retirados do grafo

3.6. Caminho

É um conjunto qualquer de arcos de um grafo.

Ex.: Para o grafo são caminhos DA e BCDA.

3.7. Número Cromático

É o menor valor de cores necessárias para colorir os vértices de um grafo, sem


que vértices adjacentes tenham a mesma cor.

86
Ex.:

número de cores (n) = 2 e n=4

4. Tipos de Grafos

4.1. Grafo parcial

Se G2 é um grafo parcial de G1, então G2 possui um mesmo conjunto de


vértices, mas não o mesmo de arestas, do grafo G1.

Ex.:

4.2. Subgrafo

Se o grafo G2 é um subgrafo de G1, então G2 possui um número de vértices


inferior aos de G1, embora mantendo o número de curvas entre os vértices
existentes.

Ex.:

87
4.3. Subgrafo parcial

Se o grafo G2 é um subgrafo parcial de G1, então G2 é um subgrafo que não


mantém todas as curvas entre os vértices existentes.

Ex.:

4.4. Grafo valorado


É o grafo que possui valores nos vértices e nas curvas.
Ex.:

4.5. Multigrafo

É o grafo que possui ao menos um par de vértices ligados por duas ou mais
curvas.

88
Ex.:

4.6. Grafo denso

É o grafo que possui alta cardinalidade de vértice ou curva.

4.7. Grafo pouco povoado

É o grafo que possui baixa cardinalidade de vértice ou curva.

4.8. Grafo conexo

É aquele onde qualquer par de vértice for conexo.


Ex.:

Contra-exemplo:

4.9. Grafo-árvore

É aquele que possui uma das seguintes características:

1. é conexo e sem ciclos;


2. é conexo e tem n vértices e n-1 linhas;
3. sem ciclos e tem n vértices e n-1 linhas;

89
4. sem ciclos e, inserindo uma nova linha, é possível formar um ciclo.

Ex.:

4.10. Grafo planar

É aquele que permite a representação no plano sem que as linhas se cruzem.


Ex.:

4.11. Grafo de Kuratowski

É um tipo de grafo aceito como não-planar.

Exs.:

4.12. Rede de PERT

É um grafo dirigido valorado que possui uma entrada, uma saída e não tem
laços nem ciclos. A entrada é o vértice que serve como extremidade final para
arcos e saída é o vértice que serve apenas como extremidade inicial para arcos.
Este tipo de grafo é utilizado na análise de eventos temporais, que necessitam
do acompanhamento do fluxo de dados em relação ao tempo.

90
Ex.:
Uma fábrica possui cinco módulos de processamento robotizado. Cada módulo
possui uma identificação e uma função específica. A rede de PERT ao lado
representa o tempo de uma peça sendo processada entre os módulos.

5. Representação dos Grafos

Há diversas formas de representação de grafos em estruturas de dados:


podemos representar as adjacências entre os vértices, as arestas, e assim por
diante. A seguir vamos analisar a forma de representação através da matriz de
adjacências e lista de adjacências , onde descreve-se a relação entre os
vértices.

5.1. Matriz de Adjacências

Podemos representar um grafo através de uma matriz de dimensão C(V) x C(V),


onde o conteúdo poderá ser de números binários (0;1) ou inteiros (-1;0;1).
Para exemplificarmos, analisemos o grafo G :

A cardinalidade de vértices do grafo G é 6, portanto, para sua representação,


deveremos ter uma matriz de adjacências 6x6. Utilizando valores binários,
podemos representar desta forma:

91
ma[i,j] =

Conforme observamos, a matriz de adjacências ao lado foi formada seguindo a


regra: ma[i,j] = 1, se i é adjacente a j, 0 caso contrário . Como estamos
trabalhando com um dígrafo, devemos estabelecer qual é a origem e qual é o
destino. No exemplo apresentado, a origem está definida pela letra indicadora
de linha. Por exemplo, A está conectado com B , mas não o contrário, por isso
ma[A,B] é 1 e ma[B,A] é 0.

Para resolvermos isso, podemos fazer uma representação alternativa: ma[i,j] = -


1 se i é origem da adjacência com j, 1 se i é o destino da adjacência com j e
0 para os demais vértices não envolvidos na adjacência . Isso é sintetizado
na seguinte matriz:

ma[i,j] =

Como exemplo, observemos o elemento ma[A,B] , ma[A,C] e ma[A,D] , que


possuem valor -1. Isto indica que A é origem de arcos para C , D e E . Também
observemos ma[F,D] e ma[F,E] , com valor 1, indicando que F recebe os arcos
de D e E .

Apesar da metodologia empregada, observa-se que a dada aplicação que


necessite de alteração de grafo, seria inadequada a representação através de
estruturas fixas, exigindo, então, estruturas dinâmicas.

5.2. Lista de Adjacências

Para que seja possível a remodelagem de um grafo em tempo de execução,


torna-se necessária a alocação dinâmica de sua representação. Por isso, a
representação de adjacências entre vértices pode ser feita através de listas
lineares.

92
Sua constituição é realizada por um vetor dinâmico ou lista encadeada formando
um índice de vértices. De cada elemento de índice parte uma lista encadeada
descrevendo os vértices adjacentes conectados.

Como exemplo, para o grafo G apresentado na seção anterior, visualizaremos a


seguinte representação:

A lista encadeada é formada por nodos que contém o dado do vértice (letra) e o
ponteiro para o vértice adjacente ao indicado no índice (vetor descritor dos
vértices).

Eventualmente os nodos poderão exigir outros campos, tais como marcação de


visita ao vértice, dados adicionais para processamento da seqüência do grafo,
etc.

Esta é a forma de representação mais flexível para a representação de grafos.


Obviamente que aplicações específicas permitirão o uso de outras formas de
representação. Cabe aqui salientar que é possível a descrição de grafos através
de suas arestas, o que pode exigir uma matriz para sua descrição.

5.3. Matriz de Incidência

Este tipo de matriz representa um grafo a partir de suas arestas. Como exige
muitas vezes a alocação de uma matriz maior do que no método da matriz de
adjacências, não é tão utilizada quanto aquela. A matriz alocada deverá ter
dimensões C(V) x C(A).

O princípio desta representação está na regra: mi[i,j] = 1 se o vértice i incide


com a aresta j, 0 caso contrário . Exemplificando a partir do grafo G
anteriormente apresentado, teremos uma matriz:

93
i[i,j] =

6. Caminhamento em Grafos

Para percorremos os vértices dos grafos nas representações analisadas


anteriormente, é necessário que tenhamos técnicas para caminhamento.
Veremos duas delas: em amplitude e em profundidade.

6.1. Caminhamento em Amplitude

Este caminhamento é feito através dos seguintes passos:


Escolhe-se um vértice para o início do caminhamento;
visitam-se os vértices adjacentes, marcando-os como visitados;
coloca-se cada um dos vértices adjacentes numa fila;
após visitados os vértices adjacentes, o primeiro da fila torna-se o novo vértice
inicial e reinicia o processo;
termina quanto todos os vértices tiverem sido visitados ou o vértice procurado for
encontrado.

Assim, para o grafo K a seguir:

94
a ordem de visitação, se for escolhido inicialmente o vértice
C, o adjacente será D (primeiro da fila). Como não há outro
vértice adjacente, escolhe-se o primeiro da fila (D) para
recomeçar o processo. O adjacente a D é B, o qual vai para
a fila. E novamente não há outro adjacente, então escolhe-
se o primeiro da fila, que é B. Repete-se o processo,
visitando-se A, que vai para a fila. Mais uma vez, recomeça
as visitas a partir de A, que era o primeiro da fila. Visita-se
B, mas verifica-se que ele já está marcado como visitado.
Então, visita-se C, mas ele também já foi marcado.
Finalmente, visita-se D, que também já foi visitado,
encerrando, então o caminhamento. A ordem de
caminhamento pode ser vista ao lado.

O caminhamento em amplitude é realizado quando há interesse em avaliar-se todos os


vértices principais antes de suas derivações posteriores. O efeito contrário pode ser obtido
com o caminhamento em profundidade.

6.2. Caminhamento em Profundidade

Este tipo de caminhamento é feito quando pretende-se explorar ao máximo determinada


ramificação do grafo. Seu algoritmo é composto pelos passos:

Escolhe-se um vértice inicial;

visita-se um primeiro vértice adjacente, marcando-o como visitado;

coloca-se o vértice adjacente visitado numa pilha;

o vértice visitado torna-se o novo vértice inicial;

repete-se o processo até que o vértice procurado seja encontrado ou não haja mais
vértices adjacentes. Se verdadeiro, desempilha-se o topo e procura-se o próximo
adjacente, repetindo o algoritmo;

o processo termina quando o vértice procurado for encontrado ou quando a pilha


estiver vazia e todos os vértices tiverem sido visitados.

Seja o grafo T a seguir:

95
a seqüência de caminhamento inicia com a escolha de um vértice, digamos, A. Sendo A o
vértice inicial, visita-se primeiro um de seus vértices adjacentes, por exemplo, B.
Empilha-se A e B torna-se o novo vértice inicial. Mais uma vez, escolhe-se um adjacente,
no caso há só um, E. Empilha-se B e E torna-se o novo vértice inicial. Como não há mais
vértices adjacentes, desempilha-se o topo, ou seja, B. Mas B também não tem mais
adjacentes, então desempilha-se A, o qual possui C como novo adjacente. Então C torna-
se o novo vértice inicial, empilhando-se novamente A. O vértice adjacente F é escolhido
e C é empilhado. Como F não tem adjacentes, desempilha-se C e visita-se D. O vértice C
é empilhado novamente e D é o novo inicial. Como seu adjacente já foi visitado,
desempilha-se C. Como C não tem mais adjacentes, desempilha-se A. Como A não tem
mais adjacentes, termina a busca. O grafo abaixo indica a ordem de visitação.

7. ORDENAÇÃO

Ordenação dos dados (classificação)


1. Introdução

O objectivo da ordenação dos elementos de uma estrutura de dados, é facilitar e


aumentar a eficiência das operações de pesquisa sobre esses dados. A ordenção pode
ser crescente ou decrescente. O problema a ser resolvido com a ordenação é o
seguinte:

Entrada: sequência de n números <a1,a2, …, an>

96
Saída: sequência dos n números de entrada reordenada, gerando <a1’, a2’, …, an’>
de forma que: a1’≤ a2’, …, ≤ an’ (para o caso de ordenação crescente) ou
a1’≥ a2’, …, ≥ an’ (para o caso de ordenação decrescente).

A sequência de entrada, normalmente, é um vector com n elementos, embora possa


ser representada por intermédio de outras estruturas de dados como por exemplo, uma
lista encadeada.

Na prática os números a serem ordenados, raramente, são valores isolados.


Normalmente, cada número é componente de um conjunto de dados denominda
registo, sendo que um conjunto de registos forma uma tabela. Cada registo contém
uma chave, que é o valor a ser ordenado, e demais valores que sempre acompanham a
chave. Assim, numa operação de ordenação, sempre que for preciso trocar a posição
de uma chave, será necessário alterar a posição de todos os elementos do registo. Na
prática, quando os registos possuem uma grande quantidade de dados (além da
chave), a ordenação é realizada sobre um vector (ou lista) de ponteiros para os
registos, com o objectivo de minimizar as operações de movimentação de dados.

Exemplos:

A- Contiguidade Física: As entradas são fisicamente rearranjadas (todos


os elementos de um registo são ordenados fisicamente)

Tabela não ordenada Tabela ordenada

Reg. Chave demais campos reg. Chave demais campos


1 3 xxx xxx xxx 1 1 kkk kkk kkk
2 7 yyy yyy yyy 2 2 yyy yyy yyy
3 5 zzz zzz zzz 3 3 xxx xxx xxx
4 1 kkk kkk kkk 4 4 ttt ttt ttt
5 4 ttt ttt ttt 5 5 zzz zzz zzz
6 2 uuu uuu uuu 6 6 yyy yyy yyy

B- Vector Indirecto de ordenação: As entradas são mantidas nas


posições originais. A sequência é dada por um vector gerado durante o
processo de classificação (não envolve movimentação dos registos em
uma tabela).

Tabela desordenada Vector de


ordenação
Reg. Chave demais campos Indice da tabela

97
1 3 xxx xxx xxx 1 4
2 7 yyy yyy yyy 2 6
3 5 zzz zzz zzz 3 1
4 1 kkk kkk kkk 4 5
5 4 ttt ttt ttt 5 3
6 2 uuu uuu uuu 6 2

C- Encadeamento: As entradas são mantidas nas posições originais. É


formada uma listra encadeada com a ordenação. Utiliza-se um campo a
mais na tabela para armazenamento da lista, e não mais vector
adicional (vector indirecto de ordenação). É preciso utilizar um
ponteiro para o primeiro elemento.

reg. chave demais campos Campo proximo


1 3 xxx xxx xxx xxx xxx xxx 5
2 7 yyy yyy yyy Yyy yyy yyy -
3 5 zzz zzz zzz Zzz zzz zzz 2
4 1 kkk kkk kkk Kkk kkk kkk 6
5 4 ttt ttt ttt Ttt ttt ttt 3
6 2 uuu uuu uuu Uuu uuu uuu 1

4 Ponteiro para o primeiro elemento da tabela (reg. 4 contém chave 1)

Os métodos de ordenação podem ser plicados para classificação interna ou


externa. Uma ordenação interna é utilizada quando o conjunto de dados a ser classificado
é pequeno, podendo a ordenação ser realizada inteiramnente (ou quase inteiramente) na
memória principal. A ordenação externa é utilizada quando o conjunto de dados é grande
a ponto da ordenação não poder ser realizada na memória principal. Nesse caso a
ordenação é realizada sobre dados armazenados na memória secundária ( disco,fita,...).

No presente texto serão abordados os seguintes métodos de ordenação interna:


1. Ordenação por Inserção
- inserção directa
- incrementos decrescentes (Shell sort)
2. Ordenação por Troca
- método da bolha (Bubble sort)

98
- método da troca e partição (quicksort)
3. Ordenação por Selecção
- selecção directa
- selecção em árvore (heapsort)

2. Ordenação por Inserção


Nesse método os elemntos são inseridos na sua posição correcta, em relação aos
elementos já classificados.
2.1. Inserção Directa
É o método mais simples, utilizado para um conjunto pequeno de dados. Possui
baixa eficiência. Nesse método o vector a ser ordenado é dividido em dois segmentos: o
primeiro segmento contém os elemntos já ordenados; e o segundo segmento contém os
elemntos a serem ordenados.
Algoritmo:
- Primeiro elemento está no vector ordenado e os demais no vector desordenado;
- Retirar o primeiro elemento do vector desordenado e colocá-lo no vector ordenando, na
sua posição correcta;
- Repetir o processo para todos os elementos do vector desordenado.

99
Const
TAM = 10
Tipos
V = vector [1..TAM] de inteiros
Var
Vet:v
I , j ,k , temp:inteiro
Achei:logico
Inicio
Para i = 1 até TAM faça
Leia(vet[i])
Fim-para
Para i = 2 até TAM faça
J=1
Achei = Falso
Enquanto (j<i) e (Nao achei) faça /* Compara o */
Se vet[i] < vet[j] Então /* vet[i] com */
Achei = Verdadeiro /* os que estão */
Senão /* à sua */
J = j +1 /* esquerda */
Fim-se
Fim-Enquanto
Se achei Então
Temp = vet[i]
K=i–1
Enquanto k >= j Faça /* Desloca o */
Vet[k+1] = vet[k] /* vector para */
K = k- 1 /* a direita */
Fim-Enquanto
Vet[j] = temp
Fim-Se
Fim-Para
Fim .

Outra versão:
Const
TAM = 10
Tipos
V = vector[1..TAM] de inteiros
Var
Vet:v
I, j, k, temp: inteiro
Achei:Logico
Inicio
Para i = i até TAM Faça
Leia (vet[i])
Fim-Para
Para j = 2 até TAM Faça
Chave = vet[j]
I=j–1
Enquanto (i > 0) e (vet[i] > chave) Faça
Vet[i + 1] = vet[i]

100
I=i–1
Fim-Enquanto
Vet[i + 1] = chave
Fim-Para
Fim.

2.2. Incrementos Decrescentes (Shell sort)


Proposto por Ronaldo L. Shell (1959), é uma extensão do algoritmo de inserção
directa. A diferença com relação à inserção directa é o número de segmentos do vector.
Na inserção directa é considerado umúnico segmento do vector onde os elementos são
inseridos ordenadamente. No método do Shell são considerados diversos segmentos.
A ordenação é realizada em diversos passos. A cada passo está associado um
incremento I, o qual determina os elementos que pertencem a cada um dos segmentos:
- segmento 1 – vet[1], vet[1+I], vet[1+2I],...
- segmento 2 – vet[2], vet[2+I], vet[2+2I],...
...
segmento k – vet[k], vet[k+I], vet[k+2I],...
A cada passo todos os elementos (segmentos) são ordenados isoladamente por
inserção directa. No final de cada passo o processo é repetido para um novo incremento I
igual a metade do anterior, até que seja executado um passo com incremento I=1. O valor
do incremento I é sempre uma potência inteira de 2. O valor do incremento inicial é dado
por 2 ** NP, onde NP é o número de passos para ordenar o vector ( fornecido pelo
usuário, NP é uma aproximação incial ). Assim, para NP = 3 o valor do incremento em
cada passo seria:
- I = 2 ** 3 =8
- I = 2 ** 2 =4
- I = 2 ** 1 =2
- I = 2 ** 0 =1
Exemplo: NP=2
Vector original (desordenado)
1 2 3 4 5 6 7 8 9 10 11 12
15 27 40 13 19 20 45 35 50 41 25 10

Primeiro passo: I = 2** NP = 4

Segmento 2
Segmento 1 Segmento 4
2 6 1
1 5 9 0 4 8 1
15 1 5 27 directa
2 4 em cada 2
Aplicando inserção
9 0 0 1 13 3 1
segmento:
5 0

Segmento 3 10 1 3
3 5
3 7 1
25 4 4 1
15 1 5
9 0 40 0 4 5 2
5 5
101
27 2 4
0 1
Obtém-se o vector:

1 2 3 4 5 6 7 8 9 10 11 12
15 20 25 10 19 27 40 13 50 41 45 35

Segundo passo : I = IDIV 2 = 2 (ou: NP – 1, I = 2 ** NP = 2)

Segmento 1 Segmento 2
1 3 5 7 9 11
15 20 25 10 19 27 2 4 6 8 10 12
20 10 27 13 41 35

Aplicando inserção directa em cada segmento:

10 13 20 27 35 41
15 19 25 40 45 50

Obtém-se o vector:

1 2 3 4 5 6 7 8 9 10 11 12
15 10 19 13 25 20 40 27 45 35 50 41

Terceiro passo: I = IDIV 2 = 1 (ou: NP – 1, I = 2 ** NP = 1)

Nesse último passo os elementos estão próximos das suas posições finais, o que leva a
um menor número de trocas.

Aplicando inserção directa ao vector obtido no passo anterior obt’em-se o vector


ordenado:

1 2 3 4 5 6 7 8 9 10 11 12
10 13 15 19 20 25 27 35 40 41 45 50

Algoritmo:
const TAM = 12
Tipos v = vector [1..TAM] de inteiros
var vet: v
np, i, j, inc : inteiro
Início Para i = 1 até TAM Faça
Leia (vet [i] )
Fim - Para

102
Leia (np)
Para i = np até 0 passo -1 Faça
Inc = 2 * * i
Para j = 1 até inc Faça
Método_Shell (vet, inc, j, TAM)
Fim_Para
Fim-Para
Fim.

/*************************************************************/
Procedimento Metodo_shell (Ref vet, r, s, n)
Inicio
Var

i, j, k, temp: inteiro
achei: lógico

Para i = (s + r) até n passo r Faça


J = s
achei = FALSO
Enquanto ( j < i) E (Não achei)
Se vet [i] < vet [j] então
Achei = VERDADE

Senão
j = j + r
Fim_se
Fim enquanto
Se achei Então
temp = vet [i]
k = i + r
Enquanto k > (j - r) Faça
Vet [k + r] = vet[k]
K= k - r
Fim_enquanto
vet[j] = temp
Fim_se
Fim_para
Fim _Método_Shell

3.Ordenaçao por Troca

Numa abordagem, durante o caminhamento no vector, se os dois elementos são


encontrados fora de ordem, suas posições são trocados. São realiazadas comparações

103
sucessivas de pares de elementos. A estratégia de escolha de pares de elementos
estabelece a diferença entre os dois métodos de ordenação por troca.

3.1 Método da Bolha (bubble sort)

É um método bastante simplis, porém lento. Onome bolha se deve ao fato de que os
flutuam até a sua corecta posição como bolhas. O Algorítmo é o seguinte:

- A cada passo, cada elemento é comparado com o próximo, Se o elemento estiver fora de
ordem, a troca é realizada
- Realizam-se tantos passos quantos necessários até que não ocorram mais trocas.

Obs. Logo no primeiro passo o mair valor vai para o fim.

Exemplo: 500 85 515 60 910 170 890 275 650 430

Passo 1: 85 500
60 515
170
910
890 910
257 910
650 910
430 910
85 500 60 515 170 890 275 650 430 910
Passo 2: 85 60 500 170 515 275 650 430 890 910
Passo 3: 60 85 170 500 275 515 430 650 890 910
Passo 4: 60 85 170 275 500 430 515 650 890 910
Passo 5: 60 85 170 275 430 500 515 650 890 910
Passo 6: nenhuma troca

Algorítmo

/* Neste algorítmo , a cada passo, a variavel K contem a última posição trocada


Após esta, todas já estão classificadas.
*/
Const
TAM = 20
Tipos

V = vector [1...TAM] de inteiros

Var

Vet: v
i, temp, lim , k: inteiro
Troca: lógica

104
Início
Para i = 1 até TAM Faça
Leia (vet[i])
Fim _Para
Troca = VERDADEIRO
Lim = TAM – 1

Enquanto troca Faça


Troca = FALSO
Para i = 1 até lim Faça
Se vet[i] > vet [ i +1 ] então
tem = vet[i]
vet [i] = vet [ i + 1 ]
vet [ i + 1 ] = tem
k= i
troca = verdadeiro
Fim-se
Fim-para
Lim = k
Fim-enquanto
Fim.

3.2 Método de troca e Partição (quic sort)

É o mais rápido entre os métodos apresentados até o momento, e também o mais


utilizado.Esse método foi proposto por C. A. R. Hoare em 1962 e parte do princímpio
que é mais rápido classificar dois vectores com n/2 elementos cada um, do que um com n
elementos (dividir um problema maior em dois menores).

A parte maisdelicada do método é o particionamento do vector. O vector é particionado


em tês segmentos:

V[1],.....,V[i - 1] V[i] V[i + 1],.....,V[n]


( segmento 1) ( segmento 2) ( segmento 3)

A partição é realizada através de escolha arbitrária de elementos menor ou igual a M (um


número pré-estabelecido), aplica-se um método simplis de ordenação.

o algorítmo do quick sort é o seguinte:

 Escolher arbitrariamente um elemento do vector (normalmente o meio) e colocá-


lo em uma variável auxiliar X;
 Inicializar dois ponteiros I e J (I=1 e J=n);
 Percorrer o vector a partir da direita até que se encontre um V[I] ≥ X
(incrementando o valor de I)

105
 Percorrer o vector a partir da direita até que se encontre um V[I] ≤ X
(decrementando o valor de J);
 Trocar os elementos V[I] e V[J] (estão forado lugar) efazer I =I +1 e J =J – 1;
 Continuar esses processos até que I e J se cruzem em algum ponto do vector;
 Aós obtidos os dois elementos do vector através do processo de parição, cada um
é ordenado recursivamnte

Algoritmo:
Procedimento Quicksort (esq, dir: inteiro)
Início
Var
X, i, j, aux :inteiro;
i = esq
j = dir
x = v[ (i + j) Div 2]
Repita
Enquanto x > v [i] faça
i = i +1
Fim-Enquanto
Enquanto x < v [j] faça
j=j-1
Fim-enquanto
Se i <= j Então
aux = v[i]
v [i] = v [j]
v [j] = aux
i=i+1
J=J–1
Fim-se
Até que i > j
Se esq < j Então
Quicksort (esq, j)
Fim-se
Se dir > i Então
Quicksort (i, dir)
Fim-se
Fim.

4. Ordenação por selecção

Nessa abordagem, é realizada uma selcção sucessiva do menor (ou maior) valor
contido no vector. A cada passo este menor (maior) valor é colocado na sua posição
correcta. Repete-se o processo para o segmento que contém os elementos não
seleccionados.

106
5. Selecção Directa

- A cada passo encontra-se o menor elemento dentro do segmento com os


elementosnão seleccionados;
- Troca-se este elemento com o primeiro elemento do segmento;
- Actualiza-se o tamanho do segmento (menos um elemento);
- Este processo é repetido até que o segmento fique com apenas um elento.

Exemplo:
19 25 10 18 35 17 15 13 TAM
=8
10 25 19 18 35 17 15 13 TAM
=7
10 13 19 18 35 17 15 25 TAM
=6
10 13 15 18 35 17 19 25 TAM
=5
10 13 15 17 35 18 19 25 TAM
=4
10 13 15 17 18 35 19 25 TAM
=3
10 13 15 17 18 19 35 25 TAM
=2
10 13 15 17 18 19 25 35 TAM
=1

Algorítimo:
Const:
TAM = 15

Tipos
V = vector [1..TAM] de inteiros

Var
Vet: v
I, j, temp, pos_menor : inteiro
Início
Para i = 1 até TAM faça
Leia ( vet [i] )
Fim-para
Para i = 1 até TAM - 1 faça
Pos_menor = i
Para j = i + 1 até TAM faça
Se vet [j] < vet [pos_menor] Então
Pos_menor = j

107
Fim-se
Fim-para
Temp = vet [i]
Vet [i] = vet [pos_menor]
Vet [pos_menor] = temp
Fim-para
Fim.

4.2. Selecção em Árvore (Heap Sort)

Utiliza uma estrutura de árvore binária para a ordenação. A ordenação é realizada


em duas fases:

1 ª Fase:
- Monta-se uma árvore binária (heap) contendo todos os elemento do
vector de forma que
o valor contido em qualquer modo seja maior do que os valores de seus
sucessores.
A árvore binária é estruturada no próprio vector da seguinte forma:

a. Sucessor á esquerda de i: 2i (se 2i < a)


b. Sucessor á direita de i: 2i + 1 (se 2i + 1 < n)

- Trasnsformação da árvore num heap: é realizada do menor nivel até a


raiz, trocando-se cada
nodo com maior de seus sucessores imediatos. Repete-se este processo
até cada nodo ser maior
que seus sucessores imediatos.

2ª Fase:
- Após a formação do heap segue-se a fase de classificação propriamente
dita, na qual o valor
que está na raiz da árvore (maior valor contido na árvore) é colocado na
sua posição correcta,
trocando-o com o elemento de maior índice de árvore (a árvore fica com
1 elemento a menos).
Este novo elemento colocado na raiz pode violar a propriedade do heap,
de modo que deve-se
restaurar o heap novamente. Este procedimento é repetido até que a
árvore fique com um unico
elemento.

108
Exercício 6
56 30 1 38 90 11 95
(1,7)
Esq. = 1...... I=1
Dir =7.........J =7 Y= Vet [ (I+j) Div 2] = Vet [ (1+7) Div 2] = Vet [4] = 38

Trocal 1: I=2, J=7


56 95 1 38 90 11 30
I=3 J=6 {após a troca o incrimenta e o J discrimenta uma unidade}

Troca 2: I=3, J=5


56 95 90 38 1 11 30
I=4 J=4

Troca 3: I=4, J=4


56 95 90 38 1 11 30
I=5 J=3 {Houve troca sem nenhum efeito visivel}
O ‘I’ já é Superior a ‘J’ por isso fazemos um novo chamamento
ESQUERDA DIREITA
Esq < J? Dir > i ?
A1 <3 True B 7>5 True {Fica para depois de tartar toda a parte
esquerda}
A (1,3)
Esq. = 1...... I=1
Dir =3.........J =3 Y= Vet [2]=95

Troca 4: I=1, J=2


95 56 90 38 1 11 30
I=2 J=1
O I já é Superior a J por isso fazemos um novo chamamento
ESQUERDA DIREITA
Esq < J? Dir > i ?
C1 <1 False Fim e vai a direita D 3>2 True {resolve-se agora porque a esquerda
é falso}
D (2,3)
Esq. = 2...... I=2
Dir =3.........J =3 Y= Vet [3]=90

Troca 5: I=2, J=3


95 90 56 38 1 11 30
I=3 J=2
O I já é Superior a J por isso fazemos um novo chamamento
ESQUERDA DIREITA
Esq < J ? Dir > I ?

109
E2 <1 False Fim e Vai a direita F3>2 False {Voltamos para traz B o ultimo que
deixamos para depois}

B (5,7)
Esq. = 5...... I=5
Dir =7.........7=3 Y= Vet [6]=11
Troca 6: I=5, J=7
95 90 56 38 30 11 1
I=6 J=6

......etc,...

fim

110

Você também pode gostar