1 Árvore Binária de Busca

Conforme já vimos, árvores binárias de busca (ABB) seguem as seguintes propriedades:

≥ x          ≤ x









x

As árvores binárias de busca permitem armazenar dados com bastante eficiência. A inserção, busca e remoção tem complexidade O(h) em que h é a altura da árvore.

Entretanto um problema pode acontecer dependendo da ordem em que são inseridos os dados na árvore. Um caso extremo, porém bastante possível, é que os dados entrem já ordenados. Nessa situação, uma árvore binária acaba virando uma lista ligada.

0123456789

2 Árvore Binária de Busca Balanceadas

Uma forma de evitar esse tipo de degeneração é através das árvores binárias de busca balanceadas (ABBB). Que buscam manter a altura da árvore limitadas a O(log n) em que n é a quantidade de nós da árvore. Algumas ABBBs utilizam rotações para manter esse balanceamento, as rotações podem ser para a esquerda ou para direita, e elas devem manter as propriedades da ABB, ou seja os nós da esquerda são menores que a raiz, e os da direita são maiores. Vamos começar com uma rotação para a esquerda, no exemplo abaixo, o que queremos é fazer o 7 passar a ser a raiz, note que a subárvore α tem elementos menores que 5, β tem elementos maiores que 5 e menores que 7, e que γ tem elementos maiores que 7. Devemos manter essas subárvores nos locais corretos.

       7γ            β





           α










5                                    β
                                       α







                            5





γ










7

Vamos codar em python essa rotação, levando em conta que a nossa classe nó é a seguinte:

class noh: 
   def __init__(self, dado): 
       self.dado = dado 
       self.esq = None 
       self.dir = None 
 
def rotacionaEsquerda(y): 
   x = y.dir 
   y.dir = x.esq 
   x.esq = y 
   return x

Note que a rotação funciona mesmo que as subárvores sejam None. A rotação a direita é análoga.

7γ                           5β         α              7γ  α         β







5

def rotacionaDireita(y): 
   x = y.esq 
   y.esq = x.dir 
   x.dir = y 
   return x

Fazendo as rotações adequadas, conseguiremos deixar as árvores balanceadas. Entretanto não é trivial saber quais rotações são necessárias. Por exemplo, uma árvore que armazenamos primeiro o 5, depois o 7 e por fim um 6, ficaria desbalanceada da seguinte forma:

5          57                                                                                                     576

Se tentarmos fazer uma rotação a esquerda, teremos:

576       756

A árvore continua desbalanceada, e podemos observar que uma rotação a direita só retornaria ao estado anterior. Podemos verificar que com algumas rotações adicionais nas subárvores, poderíamos enfim deixar a árvore balanceada.

Vale notar que deixar a árvore de busca perfeitamente balanceada, embora fosse muito eficiente para as operações de busca, seria demasiadamente custoso nas operações de inserção e remoção. Então o que buscamos são árvores suficientemente balanceadas. Uma das estruturas de dados que segue esse preceito, é a árvore Rubro-Negra, que veremos seu funcionamento na próxima sessão.

3 Árvore Rubro-Negra

As árvores binárias de busca rubro-negras, além das propriedades de uma ABB, ainda possuem mais algumas:

I

Todo nó é vermelho ou negro.

II

Nas árvores esquerdistas um nó vermelho sempre é filho esquerdo do seu pai. Como o nó raiz é negro.

III

Um nó vermelho só tem filhos negros.

IV

As árvores vazias (None) são as folhas e são negras.

V

Para cada nó x, o caminho para qualquer folha descendente (None) tem o mesmo número de nós negros. Não contamos o próprio nó x e chamamos essa quantidade de altura negra.

Verifique que a seguinte árvore é rubro-negra esquerdista.

5            7None             6None   None            3      4None  None              2None            1None  None

Por quê esse tipo de árvore é balanceado? Essa pergunta exigirá uma prova mais formal, mas por enquanto vou ficar com uma intuição que deve ser suficiente para te convencer: Como não existem dois nós vermelhos consecutivos, para alcançar uma folha, o melhor caso seria se o caminho só tivesse nós negros, digamos h nós. Para alcançar qualquer outro nó folha, o pior caso seria um caminho composto por nós vermelhos e negros alternados. Mas como o número de nós negros é o mesmo, o caminho total no pior caso seria 2h.

Vamos modificar o código em python para representar um nó. Acrescentando o atributo cor como um boleano, se o nó tiver cor (se cor = True) o nó é vermelho, se ele não tem cor (se cor = False) ele é negro.

class noh: 
   def __init__(self, dado): 
       self.dado = dado 
       self.esq = None 
       self.dir = None 
       self.cor = True

Suponha que uma árvore rubro-negra esteja respeitando todas as propriedades, como vamos adicionar um novo nó, se ele for preto podemos estragar a propriedade da altura negra, por isso, sempre que adicionamos um novo nó, ele será vermelho.

5NNoonnee       54NNNooonnneee

