• Nenhum resultado encontrado

Algoritmos e Estruturas de Dados. Lição n.º 22 Árvores binárias de busca

N/A
N/A
Protected

Academic year: 2021

Share "Algoritmos e Estruturas de Dados. Lição n.º 22 Árvores binárias de busca"

Copied!
27
0
0

Texto

(1)

Algoritmos e Estruturas de Dados

Lição n.º 22

(2)

Árvores binárias de busca

Árvores binárias de busca.

Árvores equilibradas

Outras classes de árvores.

(3)

Classe Table<K, V>, revisão

Eis uma classe abstrata

Table

típica, representando

tabelas sem chaves duplicadas:

public abstract class Table<K extends Comparable<K>, V>

{

public abstract V get(K key);

public abstract void put(K key, V value);

public abstract void delete(K key);

public abstract boolean has(K key);

public abstract boolean isEmpty();

public abstract int size();

public abstract Iterable<K> keys();

}

K

é o tipo das chaves;

V

é o tipo dos valores.

A função

get

é a função de busca; a função

put

acrescenta um par à tabela; a função

delete

remove da tabela o par com a chave dada; a função

has

indica se existe na tabela um par com a

chave dada; a função

isEmpty

indica se a tabela está vazia, isto é, se não tem elementos; a função

size

calcula o número de pares na tabela; a função

keys

retorna uma estrutura iterável com

(4)

Árvores de nós, revisão

Convencionalmente, em Java, as árvores binárias são

representadas por um conjunto de nós, cada um com

valor e com duas referências, uma para o nó que

representa a subárvore esquerda e a outra para a

referência que representa a subárvore esquerda.

As árvores de busca implementam as tabelas; por

isso, em cada nó guarda-se uma chave e o valor

respetivo.

Para cada árvore, recursivamente, a subárvore da

esquerda tem chaves menores do que a chave da

árvore e a árvore da direita tem chaves maiores do

que a chave da árvore.

(5)

Classe BinarySearchTree<K, V>

Eis a programação convencional, em Java:

public class BinarySearchTree<K extends Comparable<K>, V> extends Table<K, V>

