Skip to content

Trabalho de algoritmos e estrutura de dados 2. Campus Divinópolis CEFET-MG

Notifications You must be signed in to change notification settings

Gustavo01rb/Arvore-Rubro-Negra

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 

Repository files navigation

Árvore Rubro-Negro

Gustavo Rodrigues Barcelos

Introdução

Árvores do tipo rubro-negra ou vermelho-preto são árvores binárias de busca balanceadas, que, de maneira simplificada, é uma árvore de busca binária que insere e remove de forma inteligente, para assegurar que a árvore permaneça aproximadamente balanceada.

Essa estrutura foi inventada no ano de 1972 por Rudolf Bayer, um professor emérito de informática na Universidade Técnica de Munique, que nomeou a estrutura de “Árvores binárias B simétricas” e posteriormente foi renomeada em um artigo publicado em 1978 escrito por Leonidas J. Guibas e Robert Sedgewick.

Complexidade de Tempo em Notação big O

Algoritmo Caso médio Pior médio
Espaço O(n) O(n)
Busca O(log n) O(log n)
Inserção O(log n) O(log n)
Remoção O(log n) O(log n)

Execução do código

Códigos compilados e executados em: gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0

Há duas versões para testes dentro da pasta 'codigo':

  1. gcc Main.c -o Main.c
  2. gcc Testes.c -o Main.c

A primeira opção é um teste pré-definido já a segunda é um teste livre para o usuário. Na exibição, a ávore aparecerá deitada, considere cima como direita e baixo com esquerda.

Estrutura de dado

A estrutura usada para cada nó de uma árvore rubro-negra é extremamente semelhante a de uma árvore binária simples, a pricipal diferença se dá no acrécimo de uma variável do tipo bolleana para indicar a cor que um determinado nó representa. Nesse caso foi adotado 'false' para a cor vermelha e 'true' para a cor preta.

Dessa forma a estrutura de um nó possui os seguintes atributos:

  • Chave
  • Cor
  • Filho Esquerdo
  • Filho Direito
  • Pai

Caso não exista um pai ou um filho de um nó o atributo do ponteiro correspondente apontará para NULL. Nesse código considera-se que esses valores 'NULL' possuem a cor negra e também são tratados como se fossem ponteiros para as folhas da árvore

struct Tree{
    capsule key;
    bool cor; // false = vermelho; true = preto
    Tree *esq;
    Tree *dir;
    Tree *pai;
};

Propriedades

Uma árvore Rubro-Negra é uma árvore de busca binária que satisfaz as seguintes propriedades Rubro-Negra:

  1. Um nó é vermelho ou é preto
  2. A raiz é preta
  3. Toda folha (NULL) é preta
  4. Se um nó é vermelho então ambos os seus filhos são pretos
  5. Para cada nó p, todos os caminhos desde p até as folhas contêm o mesmo número de nós pretos

Essas regras asseguram uma propriedade crítica das árvores rubro-negras: que o caminho mais longo da raiz a qualquer folha não seja mais do que duas vezes o caminho mais curto da raiz a qualquer outra folha naquela árvore. O resultado é que a árvore é aproximadamente balanceada.

Exemplo de árvore Rubro-Negra:

Rotações

A árvore Rubro-Negro é um sistema muito delicado, que pode ser facilmente desbalanceado a partir de operações de inserção e remoção por exemplo. Para corrigir isso é utilizado um esuqema de rotações.

Existem 4 Tipos de rotação e são elas:

Rotação simples a direita

  • Seja X o pai de T
  • Torne o filho à direita de T o filho à esquerda de X.
  • Torne X o filho à direita de T

Código utilizado:

void RSD(Tree **t){
    Tree *no = (*t);
    Tree *aux = no->pai;
    Tree *vo = avo(no);
    if(no->pai->esq == no){
        if( avo(no) != NULL){
            Tree *sub = (Tree *)malloc(sizeof(Tree));
            vo->esq = no;
            no->pai = vo;
            sub->pai = no;
            sub->dado = aux->dado;
            sub->esq = no->dir;
            sub->dir = aux->dir;
            no->dir = sub;
            free(aux);
        }else{
            /*Swap*/
                capsule temp;
                temp = aux->dado;
                aux->dado = no->dado;
                no->dado = temp;
            /*Fim Swap*/
            aux->esq = no->esq;
            aux->esq->pai = aux;
            no->esq = no->dir;
            no->dir = aux->dir;
            if(no->dir != NULL)
                no->dir->pai = no;
            aux->dir = no;
        }
    }else if(no->pai->dir == no){
        aux = no->esq;
        no->pai->dir = aux;
        aux->pai = no->pai;
        no->esq = aux->dir;
        aux->dir = no;
        no->pai = aux;
    }
}