Dessa forma não estragamos a propriedade V. Se o nó 5 for negro, nada precisa ser feito e a árvore continua correta. Entretanto se o nó 5 for vermelho estaremos violando a propriedade III.

54NNNooonnneee

Deixaremos assim por enquanto, e consertaremos esse problema depois. Considere agora o caso em que o 4 já estava na árvore e o cinco é o novo nó a ser colocado.

4NNoonnee       45NNNooonnneee

Agora, embora conservamos a propriedade V, estamos violando a propriedade II. Podemos consertar essa propriedade com uma rotação a esquerda.

45NNNooonnneee       54NNNooonnneee

Que cores devem ser os nós 4 e 5, para respeitar a propriedade V? Lembre-se que a árvore em que apenas o 4 estava presente atendia todas as propriedades. Vamos considerar 2 casos: o caso em que 4 era negro, e o caso em que era vermelho.

Caso negro: Observamos que não podem ser 4 e 5 negros, já que isso aumentaria a altura negra das folhas filhas de 4. Também observamos que se 4 for negro e 5 vermelho também estaríamos reduzindo a altura negra da folha filha de 5. Concluímos que o 4 precisa ser vermelho! Já o 5 não poderá ser vermelho pois também estaríamos reduzindo a altura negra de todas as folhas da subárvore.

Caso vermelho: Nesse caso, obviamente não podemos fazer nenhum deles negros já que estaríamos mudando a altura negra. Então deixamos ambos vermelhos.

45NNNooonnneee       54NNNooonnneee

45NNNooonnneee       54NNNooonnneee

Novamente nesse último caso, estamos violando a propriedade III, mas deixaremos assim temporariamente. Observe que o que fazemos então é que o filho esquerdo da raiz da subárvore é sempre vermelho e a raiz da subárvore tem a mesma cor da raiz antiga. Podemos então alterar a função de rotacionar a esquerda.

def rotacionaEsquerda(y): 
   x = y.dir 
   y.dir = x.esq 
   x.esq = y 
   x.cor = y.cor 
   y.cor = True 
   return x

Agora suponha que temos a árvore abaixo e inserimos o nó 3 (ou que ele já estivesse lá e fosse vermelho. Novamente não podemos simplesmente remover a cor do 3 ou do 5, pois isso afetaria a propriedade da altura negra. Então a solução é fazer uma rotação a direita no 7, que vai transformar a linha colocando o 5 na raiz e o 3 e o 7 como irmãos, da mesma forma que na rotação a esquerda a raiz da subárvore recebe a cor da raiz antiga, e podemos atualizar a rotação a direita.

7      8                                       5 6                      3 4        1        7 8        6       3 4        1














5

O código dessa rotação segue abaixo:

def rotacionaDireita(y): 
   x = y.esq 
   y.esq = x.dir 
   x.dir = y 
   x.cor = y.cor 
   y.cor = True 
   return x

Só que agora temos um nó vermelho que é filho direito. Mas se fizermos uma rotação a esquerda voltamos no problema anterior. Então vamos precisar de mais uma operação, que é a sobeVermelho. Note que dois filhos vermelhos e um pai negro, se o vermelho vai para o Pai a altura negra permanece a mesma. E mais, note que a raiz é o único nó que pode trocar de vermelho para negro sem prejudicar a propriedade da altura negra. Segue o código da sobeVermelho.

         8        6         4        1






       7                  3
















5 5      7 8        6       3 4        1

Nesse caso, se o 5 for a raiz, ela também precisa ficar negra.

def sobeVermelho(y): 
   y.cor = True 
   y.esq.cor = False 
   y.dir.cor = False

Por fim precisamos de 2 funções auxiliares:

def ehVermelho(y): 
   if y == None: 
       return False 
   else: 
       return y.cor 
 
def ehNegro(y): 
   if y == None: 
       return True 
   else: 
       return y.cor == False

e a inserção:

 
 
def insere_aux(raiz, dado): 
   if raiz == None: 
       return noh(dado) 
   elif dado < raiz.dado: 
       raiz.esq = insere_aux(raiz.esq, dado) 
   elif dado > raiz.dado: 
       raiz.dir = insere_aux(raiz.dir, dado) 
   else: 
       #jah existe esse dado, ignorar 
       return raiz 
 
   if ehVermelho(raiz.dir) and ehNegro(raiz.esq): 
       raiz = rotacionaEsquerda(raiz) 
   if ehVermelho(raiz.esq) and ehVermelho(raiz.esq.esq): 
       raiz = rotacionaDireita(raiz) 
   if ehVermelho(raiz.esq) and ehVermelho(raiz.dir): 
       sobeVermelho(raiz) 
   return raiz 
 
def insere(raiz, dado): 
   raiz = insere_aux(raiz, dado) 
   raiz.cor = False 
   return raiz

A busca é igual ao da árvore binária.

def busca(raiz, dado): 
   if raiz == None: 
       return raiz 
   if dado < raiz.dado: 
       return busca(raiz.esq, dado) 
   if dado > raiz.dado: 
       return busca(raiz.dir, dado) 
   return raiz