Вы находитесь на странице: 1из 74

1 Tipos Abstratos de Dados...........................................................................................................................

1.1 Tipos Primitivos....................................................................................................................................3

1.2 Apontadores.........................................................................................................................................3

1.3 Vetores e Matrizes ...............................................................................................................................4

1.4 Tipos Novos .........................................................................................................................................4

1.5 Constantes ...........................................................................................................................................4

1.6 Classes ................................................................................................................................................5

2 Listas lineares .............................................................................................................................................7

2.1 Duas abordagens Básicas para Implementação .................................................................................7

2.1.1 Vetor .............................................................................................................................................7

2.1.2 Encadeamento............................................................................................................................12

2.2 Formas Restritas de Acesso à Listas ................................................................................................18

2.2.1 Pilha ou LIFO.............................................................................................................................18

2.2.2 Aplicações de Pilhas...................................................................................................................19

2.2.3 Fila ou FIFO...............................................................................................................................23

2.2.4 Fila Circular.................................................................................................................................24

2.2.5 Aplicações de Filas.....................................................................................................................25

2.3 Multilistas ...........................................................................................................................................26

2.4 Comparando Implementações: Vetores × Listas Encadeadas..........................................................27

3 Árvores ......................................................................................................................................................29

3.1 Representação de Árvores ................................................................................................................29

3.1.1 Representação Seqüencial ........................................................................................................30

3.1.2 Representação por Pai ...............................................................................................................30

3.1.3 Listas de Filhos...........................................................................................................................30

3.1.4 Lista encadeada .........................................................................................................................31

3.2 Árvores Binárias.................................................................................................................................31

3.2.1 Transformação em Árvore Binária..............................................................................................32

3.2.2 Percurso em Árvore Binária .......................................................................................................33

3.2.3 Percurso Usando um Campo Pai ...............................................................................................35

3.2.4 Aplicações de Árvores ................................................................................................................37

1
4 Algoritmos de busca..................................................................................................................................45

4.1 Busca Seqüencial ..............................................................................................................................45

4.2 Busca Seqüencial Ordenada .............................................................................................................46

4.2.1 Busca Indexada ..........................................................................................................................46

4.2.2 Busca Binária..............................................................................................................................49

4.2.3 Busca Através de Árvore Binária de Busca................................................................................50

4.3 Busca Através de Árvore Balanceada ...............................................................................................54

4.3.2 Árvores-B ....................................................................................................................................57

4.4 Busca em Tabelas Não Ordenadas...................................................................................................58

4.4.1 Busca Através de Árvore Binária de Acesso..............................................................................58

4.4.2 Busca por Cálculo de Endereço (“Hashing”) ..............................................................................61

5 Métodos de classificação ..........................................................................................................................67

5.1 Classificação por Troca .....................................................................................................................67

5.1.1 Método da Bolha.........................................................................................................................67

5.1.2 Método por Troca e Partição ......................................................................................................68

5.2 Classificação por Seleção e Por Árvore ............................................................................................71

5.2.1 Método da Seleção Direta ..........................................................................................................71

5.3 Classificação por Inserção.................................................................................................................72

5.3.1 Método da Inserção Direta .........................................................................................................72

5.3.2 Método Shell Sort .......................................................................................................................73

2
1 TIPOS ABSTRATOS DE DADOS
Um tipo abstrato de dados define o conjunto de valores que uma variável ou constante pode assumir
bem como os métodos (operações) que podem ser realizados sobre essa variável ou constante.

1.1 Tipos Primitivos

Os tipos primitivos fundamentais são os valores numéricos, os valores lógicos e os caracteres para
impressão. Tais tipos são denotados por numérico, lógico e caracter. A declaração de variáveis segue
a forma:

declare <var 1>, <var 2>, ..., <var n> <tipo>


Exemplo:

declare Área, Raio numérico


declare Achou lógico

1.2 Apontadores

Uma variável apontadora poderá conter apenas endereços de memória. Como uma variável “ordinária”,
que nada mais é do que um endereço de memória (isto é, um nome de variável é um rótulo para um
endereço de memória), uma variável apontadora pode ser usada para referências indiretas a outra variável.
Variáveis apontadoras são geralmente usadas para referenciar área de memória dinâmica (geralmente
para armazenamento de dados), isto é, memória que é alocada durante a execução do programa e não
1
quando o programa é carregado . O símbolo ‘∗’ indica que a variável declarada é apontadora; o tipo ao
lado do símbolo informa que a variável apontadora só poderá conter endereços (apontar) de variáveis
daquele tipo.

Exemplo:
01 include<string.h>
02 void main(){
03 char Nome[] = “maria”, ∗p1, ∗p2;
04 int i;
05 p1 = malloc(strlen(Nome)+1);
06 p2 = p1;
07 for (i=0;i < strlen(Nome);i++){
08 ∗p2 = Nome[i];
09 p2++;
10 }
11 ∗p2 = ‘\0’;
12 free(p1);
13 }

No código acima, em linguagem C, são declaradas (reservado memória) para três variáveis: uma cadeia
de caracteres (“string”), já inicializado com cinco caracteres (seis “bytes”, correspondentes aos caracteres
‘m’, ‘a’, ‘r’, ‘i’, ‘a’ e ‘\0’); dois apontadores de caracteres (identificados pelo símbolo ‘*’ antes do nome); e
uma variável inteira. A Figura 1.1 (a) mostra a memória de dados do programa até a linha 05. É assumido
que os endereços (virtuais) do programa exigem no máximo dois “bytes”. Na linha 05, é alocada
dinamicamente uma quantidade de “bytes” igual àquela da variável Nome. Essa área de memória é
apontada pela variável p1; a memória de dados programa até a linha 07 é mostrada na Figura 1.1 (b). Na

1
Normalmente, as variáveis e o código do programa que têm seu espaço em memória alocado estaticamente, antes do início da
execução.

3
linha 12, a memória do programa tem a configuração mostrada na Figura 1.1 (c). Na linha 12, a memória
alocada é liberada dinamicamente e o programa volta a ter apenas 12 “bytes” de dados (Figura 1.1 (d)).

Nome 1000 ‘m ‘ a’ Nome 1000 ‘m’ ‘al’ Nome 1000 ‘m’ ‘ a’ Nome 1000 ‘mI ‘ a’
1002 aI’
‘ r’ ...
‘i’ 1002 ‘ r’ ...
‘i’ 1002 ‘ r’ ‘i’ 1002 ‘ ’r’ ...
‘i’
1004 ‘ a’ ‘ /0’ 1004 ‘ a’ ‘ /0’ 1004 ‘ a’ ‘ \0’ 1004 ‘ a’ ‘ \0’
p1 1006 ? p1 1006 1112 p1 1006 1112 p1 1006 1110
p2 1008 ?
? p2 1008 1112 p2 1008 1117 p2 1008 1117
i 1010 ? i 1010 ? i 1010 5 i 1010 5
1112 ? ? 1112 ‘m’ ‘ a’
1114 ? ? 1114 ‘ r’ ‘i’
1116 ? ? 1116 ‘ a’ ‘ \0’
(a) (b) (c) (d)

Figura 1.1 Tamanho e conteúdo da memória durante a execução do programa.

A forma de alocação dinâmica de memória mostrada acima é excessivamente explícita. Neste


trabalho, utilizar-se-á uma forma de alocação dinâmica implícita e mais simples, executada através da
instanciação de classes (vide adiante).

1.3 Vetores e Matrizes

Vetores e matrizes consistem de componentes homogêneos (ou seja, do mesmo tipo), com uma ou
duas dimensões, respectivamente. A declaração de vetores segue a forma:

declare <tipo primitivo> <var 1[tamanho 1]>, <var 2[tamanho 2]>, ..., <var
n[tamanho n]>
onde tamanho é um valor inteiro positivo.

Exemplo:
declare Nome[30] caracter
declare Código[25] numérico
De maneira análoga, a declaração de uma matriz segue a forma:

declare <var 1[dimensao 1.1][dimensao 1.2]>, <var 2 dimensao 2.1][dimensao


2.2]>, ..., <var n dimensao n.1][dimensao n.2]> <tipo>

onde dimensao é um valor inteiro positivo.

Exemplo:
declare M[4][3] numérico

1.4 Tipos Novos

Tipos Novos são tipos definidos a partir de tipos pré-existentes. A declaração de tipos novos segue a
forma:

tipo <nome do tipo> <tipo pre-existente>


Exemplo:
tipo Elemento numérico

1.5 Constantes

Constantes são nomes simbólicos para valores pré-definidos. A declaração de constantes segue a
forma:

<nome da constante> = <valor>

4
Exemplo:

PI = 3.1415

1.6 Classes

Uma classe define um tipo e os métodos que podem ser executados sobre instâncias do tipo. Os
métodos podem ser públicos e privados. Um método não pode ser utilizado fora da classe; os métodos
públicos são aqueles que podem ser usados sobre uma instância da classe (variável cujo tipo é a classe).
De maneira análoga, uma classe possui variáveis privadas e variáveis públicas. A declaração de uma
classes segue a forma:

classe <nome da classe>


<tipos da classe> <visão>
<variáveis da classes> <visão>
<métodos da classes> <visão>
fim da classe
Exemplo:
classe empresa público
classe pessoa privado
declare Nome[30] caracter privado
declare Código numérico privado
método pessoa(N[30] caracter, C numérico) privado
Nome ← N
Código ← C
fim método
fim classe

declare Empregados[50] pessoa privado


declare Último numérico privado

método empresa() público


Último ← 0
fim método

método insereEmpregado(N[30] caracter, C numérico) lógico público


declare i numérico
se Último = 50
então retorne falso
fim se
i ← 0
enquanto i < Último faça
se Empregados[i].Código = C
então
retorne falso
fim se
fim para
Empregados[Último] ← pessoa(N, C)
Último ← Último + 1
retorne verdadeiro
fim método
fim classe

A criação da lista (inicialização) é feita através da associação do método empresa() (construtor da


classe) com uma variável da classe empresa, como visto no exemplo a seguir.

5
Exemplo:
algoritmo
declare Firma empresa
declare Cód numérico
declare Empregado[30] caracter
Firma.empresa()
repita
leia Cód
se Cód ≠ 0
então
leia Empregado
se Firma.insereEmpregado(Empregado, Cód) = falso
então
escreva “Empresa cheia!”
Cód ← 0
fim se
fim se
até Cód = 0
fim algoritmo

A empresa criada poderia ser vista como algo semelhante à Figura 1.2.

0 “iara” 090
1 “ava” 092
2 “josé” 870
3 “noé” 013
4 “ilka” 200
5 “ivo” 400
6 “eva” 612
7 “zoé” 009
8 “maria” 090
9 “paulo” 092
10 “flávia” 870 último
11
...
47
48
49

Figura 1.2 Uma instância da classe empresa.

6
2 LISTAS LINEARES
Uma lista é uma seqüência finita de itens de dados (elementos). Cada elemento tem uma posição (1º ,
2º , ...), podendo haver, também, uma ordem relativa aos valores dos elementos (por exemplo, os
elementos podem estar ordenados alfabeticamente). Em implementações simples de listas todos os
elementos possuem o mesmo tipo de dado, ou seja, são listas homogêneas.

Exemplo:

<1, 20, 35, 40 0, 110, 15> (lista de inteiros)

<’Z’, ‘ &’, ‘ a’, ‘ ç’> (lista de caracteres)

Não há no entanto qualquer objeção conceitual para listas heterogêneas (cujos elementos podem ser
de tipos diferentes).

Uma lista vazia é uma lista que não contém elementos; o comprimento de uma lista é o número de
elementos na lista; o cabeça (“head”) representa o início da lista, ele isto é, é o primeiro elemento da lista; a
cauda (“tail”) representa o final da lista

Em uma lista ordenada, os elementos estão posicionados em ordem ascendente ou descendente de


valor; em uma listas não ordenada não há relações entre valores e posições.

Exemplo:
classe Lista público
...
método novaLista() público
...
fim método
método inicializa() lógico público
...
fim método
método insere(Elemento) lógico público
...
fim método
método remove(Elemento) lógico público
...
fim método
fim classe

2.1 Duas abordagens Básicas para Implementação

2.1.1 Vetor

Uma lista linear pode ser implementada de forma estática, isto é, o tamanho máximo da lista e toda
a memória necessária para esse tamanho máximo são definidos e alocados antes da execução (quando a
lista é criada). O uso de vetores explora a seqüencialidade da memória (contiguidade física). A Figura 2.1
mostra uma lista com oito elementos. Os elementos são vetores de caracteres (“strings”).

7
0 “ana” 090
1 “ava” 092
2 “eva” 170
3 “iara” 213
4 “ilka” 240
5 “ivo” 300
6 “noé” 312
7 “zoé” 609
8
9
10

Figura 2.1 Lista com oito elementos

Inserção de Elementos: a operação de inserção (e também remoção) de elementos em uma lista


representada através de um vetor, pode exigir que uma série de elementos tenha que ser realocada. Se
quisermos inserir em uma posição qualquer i, se há n elementos, teremos que mover n-i elementos para
criar um espaço para inserção. Da mesma forma, para remover elementos, será necessário mover outros
elementos para preencher o espaço vazio, já que estamos mantendo os elementos da lista em bloco na
parte esquerda do vetor. Remover um elemento da posição i implica em deslocar n-i elementos. Em
contrapartida, o acesso aleatório a elementos é simples, assim como os processos de inserir e remover
elementos no final da lista. Em suma, quando as operações para inserção e remoção de elementos não
forem feitas no final da lista exigem a realocação de elementos.

No exemplo a seguir, a lista tem um tamanho máximo de 100 e a inserção é feita em ordem alfabética.
No pior caso, a realocação poderá envolver 99 elementos, se a lista estiver quase cheia e se o novo
elemento será inserido na primeira posição da lista.

Exemplo:
classe lista público
tipo nome caracter[30] público
tipo chave numérico público
classe elemento público
Nome nome público
Chave chave público
fim classe

declare Vetor[100] elemento privado


declare TAM_MAX numérico privado
declare NúmeroEl numérico privado

método lista() público


TAM_MAX 100
NúmeroEl 0
fim método

método insere(E elemento) lógico público


declare i,j numérico
declare Aux, Aux1 elemento
se NúmeroEl < TAM_MAX
então
i 0
enquanto E.Chave > Vetor[i].Chave e i < NúmeroEl faça
se E.Chave = Vetor[i].Chave
então
retorne falso
fim se
i i+1

8
fim enquanto
j NúmeroEl
enquanto j > i faça
Aux1 Vetor[i]
Vetor[j] Vetor[j-1]
j j-1
fim enquanto
Vetor[i] E
NúmeroEl NumeroEl+1
retorne verdadeiro
senão
retorne falso
fim se
fim método
fim classe

Na classe, é necessário manter o tamanho máximo e o número atual de elementos (ou posição do
último elemento da lista). Se a lista não é ordenada, a remoção é simples: O elemento é removido e o
último ocupa seu lugar.

Exemplo:
método remove(C chave) lógico público
declare i numérico
i 0
enquanto C ≠ Vetor[i].Chave e i < NumeroEl faça
i i+1
fim enquanto
se i = NumeroEl
então
retorne falso
senão
Vetor[i] Vetor[NumeroEl-1]
NumeroEl NumeroEl-1
retorne verdadeiro
fim se
fim método

A Figura 2.2 mostra uma possível estado de uma instância da classe lista considerando que as
operações de inserção e remoção mantêm a lista ordenada.

9
0 “ana”
1 “ava”
2 “eva”
3 “iara”
4 “ilka”
5 “ivo”
6 “noé”
7 “rosa”
8 “sara”
9 “toni”
10 “zoé” NumeroEl = 11
11
...
97
98
99

Figura 2.2 Uma instância da classe lista.

2.1.1.1 Vetor com Apontadores e Lista de Livres


A necessidade de realocação de elementos de listas baseadas em vetores, quando de operações
de inserção ou remoção aleatórias, pode ser contornada abrindo-se mão da obrigatoriedade da seqüência
de elementos obedecer a seqüência na qual eles são armazenados na memória. Isso cria a necessidade de
um campo adicional para representar essa seqüencialidade bem como de uma lista de posições livres. Na
Figura 2.3 é mostrada uma lista desse tipo. Um campo adicional é acrescentado em cada posição do vetor.
Ele indica qual é o próximo elemento na seqüência, através de seu índice. Duas variáveis indicam qual é o
primeiro elemento da lista de elementos e qual é a primeira posição livre. Se o campo próximo contém um
marcador especial (por exemplo, um índice inválido, como –1), o elemento é o último da lista.

Essa abordagem elimina a necessidade de realocação de elementos quando de inserções ou


remoções aleatórias, mas torna mais lento o acesso de elementos aleatórios, já cria a necessidade de que a
lista seja percorrida até que o elemento seja encontrado.

0 16
1
1 0 0
2 “eva” 17
3 -1
4 “noé” 10
5 “ilka” 13
6 3
7 “ava” 2
livre 8 1
9 6
10 “zoé” -1
11 12
12 15
13 “ivo” 4
14 20
15 9
16 14
17 “iara” 5
início 18 “ana 7
19 11
20 19

Figura 2.3 Lista baseada em vetor com seqüencialidade forçada e lista de posições livres.

10
Inserção de Elementos: o elemento é inserido na primeira posição da lista de livres. Após, a lista é
percorrida até que seja encontrado o elemento que deverá anteceder o elemento inserido. Os ponteiros e
os campos indicando próximo devem ser atualizados. No exemplo abaixo, a inserção é feita de modo a
manter a lista ordenada alfabeticamente.