Rotação simples a esqueda

  • Seja X o pai de T
  • Torne o filho à esquerda de T o filho à esquerda de X.
  • Torne X o filho à esquerda de T

Código Utilizado:

void RSE(Tree **t){
    Tree *no = (*t);
    Tree *aux = no->pai;
    Tree *vo = avo(no);
    if(no->pai->dir == no){
        if( vo != NULL){
            Tree *sub = (Tree *)malloc(sizeof(Tree));
            vo->dir = no;
            no->pai = vo;
            sub->pai = no;
            sub->cor = aux->cor;
            sub->dado = aux->dado;
            sub->dir = no->esq;
            sub->esq = aux->esq;
            no->esq = sub;
            free(aux);
        }else{
            /*Swap*/
                capsule temp;
                temp = aux->dado;
                aux->dado = no->dado;
                no->dado = temp;
            /*Fim Swap*/
            aux->dir = no->dir;
            aux->dir->pai = aux;
            no->dir = no->esq;
            no->esq = aux->esq;
            if(no->esq != NULL)
                no->esq->pai = no;
            aux->esq = no;
        }
    }else if(no->pai->esq == no){
        aux = no->dir;
        no->pai->esq = aux;
        aux->pai = no->pai;
        no->dir = aux->esq;
        aux->esq = no;
        no->pai = aux;
    }

}

Rotação Dupla a direita

Pode ser obtida a partir de uma rotação simples a esquerda seguida de uma rotação simples a direita

Código utilizado:

void RDD(Tree **t){
    RSE(t);
    RSD(&(*t)->esq);
}

Rotação Dupla a esquerda

Pode ser obtida a partir de uma rotação simples a direita seguida de uma rotação simples a esquerda

Código utilizado:

void RDE(Tree **t){
    RSD(t);
    RSE(&(*t)->dir);
}

Inserções

Para garantir o balanceamento de uma árvore Rubro-Negra a cada inserção é realizada uma série de verificações com intuito de garantir que suas propriedades sejam sempre verdadeiras. Caso haja algum caso em que as propriedades não são satisfeitas será necessário realizar uma série de rotações e tratamento de cores. Sempre que um elemento é inserido em uma árvore sua cor será vermelha, caso o nó seja raiz sua cor será imediatamente trocada por preta para satisfazer a propriedade 2. Caso o nó inserido não seja o nó raiz deve-se verificar a propriedade 4.

Para facilitar o processo de busca por 'parentes' do nó, utilizamos as seguintes funções para isso:

Tree* avo(struct Tree *no){
	if ((no != NULL) && (no->pai != NULL))
		return no->pai->pai;
	else
		return NULL;
}

Tree* tio(struct node *no){
	Tree *vo = avo(no);
	if (vo == NULL)
		return NULL; // Não ter avô significa não ter tio
	if (no->pai == vo->esquerda)
		return vo->direita;
	else
		return vo->esquerda;
}

Para garantir as propriedade da árvores Rubro-Negra as verificações são realizadas em até 5 etapas

Caso 1

O nó inserido encontra-se na raiz da árvore. Sua cor será alterada para satisfazer a propriedade 2.

void Caso1(Tree **t){
    Tree *no = (*t);
    if(no->pai == NULL)
        no->cor = true;
    else
        Caso2(t);
}

Caso 2

O pai do novo nó P é preto. Neste caso a Propriedade 2 claramente não é invalidada. A Propriedade 4 tampouco é ameaçada pois o novo nó N tem como filhos dois nós-folha pretos, e sendo N vermelho, os caminhos passando por cada um dos seus filhos têm o mesmo número de nós pretos - da mesma forma que os caminhos que passavam pelo nó-folha preto que foi substituído por N. Por fim, neste caso, a árvore continua válida após a inserção, não sendo necessária mais alterações.

void Caso2(Tree **t){
    Tree *no = (*t);
    if(no->pai->cor == true)
        return;
    else
        Caso3(t);
}

Caso 3

O novo nó N inserido possui o pai P e o Tio T pintados de vermelho, condição que fere a propriedade 2. Nesse caso pinta-se o pai e o tio e N para preto e seu avô para veremelho. Porém, o avô pode estar violando agora a Propriedade 2 ou 4. Para consertar isso, uma "chamada recursiva" do procedimento de Caso1 é iniciada passando o avô como parâmetro

void Caso3(Tree **t){
    Tree *no = (*t);
    Tree *ti = tio(no);
    if( (ti != NULL) && ti->cor == false){
        no->pai->cor = true;
        ti->cor = true;
        Tree *av = avo(no);
        av->cor = false;
        Caso1(&av);
    }else
        Caso4(t);
}

Caso 4