{

private Node root;

private class

Node

{

private

final

K key;

private V value;

private Node left;

private Node right;

private int size; // size of subtree

public Node(K key, V value, int size)

{

this.key = key;

this.value = value;

this.left = null;

this.right = null;

this.size = size;

}

}

Note bem: a chave, uma vez fixada no

construtor, não pode ser mudada.

O nó mantém também o tamanho da

árvore representada pelo nó.

(6)

Procurando na árvore

Procura-se pela chave.

Se a chave for igual à chave do nó, o valor é o valor do nó.

Se não, se a chave for menor do que a chave do nó, procura-se na

subárvore esquerda.

Se não, procura-se na subárvore direita.

A função pública, que procura na árvore toda, usa uma função

privada que procura numa dada subárvore:

20140521 Algoritmos e Estruturas de Dados 6

public V get(K key)

{

return get(key, root);

}

private V get(K key, Node x)

{

V result = null;

if (x != null)

{

int cmp = key.compareTo(x.key);

if (cmp < 0)

result = get(key, x.left);

else if (cmp > 0)

result = get(key, x.right);

else

result = x.value;

}

return result;

Na verdade, a sequência de

comparações usada é “menor do

que a chave do nó”, “maior do que

a chave do nó” e, se não uma nem

outra, “igual à chave”.

Quando a chave não existe,

a função retorna

null

.

(7)

Função has

Programa-se simplesmente em termos da função

get

:

public boolean has(K key)

{

return get(key) != null;

}

(8)

Acrescentando à árvore

Acrescenta-se uma chave e um valor.

Se a árvore for vazia, passa a ter um nó.

Se não, se a chave for igual à chave do nó, o valor substitui o valor

do nó.

Se não, se a chave for menor do que a chave do nó, acrescenta à

subárvore da esquerda

Se não, acrescenta à subárvore direita.

Tal como no

get

, a função pública, que acrescenta à árvore, usa uma

função privada que acrescenta a uma dada subárvore e que retorna

uma referência para a árvore onde foi feito o acrescento.

Essa referência substitui a referência inicial.

No final, atualiza o tamanho da árvore representada pelo nó,

recorrendo ao tamanho de cada uma das subárvores.

Observe com atenção e aprenda a técnica.

(9)

Função put

public void put(K key, V value)

{

root = put(key, value, root);

}

private Node put(K key, V value, Node x)

{

Node result = x;

if (result == null)

result = new Node(key, value, 1);

else

{

int cmp = key.compareTo(x.key);

if (cmp < 0)

result.left = put(key, value, x.left);

else if (cmp > 0)

result.right = put(key, value, x.right);

else

result.value = value; // key exists, replace value

result.size = 1 + size(result.left) + size(result.right); // invariant

}

return result;

}

Estude bem esta função!

Repare que ela devolve uma referência

para o nó que representa a árvore à qual

o novo valor foi acrescentado. Se a

árvore era vazia, é uma referência para

um novo nó. Se não, é a referência para a

própria árvore após o valor ter sido

recursivamente acrescentado ao filho

esquerdo ou ao filho direito (consoante

o chave é menor ou maior que o chave

da raiz), ou após o valor da raiz ter sido

modificado, se a chave for igual à chave

da raiz.

Em todos os casos, o valor do tamanho é recalculado à saída,

usando os valores dos filhos, já calculados anteriormente.

(10)

Estas são simples:

Funções size, isEmpty

20140521 Algoritmos e Estruturas de Dados 10

public int size()

{

return size(root);

}

private int size(Node x)

{

int result = 0;

if (x != null)

result = x.size;

return result;

}

public boolean isEmpty()

{

return root == null;

}

A função pública

size

, que

dá o tamanho da árvore,

usa uma função privada que

dá o tamanho da árvore

representada pelo nó dado.

(11)

Para estudar pequenos exemplos, usaremos uma

função toString, análoga à da classe

Tree<T>

:

Função toString

public String toString()

{

String result = toString(root);

return result;

}

public String toString(Node x)

{

String s = x == null ? "" :

"<" + x.key + " " + x.value + ">" +

toString(x.left) + toString(x.right);

return "(" + s + ")";

}

Uma árvore é representada por uma

cadeia que começa por “(” e termina

por “)”. Se for vazia, não há mais nada;

se não, há a chave e o valor, seguidos

da cadeia da subárvore esquerda,

(12)

Eis uma função de teste, que aceita números e os vai

acrescentando à árvore de busca, mostrando-a a cada passo:

Testando a função put

20140521 Algoritmos e Estruturas de Dados 12

public static void testPut()

{

BinarySearchTree<Integer, Integer> t =

new BinarySearchTree<Integer, Integer>();

while (!StdIn.isEmpty())

{

int x = StdIn.readInt();

t.put(x, x*x);

StdOut.println(t);

StdOut.println("size: " + t.size());

}

}

Para simplificar fazemos o valor

ser o quadrado da chave.

$  java  ...  BinarySearchTree   4   (<4  16>()())   size:  1   12   (<4  16>()(<12  144>()()))   size:  2   3   (<4  16>(<3  9>()())(<12  144>()()))   size:  3   8   (<4  16>(<3  9>()())(<12  144>(<8  64>()())()))   size:  4   9   (<4  16>(<3  9>()())(<12  144>(<8  64>()(<9  81>()()))()))  

(13)

Vejamos um exemplo com uma árvore de estudantes, em que

chave é o número de aluno e o valor é o nome.

Carregamos a árvore por leitura a partir de um ficheiro cujo

nome é indicado na linha de comando:

BinarySearchTree<Integer, String>

public static void treeRead(In f, BinarySearchTree<Integer, String> t)

{

while (!f.isEmpty())

{

int x = f.readInt();

f.readChar(); // skip one space

String s = f.readLine();

t.put(x, s);

}

}

Eis um troço do ficheiro:

...  

36975  GONÇALO  JORGE  FERNANDES  RUAS   45171  GONÇALO  MARTINHO  MENDES  BARRACOSA   21129  JOÃO  DAVID  BÔTO  PANCINHA  

42281  JOÃO  FILIPE  GONÇALVES  CABEÇADAS  MATOS   45400  JOÃO  FILIPE  SILVA  NUNES  COELHO  

44610  JOÃO  PAULO  REIS  ROSA   35540  JOÃO  PEDRO  MATIAS  NUNES  

25164  JORGE  FERNANDO  PACHECO  MARTINS   44543  JOSÉ  DANIEL  RIBEIRO  SALVADO   2901  JOSÉ  GABRIEL  FERNANDES  CHAVECA   47712  JOSÉ  MIGUEL  FORJA  PINTADO  ALVES  

Note bem: o ficheiro está

ordenado pelo nome, não pelo

(14)

Usamos a tabela de estudantes, previamente lida.

Observe:

Testando a função get

20140521 Algoritmos e Estruturas de Dados 14

public static void testGet(String[] args)

{

BinarySearchTree<Integer, String> students =

new BinarySearchTree<Integer, String>();

In f = new In(args[1]);

treeRead

(f, students);

StdOut.println("size: " + students.size());

while (!StdIn.isEmpty())

{

int x = StdIn.readInt();

String s = students.get(x);

StdOut.println(s);

}

}

$  

java  ...  BinarySearchTree  2  ../work/inscritos_por_nome.txt    

size:  51  

47414  

RAQUEL  CATARINA  CARRIÇO  TOMÉ  

34749  

PEDRO  MIGUEL  GONÇALVES  SILVA  

32901  

null  

(15)

Não tem nada de especial. Fica aqui apenas para

completar o exercício:

Função main

public static void main(String[] args)

{

int choice = 1;

if (args.length >= 1)

choice = Integer.parseInt(args[0]);

switch (choice) {

case 1:

testPut();

break;

case 2:

testGet(args);

break;

default:

break;

}

}

(16)

Removendo da árvore

Remove-se indicando a chave.

Se a árvore for vazia, fica na mesma.

Se não, se a chave for igual à chave do nó, remove-se

esse nó.

Se não, se a chave for menor do que a chave do nó,

remove-se da subárvore esquerda

Se não, remove-se da subárvore direita.

Para remover a raiz, troca-se o valor com o valor do

nó seguinte e remove-se o nó seguinte.

No final, atualiza-se o tamanho da árvore

representada pelo nó, recorrendo ao tamanho de

cada uma das subárvores.

(17)

Função remove

public void delete(K k)

{

root = delete(k, root);

}

private Node delete(K k, Node x)

{

Node result = null;

if (x != null)

{

int cmp = k.compareTo(x.key);

if (cmp == 0)

result =

deleteRoot

(x);

else

{

if (cmp < 0)

x.left = delete(k, x.left);

else

x.right = delete(k, x.right);

x.size = 1 + size(x.left) + size(x.right);

result = x;

}

}

return result;

Se a chave é igual à chave do nó,

remove-se esremove-se nó, com a função

deleteRoot

.

O tamanho da árvore pendurada no nó é atualizado à saída.

Se não, remove-se à esquerda ou à

direita, consoante a chave é menor ou

maior que a chave do nó.

Se o nó for null, o que acontecerá

recursivamente quando a chave não

existir, o resultado é null também.

(18)

Função deleteRoot

20140521 Algoritmos e Estruturas de Dados 18

private Node deleteRoot(Node x)

{

assert x != null;

Node result;

if (x.right == null)

result = x.left;

else if (x.left == null)

result = x.right;

else

{

Node t = x;

x = first(x.right);

x.right = deleteFirst(t.right);

x.left = t.left;

x.size = 1 + size(x.left) + size(x.right);

result = x;

}

return result;

}

Se ambos os filhos não forem

null

, repare bem: o valor do

nó passa a ser o valor do nó seguinte, obtido através da

função

first

, aplicada à subárvore direita. Depois, remove-se

o “primeiro” nó da subárvore direita. Não mexe na

subárvore esquerda. O tamanho é recalculado.

Se o filho direito do nó for

null

, o resultado

de remover o nó é um apontador para o

filho esquerdo e vice-versa.

(19)

Funções first e deleteFirst

private Node first(Node x)

{

Node result = x;

while (result.left != null)

result = result.left;

return result;

}

private Node deleteFirst(Node x)

{

Node result = x.right;

if (x.left != null)

{

x.left = deleteFirst(x.left);

x.size = 1 + size(x.left) + size(x.right);

result = x;

}

return result;

}

O primeiro nó é o nó mais à esquerda.

Iterativamente “descemos” pelo nó da

esquerda, enquanto houver nó da esquerda.

Se o filho esquerdo for

null

, o primeiro é a

raiz e, portanto, remover o primeiro é

remover o nó da raiz, e neste caso, o

resultado é o filho direito.

Se o filho esquerdo não for

null

, então

remove-se recursivamente o primeiro do

filho esquerdo, etc.

(20)

Criamos uma árvore aleatória e depois vamos removendo

elementos, pela chave:

Testando a função delete

20140521 Algoritmos e Estruturas de Dados 20

public static BinarySearchTree<Integer, Integer> randomTree(int n) {

BinarySearchTree<Integer, Integer> result = new BinarySearchTree<Integer, Integer>(); int[] a = new int[n];

for (int i = 0; i < n; i++) a[i] = i; StdRandom.shuffle(a); for (int x : a) result.put(x, x*x); return result; }

public static void testDelete(String[] args) {

int n = 15;

if (args.length > 1)

n = Integer.parseInt(args[1]);

BinarySearchTree<Integer, Integer> t = randomTree(n); StdOut.println(t); StdOut.println("size: " + t.size()); while (!StdIn.isEmpty()) { int x = StdIn.readInt(); t.delete(x); StdOut.println(t); StdOut.println("size: " + t.size()); } }

(21)

Com uma árvore com 20 nós inicialmente:

Testando a função delete, exemplo

$  java  ...  BinarySearchTree  3  20   (<15  225>(<3  9>(<1  1>(<0  0>()())(<2  4>()()))(<5  25>(<4  16>()())(<14  196>(<8  64>(<7  49>(<6  36>()())()) (<11  121>(<10  100>(<9  81>()())())(<13  169>(<12  144>()())())))())))(<16  256>()(<18  324>(<17  289>()()) (<19  361>()()))))   size:  20   9   (<15  225>(<3  9>(<1  1>(<0  0>()())(<2  4>()()))(<5  25>(<4  16>()())(<14  196>(<8  64>(<7  49>(<6  36>()())()) (<11  121>(<10  100>()())(<13  169>(<12  144>()())())))())))(<16  256>()(<18  324>(<17  289>()())(<19  361>() ()))))   size:  19   16   (<15  225>(<3  9>(<1  1>(<0  0>()())(<2  4>()()))(<5  25>(<4  16>()())(<14  196>(<8  64>(<7  49>(<6  36>()())()) (<11  121>(<10  100>()())(<13  169>(<12  144>()())())))())))(<18  324>(<17  289>()())(<19  361>()())))   size:  18   0     (<15  225>(<3  9>(<1  1>()(<2  4>()()))(<5  25>(<4  16>()())(<14  196>(<8  64>(<7  49>(<6  36>()())())(<11   121>(<10  100>()())(<13  169>(<12  144>()())())))())))(<18  324>(<17  289>()())(<19  361>()())))   size:  17   19   (<15  225>(<3  9>(<1  1>()(<2  4>()()))(<5  25>(<4  16>()())(<14  196>(<8  64>(<7  49>(<6  36>()())())(<11   121>(<10  100>()())(<13  169>(<12  144>()())())))())))(<18  324>(<17  289>()())()))   size:  16   15   (<17  289>(<3  9>(<1  1>()(<2  4>()()))(<5  25>(<4  16>()())(<14  196>(<8  64>(<7  49>(<6  36>()())())(<11   121>(<10  100>()())(<13  169>(<12  144>()())())))())))(<18  324>()()))   size:  15  

(22)

Outras funções

Podemos acrescentar as funções de ordem,

minimum

,

maximum

,

rank

e

select

, por analogia com

a classe

Tree<T>

. Por exemplo:

O iterador

keys

é análogo ao iterador

items

da

classe

Tree<T>

.

20140521 Algoritmos e Estruturas de Dados 22

public K minimum()

{

return minimum(root);

}

private K minimum(Node x)

{

return x.left == null ? x.key : minimum(x.left);

}

(23)

Árvores perfeitamente equilibradas

Uma árvore é perfeitamente equilibrada se para cada

nó o tamanho das suas subárvores diferir de 1 no

máximo. Exemplos:

65

80

32

71

81

44

12

24

99

31

16

4

52

5

61

91

32

2

14

11

59

41

12

33

42

54

7

1

Esta não é

perfeitamente

equilibrada.

(24)

Árvores equilibradas

Uma árvore é equilibrada se para cada nó a altura da

suas subárvores diferir de 1 no máximo.

Manter uma árvore equilibrada é menos complicado

do que mantê-la perfeitamente equilibrada, ao pôr e

ao remover.

20140521 Algoritmos e Estruturas de Dados 24

4

12

8

9

14

15

1

Esta é equilibrada mas

não perfeitamente

equilibrada.

(25)

Árvores AVL

Uma árvore AVL é uma árvore de busca equilibrada.

“AVL” são as iniciais dos inventores, os matemáticos

russos Adelson-Velskii e Landis (1962).

Em certas implementações, os nós das árvores AVL

têm um membro que guarda a altura da subárvore

cuja raiz é esse nó, ou então a diferença da altura

com a subárvore “irmã”.

Após inserir um elemento, a árvore reequilibra-se

automaticamente, se tiver ficado desequilibrada.

Idem, ao remover um elemento.

As árvores AVL garantem comportamento

(26)

Árvores rubinegras (

red

-black)

Uma árvore rubinegra (red-black tree) é uma árvore binária de

busca, com sentinela, em que cada nó tem uma de duas cores:

vermelho ou preto.

Restringindo as maneiras de colorir os nós ao longo dos caminhos

da raiz até às folhas, garante-se que nenhum caminho da raiz até

uma folha tem mais do dobro dos nós do que qualquer outro.

Assim, a árvore fica “aproximadamente” equilibrada.

Quando se insere ou remove um elemento, a árvore reorganiza-se

automaticamente, de maneira a repor a propriedade das árvores

rubinegras, isto é, de maneira a que nenhum caminho seja mais do

dobro dos nós que qualquer outro.

Isto garante comportamento logarítmico em inserções, buscas e

remoções.

As árvores AVL são mais “rígidas” do que as rubinegras, e por isso

trabalham mais para inserir ou remover, mas menos para procurar.

(27)

Árvores chanfradas

As árvores chanfradas (splay trees) são árvores binárias de

busca auto-ajustáveis.

Depois de uma operação de acesso—busca, inserção ou

remoção– o elemento acedido é promovido até à raiz.

A ideia é acelerar os acessos subsequentes aos nós mais

recentemente acedidos.

A promoção é feita de dois em dois níveis, excepto quando o

nó está no primeiro nível.

Foram inventadas por Sleator e Tarjan em 1985. Daniel Sleator

é professor na Universidade Carnagie-Mellon e Robert Tarjan

é professor na Universidade de Princeton.

As árvores podem estar bastante desequilibradas

episodicamente, e não garantem comportamento logarítmico

em todas as operações, mas têm comportamento logarítmico

“em média”.

Referências

Documentos relacionados

Na aplicação das políticas contábeis da Companhia e das controladas, a Administração deve fazer julgamentos e elaborar estimativas a respeito dos valores contábeis

Promovido pelo Sindifisco Nacio- nal em parceria com o Mosap (Mo- vimento Nacional de Aposentados e Pensionistas), o Encontro ocorreu no dia 20 de março, data em que também

No entanto, expressões de identidade não são banidas da linguagem com sentido apenas porque a identidade não é uma relação objetiva, mas porque enunciados de identi- dade

Tabela de medidas: todas as medidas especificadas na tabela de medida em anexo se referem à peça pronta, já tendo recebido a 1ª lavagem caso haja diferença na peça

O capítulo 4, Tópico discursivo, é dedicado à organização tópica, ou temática, do texto. Trata-se de um dos fatores mais importantes para a construção da sua coerência

Não só pelo carácter inovador da Directiva-Quadro da Água, no que respeita à exigência face à gestão deste tipo de informação, mas por todos os efeitos que esta directiva

H111 Analisar alguns fenômenos associados (à teoria da relatividade restrita, dilatação do tempo, contração do espaço, relação massa – energia).... H112 Diferenciar radiações

Os roedores (Rattus norvergicus, Rattus rattus e Mus musculus) são os principais responsáveis pela contaminação do ambiente por leptospiras, pois são portadores