Exemplo:
classe lista público
tipo elemento caracter[30] público
TAM_MAX = 100
declare Vetor[100] nó privado
declare NúmeroEl numérico privado
declare Início, Livre numérico privado

método lista() público


declare i numérico
NúmeroEl ← Livre i 0
Início -1
enquanto i < TAM_MAX-1 faça
vetor[i].Próximo i+1
i i+1
fim enquanto
vetor[i].Próximo -1
fim método

método insere(E elemento) público


declare Temp, P1, P2, Temp numérico
se Livre ≠ -1
então
Vetor[Livre].Elemento E
Temp Livre
Livre Vetor[Livre].Próximo
se Início = -1
então
Início ← Temp
Vetor[Início].Próximo ← -1
senão
se NúmeroEl = 1 e Vetor[Temp].Elemento < Vetor[Início].Elemento
então
Vetor[Temp].Próximo ← Início
Início ← Temp
senão
P1 ← Início
enquanto Vetor[P1].Elemento < Vetor[Temp].Elemento e P1 ≠ -1 faça
P2 ← P1
P1 ← Vetor[P1].Próximo
fim enquanto
Vetor[Temp].Próximo ← Vetor[P2].Próximo
Vetor[P2].Próximo ← Temp
fim se
fim se
NúmeroEl ← NúmeroEl+1
retorne verdadeiro
senão
retorne falso
fim se
fim método
fim classe

11
2.1.2 Encadeamento

Uma lista linear também pode ser implementada de forma dinâmica, isto é, o tamanho máximo da
lista é (teoricamente) infinito. Para tal, são utilizados apontadores que conectam nós criados conforme a
necessidade. Cada nó irá conter um elemento da lista. Um aspecto fundamental no projeto da lista é a
estrutura dos nós que compõem sua implementação.

2.1.2.1 Lista com Encadeamento Simples


Nesse tipo de lista, cada nó possui apenas um apontador para o nó que compõem o próximo
elemento da lista. Uma representação gráfica para listas com encadeamento simples é mostrada na Figura
2.4.

“ana” → “ava” → “eva” → “iara” → “ilka” → “ivo” → “noé” → “zoé” ⁄

início

Figura 2.4 Lista encadeada simples.

Lista implementadas com encadeamento evitam o problema de limitar o número de elementos que
podem ser inseridos na lista (na verdade, o limite é a memória disponibilizada para o programa). Um nó
pode ser definido conforme o exemplo a seguir.

Exemplo:
tipo elemento caracter[30] público
classe nó privado
declare Elemento elemento público
declare Próximo nó público
método nó(E elemento, P nó) público
Elemento ← E
Próximo ← P
fim método
fim classe

No exemplo acima, um nó é composto de dois campos, Elemento, um vetor de caracteres


(“string”), e Próximo, um apontador para um outro nó. Assim, o campo Próximo de um nó deverá conter o
endereço do nó seguinte a ele na lista (na criação do nó, esse campo não aponta para ninguém). A classe
nó, por ser privada, deveria ser declarada dentro da classe lista, fazendo com que instâncias de nó, bem
como suas variáveis internas e métodos públicos, possam ser criados e usados (sejam visíveis) apenas
dentro da classe lista.

Inserção de Elementos: quando consideramos as operações de inserção (e também remoção) de


elementos em uma lista com encadeamento simples, percebemos que, para efetuar a operação, é preciso
percorrer todos os elementos que estão antes da posição desejada. Esse processo pode tornar a
implementação dessas operações ineficiente. De maneira genérica, a inserção em uma lista encadeada
simples exige:

1. criação um novo nó com que receberá o elemento a ser incluído;

2. fazer com que um apontador encontre a posição anterior àquela aonde o novo nó deverá ser
incluído;

3. fazer com que o campo próximo do novo nó aponte para o nó seguinte ao dessa posição; e

4. fazer com que o campo próximo do nó da posição encontrada aponte para o novo nó.

A Figura 2.5 ilustra esse processo.

12
“ana” → “ava” → “eva” → “iara” → “ilka” → “ivo” → “noé” → “zoé”

início “igor” 

temp

“ana” → “ava” → “eva” → “iara” → “ilka” → “ivo” → “noé” → “zoé” ⁄


 

início p “igor” 

temp

“ana” → “ava” → “eva” → “iara” → “ilka” → “ivo” → “noé” → “zoé” ⁄


 

início p “igor” 

temp

“ana” → “ava” → “eva” → “iara” “ilka” → “ivo” → “noé” → “zoé” ⁄


  

início p “igor” 

temp

“ana” → “ava” → “eva” → “iara” “ilka” → “ivo” → “noé” → “zoé” ⁄



  

início p “igor” 

temp

“ana” → “ava” → “eva” → “iara” → “igor” → “ilka” → “ivo” → “noé” → “zoé” ⁄


  

início p temp

Figura 2.5 Inserção em uma lista encadeada simples.

O processo acima funciona para o caso geral. Contudo, é necessário um tratamento especial no caso
da lista não possuir elementos ou no caso do novo elemento dever ser incluído antes do início. No exemplo
a seguir, os elementos são inseridos obedecendo à ordem alfabética.

Exemplo:
classe lista público
classe nó privado
...
fim classe

declare NúmeroEl numérico privado


declare Início nó privado

método lista() público


NúmeroEl ← 0
Início ← NULO
fim método

método insere(E elemento) público


declare Temp, P, Q nó
P ← Início
se E < P.Elemento ou P = NULO
então
Início ← Temp.nó(E, P)
senão
enquanto P.Elemento < E e P ≠ NULO faça
Q ← P
P ← P.Próximo
fim enquanto
Q.Próximo ← Temp.nó(E, P)
fim se

13
NúmeroEl ← NúmeroEl+1
fim método
fim classe

A remoção de elementos também exigirá um teste para o caso da lista conter apenas um elemento.

2.1.2.2 Lista com Encadeamento Simples e Definição de Elemento Corrente


Em uma definição genérica de listas, temos sempre que indicar uma posição específica onde uma
inserção ou retirada vai acontecer. No entanto, é bastante comum que uma série de operações sejam
realizadas na mesma “região” da lista (várias posições próximas umas das outras).

Para simplificar a execução de um conjunto de operações em uma região da lista poderíamos


modificar nossa definição de lista. Uma alternativa seria manter uma indicação de uma posição “corrente”
onde todas as operações acontecem (Figura 2.6).

“ana” → “ava” → “eva” → “iara” → “ilka” → “iv → “noé” → “zoé” ⁄


o”
início corrente

Figura 2.6 Lista encadeada simples com apontador da posição corrente.

Inserção de Elementos: na inserção, o elemento será inserido na posição “corrente”. De maneira genérica,
a inserção em uma lista encadeada simples com posição corrente exige:

1. criação um novo nó com que receberá o elemento a ser incluído;

2. fazer com que o campo próximo do novo nó aponte para o nó seguinte ao corrente;

3. fazer com que o campo próximo do nó corrente aponte para o novo nó; e

4. fazer com que o novo nó passe a ser o corrente.

A Figura 2.7 ilustra esse processo.

“ana” → “ava” → “eva” → “iara” → “ilka” → “ivo” → “zoé” ⁄





início corrente “igor” 



temp

“ana” → “ava” → “eva” → “iara” → “ilka” → “ivo” → “zoé” ⁄


 
 

início corrente “igor” 



temp

“ana” → “ava” → “eva” → “iara” “ilka” → “ivo” → “zoé” ⁄



 

 

início corrente “igor” 



temp

“ana” → “ava” → “eva” → “iara” → “igor” → “ilka” → “ivo” → “zoé” ⁄


 

início corrente

Figura 2.7 Inserção em uma lista encadeada simples com apontador da posição corrente.

Exemplo:
classe lista público
classe nó privado
...

14
fim classe
declare NúmeroEl numérico privado
declare Início, Corrente nó privado

método lista() público


NúmeroEl ← 0
Início ← NULO
fim método

método insere(E elemento) público


declare Temp nó
se NúmeroEl = 0
então
Início ← Temp.nó(E, NULO)
senão
Corrente.Próximo ← Temp.nó(E, Corrente.Próximo)
fim se
Corrente ← Temp
NúmeroEl ← NúmeroEl+1
fim método
fim classe

Remoção de Elementos: na remoção, o elemento a ser removido será aquele apontado por corrente.

Pode ser útil também incluir na nossa representação de lista uma indicação de fim da lista (Figura
2.8). Assim podemos tratar de inserções, remoções e consultas no início e no fim da lista usando os
indicadores “início” e “fim”. Quando essas operações são realizadas em uma posição intermediária usamos
o indicador “corrente”.

“ana” → “ava” → “eva” → “iara” → “ilka” → “ivo” → “noé” → “zoé” ⁄

início corrente fim

Figura 2.8 Lista encadeada simples com apontador da posição corrente e final.

2.1.2.3 Lista com Encadeamento Simples e Nó “Cabeça”


Um problema nas abordagens acima é a exigência de um tratamento especial no caso da lista não
possuir ou possuir apenas um elemento. Uma solução para o problema acima consiste em sempre manter
um nó cabeça (“header”). Ele é um nó igual aos outros mas ignorado como elemento real, isto é, ele já
passa a existir quando a lista é criada e não pode nunca ser removido. Isso elimina os casos especiais de
na inserção e na remoção; o (pequeno) custo adicional é o espaço ocupado (e não usado) pelo cabeça. A
Figura 2.9 mostra uma lista com um no cabeça.

→ “ana” → “ava” → “eva” → “iara” → “ilka” → “ivo” → “noé” → “zoé” ⁄

cabeça

Figura 2.9 Lista encadeada simples com nó cabeça.

Inserção de Elementos: o uso do nó cabeça dispensa a necessidade do teste de lista vazia e inserção na
posição anterior ao início da lista, conforme o exemplo abaixo.

Exemplo:
classe lista público
classe nó privado
...
fim classe

15
declare NúmeroEl numérico privado
declare Cabeça nó privado

método lista() público


NúmeroEl ← 0
Cabeça.nó(“”, NULO)
fim método

método insere(E elemento) público


declare Temp, P, Q nó
Q ← Cabeça
P ← Cabeça.Próximo
enquanto P.Elemento < E e P ≠ NULO faça
Q ← P
P ← P.Próximo
fim enquanto
Q.Próximo ← Temp.nó(E, P)
NúmeroEl ← NúmeroEl+1
fim método
fim classe

2.1.2.4 Lista com Duplo Encadeamento


As listas com encadeamento simples só podem ser percorridas do início para o final. Além disso, a
inserção e remoção exigem o uso de dois ponteiros, quando deseja-se inserir (ou remover) um elemento de
uma posição arbitrária. O uso de dois ponteiros em cada nó, um apontando para o próximo elemento na
lista e outro apontando para o anterior, torna mais simples operações de inserção e remoções arbitrárias.
Nesse caso, o nó tem que ser modificado para incorporar mais um ponteiro.

Inserção de Elementos: a inserção em uma lista duplamente encadeada exige:

1. encontrar a posição onde o novo nó deverá ser incluído;

2. criar um novo nó que receberá o elemento a ser incluído;

3. acertar os ponteiros dos campos Próximo e Anterior do nó a ser incluído, e dos nós que serão
o anterior e o seguinte a ele na lista.

A Figura 2.10 ilustra esse processo.

16
“ava” “eva” “iara” “ilka” “ivo” “zoé” ⁄


cabeça temp 

“igor”

“ava” “eva” “iara” “ilka” “ivo” “zoé” ⁄


     

 

cabeça p temp 

“igor”

“ava” “eva” “iara” “ilka” “ivo” “zoé” ⁄


     

 

cabeça temp 

“igor” p

“ava” “eva” “iara” “ilka” “ivo” “zoé” ⁄


     

  

cabeça temp 

“igor” p

“ava” “eva” “iara” ← “ilka” “ivo” “zoé” ⁄


    

cabeça temp

“igor” p

“ava” “eva” “iara” ← “ilka” “ivo” “zoé” ⁄


cabeça temp

“igor” p

“ava” “eva” “iara” “igor” “ilka” “ivo” “zoé” ⁄


cabeça temp p

Figura 2.10 Inserção em uma lista duplamente encadeada com nó cabeça.

Exemplo:
classe lista público
tipo elemento caracter[30] público
classe nó privado
declare Elemento elemento público
declare Próximo nó público
declare Anterior nó público

método nó(E elemento, A nó, P nó) publico


Elemento ← E
Anterior ← A
Próximo ← P
fim método
fim classe

declare NúmeroEl numérico privado


declare Cabeça nó privado

método lista() público


NúmeroEl ← 0
Cabeça.nó(“”, NULO, NULO)
fim método

método insere(E elemento) público


declare Temp, P nó

17
P ← Cabeça
enquanto P.Elemento < E e P.Próximo ≠ NULO faça
P ← P.Próximo
fim enquanto
se P.Elemento < E e P.Próximo = NULO
então
P.Próximo ← Temp.nó(E, P, NULO)
senão
(P.Anterior).Próximo ← Temp.nó(E, P.Anterior, P)
P.Anterior ← Temp
fim se
NúmeroEl ← NúmeroEl+1
fim método
fim classe

2.1.2.5 Lista Duplamente Encadeada Circular


Uma lista circular é uma lista na qual o último elemento aponta para o primeiro, dispensando a
necessidade de um apontador de fim bem como teste para verificar se o elemento a ser inserido será o
último da lista. A Figura 2.11 mostra uma lista circular.

“ava” “eva” “iara” “ilka” “ivo” “zoé”




cabeça

Figura 2.11 Lista circular.

2.2 Formas Restritas de Acesso à Listas

Determinadas aplicações exigem que somente o elemento do início e/ou final de uma lista só sejam
acessíveis. Conforme a política de acesso, tais listas são chamadas de pilhas, filas e deques. Todas essas
formas podem ser implementadas através de vetores ou listas encadeadas.

2.2.1 Pilha ou LIFO

Em uma pilha, os elementos são inseridos no final e removidos também do final da lista, ou seja, ou,
os elementos saem da pilha na ordem reversa de chegada, em uma política chamada LIFO (“last in first
out”, ou último a chegar, primeiro a sair). Ao contrário do que ocorre com um vetor, uma pilha é um objeto
dinâmico, constantemente mutável.

Em uma pilha, a operação de inserção de um elemento na lista é chamada empilha (“push”) e a


operação de remoção é chamada de desempilha (“pop”).

Exemplo:
classe pilha público
MAX_El = 30
declare Topo numérico privado
declare Pilha[MAX_El] elemento privado

método pilha() público


Topo ← 0
fim método

método empilha(E elemento) lógico público


se Topo < MAX_EL
então
Pilha[Topo] ← E

18
Topo ← Topo+1
retorne verdadeiro
senão
retorne falso
fim se
fim método

método desempilha() elemento público


se Topo > 0
então
Topo ← Topo-1
retorne Pilha[Topo+1]
senão
retorne EL_NULO
fim se
fim método
fim classe

No exemplo acima, a pilha é implementada através de um vetor. Se elemento é simplesmente um


“string” uma configuração possível para a pilha pode ser aquela mostrada na Figura 2.12. EL_NULO deve
ser um valor de retorno pré-definido para elemento. Em muitas implementações, os métodos empilha e
desempilha, ao invés de retornar valores, imprimem mensagens de erro quando as operações não podem
ser realizadas (“stack overflow” e “stack underflow” – estouro de pilha e pilha vazia, respectivamente) e
abortam a execução.

0 “ana”
1 “ava”
2 “eva”
3 “iara”
4 “ilka”
5 “ivo”
6 “noé”
7 “zoé”
topo 8
9
10
11
12

Figura 2.12 Pilha implementada através de vetor.

2.2.2 Aplicações de Pilhas

Pilhas são estruturas de dados utilizadas para os mais diversos fins em aplicações. Nesta seção,
serão vistas apenas algumas das aplicações mais comuns.

2.2.2.1 Análise de Expressões


Pilhas podem ser utilizadas com o intuito de realizar a avaliação sintática de linguagens. Seja, por
exemplo, uma expressão matemática que inclui vários conjuntos de parênteses aninhados:

7–((X*((X+Y)/(J-3))+Y)/(4-2))

Queremos garantir que os parênteses estejam corretamente agrupados, ou seja, verificar se (1) existe um
número igual de abre parênteses e fecha parênteses; e (2) todo fecha parêntese está precedido por um
abre parêntese correspondente. Assim, expressões como

((A+B) ou A+B(

19
violam o critério 1, e expressões como

)A+B(-C ou (A+B)) – (C+D

violam o critério 2.

Para solucionar esse problema, imagine que cada abre parêntese como uma abertura de escopo e
cada fecha parêntese como um fechamento de escopo. A profundidade do aninhamento em um
determinado ponto de uma expressão é o número de escopos abertos mas ainda não fechados nesse
ponto, isto é, o número de abre parênteses cujos fecha parênteses ainda não foram encontrados. A
contagem de parênteses em determinado ponto será o número de abre parênteses menos o número de
fecha parênteses encontrados ao avaliar-se a expressão da esquerda para direita. As duas condições
necessárias para que uma expressão esteja correta são (1) a contagem de parênteses final é zero; e (2) a
contagem de parêntese em cada ponto da expressão não pode ser negativa. A contagem de cada ponto
das expressões acima ficaria:

7 - ( ( X * ( ( X + Y ) / ( J - 3 ) ) + Y ) / ( 4 - 2 ) )
0 0 1 2 2 2 3 4 4 4 4 3 3 4 4 4 4 3 2 2 2 1 1 2 2 2 2 1 0
( ( A + B )
1 2 2 2 2 1
A + B (
0 0 0 1
) A + B ( - C
-1 -1 -1 -1 0 0 0
( A + B ) ) - C + D
1 1 1 1 0 -1 -1 -1 -1 -1

Exemplo:
classe expressão_arit público
método avalia(Expressão expressão) lógico público
declare P pilha
declare i numérico
i ← 0
P.pilha()
enquanto Expressão[i] ≠ ‘\0’ faça
se Expressão[i] = ‘(‘
então
P.empilha(‘(‘)
senão
se Expressão[i] = ‘)‘ e P.desempilha() = EL_NULO
então
retorne falso
fim se
fim se
i ← i+1
fim enquanto
se P.desempilha() ≠ EL_NULO
então
retorne falso
senão
retorne verdadeiro
fim método
fim classe

No exemplo acima, elemento é um caracter e a avaliação restringe-se à parentização. O problema


anterior pode ser mais complexo, se forem considerados outros tipos de delimitadores de escopo, como na
expressão abaixo:

20
7–[{X*[(X+Y)/(J-3)]+Y}/(4-2)]

Neste caso, é necessário avaliar o número e o tipo de escopo aberto ou fechado. Sempre que um
símbolo indicando abertura de escopo é encontrado, ele é empilhado; quando um símbolo indicando
fechamento de escopo é encontrado, a pilha é examinada: se ela estiver vazia, a expressão é inválida.
Contudo, se a pilha não está vazia, o símbolo é desempilhado e é verificado se ele é o símbolo de abertura
de escopo correspondente ao símbolo de fechamento de escopo encontrado. Se for, o processo continua;
senão, a expressão é inválida. Quando o final da expressão é encontrada, a pilha deverá estar vazia; se ela
não estiver, a expressão é inválida.

O exemplo a seguir é uma possível implementação do avaliador de expressões mais complexas, na


qual a avaliação restringe-se aos símbolos de abertura e fechamento de escopo. É assumido que a
expressão é armazenada em um vetor de caracteres e seu final é delimitado pelo caracter nulo.

Exemplo:
classe expressão_arit público
método abre_escopo (ch caracter) caracter privado
se ch = ‘)‘
então
retorne ‘(‘
fim se
se ch = ‘]‘
então
retorne ‘[‘
fim se
se ch = ‘}‘
então
retorne ‘{‘
fim se
retorne EL_NULO
fim método

método avalia(Expressão expressão) lógico público


declare P pilha
declare i numérico
i ← 0
P.pilha()
enquanto Expressão[i] ≠ ‘\0’ faça
se Expressão[i] = ‘(‘ ou Expressão[i] = ‘[‘ ou Expressão[i] = ‘{‘
então
P.empilha(Expressão[i])
fim se
se Expressão[i] = ‘)‘ ou Expressão[i] = ‘]‘ ou Expressão[i] = ‘}‘ e
P.desempilha() = ≠ abre_escopo(Expressão[i]
então
retorne falso
fim se
i ← i+1
fim enquanto
se P.desempilha() ≠ EL_NULO
então
retorne falso
fim se
retorne verdadeiro
fim método
fim classe

21
2.2.2.2 Controle de Chamada de Subrotinas
A execução de um programa é, a princípio, seqüencial, isto é, é executada uma instrução (“linha”), após
a seguinte e assim por diante. Determinadas instruções, como instruções de laço, podem quebrar essa
seqüência de execução, mas de uma maneira simples: a execução é desviada para um determinado ponto
(endereço) do programa onde as instruções voltam a ser executadas de maneira seqüencial. Em uma
chamada de subrotina, o processo não é tão simples, já que ela implica em um desvio para, a grosso modo,
um outro programa. Quando a execução desse programa termina, a execução deve voltar para o ponto
posterior à chamada da subrotina. Para complicar ainda mais, existe a possibilidade da passagem de
parâmetros e retorno de valor. No exemplo abaixo, é mostrada a execução de um programa que chama
uma subrotina que calcula a série de Fibonacci de um número N qualquer. A seta à esquerda indica o ponto
do programa que está sendo executado.

Exemplo:
método fibonacci(N numérico) numérico público
declare i, Fibonacci numérico
Fibonacci ← i ← 0
enquanto i ≤ N faça
Fibonacci ← Fibonacci + i
fim enquanto
retorne Fibonacci
fim método
...
K ← fibonacci(5)
escreva K

método fibonacci(N numérico) numérico público


declare i, Fibonacci numérico
Fibonacci ← i ← 0
enquanto i ≤ N faça
Fibonacci ← Fibonacci + i
fim enquanto
retorne Fibonacci
fim método
...
K ← fibonacci(5)
escreva K

método fibonacci(N numérico) numérico público


declare i, Fibonacci numérico
Fibonacci ← i ← 0
enquanto i ≤ N faça
Fibonacci ← Fibonacci + i
fim enquanto
retorne Fibonacci
fim método
...
K ← fibonacci(5)
escreva K

O controle de chamadas de subrotinas é gerenciado através do uso de uma pilha. Quando da


chamada da subrotina, são empilhados o endereço de retorno da subrotina bem como os parâmetros. No
início da execução, são desempilhados os parâmetros da chamada; no final da execução da subrotina,
empilhado o valor de retorno (na verdade, o endereço de retorno é desempilhado antes, para que a
execução não vá para um endereço que é, na verdade, o valor de retorno).

22
Para o exemplo anterior, suponha que o endereço da instrução escreva K é o 2000 e o endereço
onde inicia a subrotina é o endereço 1000. Suponha, ainda, que “quem” informa o ponto (endereço) do
programa que deve ser executado é um registrador chamado CP e que a pilha é uma variável P. Nesse
caso, a seqüência de instruções para gerência da chamada da subrotina poderia ser:

P.empilha(CP)
P.empilha(5)
CP fibonacci
N P.desempilha()
...
P.empilha(Fibonacci)
K P.desempilha()
CP P.desempilha()

Na verdade, a implementação de chamadas de subrotinas pode ser feita utilizando duas (ou mais
pilhas). Uma é usada para empilhamento de endereços de retorno; a outra, para empilhamento de dados.
Se a pilha usada é implementada através de um vetor, uma chamada recursiva ou várias chamadas de
subrotinas diferentes aninhadas pode ocasionar um erro de estouro de pilha (“stack overflow”).

2.2.3 Fila ou FIFO

Em uma fila, os elementos são inseridos no final (topo) e removidos do início (base) da lista, ou seja, ou,
os elementos saem da pilha na ordem de chegada, em uma política chamada FIFO (“first in first out”, ou
primeiro a chegar, primeiro a sair).

Exemplo:
classe fila público
MAX_El = 30
declare Base, Topo numérico privado
declare Fila[MAX_El] elemento privado

método fila() público


Topo ← Base ← -1
fim método

método enfila(E elemento) lógico público


se Topo < MAX_EL
então
Topo ← Topo+1
Fila[Topo] ← E
retorne verdadeiro
senão
retorne falso
fim se
fim método

método desenfila() elemento público


declare Aux elemnto
se Base = Topo
então
retorne EL_NULO
senão
Base ← Base+1
Aux ← Fila[Base]
se Base = Topo
então
Topo ← Base ← -1

23
fim se
retorne Aux
fim se
fim método
fim classe

A implementação do método insere acima impede que todo o vetor seja usado para inserções, isto é,
ocorrerá “overflow” quando ainda houverem posições disponíveis, conforme a figura abaixo.

base -1
0 “ana” 0 “ana”
1 “ava” 1 “ava”
2 “eva” 2 “eva”
3 “iara” base 3 “iara”
4 “ilka” 4 “ilka”
5 “ivo” 5 “ivo”
6 “noé” 6 “noé”
7 “zoé” 7 “zoé”
8 “maia” 8 “maia”
9 “paulo” 9 “paulo”
topo 10 topo 10
(a) (b)

Figura 2.13 Fila implementada através de vetor. Após 10 inserções (a), a fila está cheia e não permite
a inserção de mais elementos; após quatro remoções, existem posições livres para inserção mas
que não podem ser utilizadas.

2.2.4 Fila Circular

Uma fila circular é uma fila na qual a base e o topo podem circular do final da estrutura para seu início.

Exemplo:
classe fila_circular público
MAX_El = 30
declare Base, Topo numérico privado
declare Fila_circular[MAX_El] elemento privado

método fila_circular() público


Topo ← Base ← MAX_EL –1
fim método

método enfila(E elemento) lógico público


se Topo = MAX_EL - 1
então
Topo ← 0
senão
Topo ← Topo+1
fim se
se Topo ≠ Base
então
Fila_circular [Topo] ← E
retorne verdadeiro
senão
retorne falso
fim se
fim método

24
método desenfila() elemento público
se Base = Topo
então
retorne EL_NULO
senão
se Base = MAX_EL - 1
então
Base ← 0
senão
Base ← Base+1
fim se
retorne Fila_circular[Base]
fim se
fim método
fim classe

2.2.5 Aplicações de Filas

Filas são utilizadas em muitas aplicações para implementar uma política de seleção em ambientes
onde determinadas entidades competem por recursos. Em um sistema operacional multitarefa, por exemplo,
pode existir uma fila de tarefas associada a cada recursos do sistema. Por exemplo, a fila da tarefas
aguardando para utilizar o processador, a fila de tarefas aguardando a impressora, disco etc. Em roteadores
de redes de computadores, nos quais pacotes chegam e têm que aguardar para serem enviados adiante
para o próximo nó da rede, até chegarem ao destino final, filas podem ser usadas para definir a política de
seleção de qual pacote aguardando no roteador será o próximo a ser enviado adiante. A Figura 2.14 ilustra
esse processo.

Nos roteadores, os Os roteadores


pacotes são armaze- também recebem
nados temporariamente pacotes de outras
No receptor, os
em uma fila para se- aplicações.
pacotes são
rem passados adiante montados



O emissor transfere
o arquivo solicitado

Antes de ingressar
na rede, o arquivo é
quebrado em
pacotes

Figura 2.14 Filas usadas nos roteadores da rede para armazenamento temporário de pacotes.

25
2.3 Multilistas

Em algumas aplicações, os elementos de uma lista podem ser outras listas (e os elementos dessas
listas, podem ser também listas de listas de listas...).

Exemplo:
Seja um sistema operacional multitarefa, no qual cada processo ou tarefa é representado por um
registro descritor. O registro descritor contém, entre outras informações, o identificador único da tarefa,
sua prioridade, campos para salvamento dos valores dos registradores da máquina, indicadores de
arquivos abertos etc. A prioridade indica a precedência na obtenção do uso do processador. Em geral,
um menor valor implica numa maior prioridade. Assim, a maior prioridade é a zero. Uma tarefa pode
estar no estado pronta (se ela espera pelo uso do processador), ativa (se ela detém o processador),
suspensa, adormecida etc. Em um determinado instante, haverá apenas uma tarefa ativa. O descritor de
tarefas pode ser representado através da seguinte classe:

classe descritor privado


declare Identificador numérico público
declare Prioridade numérico público
declare Registradores registradores
declare ArquivosAbertos arquivos
declare Próximo descritor público
declare Anterior descritor público
método descritor() privado
Próximo ← Anterior ← NULO
fim método
fim classe

Um escalonador de tarefas – o método que seleciona a tarefa que vai ganhar o acesso ao processador -
poderia ter o seguinte protótipo:

método escalona (D descritor) descritor privado

O método recebe como argumento o descritor da tarefa que está perdendo o processador e retorna o
descritor da tarefa selecionada para utilizar o processador (a de maior prioridade). A fila de processos pode
ser representada por várias filas de descritores, conforme a figura abaixo (por razões de clareza, nos nós
representando os descritores aparece apenas o identificador das tarefas).

26
Prioridade Prioridade Prioridade
0 1 2
Início → → → ⁄
 → ↓ ↑ ↓ ↓ ↑
 8900 ⁄ 3456 8902 Tarefas
 ↓ ↑ ↓ ↑ prontas
 1700 ⁄ 2001
 ↓ ↑
 1313
 ↓ ↑
 ⁄ 1245

→ → → ⁄
 ↓ ↑ ↓ ↓ ↑
 2345 8910 ⁄ 3344 Tarefas
 ↓ ↑ ↓ ↑ suspensas
 1702 3345
 ↓ ↑ ↓ ↑
 ⁄ 1314 ⁄ 1244



→ → → ⁄
 ↓ ↑ ↓ ↑ ↓ ↑
 3460 3458 1245 Tarefas
 ↓ ↑ ↓ ↑ ↓ ↑ adormecidas
adormecidas
 ⁄ 2300 ⁄ 1699 ⁄ 2002





⁄ → → → ⁄
↓ ↑ ↓ ↑ ↓ ↑
2348 4567 1234 Tarefas
↓ ↑ ↓ ↑ ↓ ↑ aguardando
E/S
2350 ⁄ 3367 4567
↓ ↑ ↓ ↑
3463 ⁄ 2345
↓ ↑
⁄ 1089

Figura 2.15 Multilista representando as filas de tarefas. As listas contendo os descritores de tarefas
devem ser, de fato, filas (FIFO); apontadores adicionais podem ser usados para melhorar o
desempenho.

2.4 Comparando Implementações: Vetores × Listas Encadeadas

Tanto a implementação de listas baseadas em vetores quanto a implementação baseada em


encadeamento possuem vantagens e desvantagens.

Com relação ao espaço (memória), a implementação baseada em vetores exige um tamanho pré-
determinado, o que impede que a lista cresça além desse tamanho e também possibilita uma
superestimação do espaço (uma lista com poucos elementos pode estar ocupando espaço inutilmente). A
implementação baseada em encadeamento, em contrapartida, faz com que a lista ocupe somente o espaço

27
necessário para os elementos que estão na lista, mas exige espaço de armazenamento adicional para os
apontadores. Em geral, listas encadeadas requerem menos espaço quando há poucos elementos na lista.
No caso geral, considerando espaço de armazenamento, listas encadeadas são melhores quando o número
de elementos varia muito ou é desconhecido e vetores são melhores quando sabemos aproximadamente o
tamanho que a lista vai ter.

Com relação ao tempo necessário para acesso, os vetores são mais rápido para acesso aleatório por
posição (operações como para obter o próximo elemento ou o anterior são muito simples); em listas com
encadeamento, o acesso a um elemento pode requerer que seja percorrida quase toda lista. As operações
de inserção e remoção, por outro lado, são simples em listas encadeadas (em vetores, podem exigir a
movimentação de vários elementos). Assim, em aplicações onde operações de inserção e remoção de
elementos são freqüentes, a implementação da lista através de encadeamento pode ser mais adequada.

28
3 ÁRVORES
Uma árvore é um conjunto de elementos (nós) entre os quais é mantida uma relação de hierarquia ou
de composição. A raiz da árvore é o nó que dá origem à árvore, não tendo antecessor. Cada nó da árvore é
raiz de uma sub-árvore, que por sua vez também é uma árvore. Um nó qualquer pode ter, no máximo, um
antecessor direto, e, conceitualmente, qualquer número de sucessores. Um nó sucessor direto de outro nó é
dito seu nó filho; analogamente, um nó antecessor direto de um outro nó é dito seu nó pai. Nós irmãos são
nós que têm o mesmo antecessor direto (o mesmo nó pai). Um nó terminal ou folha é um nó que não possui
filhos; os nós que não são folhas são chamados de nós não terminais ou internos. O grau de um nó é o
número de sub-árvores de um nó. O caminho entre n1 e nk é a seqüência de nós de n1 até nk, na qual n2 é
filho de n1, ..., nk é filho de nk-1; o comprimento de um caminho é o número de nós de um caminho menos
um; a profundidade de um nó é o comprimento do caminho da raiz da árvore até o nó (todos os nós de
profundidade k estão no nível k da árvore. Uma floresta é um conjunto de árvores disjuntas. Alguns dos
conceitos acima são mostrados na Figura 3.1.

Floresta
Nó raiz
Nó pai dos
‘I’ nós J e K
‘A’ Nó de grau 2

‘J’ ‘K’
‘B’ ‘C’ ‘D’ ‘E’

Nós folhas
‘F’ ‘G’ ‘H’

Figura 3.1 Conceitos de árvores.

Uma árvore é dita ordenada se a ordem das sub-árvores é significativa, como na Figura 3.2.

‘ A’ ‘ A’

é diferente de ‘ B’
‘ B’ ‘ C’ ‘ C’

Figura 3.2 Árvores ordenadas.

3.1 Representação de Árvores

As árvores podem ser representadas através de vetores ou através de encadeamento.

29
3.1.1 Representação Seqüencial

Na representação seqüencial, um vetor é usado para representar a árvore. No vetor, o nó é


seguido por seu grau (número de filhos). Para a árvore maior da Figura 3.1, a representação seqüencial é
mostrada da Figura 3.3.

0 ‘ A’
1 4
2 ‘ B’
3 0
4 ‘ C’
5 1
6 ‘ F’
7 0
8 ‘ D’
9 0
10 ‘ E’
11 2
12 ‘ G’
13 0
14 ‘ H’
15 0

Figura 3.3 Árvore: representação seqüencial.

A representação seqüencial é interessante para armazenamento em meio permanente, não para


manipulação em geral, já que as operações (inserções, remoções, localização dos nós etc.) são
relativamente demoradas.

3.1.2 Representação por Pai

Na representação pelo nó pai, também um vetor é usado para representar a árvore. No vetor, os
índices correspondem aos nós e para cada nó, é armazenado o índice correspondente ao seu pai. Para a
árvore da maior da Figura 3.1, a representação por pai é mostrada da Figura 3.4 (‘ A’ = 0, ‘ B’ = 1, ‘ C’ =2, ...).
Para os nó contendo ‘ B’, ‘ C’, ‘ D’ e ‘ E’ (índices1, 2, 3 e 4), por exemplo, o nó pai é o ‘ A’ (0).

0 -1
1 0
2 0
3 0
4 0
5 3
6 4
7 4

Figura 3.4 Árvore: representação por pai.

Essa representação é interessante para aplicações ou operações nas quais é importante saber o
pai dos nós. Em contrapartida, a representação por pai dificulta a obtenção de informações sobre filhos,
além de apresentar dificuldades nas operações de inserção e remoção.

3.1.3 Listas de Filhos

Na representação por lista de filhos, para cada nó (armazenados em uma lista), há uma lista
encadeada contendo seus filhos. A Figura 3.5 mostra uma representação de árvore por lista de filhos.

30
0 ‘ A’ → 1 → 2 → 3 → 4 ⁄
1 ‘ B’ ⁄
2 ‘ C’ → 5 ⁄
3 ‘ D’ ⁄
4 ‘ E’ → 6 → 7 ⁄
5 ‘ F’ ⁄
6 ‘ G’ ⁄
7 ‘ H’ ⁄

Figura 3.5 Árvore: representação por lista de filhos.

Essa representação proporciona flexibilidade para manipulação de nós filhos, mas limita o número
máximo de nós se a lista de pais é implementada através de um vetor.

3.1.4 Lista encadeada

Se uma lista encadeada é usada para representar uma árvore, os nó devem ter um número de
campos representando apontadores, igual ao grau máximo da árvore (Figura 3.6). Tal representação
oferece maior flexibilidade e eficiência na manipulação porém pode acarretar desperdício de memória, já
que muitos campos de para apontadores podem ficar vazios. Outra dificuldade refere-se à definição do
número de campos, já que nem sempre é possível prever o grau máximo da árvore.

‘ A’ ⁄ raiz

‘ B’ ⁄ → ‘ C’ → ‘ D’ ⁄ → ‘ E’ ⁄
↓ ↓
‘ F’ ⁄ ⁄ ‘ G’ ⁄ → ‘ H’ ⁄ ⁄

(a)

‘ A’ raiz
   

‘ B’ ⁄ ⁄ ⁄ ⁄ ‘ C’ ⁄ ⁄ ⁄ ‘ D’ ⁄ ⁄ ⁄ ⁄ ‘ E’ ⁄ ⁄

 

‘ F’ ⁄ ⁄ ⁄ ⁄ ‘ G’ ⁄ ⁄ ⁄ ⁄ ‘ H’ ⁄ ⁄ ⁄ ⁄

(b)

Figura 3.6 Árvore: representação por lista encadeada: (a) através de multilista; (b) com apontadores
para todos os filhos.

3.2 Árvores Binárias

Uma alternativa para tornar mais econômica a representação, é transformar a árvore n-ária em árvore
binária. Desta forma, todos os nós terão uma representação com dois campos ponteiros. Uma árvore binária
é uma árvore na qual o grau de cada nó varia de 0 a 2. Isso significa que em uma árvore binária um nó
pode não ter filhos, ou pode ter uma sub-árvore à esquerda, ou pode ter uma sub-árvore à direita, ou pode
ter uma sub-árvore à esquerda e à direita, como na Figura 3.7.

31
‘ A’

’B’ ’C’

‘ D’ ‘ E’ ’G’

’F’

Figura 3.7 Exemplo de árvores binária.

Por definição, toda árvore binária é ordenada. Um nó de uma árvore binária deve conter campos
para referenciar a seus dois filhos e o campo do conteúdo do nó.

Exemplo:
classe nó privado
tipo elemento caracter público
declare Elemento elemento público
declare Esquerda nó público
declare Direita nó público
método nó(E elemento) público
Esquerda ← Direita ← NULO
Elemento ← E
fim método
fim classe

3.2.1 Transformação em Árvore Binária

O processo para transformar uma árvore qualquer em árvore binária, consiste de dois passos:

1. ligar os nós irmãos; e

2. remover as ligações entre pais e filhos, com exceção do primeiro filho.

A Figura 3.8 ilustra o processo.

32
‘ A’
‘ A’ ‘ A’
‘ B’

‘ B’ ‘ C’ ‘ D’ ‘ E’ ‘ B’ ‘ C’ ‘ D’ ‘ E’
‘ C’

‘ F’ ‘ G’ ‘ H’ ‘ F’ ‘ G’ ‘ H’
‘ F’ ‘ D’

‘ E’

‘ G’

‘ H’

Figura 3.8 Transformação de uma árvore de grau quatro em árvore binária.

3.2.2 Percurso em Árvore Binária

Percursos em árvores são métodos de exame sistemático dos nós de uma árvore binária, de acordo
com uma determinada ordem, de modo que cada nó seja examinado exatamente uma vez. Tais métodos
podem ser usados para fins de consulta ou de alteração de nós.

Há três métodos ou ordens de percurso principais: percurso em pré-ordem (ou pré-fixado), percurso
em pós-ordem (ou pós-fixado) e percurso em ordem (ou central). Todos métodos de percurso são,
implicitamente, recursivos.

3.2.2.1 Percurso em Pré-ord em


No percurso em pré-ordem ou pré-fixado, cada nó é “visitado” antes da visita a seus filhos (da
esquerda para a direita, ou da direita para a esquerda)

Percurso em pré-ordem à esquerda:

1. visita raiz;

2. percorre sub-árvore da esquerda; e

3. percorre sub-árvore da direita.

A Figura 3.9 ilustra esse processo considerando a árvore binária da Figura 3.7.

33
‘ A’ ‘ A’ ‘ A’ ‘ A’

’B’ ’C’ ’B’ ’C’ ’B’ ’C’ ’B’ ’C’

‘ D’ ’E’ ’F’ ‘ D’ ’E’ ’F’ ‘ D’ ’E’ ’F’ ‘ D’ ’E’ ’F’

’G’ ’G’ ’G’ ’G’

‘ A’ ‘ A’ ‘ A’

’B’ ’C’ ’B’ ’C’ ’B’ ’C’

‘ D’ ’E’ ’F’ ‘ D’ ’E’ ’F’ ‘ D’ ’E’ ’F’

’G’ ’G’ ’G’

Figura 3.9 Percurso em pré-ordem ou pré-fixado: ‘A’ ’B’ ’D’ ’E’ ’C’ ’F’ ’G’.

Conforme dito, a implementação dos métodos para percurso pode ser recursiva.

Exemplo:
classe árvore_bin público

classe nó privado
tipo elemento caracter público
declare Elemento elemento público
declare Esquerda nó público
declare Direita nó público

método nó(E elemento) público


Esquerda ← Direita ← NULO
Elemento ← E
fim método
fim classe

declare Raiz nó privado


método árvore_bin() público
Raiz ← NULO
fim método

método raiz() nó público


retorne Raiz
fim método

método visita(P nó) privado


escreva P.Elemento
fim método

método pré_ordem(P nó) público


se P ≠ NULO
então
visita(P)
pré_ordem(P.Esquerda)
pré_ordem(P.Direita)
fim se

34
fim método
fim classe

A chamada do método poderia ser feita da seguinte forma:

declare A árvore_bin
A.árvore_bin()
...
A.pré-ordem(raiz())

3.2.2.2 Percurso em Pós-ordem


No percurso em pós-ordem ou pós-fixado, cada nó é “visitado” somente após a visita a seus filhos
(da esquerda para a direita, ou da direita para a esquerda)

Percurso em pós-ordem à esquerda:

1. percorre sub-árvore da esquerda;

2. percorre sub-árvore da direita; e

3. visita raiz.

Para a árvore binária da Figura 3.7, a ordem de visitas seria ‘ D’ ’E’ ’B’ ’G’ ’F’ ’C’ ‘A’.

3.2.2.3 Percurso em Ordem


No percurso em ordem ou central, para um nó qualquer, primeiro seu filho da esquerda é “visitado”
(ou da direita, incluindo sua sub-árvore), depois o nó, e finalmente seu(s) filho(s) da direita (ou da esquerda,
incluindo sua sub-árvore).

Percurso em ordem à esquerda:

1. percorre sub-árvore da esquerda;

2. visita raiz; e

3. percorre sub-árvore da direita.

Para a árvore binária da Figura 3.7, a ordem de visitas seria ‘ D’ ’B’ ’E’ ‘A’ ’F’ ’G’ ’C’.

Outros três percursos são obtidos invertendo-se a ordem de visita às sub-árvores da esquerda e da
direita: pré-fixado à direita, pós-fixado à direita e central à direita. Para a árvore binária da Figura 3.7, a
ordem de visitas seria ‘ A’ ’C’ ’F’ ’G’ ’B’ ’E’ ’D’ ( pré-fixado à direita), ‘ G’ ’F’ ’C’ ’E’ ’D’ ’B’ ‘ A’ ( pós-fixado à
direita) e ‘ G’ ’F’ ’C’ ‘ A’ ’E’ ’B’ ’D’ (central à direita).

3.2.3 Percurso Usando um Campo Pai

Todos as estratégias para percurso descritas anteriormente podem ser implementadas também de
forma não recursiva. Porém, a implementação não recursiva é complexa e exige o uso de uma pilha. Se um
campo adicional, referenciando o pai do nó, for acrescido aos nós, a implementação não recursiva torna-se
bem mais simples.

Exemplo:

classe árvore_bin público


classe nó privado
declare Elemento elemento público

35
declare Esquerda nó público
declare Direita nó público
declare Pai nó público
método nó(E elemento, P nó) público
Esquerda ← Direita ← NULO
Pai ← P
Elemento ← E
fim método
fim classe

declare Raiz nó privado


método árvore_bin() nó público
Raiz ← NULO
fim método

método raiz() nó público


retorne Raiz
fim método

método visita(P nó) privado


escreva P.Elemento
fim método

método ordem() público


declare P, Q nó
P ← raiz()
repita
enquanto P ≠ NULO faça
Q ← P
P ← P.Esquerda
fim enquanto
se Q ≠ NULO
então
visita(Q)
P ← Q.Direita
fim se
enquanto Q ≠ NULO e P = NULO faça
repita
P ← Q
Q ← P.Pai
até P.Esquerda = NULO ou Q = NULO
se Q ≠ NULO
então
visita(Q)
P ← Q.Direita
fim se
fim enquanto
até Q = NULO
fim método
fim classe

No percurso em ordem acima, um nó é visitado quando seu filho da esquerda é reconhecido como
sendo nulo ou quando ele é alcançado depois de subir a partir de seu filho da esquerda. Os percursos de
pré-ordem e pós-ordem são semelhantes, exceto pelo fato de que, na pré-ordem, um nó só é visitado
quando ele é alcançado descendo a árvore e, na pós-ordem, quando seu filho da direita é reconhecido
como nulo ou quando é alcançado depois de subir a partir de seu filho da direita

36
3.2.4 Aplicações de Árvores

Existem diversas aplicações para árvores. Aqui serão mostradas apenas algumas para ilustrar a
utilidade desse tipo de estrutura de dados.

3.2.4.1 Representação de Expressões Aritméticas


Uma expressão aritmética contendo operadores unários e binários pode ser representada através
de uma árvore binária na qual:

1. operandos são sempre folhas das árvores;

2. operadores são sempre raízes das sub-árvores;

3. operador de menor prioridade aparece na raiz da árvore; sub-expressão à esquerda deste


operador aparece na sub-árvore da esquerda; sub-expressão à direita aparece na sub-árvore
da direita.

Exemplo:
Para a expressão (Fat – X)* 5 + Y/2, a árvore binária correspondente é mostrada na Figura 3.10.

‘ +’

’∗’ ’/’

‘ -’ 5 ’Y’ 2

‘fat’ ‘ X’

∗5 + Y/2.
Figura 3.10 Árvore binária representando a expressão (Fator – X)∗

Os percursos em expressões representadas por árvores binárias definem a seqüência de entrada


dos dados para o cálculo, ou a seqüência usada para avaliação da expressão.

O percurso pré-fixado é usado para expressão representadas na notação polonesa, na qual os


operadores aparecem antes dos operandos. Para a expressão (Fat – X)∗5 + Y/2, usando o percurso pré-
fixado à esquerda, obter-se-ia a ordem de visitas correspondente à notação polonesa: − ∗ Fat X – 5 +
/ Y – 2. O percurso pós-fixado é usado para expressão representadas na notação polonesa reversa (usada
em máquinas de calcular) , na qual os operandos aparecem antes dos operadores. Para a expressão (Fat –
X)× 5 + Y/2, usando o percurso pós-fixado à esquerda, obter-se-ia a ordem de visitas correspondente à
notação polonesa inversa: Fat X – 5 ∗ Y 2 / +.

O percurso central permite que se recupere a expressão na forma original. Para a expressão (Fat –
X)∗5 + Y/2, usando o percurso central à esquerda, obter-se-ia a ordem de visitas Fat – X ∗ 5 + Y /
2.

37
3.2.4.2 Algoritmo de Huffman
Seja um alfabeto consistindo de n símbolos e uma mensagem representada através desse alfabeto.
Deseja-se codificar essa mensagem através de um cadeia de bits, atribuindo-se um código para cada
símbolo. Por exemplo, se os símbolos são α, β, χ e δ, os códigos poderiam ser:

Símbolo Código
α 00
β 01
χ 10
δ 11

A mensagem αβαχχδαα seria codificada como 0001001010110000, exigindo um total de 16 bits.


Contudo, poder-se-ia usar códigos de tamanho variável, onde os códigos com menos bits são usados para
representar os símbolos que aparecem com maior freqüência nas mensagens:

Símbolo Código
α 0
β 110
χ 10
δ 111

Neste caso, a mensagem αβαχχδαα seria codificada como 01100101011100, exigindo um total de 15
bits. Em uma mensagem extensa, a economia de bits poderia ser substancial. Na decodificação da
mensagem, é feita uma varredura da esquerda para direita. Se for encontrado um 0, o símbolo é α, caso
contrário ele será β, χ ou δ e o próximo bit será examinado. Se o segundo bit for 0, o símbolo é χ; se for 1, o
terceiro bit deverá ser examinado para verificar se o símbolo é β ou δ. Quando um símbolo é identificado, o
processo reinicia no bit seguinte para identificar o próximo símbolo. Um método para codificação
(compressão) de dados muito usado baseia-se na freqüência de ocorrências dos símbolos em uma
mensagem usando um alfabeto predefinido. Os dois símbolos com menor freqüência são combinados
dando origem a novos símbolos cuja freqüência é a soma das freqüências dos símbolos componentes. O
processo repete-se até que o símbolo composto contenha todo o alfabeto. Para o alfabeto do exemplo, o
algoritmo consiste dos seguintes passos:

1. determinar a freqüência de cada símbolo: α (4), β (1), χ (2) e δ (1);

2. diferenciar os códigos dos dois símbolos que aparecem com menos freqüência: β e δ. O último bit
diferencia um do outro (0 para β e 1 para δ);

3. combinar esses símbolos em um único símbolo isolado cuja freqüência é a soma das freqüências
dos símbolos que o compõe: βδ (2);

4. para o novo alfabeto obtido, determinar a freqüência de cada símbolo: α (4), βδ (2) e χ (2);

5. diferenciar os códigos dos dois símbolos que aparecem com menos freqüência: βδ e χ. O último
bit diferencia um do outro (0 será o código de χ e 1 o código de βδ);

6. combinar esses símbolos em um único símbolo isolado cuja freqüência é a soma das freqüências
dos símbolos que o compõe: χβδ (4);

7. diferenciar os códigos dos dois símbolos do alfabeto: α e χβδ. O último bit diferencia um do outro
(0 para α e 1 para χβδ);

38
8. o símbolo αχβδ contém o alfabeto inteiro e é representado por uma seqüência de bits nula
(tamanho zero);

No início da decodificação, antes de qualquer bit ser examinado, sabe-se que qualquer símbolo da
mensagem estará contido na cadeia αχβδ. Os dois símbolos que formam αχβδ (α e χβδ) recebem códigos
0 e 1, respectivamente. Assim, se um 0 é encontrado, sabe-se que o símbolo é α; se for encontrado um 1, o
símbolo será ou χ ou β ou δ e o bit seguinte indica se o símbolo é χ ou βδ. Se o símbolo for βδ, o bit
seguinte indica se o símbolo é β (0) ou δ (1).

A ação de combinar dois símbolos em um indica a adequação do uso de uma arvore binária, onde
cada nó representa um símbolo e as folhas os símbolos do alfabeto original. Tal árvore é chamada de
Árvore de Huffman. Para o alfabeto definido anteriormente, a Figura 3.11 mostra a Árvore de Huffman
correspondente. Cada nó na árvore contém o símbolo e sua freqüência.

Construída a Árvore de Huffman, o código de qualquer símbolo do alfabeto pode ser obtido,
começando na folha e subindo até a raiz. O código é inicializado com nulo e toda vez que um desvio à
esquerda ocorre, será incluído um 0 no início do código; toda vez que um desvio à direita ocorre, será
incluído um 1 no início do código.

‘αβχδ’
8

’ α’ ’βχδ’
4 4

‘χ’ ‘βδ’
2 2

‘β’ ‘δ’
1 1

Figura 3.11 Árvore de Huffman.

No exemplo abaixo é mostrado o algoritmo de Huffman. Os argumentos do construtor são os símbolos


e a freqüência de cada símbolo do alfabeto armazenados em vetores (símbolo e freqüência) de N posições
(N é o número de símbolos do alfabeto) onde a i-ésima posição indica a freqüência do i-ésimo símbolo (as
freqüências estão armazenadas em ordem crescente). O algoritmo inicia alocando nós para todos os
símbolos do alfabeto e criando um vetor de apontadores (de tamanho N) para os nós criados. A lista
Nós_raízes é uma lista na qual os elementos são inseridos em posições conforme a freqüência, em
ordem crescente; o elemento a ser removido será sempre o primeiro da lista. Os nós da árvore, além dos
ponteiros para os filhos, têm um ponteiro para o nó pai e campos para armazenar o símbolo e sua
freqüência. O vetor Posição armazena um apontador para os nós folhas da (futura) Árvore de Huffman.

39
Exemplo:
classe nó público
declare Direita, Esquerda, Pai nó
declare Freqüência numérico
declare Elemento elemento

método nó(E elemento, F numérico, D nó, E nó, P nó) público


Direita ← D
Esquerda ← E
Freqüência ← F
Elemento ← E
Pai ← P
fim método
fim classe

classe lista público


declare Início nó
método insere(P nó) público
...
fim método
método remove() nó público
...
fim método
fim classe

classe árvore_bin público


declare Raiz nó
...
fim classe

classe árvore_Huffman público


método árvore_Huffman(Símbolo[N] elemento, Freqüência[N] numérico) público
declare i numérico
declare Posição[N], P, Q, R nó
declare Nós_raízes lista
declare Árvore_Huffman árvore_binária
declare Código[N] bits
Nós_raízes.lista()
Árvore_Huffman.árvore_binária()
i ← 0
enquanto i < N faça /* Constrói lista de folhas */
Posição[i] ← P.nó(símbolo[i], Freqüência[i], NULO, NULO,NULO)
Nós_raízes.insere(P)
i ← i+1
fim enquanto
enquanto i > 1 faça /* Combina duas ramificações em uma só */
P ← Nós_raízes.remove()
Q ← Nós_raízes.remove()
Árvore_Huffman.insere(R.nó(P.Elemento+Q.Elemento, P.Freqüência+Q.
Freqüência, P, Q, NULO))
P.Pai ← Q.Pai ← R
Nós_raízes.insere(R)
i ← i-1
fim enquanto
i ← 0
enquanto i < N faça /* Encontra códigos */
P ← Posição[i]
Código[i] ← ‘’ /* String nulo */
enquanto P ≠ Árvore_Huffman faça

40
se (P.Pai).Esquerda = P
então
Código[i] ← ‘0’ + Código[i]
senão
Código[i] ← ‘1’ + Código[i]
fim se
P ← P.Pai
fim enquanto
i ← i+1
fim enquanto
fim método
fim classe

A Figura 3.12 mostra o processo de construção da Árvore de Huffman da Figura 3.11, executado
pelo primeiro laço do método árvore_Huffman. No primeiro laço do método, é construída a lista de
prioridades inicial Nós_raízes, contendo os (futuros) nós folhas da Árvore de Huffman (Figura 66 (a)). No
segundo laço, são construídos os nós e inserido na árvore, além das folhas, os nós representando os
símbolos combinados. Os elementos da lista Nós_raízes são removidos em duplas e combinados criando
uma nova raiz de sub-árvore que é, depois, inserida na lista Nós_raízes. O terceiro laço do método
árvore_Huffman coloca no vetor Código os códigos de cada um dos símbolos iniciais do alfabeto, na
forma de uma cadeia de bits. O laço interno serve para definir os bits, conforme a posição relativa do nó na
árvore.

41
’ α’ ’ α’
Árvore_Huffman
4 4

Nós_raizes

‘χ’ ‘χ’ ‘βδ’


2 2 2

Nós_raizes

‘β’ ‘δ’ ‘β’


‘δ’
1 1 1
1

(a) (b)

Árvore_Huffman ‘αβχδ’ Árvore_Huffman


8

Nós_raizes

’ α’ ’βχδ’ ’ α’ ’βχδ’
4 4 4 4

‘χ’ ‘βδ’ ‘χ’ ‘βδ’


2 2 2 2

‘β’ ‘β’ ‘δ’


‘δ’
1 1 1 1

(c) (d)

Figura 3.12 Construção de uma Árvore de Huffman. As linhas mais grossas representam a lista de
nós raízes de sub-árvores, que é modificada a medida que os nós são combinados.

42
3.2.4.3 Árvores de Jogos
Árvores de jogos (“game trees”) são árvores genéricas (com qualquer grau) que representam as
possibilidades de jogadas para um jogador a partir de um estado do jogo. Através da construção de uma
árvore de jogo pode-se determinar qual é a próxima jogada “mais promissora” tendo-se como critério o valor
de retorno do método avalia que informa quão bom um determinado estado do jogo (posição do tabuleiro)
é para um determinado jogador.

Exemplo:
Método avalia para o jogo da velha como sendo o número de linhas, colunas e diagonais que ainda
estão "abertas" para um jogador (número de situações nas quais um jogador ainda pode jogar para
vencer), menos o número de linhas, colunas e diagonais abertas para o seu adversário:
avalia(i numérico, j numérico, Jogador caracter) numérico

O problema do método avalia é que ele calcula estaticamente o valor de uma posição do tabuleiro sem
levar em conta as possíveis futuras jogadas do adversário e do próprio jogador. Se for possível prever por
antecipação o desenrolar do jogo, a escolha da próxima jogada mais promissora melhora
consideravelmente. O nível de previsão de uma árvore de jogo corresponde ao número de jogadas futuras
a considerar a partir de um dado estado do jogo, que corresponde à profundidade da árvore de jogo.
Quanto maior o nível de previsão, maior a quantidade de memória exigida para armazenar a árvore com
suas numerosas possibilidades, mas menor a probabilidade de se escolher uma má jogada. A Figura 3.13
mostra uma árvore de jogo com nível de previsão 1. Nas folhas são representadas todas possibilidades de
jogada do jogador ‘×’ nas quais será aplicado o método avalia(i, j, ‘×’). Na terceira grade, por
exemplo, o método avalia(2, 3, ‘×’) (segunda linha, terceira coluna) retorna 2, pois 3 (situações ainda
abertas para ‘×’) – 1 (situações ainda abertas para ‘Ο’) = 2.

Figura 3.13 Avaliação com nível de previsão 1.

Após a construção da árvore de jogo com um determinado nível de previsão, para avaliar a melhor
jogada para um jogador J basta:

1. avaliar as posições nas folhas da árvore com a função de avaliação estática avalia;

2. para cada nó correspondendo à vez do jogador J escolher o valor máximo dos filhos;

3. para cada nó correspondendo à vez do adversário de J escolher o valor mínimo dos filhos (supondo
que o adversário fará a jogada que é pior para J).

Este método chama-se MINIMAX porque, a medida que a árvore é escalada, as funções máximas e
mínimas são aplicadas alternadamente. Na Figura 3.14, o movimento que o jogador ‘Ο’ (designado como +)
deve selecionar, dada a posição do tabuleiro no nó raiz, é aquele que maximiza seu valor, ou seja, i = 1, j =
1. Feita essa jogada, o jogador ‘×’ (designado por –) deve selecionar a posição que minimiza o valor de ‘Ο’
(quinta ou sétima grade, da esquerda para a direita).

43
Figura 3.14 Aplicação do método MINIMAX em uma árvore de Jogos de três níveis. Muitas das
jogadas possíveis não aparecem por representarem situações simétricas às situações mostradas.

3.2.4.4 Representação de Estruturas Hierárquicas


Em muitas aplicações existe, intrinsecamente, uma estrutura hierárquica entre os objetos de dados.
Por exemplo, as unidades que compõem a estrutura universitária (reitoria, pró-reitorias, centros e institutos,
departamentos e cursos). Árvore são estrutura de dados bastante adequadas para representação de
hierarquias.

Em determinadas aplicações, também, existem objetos que são compostos ou que podem ser refinados
em objetos do mesmo tipo. Por exemplo, um determinado item produzido por uma indústria pode ser
representado através de seu código, descrição, custo e tempo de fabricação e itens que o compõe, que
são, por sua vez, representados da mesma forma.

Exemplo:
O liqüidificador modelo Popular, fabricado por determinada indústria, é composto dos seguintes itens:
um Conjunto da Base do Liqüidificador, um Copo e uma Tampa de Copo. O Conjunto da Base do
Liqüidificador pode ser explodido em uma Capa, uma Tampa Inferior, quatro Pés, um Conjunto do
Motor, uma Chave de Velocidades e um Conjunto de Parafusos. Este último item, por sua vez, pode ser
refinado em cinco Parafusos do Tipo M1, três Parafusos do Tipo M2 e quatro Parafusos do Tipo M3.

A forma de representação dos dados acima na forma de árvore facilita uma série de pesquisas úteis
para o controle de produção. Por exemplo, tempo e custo total de produção de um determinado item.

44
4 ALGORITMOS DE BUSCA
Um algoritmo de busca é um algoritmo que aceita um argumento C e tenta encontrar um registro cuja
chave seja C.

4.1 Busca Seqüencial

A busca seqüencial é a forma mais simples de busca. Ela é aplicada a uma tabela organizada como um
vetor ou como uma lista encadeada. Operações de busca foram vistas no Capítulo 2. De maneira genérica,
um algoritmo de busca seqüencial em uma tabela de tamanho n segue o padrão abaixo.

Exemplo:
método busca(C chave) numérico público
declare i numérico
i 0
enquanto i < n faça
se C = Tabela[i].Chave
então
retorne i
senão
i i+1
fim se
fim enquanto
retorne NULO
fim método

Para uma tabela organizada como lista encadeada cujo nó inicial é apontado por Início, a busca
segue o padrão abaixo.

Exemplo:
método busca(C chave) nó público
declare P nó
P Início
enquanto P ≠ NULO faça
se C = P.Chave
então
retorne P
senão
P P.Próximo
fim se
fim enquanto
retorne NULO
fim método

O número de comparações dependerá de onde o registro com a chave procurada encontra-se. Se ele
for o primeiro registro da tabela, será necessária uma única comparação. Se ele for o último, serão
necessárias n comparações. Se for igualmente provável que o registro esteja em qualquer posição da
tabela, serão necessárias (n+1)/2 comparações. Em qualquer caso, o número de comparações é O(n).

Em situações reais, geralmente determinados registros são mais acessados do que outros. Por
exemplo, em um Serviço de Proteção ao Crédito (SPC), provavelmente os registros correspondentes aos

45
maus pagadores contumazes serão acessados com mais freqüência. Assim, se eles forem posicionados no
2
início da tabela, o número médio de comparações pode ser muito reduzido . Se as probabilidades de
acesso aos registros são conhecidas a priori, é conveniente que a tabela seja ordenada em ordem
decrescente de probabilidades.

O conhecimento das probabilidades de acesso, contudo, é raramente disponível. Nesse caso, é útil o
uso de algoritmos que reordenem a tabela continuamente de modo que os registros mais acessados fiquem
em seu início e os menos acessados fiquem no final. Um método usado para tal é o método mover-para-
frente, eficiente apenas para tabelas organizadas na forma de listas lineares. Nesse caso, sempre que uma
busca tiver êxito, o registro recuperado será movido para o início da lista. Outro método é a transposição, na
qual o registro recuperado é trocado pelo registro imediatamente anterior. No exemplo abaixo, é mostrada
uma possível implementação do método para busca transposta considerando que a tabela é representada
através de uma lista encadeada.

Exemplo:
método busca_tranposta(C chave) nó público
declare P, Q, R nó
P Início
Q R NULO
enquanto P ≠ NULO e C ≠ P.Chave faça
R Q
Q P
P P.Próximo
fim enquanto
se P = NULO ou Q = NULO /* Registro não encontrado ou na 1a posição */
então
retorne P
senão
Q.Próximo P.Próximo
P.Próximo Q
se R = NULO
então
Início P
senão
R.Próximo P
fim se
retorne P
fim se
fim método

4.2 Busca Seqüencial Ordenada

Se a tabela contendo os registros a serem buscados está ordenada em ordem crescente ou


decrescente de chaves, várias técnicas poderão ser empregadas para aumentar a eficiência da operação
de busca. Enquanto em uma tabela não ordenada com n registros são necessária n comparações para
determinar que um registro com determinada chave não encontra-se na tabela, em um arquivo ordenado
com chaves uniformemente distribuídas são necessária em n/2 comparações.

4.2.1 Busca Indexada

A busca indexada é uma técnica de pesquisa válida para tabelas ordenadas. Nessa estratégia, é
usada uma tabela auxiliar com tendo os índices que servem como atalhos para a tabela contendo os
registros. Sejam duas entradas da tabela auxiliar contendo as chaves i e j. A entrada contendo a chave i

2
Na verdade, para arquivos contendo milhares de registros, dificilmente será usada a busca seqüencial, já que existem outros métodos
bem mais eficientes.

46
aponta para a entrada na tabela principal onde iniciam os k registros de chave C tal que i ≤ C < j. Na tabela
auxiliar, os registro também deverão ser ordenados. Na Figura 4.1 é mostrada a tabela auxiliar e a tabela
principal para um arquivo contendo um máximo de 800 registros (k = 8). Observe que registros eliminados
são marcados como nulos para que eles sejam ignorados na pesquisa.

chave ind nome chave


0 590 8 0 “iara” 090
1 698 16 1 “ava” 092
2 802 32 2 “josé” 270
3 3 “noé” 313
... 4 “ilka” 400
5 “ivo” 403
98 NULO 784 6 “eva” 412
99 NULO 792 7 “zoé” 509
8 “maria” 590
9 “paulo” 592
10 “flávia” 570
11 “márcia” 580
12 “ana” 587
13 “carlos” 590
14 “arno” 604
15 “josé” 608
16 “enio” 698
17 “karen” 758
18 “márcia” 760
19 “valdo” 785
20 “carlos” 786
21 “lia” 790
22
...
797
798 “XXX” NULO
799 “XXX” NULO

Figura 4.1 Uma tabela seqüencial indexada.

Abaixo é mostrado um exemplo de um método para busca em uma tabela seqüencial indexada de
tamanho máximo TAM_MAX e com uma tabela auxiliar com TAM_MAX/k registros. O método busca_aux
procura na tabela auxiliar o índice onde deve iniciar a busca na tabela principal. Os métodos consideram
que a tabela é representada através de um vetor de 0 a TAM_MAX –1 posições.

Exemplo:
método busca_aux(C chave) numérico público
declare i numérico
i 0
enquanto Tabela_aux[i].Chave < C e i < TAM_MAX/k
faça
i i+1
fim enquanto
retorne Tabela_aux[i-1].Índice
fim se
fim método

método busca_seq_ind(C chave) numérico público


declare i, Limite_busca numérico
i busca_aux(C)
Limite_busca i + k
enquanto Tabela[i].Chave ≤ C e i < Limite_busca faça

47
i i+1
fim enquanto
se i = Limite_busca ou Tabela[i]. Chave ≠ C
então
retorne NULO
senão
retorne i
fim se
fim método

Inserção de Elementos: a inserção de elementos pode exigir o deslocamento de uma grande quantidade
de elementos se não existir espaço entre duas entradas já existentes da tabela. Entretanto, se um registro
estiver marcado como nulo (tiver sido eliminado), somente alguns registros precisarão ser deslocados e o
item eliminado poderá ser reescrito.

Remoção de elementos: a remoção de um registro na tabela exige apenas que ele seja marcado como
nulo na tabela principal.

Tabela secundária Tabela primária Tabela principal


chave ind chave ind nome chave
0 1117 5 0 313 8 0 “iara” 090
1 1180 10 1 580 16 1 “ava” 092
2 2 785 24 2 “mila” 098
3 3 815 32 3 “hans” 112
4 902 40 4 “gretel” 139
23 5 1117 48 5 “fritz” 140
24 6 1180 56 6 “josé” 270
7 1198 64 7 “kurt” 277
8 8 “noé” 313
... 9 “ilka” 388
10 “ivo” 403
98 NULO 784 11 “joca” 430
99 NULO 792 12 “zoé” 509
13 “maria” 590
14 “paulo” 592
15 “flávia” 570
16 “márcia” 580
17 “ana” 587
18 “márcia” 590
19 “arno” 604
20 “josé” 608
21 “enio” 698
22 “karen” 758
23 “kátia” 760
24 “valdo” 785
25 “carlos” 786
26 “lia” 790

...
7997
7998 “XXX” NULO
7999 “XXX” NULO

Figura 4.2 Uma tabela seqüencial com dois níveis de indexação.

A indexação pode envolver vários níveis, visando aumentar a eficiência. Na Figura 4.2 é mostrada
uma tabela principal indexada por dois níveis. A tabela principal pode armazenar um arquivo contendo um

48
máximo de 8000 registros. A tabela primária divide a tabela principal em intervalos de 8 registros; a tabela
secundária divide a tabela primária em intervalos de 5 registros.

4.2.2 Busca Binária

A busca binária é uma técnica de pesquisa para tabelas ordenadas que dispensa o uso de índices e
tabelas auxiliares. Nessa estratégia, a chave é comparada com a chave do registro central da tabela. Se
elas forem iguais, a busca termina; senão, a metade inferior da tabela ou superior será dividida ao meio,
repetindo-se o processo até que a tabela não possa mais ser dividida. No exemplo abaixo, é mostrada uma
implementação doa busca binária. A implementação pode ser mais simples se for realizada de forma
recursiva.

Exemplo:
método busca_binária(C chave) numérico público
declare i, Inf, Sup numérico
Inf 0
Sup TAM_MAX - 1
enquanto Inf ≤ Sup faça
i (Inf+Sup)/2
se Tabela[i].Chave = C
então
retorne i
senão
se Tabela[i].Chave > C
então
Sup i-1
senão
Inf i+1
fim se
fim se
fim enquanto
retorne NULO
fim método

Cada comparação na busca binária reduz o número de possíveis candidatos por um fator de 2.
Sendo assim, o número máximo de comparações é aproximadamente log2n (n = TAM_MAX). Assim, a
complexidade do algoritmo é O(log2n). A Figura 4.3 mostra os passos necessários para recuperação do
registro de chave 580 através de busca binária. Enquanto neste caso são necessárias 4 comparações, a
busca seqüencial exigiria 17 comparações.

49
Inf 0 “iara” 090 0 “iara” 090 0 “iara” 090 0 “iara” 090
1 “ava” 092 1 “ava” 092 1 “ava” 092 1 “ava” 092
2 “mila” 098 2 “mila” 098 2 “mila” 098 2 “mila” 098
3 “hans” 112 3 “hans” 112 3 “hans” 112 3 “hans” 112
4 “gretel” 139 4 “gretel” 139 4 “gretel” 139 4 “gretel” 139
5 “fritz” 140 5 “fritz” 140 5 “fritz” 140 5 “fritz” 140
6 “josé” 270 6 “josé” 270 6 “josé” 270 6 “josé” 270
7 “kurt” 277 7 “kurt” 277 7 “kurt” 277 7 “kurt” 277
8 “noé” 313 8 “noé” 313 8 “noé” 313 8 “noé” 313
9 “ilka” 388 9 “ilka” 388 9 “ilka” 388 9 “ilka” 388
i 10 “ivo” 403 10 “ivo” 403 10 “ivo” 403 10 “ivo” 403
11 “marta” 473 Inf 11 “marta” 473 11 “marta” 473 11 “marta” 473
12 “zoé” LO
509 12 “zoé” LO
509 12 “zoé” LO
509 12 “zoé” LO
509
13 “maria” 560 13 “maria” 560 13 “maria” 560 13 “maria” 560
14 “paulo” 562 14 “paulo” 562 14 “paulo” 562 14 “paulo” 562
15 “flávia” 570 i 15 “flávia” 570 15 “flávia” 570 15 “flávia” 570
16 “márcia” 580 16 “márcia” 580 Inf 16 “márcia” 580 i Inf Sup 16 “márcia” 580
17 “ana” 587 17 “ana” 587 i 17 “ana” 587 17 “ana” 587
18 “paula” 601 18 “paula” 601 18 “paula” 601 18 “paula” 601
Sup 19 “arno” 604 Sup 19 “arno” 604 Sup 19 “arno” 604 19 “arno” 604

Figura 4.3 Passos da busca binária do registro de chave 580.

A busca binária é um método que pode ser usado também para as pesquisas de índices necessárias
para a busca seqüencial indexada, quando a tabela de índices for muito grande. Uma desvantagem da
busca binária é que ela só pode ser usada em uma tabela armazena na forma de um vetor já que ela exige
o acesso direto aos registros.

4.2.3 Busca Através de Árvore Binária de Busca

Um árvore binária de busca (ABB) é uma árvore binária que armazena elementos em função de
uma relação de ordem. Assim, a propriedade básica de uma ABB é: todos os elementos armazenados na
sub-árvore à esquerda de um nó k serão menores que k, e os elementos armazenados na sub-árvore à
direita serão maiores que k (ou vice-versa). O percurso de uma ABB em ordem fornece uma ordem de
visitas nó com o elemento de menor valor para o nó com o elemento de maior valor; o percurso em ordem
resulta em uma ordem de visitas do menor para o maior. A Figura 4.4 mostra uma ABB.

150 “iara”

124 “ava” 300 “ivo”

113 “ana” 130 “eva” 203 “ilka”

224 “igor”

Figura 4.4 Árvore binária de busca

ABB são úteis para representar tabelas onde exista um relação de ordem entre os elementos, pois
elas tornam o processo de classificação dos elementos muito mais eficiente.

50
Os métodos básicos de uma ABB são verificação da existência, inserção e remoção de um
elemento de uma ABB.

Consulta à Elemento: pesquisa para verificar se um elemento pertence à árvore. As regras para procura
de um elemento em uma ABB são as seguintes:

1. inicia busca no nó raiz;

2. se a raiz não contém o elemento procurado, então procura à esquerda, se o elemento for menor do
que a raiz, ou à direita, no caso contrário; e

3. se for encontrado um nó nulo, o elemento não está na árvore.

Exemplo:
método busca(C chave) nó público
declare P nó
P ← Raiz
enquanto P ≠ NULO e P.Chave ≠ C faça
se C < P.Chave
então
P ← P.Esquerda
senão
se C > P.Chave
então
P ← P.Direita
senão
retorne P
fim se
fim se
fim enquanto
retorne NULO
fim método

Inserção de Elementos: os elementos sempre serão inseridos como folhas. O passo inicial é encontrar o
lugar onde o nó estaria caso estivesse na árvore (o elemento a ser inserido não pode fazer parte da árvore).
As regras para inserção de elementos em uma ABB são as seguintes:

1. se árvore é vazia, o elemento é colocado na raiz;

2. se o elemento é menor que a raiz, ele é colocado na sub-árvore da esquerda; e

3. se o elemento é maior do que a raiz, ele é colocado na sub-árvore da direita.

Exemplo:
método insere(E elemento) nó público
declare P, Q nó
P ← Raiz
se P = NULO
então
Raiz ← P.nó(E)
senão
enquanto P ≠ NULO faça
Q ← P
se E.Chave < P.Chave
então
P ← P.Esquerda
senão

51
se E.Chave > P.Chave
então
P ← P.Direita
senão
retorne NULO
fim se
fim se
fim se
fim enquanto
se E.Chave < Q.Chave
então
Q.Esquerda ← P.no(E)
senão
Q.Direita ← P.no(E)
fim se
retorne P
fim método

O método acima retorna um apontador para o nó inserido; se o elemento a ser inserido contém uma
chave igual a de outro elemento que já pertence à árvore, o método retorna nulo. Para uma árvore A, a
inserção de um elemento armazenado na variável Funcionário seria:

A.insere(Funcionário)

A forma da ABB vai depender da ordem em que os nós foram inseridos. Se os nós forem inseridos
em ordem, a árvore será uma lista linear. Em geral, deseja-se árvores “rasas” que proporcionam buscas
mais eficientes.

Remoção: deve ser feita de tal modo que, após a remoção de um elemento, a ABB permaneça ordenada.
As regras para remoção dos elementos em uma ABB são as seguintes:

1. se o nó a ser removido não possui sub-árvores, remove o nó;

2. se o nó a ser removido possui ou sub-árvore à esquerda ou sub-árvore à direita, esta sub-àrvore


passa a ocupar o seu lugar e ele é removido; e

3. se o nó a ser removido possui duas sub-árvores, coloca no seu lugar ou o maior nó da sua sub-
árvore à esquerda ou o menor nó da sua sub-árvore à direita.

Exemplo:
método mínimoDireita(P nó) nó privado
declare Q nó
Q ← P.Direita
se Q ≠ NULO
então
enquanto Q.Esquerda ≠ NULO faça
Q ← Q.Esquerda
fim enquanto
fim se
retorne Q
fim método

método filho_único(P nó) nó privado


se P.Esquerda = NULO e P.Direita ≠ NULO
então
retorne P.Direita
senão

52
se P.Direita = NULO e P.Esquerda ≠ NULO
então
retorne P.Esquerda
senão
retorne NULO /* Se P é folha ou tem dois filhos */
fim se
fim se
fim método

método remove(C chave) nó público


declare P, Q, R, S, T nó
P ← busca(C)
se P ≠ NULO /* Chave C existe */
então
Q ← P.Pai
se Q = NULO e P.Direita = P.Esquerda = NULO /* Árvore só possui P */
então
Raiz ← NULO
senão
R ← filho_único(P) /* P tem apenas uma sub-árvore? */
se R = NULO /* Não. P é folha ou tem dois filhos */
então
R ← mínimoDireita(P)
fim se
se Q.Esquerda = P
então
Q.Esquerda ← R
senão
Q.Direita ← R
fim se
se R ≠ NULO
então
S ← R.Pai
R.Pai ← Q
T ← filho_único(R)
R.Esquerda ← P.Esquerda
R.Direita ← P.Direita
se P.Esquerda ≠ NULO
então
P.Esquerda.Pai ← R
fim se
se P.Direita ≠ NULO
então
P.Direita.Pai ← R
fim se
se S.Esquerda = R
então
S.Esquerda ← T
senão
S.Direita ← T
fim se
T.Pai ← S
fim se
fim se
fim se
retorne P
fim método

53
Na Figura 4.5 é mostrada a remoção do nó cujo conteúdo apontado por P.

18 18

17 30
17 30 Q

20 40 20
P

(a)

18 18

17 30
17 30 Q

20 49
20 40 P

47 50
49 R

47 50 48

48

(b)

18 18

17 30
17 30 Q

20 40 20 47
P

35 49 S 35 49

50 32 48 50
32 47 R

48
T
(c)

Figura 4.5 Remoção em uma ABB de um nó (a) folha; (b) com uma sub-árvore; e (c) com duas sub-
árvores.

4.3 Busca Através de Árvore Balanceada

O tempo de acesso a um nó, em uma estrutura


T representada na forma de árvore, é proporcional à
quantidade de comparações realizadas até encontrá-lo. Quanto menor este tempo, maior é a eficiência no
seu acesso. Uma árvore que mantém seus nós de forma ordenada, como a ABB, permite otimizar os
acessos, uma vez que a busca é direcionada pela ordenação do nó buscado em relação aos demais

54
(maiores ou menores), evitando a procura na árvore inteira. A eficiência no acesso a um nó está relacionada
a dois fatores: (1) freqüência de acesso a cada nó e (2) organização da árvore.

A freqüência de acesso é determinada pela quantidade de vezes que um nó é procurado. Assim, se


todos os elementos forem igualmente procurados essa freqüência é uniforme. Quanto à organização da
árvore, pode-se verificar que a quantidade de comparações necessárias numa árvore ordenada para
localizar um nó está relacionada à distância entre este nó e a raiz da árvore. Assim, quanto mais perto da
raiz estiverem os nós da árvore, mais depressa eles podem ser encontrados. Se a distância de cada um
dos nós em relação à raiz for igual ou bastante próxima à média das distâncias dos nós em relação à raiz, o
tempo de acesso a cada um deles será próximo aos dos demais. Já se uma sub-árvore A1 da árvore A tiver
muitos níveis e outra sub-árvore A2 poucos níveis, procurar um nó em A1 poderá levar muito mais tempo do
que procurar um outro nó em A2, principalmente se em ambos os casos os nós procurados forem folhas.

Uma árvore que esteja organizada de forma que, para qualquer nó, o comprimento da sua sub-
árvore mais à esquerda seja igual ou com uma diferença mínima em relação aos comprimentos das suas
demais sub-árvores, é denominada de árvore balanceada. Há diferentes propostas de estruturação de
árvores balanceadas, como Árvores AVL, Árvores 2-3, Árvores-B e Árvores B+.

No caso das árvores binárias, a ordem na qual os elementos são inseridos na árvore é que vai
determinar a eficiência da localização dos mesmos. Em princípio, não é possível pré-determinar a priori a
ordem em que eles serão inseridos, para que se possa obter uma árvore balanceada. Dois casos bastante
desfavoráveis são: (1) elementos instalados na ordem de classificação (por exemplo: ‘ A’-‘ B’-‘ C’-‘ D’-‘ E’-‘ F’-
‘ G’); e (2) elementos instalados em ziguezague: o primeiro, o último, o segundo, o penúltimo, o terceiro, o
antepenúltimo etc. (por exemplo: ‘ A’-‘ G’-‘ B’-‘ F’-‘ C’-‘ E’-‘ D’). Estas situações geram árvores degeneradas
(Figura 4.6), nas quais o tempo de acesso a cada elemento seria o mesmo para uma representação por
listas.

‘ A’ ‘ A’

‘ B’ ‘ G’

‘ C’ ‘ B’

‘ D’ ‘ F’

... ...
Figura 4.6 Árvores degeneradas.

4.3.1.1 Árvores AVL (AVL-Trees)


O nome AVL vem das iniciais de quem as propôs: Adel' son-Vel' skii e Landis. As árvores AVL
pertencem ao grupo das árvores balanceadas pela altura (“height-balanced trees”). As AVL’s são árvores
binárias de busca nas quais, para qualquer nó, o comprimento de sua sub-árvore da esquerda não pode ser
diferente do comprimento de sua sub-árvore da direita em mais de um nó (ou seja, a diferença é no máximo
de 1 nó). Na Figura 4.7, são mostradas três árvores binárias. Em cada nó, é mostrado o balanceamento do
nó (diferença entre altura da sub-árvore da esquerda e da direita); a árvore do meio não é uma AVL, por
causa do nó cujo balanceamento é 2.

55
0 -1 -1

1 1 0 2 0 1

-1 0 -1 0 0 0 -1 0 0 -1 0

0 0 0 0

Figura 4.7 Três árvores binárias de busca. A central não é uma AVL.

Após a inserção ou remoção de um nó, a árvore poderá ficar desbalanceada (por um nível). Por
isso, sempre que uma destas operações for realizada, será necessário verificar o balanceamento. Se a
árvore estiver desbalanceada, será necessário aplicar uma função de reestruturação da árvore, a qual, se
for preciso, fará uma relocação de todos os nós para que a árvore permaneça ordenada e balanceada.

O balanceamento de uma árvore envolve basicamente operações de: rotação para direita, rotação
para esquerda, rotação dupla para direita e rotação dupla para esquerda (Figura 4.8).

’B’ ’C’

’C’ ’B’

<’B’ >’C’

<’C’ >’C’ <’B’ <’C’

rotação para esquerda em ‘B’

rotação para direita em ‘C’

Figura 4.8 Rotação para esquerda e para direita.

Inserção de Elementos:. As regras para inserção de elementos em uma AVL são as seguintes:

1. o percurso em ordem da árvore resultante deve ser o mesmo da árvore original, isto é, a árvore
resultante deve continuar sendo uma ABB; e

2. a árvore resultante deve continuar balanceada.

56
A forma da ABB vai depender da ordem em que os nós foram inseridos. Se os nós forem inseridos
em ordem, a árvore será uma lista linear. Em geral, deseja-se árvores “rasas” que proporcionam buscas
mais eficientes.

4.3.2 Árvores-B

Uma árvore-B (“B-tree”) é uma estrutura de dados proposta por R. Bayer e E. Mc Creight (o B de B-
Tree vem de Bayer), em 1970 cujos nós possuem ordem M. Árvores-B são bastante utilizadas para
manipulação de registros em arquivos pelo seu bom desempenho. Uma árvore-B é uma árvore de busca M-
ária que possui as seguintes características:

• a raiz ou é uma folha ou possui ao menos dois filhos;

• cada nó intermediário, com exceção da raiz, possui entre M e 2M elementos;

• o caminho entre a raiz e qualquer folha possui o mesmo comprimento; e

• um nó intermediário com k elementos (sendo k, no máximo, igual a 2M) possui k+1 filhos.

Uma árvore-B pode ser de dois tipos, com relação à distribuição dos elementos na sua estrutura: (1)
os elementos estão distribuídos na estrutura da árvore (nós intermediários e folhas); e (2) os elementos
estão apenas nas folhas, e nos nós intermediários há índices para acessar as folhas. A Figura 4.9 mostra
uma árvore-B de ordem 2 (M=2), com elementos apenas nas folhas e com índices de acesso aos elementos
nos nós intermediários.
320

430 480

380 395 406 412 451 472 493 506 511

“ana” “pedro” “helena” “heitor” “ivo” “eva” “paulo” “adão” “iara”

Figura 4.9 Exemplo de árvore-B de ordem 2.

Na árvore-B da figura anterior, a capacidade máxima do nó intermediário é de 4 elementos (2M=4):


um nó intermediário com 2 elementos (k elementos) possui 3 filhos (k+1), um nó intermediário com 3
elementos (k elementos) possui 4 filhos (k+1), um nó intermediário com 4 elementos (k elementos) possui 5
filhos (k+1). Nessa árvore-B, também, as folhas e os nós intermediários têm capacidades diferentes,
sendo a árvore apenas uma estrutura intermediária para otimizar o acesso aos elementos de uma lista
localizados nas folhas. (As folhas poderiam também representar, por exemplo, blocos ou alguma outra
estrutura em memória secundária.). Os nós intermediários contêm as chaves de acesso aos elementos da
lista (por isso alguns deles estão repetidos nos nós intermediários). Nos nós intermediários, o primeiro
elemento contém o menor valor acessado através do seu segundo filho, o segundo elemento contém o
menor valor acessado através do seu terceiro filho, o terceiro elemento contém o menor valor acessado
através do seu quarto filho, e assim por diante.

Considerando que os nós intermediários armazenarão as chaves (ci) e apontadores para os filhos
(ai), cada nó intermediário de uma árvore-B como a da Figura 4.9 possui a seguinte estrutura:

classe nó privado
declare a0, a1, a2, a3, a4 nó público

57
declare c1, c2, c3, c4 numérico público

Genericamente, as chaves dos elementos estão ordenadas: c1<c2...<cn , e as chaves de cada um


dos filhos, apontados por a0,a1,...,an, também, sendo que os elementos (ou as chaves) existentes no filho
apontado por a0 são menores que os do filho apontado por a1, que são menores que os do filho apontado

Consulta a elemento: busca para verificar se um elemento x ou de chave x pertence à árvore. As regras
para procura de um elemento em uma árvore-B são as seguintes:

1. inicia busca no nó raiz;

2. se o nó não contém o elemento procurado, então se x<c1 deve-se continuar a busca através de a0;
se c1 ≤ x < c2, deve-se continuar a busca através de a1; se c2 ≤ x < c3, deve-se continuar a busca
através de a2 ; se c3 ≤ x < c4, deve-se continuar a busca através de a3; se c4 < x deve-se continuar a
busca através de a4,

3. os passos acima são repetidos sucessivamente até que chegue-se em uma folha.

4.4 Busca em Tabelas Não Ordenadas

4.4.1 Busca Através de Árvore Binária de Acesso

A eficiência da busca binária pode ser estendida para tabelas armazenadas por encadeamento se
uma estrutura de acesso aos registros, na forma de uma árvore binária, é acrescida à estrutura. Essa
árvore, chamada árvore binária de acesso ou ABA – permite que se possa fazer acesso a um determinado
registro da tabela (implementado na forma de um nó de uma lista encadeada) quase diretamente, através
da posição do registro (como em uma tabela armazenada através de vetor).

Em uma tabela acessível via ABA, os registros são os nós folhas da árvore e todos os nós contêm o
número de folhas da sub-árvore esquerda do nó (obviamente, nos nós folhas esse valor é zero). O custo
adicional dos nós intermediários que representam a ABA é compensado pela rapidez no acesso aos
elementos da lista. Uma lista com 1000 elementos pode ser representada por uma árvore binária de
profundidade 10, sendo necessárias no máximo 11 visitas para encontrar o k-ésimo elemento da lista; no
caso de uma representação através de da lista linear, poderão ser necessárias 1000 visitas, se o elemento
é o último da lista. Genericamente, para encontrar-se um elemento k em uma lista representada através de
árvore binária, serão necessárias no máximo log2n visitas para uma lista com n elementos enquanto para
uma representação na forma de lista linear poderão ser necessárias n visitas. A Figura 4.10 mostra uma
árvore binária de acesso a uma tabela representada através de uma lista encadeada de seis elementos. O
encadeamento da lista é dispensável mas pode ser usado para permitir o acesso seqüencial aos elementos.

58
3

2 2

⁄ ⁄ ⁄
1 → 130 “eva” 1 → 300 “ivo” /

⁄ ⁄
113 “ana” → 124 “ava” 150 “iara” → 203 “ilka”

Figura 4.10 Árvore binária de acesso usada para implementar uma lista.

No exemplo a seguir, é mostrado um método para encontrar um elemento em uma árvore binária
usada para acessar uma tabela representada através de uma lista. O método mantém uma variável i
contendo o número de elementos da lista que faltam ser contados; ela é inicializada com a posição k do
elemento na lista. Nos nós, foi acrescido um apontador para o pai, visando facilitar as operações de
remoção e inserção.

Exemplo:
classe árvore_bin_acesso público
classe nó público
declare Contador numérico privado
declare Nome nome privado
declare Pai nó privado
declare Esquerda nó privado
declare Direita nó privado
...
fim classe
declare Raiz nó privado
...
método busca(k numérico) nó público
declare P nó
declare i numérico
i ← k
P ← raiz()
enquanto P.contador ≠ 0 faça
se i ≤ P.contador
então
P ← P.Esquerda
senão
i ← P.contador - i
P ← P.Direita
fim se
fim enquanto
retorne P
fim método
fim classe
o o
A Figura 4.11 ilustra o processo através das buscas do 3 e do 5 elemento de uma lista
representada através de uma árvore binária. Para facilitar a implementação da operação de remoção, foi
acrescentado aos nós um apontador para o nó pai.

59
i = 3 3 i = 5

i = 3 2 2 i = 2

1 i = 1 "eva" 1 i = 2 "ivo"

"ana" "ava" i = 1
"iara" "ilka"

o o
Figura 4.11 Localizando o 3 e o 5 elemento de uma lista representada através de uma árvore binária
de acesso.

Remoção de Elementos: a remoção de ume elemento de uma lista encadeada acessível via ABA exige o
acerto de seus contadores.

Exemplo:
classe árvore_bin_acesso público
...
método irmão(P nó) privado
declare Q nó
Q ← P.Pai
se P = Q.Esquerda
então
retorne Q.Direita
senão
retorne Q.Esquerda
fim se
fim método

método remove(k numérico) nó público


declare P, Q, R nó
P ← busca(k)
R ← P
se R ≠ NULO
então
enquanto R ≠ Raiz faça
Q ← R
R ← R.Pai
se R. Esquerda = Q
então
R.Contador ← R.Contador - 1
fim se
fim enquanto
R ← P.Pai
Q ← irmão(P)
se R = Raiz e Q = NULO /* P é o único nó da lista */
então
Raiz ← NULO
senão
R.Nome ← Q.Nome /* Pai de P recebe as informações do irmão de P */
R.Esquerda ← P.Esquerda /* Acerta o encadeamento do irmão de P */
P.Esquerda.Direita ← R
R.Direita ← P.Direita
P.Direita.Esquerda ← R

60
fim se
retorne P
fim método
fim classe

No exemplo acima, os apontadores Esquerda e Direita, que apontam para os filhos de um nó na ABA,
são também usados para implementar o duplo encadeamento na lista representada pelos nós folhas: o
ponteiro Esquerda assume o papel de “anterior” e o ponteiro Direita assume o papel de “próximo”,
conforme a figura abaixo.

1 "eva" 1 "ivo"

"ana" "ava"
"iara" "ilka"

Figura 4.12 Ponteiros com dupla função.

4.4.2 Busca por Cálculo de Endereço (“Hashing”)


A busca por cálculo de endereço é uma técnica de pesquisa para tabelas não ordenadas. Nessa
estratégia, o valor de chave mapea a posição (índice) do registro na tabela. Assim, o cálculo de endereço é,
além de um método de busca, um método de organização física de tabelas.

O mapeamento é realizado através de uma função chamada de função de espalhamento


(“hashing”), que serve para o cálculo do endereço de uma chave. Essa função, quando aplicada a uma
chave válida, deve retornar um valor que é um índice na tabela contendo os registros. A busca por cálculo
de endereço é uma técnica apropriada para aplicações onde queremos responder a pergunta: “Que registro
tem chave de valor C?”

O método é apropriado tanto para busca “interna” (em memória principal) quanto “externa” (em
disco) e é um dos métodos mais usados para organizar bases de dados em disco. Uma situação
simplificada para espalhamento seria o caso em que há n valores (números inteiros) de chave diferentes,
armazenados em uma tabela T de tamanho n. Nesse caso, o registro com chave i poderia ser armazenado
em T[i]. A função de espalhamento seria h(C)=C. No entanto, tipicamente, há muitos mais valores de chave
do que espaços na tabela. Por exemplo, o campo para chave pode receber valores de 0 a 999999, mas a
tabela pode receber no máximo 1000 registros. Nesse caso, não seria realístico reservar uma tabela com
1000000 de espaços, tendo-se que utilizar uma função de espalhamento para mapear a chave para um
intervalo muito menor de valores. Isso, contudo, possibilita que vários valores de chaves sejam mapeados
para o mesmo índice na tabela, ocorrendo, assim, colisões. Cada vez que um valor de chave é mapeado
para um índice já existente na tabela, diz-se que ocorreu uma colisão. Uma boa função de espalhamento
deve minimizar colisões (sua eliminação é praticamente impossível).

Há dois passos envolvidos no processo de encontrar um registro através do cálculo de endereço:

1. computar uma localização na tabela usando a função de espalhamento h(C); e

2. a partir da posição h(C), localizar o registro com chave C usando uma política de resolução de
colisões.

61
A função de espalhamento deve distribuir os registros de maneira uniforme na tabela. A distribuição dos
valores da função de espalhamento vai depender da distribuição da própria chave dos registros no campo
de chaves válidas. Normalmente, tem-se uma das seguintes situações:

1. nada sabe-se sobre a distribuição das chaves. Nesse caso, precisamos de uma função que gere
uma distribuição aleatória uniforme dos valores de chave; ou

2. sabe-se algo sobre a distribuição das chaves. Nesse caso, devemos usar uma função que é
dependente da distribuição evitando aglomerar valores de chave semelhantes na mesma posição
da tabela.

Exemplo:
Função de espalhamento usada para mapear chaves que são números inteiros para uma tabela de 16
posições

método h(C chave) numérico público


retorne resto(C,16)
fim método

A função acima poderia também ser usada para chaves com caracteres somando-se os valores dos
códigos ASCII de cada caracter e usando a função resto(X,Y) para fazer a distribuição.

A política de resolução de colisões pode ser dividida em duas classes: (1) espalhamento aberto, e (2)
espalhamento fechado. A diferença entre as duas está no fato de armazenar registros resultantes de
colisões fora da tabela (espalhamento aberto) ou em outra posição dentro da tabela (espalhamento
fechado).

Espalhamento aberto: também conhecido como “encadeamento separado”, é uma forma simples de
espalhamento que consiste em tratar cada posição da tabela como um apontador de início de uma lista
encadeada. Todos os registros endereçados para uma determinada posição são incluídos na lista daquela
posição. Na Figura 4.13 é mostrado um exemplo de espalhamento aberto, implementado através de um
vetor e de uma tabela de apontadores para listas encadeadas. A função de espalhamento é

método h(C chave) numérico público


retorne resto(C,7)
fim método

62
0 “iara” 021 10 “iara” “ivo” “márcia”
1 “ava” 008 12
0 → 021 → 098 → 210 ⁄
2 “mila” 044 NULO “ava” “zoé” “marta” “flávia”
3 “hans” 346 7
1 → 008 → 148 → 155 → 050 ⁄
4 “gretel” 046 NULO “mila”
5 “fritz” 152 NULO
2 → 044 ⁄
6 “josé” “hans” “kurt”
7 “kurt” 101 NULO
3 → 346 → 101 ⁄
8 “noé” “gretel”
9 “ilka”
4 → 046 ⁄
10 “ivo” 098 16 “fritz”
11 “marta” 155 15
5 → 152 ⁄
12 “zoé” 148 11
13 “maria”
6 ⁄
14 “paulo”
15 “flávia” 050 NULO

16 “márcia” 210 NULO

17 “ana”
18 “paula”
19 “arno”
(a) (b)
Figura 4.13 Espalhamento aberto com tabela implementada através de (a) um vetor e (b) através de
encadeamento.

Cada lista pode estar ordenada de diversas maneiras: ordem de entrada, ordem de valor, ordem de
freqüência de acesso etc. Organizar por ordem de valor facilita buscas quando a chave não encontra-se na
tabela. Registro removidos ou posições desocupadas podem ter o campo correspondente à chave contendo
NULO (que pode ser, por exemplo, -1).

Espalhamento fechado: também conhecido como de “endereçamento aberto” (ou “reespalhamento”), é


uma estratégia na qual todos os registros são armazenados diretamente na tabela. Cada registro i tem uma
posição ideal dada pela função de espalhamento h(C(i)). Se um registro a ser inserido já tem sua posição
ocupada (ocorreu uma colisão), ele é inserido em alguma outra posição da tabela. A mesma política para
encontrar uma posição alternativa deve então ser usada tanto na inserção quanto na busca de registros.

Existem dois tipos de espalhamento fechado: “bucket hashing” e busca linear.

No “bucket hashing”, a tabela é vista como um vetor de M posições dividido em B “buckets”


(“baldes” ou recipientes), mais uma área de “overflow”. Cada “bucket” contém M/B posições.

Exemplo:
Tabela de 28 posições, com 7 “buckets” e área de “overflow” de 6 posições. Cada “bucket” contém 3
posições (Figura 4.14). A função de espalhamento é
método h(C chave) numérico público
retorne resto(C,7)
fim método

63
0 “iara” 021
1 “ivo” 098 0
2 “márcia” 210
3 “ava” 008
4 “zoé” 148 1
5 “marta” 155
6 “mila” 044
7 2
8
9 “hans” 346
10 “kurt” 101 3
11
12 “gretel” 048
13 4
14
15 “fritz” 152
16 5
17
18
19 6
20
21 “flávia” 050
22
23
24
25
26

Figura 4.14 Espalhamento fechado.

A função de espalhamento atribui cada registro à primeira posição do “bucket”. Se a posição já está
ocupada, então uma nova posição é procurada seqüencialmente em direção ao final do “bucket”. Se todas
as posições do “bucket” estão ocupadas, então o registro é armazenado na primeira posição livre da área
de “overflow” no final da tabela. A tabela deve ser organizada de modo que o mínimo possível de registros
vá para a área de “overflow”, pois a busca dos registros nessa área é seqüencial (mais custosa).

Em uma variação da estratégia “bucket hashing”, a função de espalhamento determina uma


posição para um registro como se não estivéssemos usando “bucket hashing”. Se a posição está ocupada,
é procurada uma posição dentro do “bucket”. No exemplo anterior (tabela com 27 posições), a função de
espalhamento é

método h(C chave) numérico público


retorne resto(C,27)
fim método

Dessa maneira, como os registros não são atribuídos sempre para a primeira posição de um bucket,
reduzimos a ocorrência de colisões.

A busca linear é a forma mais clássica de espalhamento. O objetivo do processo de resolução de


colisões é encontrar uma posição vaga para o registro que pode potencialmente ser qualquer posição da
tabela. Após uma primeira colisão, uma função de “reespalhamento” rh() é aplicada sobre o resultado do
cálculo da função de espalhamento para procurarmos por uma posição vaga. Se essa nova posição
determinar uma nova colisão, a função de reespalhamento é novamente aplicada, e esse procedimento se
repete até ser encontrada uma posição vaga. Assim, uma seqüência de posições deve ser determinada
para até que seja encontrada uma posição vaga. A função de reespalhamento recebe o uma posição na
tabela e retorna a próxima posição na seqüência.

64
Exemplo:
h(C) = resto(C, TAM_MAX), onde TAM_MAX é o tamanho da tabela
rh(i) = resto((i + 1), TAM_MAX)
ou de maneira mais geral:
rh(i) = resto((i + c), TAM_MAX), onde c é uma constante.

Para a seqüência de inserções 021, 098, 008, 210, 148, 155, 044, 346, 101, 048, 152 e 050, a tabela
ficaria conforme a figura abaixo.

0
1
2
3
4
5
6
7
8 “ava” 008
9
10
11
12
13 “zoé” 148
14
15
16
17 “ivo” 098
18 “mila” 044
19 “fritz” 152
20 “marta” 155
21 “iara” 021
22 “márcia” 210
23 “flávia” 346
24 “hans” 101
25 “kurt” 048
26 “gretel” 050

Figura 4.15 Busca linear.

A seqüência de geração de índices foi a seguinte:

021 → 21
098 → 17
008 → 8
210 → 21 → 22
148 → 13
155 → 20
044 → 17 → 18
346 → 22 → 23
101 → 20 → 21 → 22 → 23 → 24
048 → 21 → 22 → 23 → 24 → 25
152 → 17 → 18 → 19
050 → 23 → 24 → 25 → 26

65
Um dos problemas da abordagem acima o agrupamento, fenômeno onde duas chaves mapeadas
para posições diferentes na tabela competem por posições no processo de reespalhamento. No exemplo,
tal fenômeno ocorreu na inserção de diversas chaves. Uma solução para esse problema envolve o uso de
funções de espalhamento que não dependem somente da chave a que são aplicadas.

Exemplo:
1) A função de espalhamento depende do número de vezes que é aplicada
rh(i,,j) = quociente((i + j), TAM_MAX) onde j indica a j-ésima aplicação da função.
Esse tipo de função pode ainda sofrer com agrupamentos quando duas chaves seguem o mesmo
percurso de reespalhamento (agrupamento secundário).

2) Espalhamento duplo: nesse tipo de espalhamento são usadas duas funções de espalhamento, h1(i) e
h2(i). A função h1 é a função de espalhamento primária e a função h2 é a função de espalhamento
secundária (reespalhamento). Se a posição h1(i) estiver ocupada, a função de reespalhamento é
utilizada.
rh(i) = resto(( i + h2(i) ), TAM_MAX)
Nesse caso a seqüência de teste depende da chave original e não da posição para onde ela é
mapeada na tabela. O espalhamento duplo evita todo tipo de agrupamento. Em uma boa
implementação de espalhamento duplo, o tamanho da tabela deve ser um número primo.

Um aspecto importante a ser considerado em tabelas cuja inserção e busca de registros é baseada no
uso de função de espalhamento refere-se à remoção de registros da tabela. Há duas considerações
importantes: (1) a remoção de registros não deve prejudicar buscas futuras e (2) posições vagas devem
estar disponíveis para futuras inserções. Uma possível estratégia de implementação consiste na colocação
de um marcador em posições de registros removidos indicando que um registro já a ocupou. Dessa
maneira, o procedimento de busca vai continuar procurando por um registro mesmo quando encontra uma
posição vaga. Um problema nessa estratégia é que posições vagas aumentam o tempo necessário para
buscas. Tal problema, contudo, pode ser resolvido através de uma reorganização periódica da tabela
usando a função de espalhamento ou através de uma reorganização localizada quando um registro é
removido.

66
5 MÉTODOS DE CLASSIFICAÇÃO
Métodos de classificação são métodos para ordenar os dados de uma tabela conforme determinada
política aplicada sobre um ou mais campos usados como chaves. O fato dos dados estarem ordenados
pode influenciar sobremaneira a eficiência das operações de busca. Imagine, por exemplo, o tempo de
busca para achar determinado assinante em uma lista telefônica não ordenada.

Existem vários métodos de classificação de dados, com diferentes desempenhos, muitas vezes
relacionados à forma como os registros estão distribuídos na tabela. A eficiência de algoritmos de
ordenação pode ser afetada pelos seguintes fatores:

1. número de registros ;

2. tamanho das chaves e dos registros; e

3. até que ponto os registros estão ordenados antes de iniciar o processo de ordenação

Normalmente, a eficiência pode ser medida pelo número de comparações efetuadas.

A seguir, serão examinados alguns métodos de classificação.

5.1 Classificação por Troca

5.1.1 Método da Bolha

O método da bolha (“bubble sort”) é um método de classificação simples de entender e


implementar, sendo, entretanto, pouco eficiente. A idéia por trás deste método é percorrer a tabela
seqüencialmente várias vezes. Em cada passagem, é comparado o elemento do registro com seu sucessor,
sendo que eles são trocados se não estiverem na ordem correta.

Exemplo:
classe tabela público

classe registro privado


declare Elemento elemento público
declare Chave numérico público
...
fim classe

TAM_MAX = ...
declare Tabela[TAM_MAX] registro privado

método bubble_sort() público


declare i, j numérico
declare Trocou lógico
declare Aux registro
Trocou ← verdadeiro
j ← 0
enquanto j < TAM_MAX-1 e Trocou faça
Trocou ← falso
i ← 0
enquanto i < TAM_MAX-j-1 faça
se Tabela[i].Chave > Tabela[i+1].Chave
então

67
Trocou ← verdadeiro
Aux ← Tabela[i]
Tabela[i] ← Tabela[i+1]
Tabela[i+1] ← Aux
fim se
i ← i+1
fim enquanto
j ← j+1
fim enquanto
fim método
fim classe

O laço mais externo do método acima controla o número de passagens; o laço mais interno é usado para
cada passagem individual. Cada nova execução do comando enquanto desse laço terá uma iteração a
menos do que a anterior (pela subtração de j), já que o final da tabela já estará ordenada. A variável
Trocou é usada para garantir que o método não tentará classificar uma tabela já ordenada. Abaixo, é
mostrada aplicação do método para uma tabela de 8 registros

25 57 48 37 12 92 86 33 i=0j=0
25 57 48 37 12 92 86 33 i=1j=0
25 48 57 37 12 92 86 33 i=2j=0
25 48 37 57 12 92 86 33 i=3j=0
25 48 37 12 57 92 86 33 i=4j=0
25 48 37 12 57 92 86 33 i=5j=0
25 48 37 12 57 86 92 33 i=6j=0
25 48 37 12 57 86 33 92 i=0j=1
25 48 37 12 57 86 33 92 i=1j=1
25 37 48 12 57 86 33 92 i=2j=1
25 37 12 48 57 86 33 92 i=3j=1
25 37 12 48 57 86 33 92 i=4j=1
25 37 12 48 57 86 33 92 i=5j=1
25 37 12 48 57 33 86 92 i=0j=2
25 37 12 48 57 33 86 92 i=1j=2
25 12 37 48 57 33 86 92 i=2j=2
25 12 37 48 57 33 86 92 i=3j=2
25 12 37 48 57 33 86 92 i=4j=2
25 12 37 48 33 57 86 92 i=0j=3
12 25 37 48 33 57 86 92 i=1j=3
12 25 37 48 33 57 86 92 i=2j=3
12 25 37 48 33 57 86 92 i=3j=3
12 25 37 33 48 57 86 92 i=0j=4
12 25 37 33 48 57 86 92 i=1j=4
12 25 37 33 48 57 86 92 i=2j=4
12 25 33 37 48 57 86 92 i=0j=5
12 25 33 37 48 57 86 92 i=1j=5

Fig. 88 Ordenação pelo método da bolha.

O método da bolha possui como vantagem a pouca exigência de memória, porém o número médio
2
de iterações é O(n ). Se a tabela está ordenada (ou parcialmente ordenada), esse número cai para O(n).

5.1.2 Método por Troca e Partição

Seja um registro a (o registro pivô) qualquer da tabela em uma determinada posição. Supondo que a
seja colocado na posição i obedecendo às seguintes condições:

68
1) cada registro nas posições de 0 a i-1 terá uma chave menor do que a chave de a; e

2) cada registro nas posições de i+1 a n terá uma chave maior do que a chave de a.

O método por troca e partição (“quick sort”) classifica a tabela dividindo-a em subtabelas e realizando, a
cada iteração, o processo acima.

Exemplo:
método partição (Limite_inf numérico, Limite_sup numérico) numérico privado
declare Inf, Sup numérico
declare Aux registro
declare Chave numérico
Inf ← Limite_inf
Sup ← Limite_sup
Chave ← Tabela[Inf]
enquanto (Inf < Sup) faça
enquanto Tabela[Inf].Chave ≤ Chave e Inf < Sup faça
Inf ← Inf + 1
fim enquanto
enquanto Tabela[Sup].Chave > Chave faça
Sup ← Sup - 1
fim enquanto
se Inf < Sup
então
Aux ← Tabela[Inf]
Tabela[Inf] ← Tabela[Sup]
Tabela[Sup] ← Aux
fim se
fim enquanto
Tabela[Limite_inf] ← Tabela[Sup]
Tabela[Sup] ← Chave
retorne Sup
fim método

método quick_sort(Limite_inf numérico, Limite_sup numérico) publico


declare i numérico
se Limite_inf < Limite_sup
então
i ← partição(Limite_inf, Limite_sup)
quick_sort(Limite_inf, i-1)
quick_sort(i+1,Limite_sup)
fim se
fim método

O método partição recebe os índices das extremidades de um segmento, coloca o registro a em sua
posição correta no segmento e retorna o índice dessa posição. Esse processo é repetido recursivamente
pelas chamadas do método efetuadas pelo método quick_sort. Abaixo, é mostrada aplicação do método
em uma tabela de 8 registros. As linhas mais largas delimitam o segmento que está sendo ordenado; os
elementos achureados correspondem à chave de ordenação correntemente usada.

69
25 57 48 37 12 92 86 33
Inf Sup
25 57 48 37 12 92 86 33
Inf Sup
25 57 48 37 12 92 86 33
Inf Sup
25 57 48 37 12 92 86 33
Inf Sup
25 12 48 37 57 92 86 33
Inf Sup
25 12 48 37 57 92 86 33
Inf Sup
25 12 48 37 57 92 86 33
Inf Sup
25 12 48 37 57 92 86 33
Inf Sup
12 25 48 37 57 92 86 33
Inf Sup
12 25 48 37 57 92 86 33
Inf Sup
12 25 48 37 33 92 86 57
Inf Sup
12 25 48 37 33 92 86 57
Inf Sup
12 25 48 37 33 92 86 57
Inf Sup
12 25 48 37 33 92 86 57
Inf Sup
12 25 33 37 48 92 86 57
Inf Sup
12 25 33 37 48 92 86 57
Inf Sup
12 25 33 37 48 92 86 57
Inf Sup
12 25 33 37 48 92 86 57
Inf Sup
12 25 33 37 48 57 86 92
Inf Sup
12 25 33 37 48 57 86 92
Inf Sup

Fig. 88 Ordenação por troca e partição.

O método de troca e partição realiza, em média, um número de iterações na O(n.logn) se os elementos da


tabela estão uniformemente distribuídos. Como algoritmo de ordenação de propósito geral (ou seja, sem
nenhuma premissa a respeito da tabela a ser ordenada), o método de troca e partição é o mais rápido.

No exemplo acima, o pivô inicial é o registro mediano da tabela. Entretanto, ele pode ser escolhido
aleatoriamente.

O melhor desempenho do método de troca e partição quando as partições obtidas têm o mesmo
tamanho; o pior caso ocorre quando os pivôs são sempre o registro com o maior ou o menor valor de chave
na partição. O método não é muito bom para tabelas pequenas (até 9 registros). Assim, o método pode ser
usado para uma ordenação parcial até que os segmentos tornem-se pequenos, quando aplica-se, então,
outro método.

70
5.2 Classificação por Seleção e Por Árvore

Uma classificação por seleção é uma estratégia de ordenação na qual sucessivos elementos são
selecionados em seqüência e dispostos em suas posições corretas pelas ordem.

5.2.1 Método da Seleção Direta

No método de seleção direta (“select sort”), em cada passagem, procura-se o registro com o maior valor
de chave, que é colocado na última posição da tabela (troca-se o registro de maior chave com o registro da
última posição). Em seguida procura-se o maior registro do restante da tabela (não ordenada), que é
colocado na penúltima posição, e assim sucessivamente, até que todos os registros estejam ordenados.

Exemplo:
método select_sort() público
declare i, j, Índice numérico
declare maior registro
j ← TAM_MAX-1
enquanto j >0 faça
maior ← Tabela[0].Chave
Índice ← 0
i ← 1
enquanto i <= j faça
se Tabela[i].Chave > maior.Chave
então
maior ← Tabela[i]
Índice ← i
fim se
i ← i+1
fim enquanto
Tabela[Índice] ← Tabela[j]
Tabela[j] ← maior
j ← j-1;
fim enquanto
fim método

A tabela abaixo mostra a ordenação usando o método select_sort descrito acima; os elementos
achureados correspondem ao maior corrente.

71
25 57 48 37 12 92 86 33 Maior = 25 j = 7 i = 1 Índice = 0
25 57 48 37 12 92 86 33 Maior = 57 j = 7 i = 2 Índice = 1
25 57 48 37 12 92 86 33 Maior = 57 j = 7 i = 3 Índice = 1
25 57 48 37 12 92 86 33 Maior = 57 j = 7 i = 4 Índice = 1
25 57 48 37 12 92 86 33 Maior = 57 j = 7 i = 5 Índice = 1
25 57 48 37 12 92 86 33 Maior = 92 j = 7 i = 6 Índice = 5
25 57 48 37 12 92 86 33 Maior = 92 j = 7 i = 7 Índice = 5
25 57 48 37 12 33 86 92 Maior = 25 j = 6 i = 1 Índice = 0
25 57 48 37 12 33 86 92 Maior = 57 j = 6 i = 2 Índice = 1
25 57 48 37 12 33 86 92 Maior = 57 j = 6 i = 3 Índice = 1
25 57 48 37 12 33 86 92 Maior = 57 j = 6 i = 4 Índice = 1
25 57 48 37 12 33 86 92 Maior = 57 j = 6 i = 5 Índice = 1
25 57 48 37 12 33 86 92 Maior = 57 j = 6 i = 6 Índice = 1
25 57 48 37 12 33 86 92 Maior = 25 j = 5 i = 1 Índice = 0
25 57 48 37 12 33 86 92 Maior = 57 j = 5 i = 2 Índice = 1
25 57 48 37 12 33 86 92 Maior = 57 j = 5 i = 3 Índice = 1
25 57 48 37 12 33 86 92 Maior = 57 j = 5 i = 4 Índice = 1
25 57 48 37 12 33 86 92 Maior = 57 j = 5 i = 5 Índice = 1
25 33 48 37 12 57 86 92 Maior = 25 j = 4 i = 1 Índice = 0
25 33 48 37 12 57 86 92 Maior = 33 j = 4 i = 2 Índice = 1
25 33 48 37 12 57 86 92 Maior = 48 j = 4 i = 3 Índice = 2
25 33 48 37 12 57 86 92 Maior = 48 j = 4 i = 4 Índice = 2
25 33 12 37 48 57 86 92 Maior = 25 j = 3 i = 1 Índice = 0
25 33 12 37 48 57 86 92 Maior = 33 j = 3 i = 2 Índice = 1
25 33 12 37 48 57 86 92 Maior = 33 j = 3 i = 3 Índice = 1
25 33 12 37 48 57 86 92 Maior = 25 j = 2 i = 1 Índice = 0
25 33 12 37 48 57 86 92 Maior = 33 j = 2 i = 2 Índice = 1
25 12 33 37 48 57 86 92 Maior = 25 j = 1 i = 1 Índice = 0

Fig. 88 Ordenação por seleção direta.

O princípio do método de seleção direta é semelhante ao do método da bolha. Seu número médio
2
de iterações também é O(n ), mas na prática, ele é mais rápido que o método da bolha por não fazer tantas
trocas de posições de registros, um processo que pode ser computacionalmente caro.

5.3 Classificação por Inserção

Uma classificação por seleção é uma estratégia de ordenação na qual sucessivos elementos são
selecionados em seqüência e dispostos em suas posições corretas pelas ordem.

5.3.1 Método da Inserção Direta

No método de inserção direta (“insertion sort”), cada registro é inserido na tabela na ordem correta.
Considera-se que a tabela é dividida em dois segmentos, um ordenado e outro não. Inicialmente, considera-
se que o primeiro registro está na ordem correta. A partir daí, por interações sucessivas, compara-se os
registros do segundo segmento com os do primeiro, no qual os registros vão sendo inseridos na ordem
correta.

Exemplo:
método troca(i numérico, j numérico) lógico privado
declare Aux registro
se i ≥ 0 e i < TAM_MAX e j ≥ 0 e j < TAM_MAX
então
Aux ← Tabela[i]
Tabela[i] ← Tabela[j]
Tabela[j] ← Aux

72
retorne verdadeiro
senão
retorne falso
fim se
fim método

método insertion_sort() público


declare i, j numérico
i ← 2
enquanto i < TAM_MAX faça
j ← i
enquanto j > 0 e Tabela[j].Chave < Tabela[j-1].Chave faça
troca(j,j-1)
j ← j – 1
fim enquanto
i ← i + 1
fim enquanto
fim método

25 57 48 37 12 92 86 33 i=2j=2
25 48 57 37 12 92 86 33 i=3j=3
25 48 37 57 12 92 86 33 i=3j=2
25 37 48 57 12 92 86 33 i=4j=4
25 37 48 12 57 92 86 33 i=4j=3
25 37 12 48 57 92 86 33 i=4j=2
25 12 37 48 57 92 86 33 i=4j=1
12 25 37 48 57 92 86 33 i=6j=6
12 25 37 48 57 86 92 33 i=7j=7
12 25 37 48 57 86 33 92 i=7j=6
12 25 37 48 57 33 86 92 i=7j=5
12 25 37 48 33 57 86 92 i=7j=4
12 25 37 33 48 57 86 92 i=7j=3
12 25 37 33 48 57 86 92 i=7j=2

O pior desempenho do método de inserção direta é obtido quando os dados já estão ordenados em
ordem decrescente; o melhor caso ocorre quando os dados já estão ordenados em ordem crescente; o
método de inserção direta pode ser uma boa escolha quando a tabela já está parcialmente ordenada.

5.3.2 Método Shell Sort

O método “shell sort” faz comparações e trocas entre elementos não adjacentes, explorando o
melhor caso do método de inserção direta. Ele procura melhorar a ordenação da tabela para que uma
ordenação por inserção direta possa terminar o trabalho, sendo, portanto, substancialmente melhor para o
pior caso da inserção direta. A tabela é dividida em subtabelas conceituais, onde cada subtabela é
ordenada por inserção direta.

Exemplo:
método insertion_sort2(n numérico, Incr numérico) privado
declare i, j numérico
declare Aux registro
i ← Incr
enquanto i < n faça
j ← i
enquanto j ≥ Incr e Tabela[j].Chave < Tabela[j-Incr].Chave faça
Aux ← Tabela[j]
Tabela[j] ← Tabela[j-Incr]

73
Tabela[j-Incr] ← Aux
j ← j - Incr
fim enquanto
i ← i + Incr
fim enquanto
fim método

método shell_sort() público


int i, j
i ← TAM_MAX/2
enquanto i >= 2 faça
j ← 0
enquanto j < i faça
insertion_sort2(TAM_MAX-j, i)
j ← j +1
fim enquanto
i ← i/2
fim enquanto
insertion_sort2(TAM_MAX, 1)
fim método

25 57 48 37 12 92 86 33 Incr=4
12 57 48 37 25 92 86 33 Incr=2
12 57 25 37 48 92 86 33 Incr=1
12 25 57 37 48 92 86 33 Incr=1
12 25 37 57 48 92 86 33 Incr=1
12 25 37 48 57 92 86 33 Incr=1
12 25 37 48 57 86 92 33 Incr=1
12 25 37 48 57 86 33 92 Incr=1
12 25 37 48 57 33 86 92 Incr=1
12 25 37 48 33 57 86 92 Incr=1
12 25 37 33 48 57 86 92 Incr=1

74

Оценить