O pai P é vermelho mas o tio U é preto; além disso, o novo nó N é o filho a esquerda de P, e P o filho a direita do seu pai V. Neste caso, uma rotação a direita que troca os papéis do novo nó N e de seu pai P deve ser realizada. Quando isso acontecer o antigo nó-pai P precisa ser tratado usando o Caso 5 (renomeando N e P) isso porque a Propriedade 4 ainda está sendo violada. Além disso, a rotação faz com que alguns caminhos passem pelo novo nó de forma diferente ao que acontecia antes, mas como os dois nós desta sub-árvore são vermelhos a Propriedade 5 não é violada (pela rotação).

void Caso4(Tree **t){
    Tree *no = (*t);
    Tree *vo = avo(no);

    if( (no->pai->esq == no) && (vo->dir == no->pai) ){ RSD(&no->pai); no = no->dir;}
    if( (no->pai->dir == no) && (vo->esq == no->pai) ){ RSE(&no->pai); no = no->esq;}

    Caso5(&no);
}

Caso 5

O pai P é vermelho mas o tio U é preto, o novo nó N é o filho direito de seu pai P, e P é o filho direito de seu pai, G. Neste caso, uma rotação a esquerda no pai de P é realizada. O resultado é uma árvore onde o antigo pai P é agora pai tanto do novo nó N quanto do avô de N, G. É sabido que G é preto, já que seu antigo filho P não poderia ser vermelho. Então as cores de P e G são trocadas, e a árvore resultante satisfaz a propriedade 4 (ambos os filhos de cada nó vermelho são pretos). A propriedade 5 (Todos os caminhos de um determinado nó até suas folhas contém o mesmo número de nós pretos) também se mantém satisfeita, já que todos os caminhos até as folhas passam pelo nó G antes, e agora todos passam pelo P. Em cada caso, este é o único nó preto da sub-árvore.

void Caso5(Tree **t){
    Tree *no = (*t);
    Tree *vo = avo(no);
    Tree *pai = no->pai;

    if(vo == NULL ) return;


    if(vo->pai != NULL){
        vo->cor = false;
        pai->cor = true;
    }

    if( (no->pai->dir == no) && (vo->dir == no->pai)) RSE(&no->pai);
    else RSD(&no->pai);

Remoções

A remoção nas árvores rubro-negras se inicia com uma etapa de busca e remoção como nas árvores binárias de busca convencionais. Então se alguma propriedade vermelho-preta for violada, a árvore deve ser rebalanceada. Dessa forma podemos organizar tudo em um passo a passo:

  • Encontre o nó v a ser removido
  • Remova o nó v da mesma forma que em uma árvore binária de pesquisa
  • Ajuste os critérios da árvore rubro-negra

No decorrer das remoções podem ocorrer 4 tipos de situações:

Situação V Sucessor
1 Vermelho Vermelho
2 Preto Vermelho
3 Preto Preto
4 Vermelho Preto

obs:Considere como sucessor o nó mais a esquerda da sub-árvore direira do nó a ser removido

Situação 1

seja v o nó a ser removido. Sendo v rubro e x, sucessor de v, também rubro, nada precisará ser feito, pois a árvore Rubro-Negra continua atendendo a todos os critérios

Situação 2

v é preto e x é vermelho. Pinte x de negro e pare.

Situação 3

v é negro e x é negro.

Caso 1:

se x é preto e x tem irmão w vermelho e pai negro.Siga os seguintes passos:

  • Rotação simples esquerda
  • Mude a cor de w para preto
  • Mude a cor do pai de x para vermelho

Caso 2:

Se x é preto, tem irmão w preto com filhos preto e pai preto. Mude a cor o irmão w para vermelho

Caso 3:

se x é preto, tem irmão w preto com filhos pretos e pai vermelho. Pinte o irmão w de vermelho e o pai de x de preto

Caso 4

Se x é preto, tem irmão w preto, tem pai de qualquer cor, tem irmão w com filho esquerdo vermelho e irmão w com filho direito preto. Siga os passos:

  • Rotação simples direita em w
  • Trocar as cores de w com seu filho esquerdo

Caso 5

Se x é preto, tem irmão w preto, tem pai de qualquer cor, tem irmão w com filho esquerdo qualquer cor e irmão w com filho direito vermelho. Siga os passos:

  • Rotação simples a esquerda
  • Mude a cor do pai para preto
  • w igual a cor anterior do pai de x
  • Mude a cor do filho direito de w para preto

Situação 4

v é vermelho e x é preto. Similar à situação 3. Siga os seguintes passos:

  • Pinte x de vermelho
  • Proceda como na situação 3

Referências

About

Trabalho de algoritmos e estrutura de dados 2. Campus Divinópolis CEFET-MG

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages