Estrutura de Dados I
Árvores

Rubro-negra esquerdista (LLRB)

A árvore rubro-negra esquerdista (LLRB) em C, com rotações, inversão de cores, inserção, as operações auxiliares da remoção, a remoção do menor e a remoção de uma chave qualquer.

A árvore rubro-negra convencional garante altura , mas a sua inserção e, principalmente, a sua remoção têm muitos casos e casos espelhados. Em 2008, Robert Sedgewick propôs uma variante com uma regra a mais, que elimina os casos espelhados: a árvore rubro-negra esquerdista, ou LLRB (do inglês left-leaning red-black tree). Com ela, a inserção e a remoção são escritas com três operações elementares e uma única função de correção.

Esta é a árvore balanceada que vamos implementar na disciplina.

Definição

Definição 1(Árvore rubro-negra esquerdista)

Uma árvore rubro-negra esquerdista (LLRB) é uma árvore rubro-negra que, além das cinco propriedades, satisfaz:

  1. se um nó tem um filho vermelho, esse filho é o esquerdo. Ou seja, nenhum nó tem filho direito vermelho: os vermelhos "se inclinam" para a esquerda.

Toda LLRB é, em particular, uma árvore rubro-negra, então ela herda o teorema da altura: .

Duas árvores. (a) Uma LLRB com raiz 50 preta; 25 vermelho à esquerda, com os filhos pretos 10 e 30; 10 tem o filho esquerdo vermelho 5; 70 preto à direita da raiz, com o filho esquerdo vermelho 60. Todos os vermelhos são filhos esquerdos. (b) Raiz 10 preta com os filhos pretos 5 e 15, e 20 vermelho como filho direito de 15: é uma rubro-negra válida, mas não uma LLRB
(a) Uma LLRB. (b) Uma rubro-negra que não é LLRB: o 20 é um filho direito vermelho.

Duas consequências diretas da propriedade 6, que vamos usar mais adiante:

Proposição 1

Em uma LLRB, nenhum nó tem dois filhos vermelhos, e nenhum nó tem apenas o filho direito.

Demonstraçãotente antes de abrir

Se um nó tivesse dois filhos vermelhos, um deles seria o direito, o que a propriedade 6 proíbe. Agora suponha que um nó tenha filho direito e não tenha filho esquerdo. Pela propriedade 6, é preto. Então o caminho de até a sua folha NULL da esquerda tem um nó preto abaixo de (a própria folha), e qualquer caminho de por tem pelo menos dois ( e uma folha NULL abaixo dele), o que viola a propriedade 5.

Em particular, um nó sem filho esquerdo também não tem filho direito: é uma folha. Isso vai simplificar a remoção do menor.

LLRB e árvores 2-3

Uma forma de enxergar a LLRB é pensar em cada nó vermelho como "colado" ao seu pai, formando com ele um único nó com duas chaves. Na figura (a), o 25 e o 50 funcionam como um nó com as chaves 25 e 50. Lida assim, toda LLRB corresponde a uma árvore 2-3, uma árvore de busca em que cada nó tem uma ou duas chaves e todas as folhas estão no mesmo nível. Foi a partir dessa correspondência que Sedgewick projetou a LLRB. Não vamos precisar dela aqui, mas quem quiser saber mais pode consultar a seção 3.3 do livro de Sedgewick e Wayne.

O TAD

A interface é a mesma da ABB e da AVL (pares chave e valor, com chaves únicas), mais uma operação pública nova, removeMenorArvore. O nó troca a altura da AVL por uma cor:

typedef enum { VERMELHO, PRETO } Cor;

typedef struct no {
    Chave chave;
    Valor valor;
    Cor cor;            /* cor do nó (NULL conta como preto) */
    struct no *esq;
    struct no *dir;
} No;

Quase todas as decisões da árvore perguntam "esse filho é vermelho?". A função noEhVermelho responde false para NULL, que é a propriedade 3 (folhas NULL são pretas) traduzida em código. Com ela, podemos perguntar sobre r->esq->esq sem testar antes se r->esq existe, desde que r->esq seja vermelho (e, portanto, não nulo) ou que saibamos por outro motivo que ele não é NULL.

/* NULL conta como preto (as folhas NULL são pretas) */
static bool noEhVermelho(const No *r) {
    return r != NULL && r->cor == VERMELHO;
}

Para imprimir a árvore deitada (raiz à esquerda e SAD em cima), cada chave aparece seguida de (V) ou (P), a sua cor.

Rotações

As rotações são as mesmas da AVL e são nomeadas da mesma forma, pela direção: na rotação à esquerda em , o filho direito sobe; na rotação à direita em , o filho esquerdo sobe. A diferença é que, na LLRB, a rotação também move a cor: é sempre vermelho, sobe e herda a cor que tinha, e desce pintado de vermelho.

Em cima, a rotação à esquerda: A, de qualquer cor, tem a subárvore alfa à esquerda e o filho direito B vermelho, com as subárvores beta e gama. Depois, B fica no lugar de A, com a cor que A tinha, e A é o filho esquerdo vermelho de B, com alfa e beta; gama fica à direita de B. Embaixo, a rotação à direita, o espelho: A tem o filho esquerdo B vermelho, e depois B sobe com a cor de A e A desce à direita, vermelho. Nos dois casos, a ordem simétrica não muda
Rotação à esquerda e à direita na LLRB. O nó que sobe herda a cor do que desce, e o que desce fica vermelho.
/* Rotação à esquerda em A: o filho direito B (vermelho) sobe */
static No *rotacaoEsquerda(No *A) {
    No *B = A->dir;
    A->dir = B->esq;
    B->esq = A;
    B->cor = A->cor;        /* B herda a cor de A */
    A->cor = VERMELHO;      /* A desce e fica vermelho */
    return B;
}

/* Rotação à direita em A: o filho esquerdo B (vermelho) sobe */
static No *rotacaoDireita(No *A) {
    No *B = A->esq;
    A->esq = B->dir;
    B->dir = A;
    B->cor = A->cor;
    A->cor = VERMELHO;
    return B;
}

A rotação à esquerda transforma um "vermelho à direita" em um "vermelho à esquerda", e a rotação à direita faz o contrário. É assim que a LLRB move os vermelhos de um lado para o outro.

Proposição 2

Se o nó que sobe é vermelho, a rotação preserva a ordem das chaves e a quantidade de nós pretos em todo caminho que entra na subárvore, de cima até cada folha NULL.

Demonstraçãotente antes de abrir

A ordem é a mesma da rotação na ABB: a em-ordem continua (na rotação à esquerda). Para as cores, seja a cor original de e conte os pretos que um caminho encontra no topo da subárvore, antes de entrar em , ou . Antes da rotação à esquerda, os caminhos até passam só por ; os caminhos até e passam por e , e é vermelho. Então todos encontram um preto se é preto, e nenhum se é vermelho. Depois, os caminhos até e passam por (com a cor ) e (vermelho), e os caminhos até passam só por : de novo, um preto se é preto, e nenhum se é vermelho. As subárvores , e não mudam por dentro. A rotação à direita é simétrica.

A hipótese de vermelho é essencial: se fosse preto, os caminhos até perderiam um preto. Em todos os usos da LLRB, a rotação é aplicada justamente para mover um vermelho, e a hipótese vale. O que a rotação pode fazer é criar, temporariamente, dois vermelhos seguidos ou um vermelho à direita, que as outras operações corrigem.

Inversão de cores

A terceira operação inverte a cor de um nó e a dos seus dois filhos.

À esquerda, o nó r preto com dois filhos vermelhos; à direita, depois da inversão, r vermelho com dois filhos pretos. Uma seta dupla entre as duas indica que a operação vale nos dois sentidos
Inversão de cores: um nó preto com dois filhos vermelhos vira um nó vermelho com dois filhos pretos, e vice-versa.
static void trocaCor(No *r) {
    r->cor = r->cor == VERMELHO ? PRETO : VERMELHO;
}

/* Inverte a cor do nó e a dos seus dois filhos */
static void inverteCorNo(No *r) {
    trocaCor(r);
    if (r->esq != NULL)
        trocaCor(r->esq);
    if (r->dir != NULL)
        trocaCor(r->dir);
}
Proposição 3

Se tem a cor oposta à dos seus dois filhos, a inversão de cores em preserva a quantidade de nós pretos em todo caminho que passa por .

Demonstraçãotente antes de abrir

Todo caminho que passa por e continua para baixo passa também por exatamente um dos seus filhos. Antes da inversão, entre e esse filho há exatamente um preto ( ou o filho, já que as cores são opostas); depois, a cor de cada um se inverte, e continua havendo exatamente um preto entre os dois. Abaixo dos filhos, nada muda.

Na inserção, a inversão é aplicada a um nó preto com dois filhos vermelhos, "empurrando" o vermelho para cima. Na remoção, ela é usada no sentido contrário, para trazer um vermelho para baixo. Em ambos os casos, ela tem efeitos colaterais: pode criar dois vermelhos seguidos (se o pai de for vermelho) e pode pintar a raiz de vermelho.

Inserção

Como na rubro-negra convencional, o novo nó entra vermelho, no lugar de um NULL, para não mudar nenhuma altura negra. Os problemas que ele pode criar são locais, e são corrigidos na volta da recursão, em cada nó do caminho, por três testes nesta ordem:

Três casos. (1) O nó 5 preto tem o filho esquerdo 2 preto e o filho direito 8 vermelho: uma rotação à esquerda em 5 deixa 8 preto em cima e 5 vermelho à esquerda, com 2 abaixo. (2) O nó 8 preto tem o filho esquerdo 5 vermelho, que tem o filho esquerdo 2 vermelho: uma rotação à direita em 8 deixa 5 preto em cima, com 2 e 8 vermelhos como filhos. (3) O nó 5 preto tem os dois filhos vermelhos, 2 e 8: a inversão de cores deixa 5 vermelho e 2 e 8 pretos
Os três casos de correção. A saída do caso 2 é exatamente a entrada do caso 3.
  1. vermelho à direita (e preto à esquerda): viola a propriedade 6. Rotação à esquerda;
  2. dois vermelhos seguidos à esquerda: viola a propriedade 4. Rotação à direita;
  3. os dois filhos vermelhos: inversão de cores, que empurra o vermelho para o pai.
/* Corrige r na volta da recursão. Três testes, nesta ordem. */
static No *balanceia(No *r) {
    if (noEhVermelho(r->dir) && !noEhVermelho(r->esq))     /* vermelho à direita */
        r = rotacaoEsquerda(r);
    if (noEhVermelho(r->esq) && noEhVermelho(r->esq->esq)) /* dois vermelhos seguidos */
        r = rotacaoDireita(r);
    if (noEhVermelho(r->esq) && noEhVermelho(r->dir))      /* dois filhos vermelhos */
        inverteCorNo(r);
    return r;
}

O nome balanceia é o mesmo da AVL, e o papel também: corrigir o nó na volta da recursão e devolver a raiz (talvez nova) da subárvore. São três if independentes, e não uma cadeia de else if, porque um caso pode produzir o seguinte no mesmo nó: a rotação do caso 2 sempre deixa um nó com dois filhos vermelhos, que é o caso 3. Esquecer isso é o erro do Programa 1.

Com balanceia pronta, a inserção tem a mesma estrutura da inserção da AVL:

  1. 1

    Desça como na ABB#

    Compare a chave com a do nó atual e desça para a esquerda ou para a direita até encontrar um NULL. Se encontrar a própria chave, só troque o valor.

  2. 2

    Crie o nó vermelho#

    No lugar do NULL, crie o novo nó, sempre vermelho.

  3. 3

    Volte corrigindo#

    Na volta da recursão, cada nó do caminho passa por balanceia, que aplica os três testes e devolve a raiz da subárvore; o pai a guarda em r->esq ou r->dir.

  4. 4

    Pinte a raiz de preto#

    A inversão de cores pode ter pintado a raiz de vermelho. Pintá-la de preto nunca quebra a propriedade 5: todos os caminhos passam pela raiz.

static No *insereNo(No *r, Chave chave, Valor valor, bool *novo) {
    if (r == NULL) {
        *novo = true;
        return criaNo(chave, valor);
    }
    if (chave < r->chave)
        r->esq = insereNo(r->esq, chave, valor, novo);
    else if (chave > r->chave)
        r->dir = insereNo(r->dir, chave, valor, novo);
    else
        r->valor = valor;               /* chave já existe: atualiza */
    return balanceia(r);                /* na volta da recursão */
}

void insereArvore(Arvore *arvore, Chave chave, Valor valor) {
    bool novo = false;
    arvore->raiz = insereNo(arvore->raiz, chave, valor, &novo);
    arvore->raiz->cor = PRETO;          /* a raiz é sempre preta */
    if (novo)
        arvore->tamanho++;
}

A função criaNo é a da ABB, com novo->cor = VERMELHO;.

A figura mostra a inserção de 1, 3, 6, 7 e 4, nessa ordem, em uma árvore vazia. Cada quadro é a árvore inteira logo depois de uma operação, e o contorno destaca o nó onde ela foi aplicada.

Treze quadros. (a) insere 1, vermelho; (b) a raiz 1 é pintada de preto. (c) insere 3, vermelho, à direita de 1; (d) rotação à esquerda em 1: 3 preto na raiz com 1 vermelho à esquerda. (e) insere 6, vermelho, à direita de 3; (f) inversão de cores em 3: 3 vermelho com 1 e 6 pretos; (g) a raiz 3 é pintada de preto. (h) insere 7, vermelho, à direita de 6; (i) rotação à esquerda em 6: 7 preto com 6 vermelho à esquerda. (j) insere 4, vermelho, à esquerda de 6, que já era vermelho; (k) rotação à direita em 7: 6 preto com 4 e 7 vermelhos; (l) inversão de cores em 6: 6 vermelho com 4 e 7 pretos; (m) rotação à esquerda em 3: 6 preto na raiz, 3 vermelho à esquerda com os filhos pretos 1 e 4, e 7 preto à direita
Inserção de 1, 3, 6, 7 e 4 em uma LLRB vazia.

A inserção do 4 é a mais interessante, porque passa pelos três casos. O 4 entra à esquerda do 6, que já era vermelho. Na volta, nada acontece no 6. No 7, o filho esquerdo 6 e o neto 4 são vermelhos (caso 2): rotação à direita e, em seguida, no mesmo nó, os dois filhos vermelhos (caso 3): inversão de cores. O 6 sobe vermelho, e, na raiz 3, ele é um vermelho à direita (caso 1): rotação à esquerda.

Pergunta

Insira as chaves 1, 2, 3, 4, 5, 6 e 7, nessa ordem, em uma LLRB vazia. Qual é a árvore final? Compare com a ABB comum e com a AVL.

Ver respostaEsconder resposta

A árvore final é cheia, com 4 na raiz, 2 e 6 no segundo nível e 1, 3, 5 e 7 nas folhas, e todos os nós pretos. Com chaves crescentes, cada nova chave entra à direita: ou vira um vermelho à direita (caso 1, rotação à esquerda), ou encontra um vermelho à esquerda e deixa o nó com dois filhos vermelhos (caso 3, inversão de cores), empurrando um vermelho para cima. Ao todo, são quatro rotações e quatro inversões. A ABB comum seria uma lista, com altura 6; a AVL, com a mesma ordem, também termina na árvore cheia.

Remoção

Remover um nó vermelho do fundo da árvore é inofensivo; remover um nó preto tira um preto dos caminhos que passavam por ele (a figura da página anterior). A estratégia da LLRB é evitar esse problema antes que ele aconteça: na descida, à procura do nó a ser removido, vamos "carregando" um vermelho para baixo, de modo que, ao chegar lá, o nó removido seja vermelho.

Concretamente, a descida mantém um invariante: ao chegar a um nó (abaixo da raiz), ele ou o seu filho esquerdo é vermelho. Antes de descer, se o filho para onde vamos não satisfaz isso, uma das duas operações auxiliares abaixo empresta um vermelho de perto. Essas correções deixam violações temporárias (vermelhos à direita, nós com dois filhos vermelhos), que a mesma balanceia da inserção desfaz na volta da recursão. A remoção corrige a árvore na ida e na volta.

Operações auxiliares

Mover um vermelho para a esquerda. Usada quando vamos descer para a esquerda e tanto r->esq quanto r->esq->esq são pretos: não há vermelho no caminho à frente. A função supõe que r é vermelho (ou é a raiz) e garante que, depois dela, r->esq ou um filho de r->esq seja vermelho.

/* r é vermelho (ou a raiz), e r->esq e r->esq->esq são pretos:
   torna r->esq ou um filho dele vermelho */
static No *moveVermelhoParaEsquerda(No *r) {
    inverteCorNo(r);
    if (noEhVermelho(r->dir->esq)) {
        r->dir = rotacaoDireita(r->dir);
        r = rotacaoEsquerda(r);
        inverteCorNo(r);
    }
    return r;
}

A inversão de cores em r desce o vermelho para os dois filhos. Isso basta quando o irmão à direita não tem filho esquerdo vermelho (a). Quando tem, a inversão deixaria dois vermelhos seguidos à direita; nesse caso, um nó do lado direito é "emprestado": rotação à direita no irmão, rotação à esquerda em r e uma nova inversão de cores (b).

Duas sequências. (a) O nó 2 vermelho tem os filhos pretos 1 e 4; a inversão de cores em 2 deixa 2 preto com 1 e 4 vermelhos, e o 1 passa a ser vermelho. (b) O nó 2 vermelho tem o filho esquerdo 1 preto e o filho direito 4 preto, que tem o filho esquerdo 3 vermelho. Depois da inversão de cores em 2, 1 e 4 ficam vermelhos, e 4 e 3 são vermelhos seguidos; a rotação à direita em 4 sobe o 3; a rotação à esquerda em 2 deixa 3 em cima, com 2 à esquerda e 4 à direita; a inversão de cores em 3 termina com 3 vermelho, 2 e 4 pretos e o 1 vermelho, filho esquerdo de 2
moveVermelhoParaEsquerda em r = 2: (a) só com a inversão de cores; (b) com o empréstimo de um nó do lado direito.

Mover um vermelho para a direita. Usada quando vamos descer para a direita e tanto r->dir quanto r->dir->esq são pretos. É o espelho da anterior, com uma simplificação: como os vermelhos ficam à esquerda, o vermelho que sobra do lado esquerdo está em r->esq->esq, e basta uma rotação à direita em r para trazê-lo.

/* r é vermelho (ou a raiz), e r->dir e r->dir->esq são pretos:
   torna r->dir ou um filho dele vermelho */
static No *moveVermelhoParaDireita(No *r) {
    inverteCorNo(r);
    if (noEhVermelho(r->esq->esq)) {
        r = rotacaoDireita(r);
        inverteCorNo(r);
    }
    return r;
}
Duas sequências. (a) O nó 10 vermelho tem os filhos pretos 5 e 15; a inversão de cores em 10 deixa 10 preto com 5 e 15 vermelhos. (b) O nó 10 vermelho tem os filhos pretos 5 e 15, e 5 tem o filho esquerdo 3 vermelho. Depois da inversão de cores em 10, 5 e 15 ficam vermelhos, e 5 e 3 são vermelhos seguidos; a rotação à direita em 10 deixa 5 em cima, com 3 à esquerda e 10 à direita, que tem o filho direito 15; a inversão de cores em 5 termina com 5 vermelho, 3 e 10 pretos e 15 vermelho, filho direito de 10
moveVermelhoParaDireita em r = 10: (a) só com a inversão de cores; (b) com uma rotação à direita.

No caso (b), o 15 termina como filho direito vermelho do 10, uma violação da propriedade 6 que existe só durante a descida: na volta, balanceia a corrige.

Por que essas funções não acessam ponteiros nulos

moveVermelhoParaEsquerda lê r->dir->esq, e moveVermelhoParaDireita lê r->esq->esq. Elas só são chamadas quando o filho do lado da descida é preto e não nulo. Pela propriedade 5, o filho do outro lado também não pode ser NULL: se fosse, os caminhos por ele teriam menos nós pretos que os caminhos pelo filho preto.

Remover o nó com a menor chave

Antes da remoção geral, vale estudar o caso mais simples: remover o nó com a menor chave, que é o nó mais à esquerda. A descida só vai para a esquerda, então só precisamos de moveVermelhoParaEsquerda.

  1. 1

    Chegou ao menor?#

    Se r->esq é NULL, r é o menor. Pela Proposição 1, ele também não tem filho direito, e, pelo invariante da descida, ele é vermelho: basta liberá-lo e devolver NULL.

  2. 2

    Prepare o caminho#

    Se r->esq e r->esq->esq são pretos, não há vermelho à frente: chame moveVermelhoParaEsquerda(r).

  3. 3

    Desça#

    Remova o menor da SAE: r->esq = removeMenor(r->esq).

  4. 4

    Volte corrigindo#

    Devolva balanceia(r). No fim, a função pública pinta a raiz de preto.

static No *removeMenor(No *r) {
    if (r->esq == NULL) {               /* o menor: é uma folha vermelha */
        free(r);
        return NULL;
    }
    if (!noEhVermelho(r->esq) && !noEhVermelho(r->esq->esq))
        r = moveVermelhoParaEsquerda(r);
    r->esq = removeMenor(r->esq);
    return balanceia(r);
}

void removeMenorArvore(Arvore *arvore) {
    if (arvore->raiz == NULL)
        return;
    arvore->raiz = removeMenor(arvore->raiz);
    if (arvore->raiz != NULL)
        arvore->raiz->cor = PRETO;
    arvore->tamanho--;
}

A figura mostra a remoção do menor na árvore obtida ao inserir as chaves de 1 a 7 (a da pergunta da seção anterior, toda preta).

Sete quadros. (a) Árvore inicial, toda preta: 4 na raiz, 2 e 6, e as folhas 1, 3, 5 e 7. (b) Inversão de cores em 4: 4, 2 e 6 vermelhos. (c) Inversão de cores em 2: 2 preto, 1 e 3 vermelhos. (d) O 1, agora vermelho, é removido. (e) Na volta, rotação à esquerda em 2: 3 sobe, preto, com 2 vermelho à esquerda. (f) Rotação à esquerda em 4: 6 sobe com a cor de 4, e 4 desce vermelho, com os filhos 3 e 5. (g) A raiz 6 é pintada de preto
Remoção do menor (1) em uma LLRB com sete nós pretos.

Na descida, as duas inversões de cores (b e c) carregam o vermelho da raiz até o 1, que é removido sem mexer em nenhuma altura negra. Na volta, as duas rotações à esquerda (e e f) corrigem os vermelhos que ficaram à direita.

Pergunta

Na figura, a inversão de cores em (b) é aplicada à raiz, que é preta, e não vermelha como moveVermelhoParaEsquerda supõe. Por que isso não causa problema?

Ver respostaEsconder resposta

Com a raiz preta e os dois filhos pretos, a inversão deixa os três vermelhos. Todos os caminhos da árvore passam pela raiz e por um dos filhos, e todos perdem dois pretos: continuam com o mesmo número de pretos entre si, e a propriedade 5 continua valendo. A violação que surge (vermelhos seguidos na raiz) é temporária: a raiz é pintada de preto no final, e balanceia corrige o resto na volta.

Remover uma chave qualquer

A remoção de uma chave qualquer combina o que já vimos. Se a chave está à esquerda, a descida é a mesma de removeMenor. Se está à direita, ou é a do próprio nó, a descida usa moveVermelhoParaDireita, depois de trazer para a direita um vermelho que esteja à esquerda.

  1. 1

    Chave à esquerda#

    Se a chave é menor que a de r, prepare o caminho como em removeMenor (com moveVermelhoParaEsquerda, se preciso) e continue na SAE.

  2. 2

    Traga o vermelho para a direita#

    Senão, se r->esq é vermelho, aplique uma rotação à direita em r: o vermelho passa para o lado por onde vamos descer.

  3. 3

    Achou, sem filho direito#

    Se a chave é a de r e r não tem filho direito, r é uma folha vermelha (pela Proposição 1 e pelo invariante): libere-a.

  4. 4

    Prepare o caminho à direita#

    Se r->dir e r->dir->esq são pretos, chame moveVermelhoParaDireita(r).

  5. 5

    Achou, com filho direito#

    Se a chave é a de r, copie para r o par do sucessor (o menor nó da SAD) e remova o sucessor com removeMenor(r->dir). Senão, continue a remoção na SAD.

  6. 6

    Volte corrigindo#

    Em todos os casos, devolva balanceia(r).

/* Supõe que a chave está na subárvore de r */
static No *removeNo(No *r, Chave chave) {
    if (chave < r->chave) {
        if (!noEhVermelho(r->esq) && !noEhVermelho(r->esq->esq))
            r = moveVermelhoParaEsquerda(r);
        r->esq = removeNo(r->esq, chave);
    } else {
        if (noEhVermelho(r->esq))       /* leva o vermelho para a direita */
            r = rotacaoDireita(r);
        if (chave == r->chave && r->dir == NULL) {
            free(r);                    /* folha vermelha */
            return NULL;
        }
        if (!noEhVermelho(r->dir) && !noEhVermelho(r->dir->esq))
            r = moveVermelhoParaDireita(r);
        if (chave == r->chave) {
            /* dois filhos: o sucessor (mínimo da SAD) toma o lugar */
            No *suc = r->dir;
            while (suc->esq != NULL)
                suc = suc->esq;
            r->chave = suc->chave;
            r->valor = suc->valor;
            r->dir = removeMenor(r->dir);
        } else {
            r->dir = removeNo(r->dir, chave);
        }
    }
    return balanceia(r);
}

void removeArvore(Arvore *arvore, Chave chave) {
    if (buscaNo(arvore->raiz, chave) == NULL)
        return;                         /* removeNo supõe que a chave existe */
    arvore->raiz = removeNo(arvore->raiz, chave);
    if (arvore->raiz != NULL)
        arvore->raiz->cor = PRETO;
    arvore->tamanho--;
}
Sete quadros. (a) A árvore toda preta com as chaves de 1 a 7 e 4 na raiz. (b) Inversão de cores em 4, que é a moveVermelhoParaDireita na raiz: 4, 2 e 6 vermelhos. (c) A raiz recebe o par do sucessor, 5. (d) A remoção do 5 original começa em 6: inversão de cores em 6, que deixa 5 e 7 vermelhos. (e) O 5 original, uma folha vermelha, é removido. (f) Na volta, rotação à esquerda em 6: 7 sobe preto, com 6 vermelho à esquerda. (g) A raiz 5 é pintada de preto: 5 com 2 vermelho (filhos pretos 1 e 3) à esquerda e 7 preto (filho esquerdo vermelho 6) à direita
Remoção do 4, que tem dois filhos: ele recebe o par do sucessor (5), e o sucessor é removido da SAD.
Sucessor, e não antecessor

Na ABB e na AVL, o nó com dois filhos recebia o par do antecessor. Aqui usamos o sucessor, porque ele é o menor da SAD, e já temos removeMenor, que remove o menor mantendo as propriedades. Com o antecessor, precisaríamos de uma removeMaior, que é justamente um dos exercícios desta página.

Por que removeArvore busca a chave antes

removeNo supõe que a chave está na árvore. Se ela não estiver, a descida pode chegar a um nó cujo filho é NULL e tentar ler, por exemplo, r->esq->esq com r->esq nulo. Por isso removeArvore busca a chave antes e não faz nada se ela não existe. Essa busca também é .

O TAD completo

O programa abaixo usa o TAD completo, com a mesma interface Arvore.h da ABB e da AVL mais removeMenorArvore. Ele insere 1, 3, 6, 7 e 4 (a sequência da figura), imprimindo a árvore depois de cada inserção, remove o menor e depois remove o 7. Na impressão deitada, a raiz fica à esquerda e a SAD em cima.

TAD Arvore (LLRB)C
Pergunta

Antes de executar, desenhe a árvore depois de removeArvore(a, 7). Que operações acontecem durante essa remoção?

Ver respostaEsconder resposta

Antes da remoção, a árvore é 6 (preto) na raiz, com 4 (preto) à esquerda, que tem o filho esquerdo 3 (vermelho), e 7 (preto) à direita. Na raiz, o filho esquerdo 4 é preto, então não há rotação; o filho direito 7 e o filho dele são pretos, então moveVermelhoParaDireita(6) inverte as cores (6, 4 e 7 ficam vermelhos) e, como 3 é vermelho, faz uma rotação à direita em 6 e uma nova inversão em 4. A árvore fica com 4 na raiz, 3 à esquerda e 6 à direita, com o 7 vermelho à direita do 6. A descida chega ao 7, uma folha vermelha, que é removida. No final, a árvore tem 4 (preto) na raiz e os filhos 3 e 6, pretos.

Altura na prática

O programa abaixo insere chaves numa LLRB vazia, primeiro em ordem crescente e depois em ordem aleatória, e compara a altura obtida com a mínima possível, , e com o limite do teorema.

altura.cC

Com chaves crescentes, que degeneram a ABB comum, a LLRB fica com a altura mínima, como a AVL. Com chaves aleatórias, ela fica um pouco mais alta, mas muito abaixo do limite do teorema.

Custos

OperaçãoABBAVLLLRB
busca no pior caso
inserção no pior caso
remoção no pior caso
altura no pior casocerca de
informação extra por nónenhumaaltura (int)cor (1 bit)

A busca é a da ABB, sem nenhuma mudança. A LLRB pode fazer mais rotações que a rubro-negra convencional (que faz no máximo duas por inserção e três por remoção), mas em troca o seu código cabe em poucas funções, e a mesma balanceia serve à inserção e à remoção.

Exercícios

Encontre e corrija o erro

Os programas abaixo usam uma LLRB simplificada (só com chaves) e imprimem a árvore deitada, com a cor de cada nó. Compare a saída com as propriedades da LLRB e com as figuras desta página.

Programa 1

llrb_elseif.cC
Resposta

A saída mostra o 6 com dois filhos vermelhos, e um deles (o 7) é filho direito: viola a propriedade 6. A função balanceia usa else if: quando o caso 2 acontece, a rotação à direita deixa o nó com dois filhos vermelhos, mas o teste do caso 3 nem é feito. Ao inserir o 4, a rotação à direita em 7 acontece e a inversão de cores em 6, não. Os três testes precisam ser independentes:

if (noEhVermelho(r->dir) && !noEhVermelho(r->esq))
    r = rotacaoEsquerda(r);
if (noEhVermelho(r->esq) && noEhVermelho(r->esq->esq))
    r = rotacaoDireita(r);
if (noEhVermelho(r->esq) && noEhVermelho(r->dir))
    inverteCorNo(r);

Programa 2

llrb_rotacao.cC
Resposta

A árvore final é uma lista para a esquerda, toda preta exceto o 4, com altura 4 para 5 chaves. As rotações não movem as cores: o nó que sobe continua vermelho e o que desce mantém a cor que tinha. Os vermelhos vão parar em lugares errados, as alturas negras deixam de ser iguais e balanceia passa a tomar decisões erradas. A rotação precisa transferir a cor:

B->cor = A->cor;        /* B herda a cor de A */
A->cor = VERMELHO;      /* A desce e fica vermelho */

Programa 3

llrb_novo.cC
Resposta

Todos os nós são pretos, e a árvore é quase uma lista para a direita (só o 4 fica à esquerda do 6). Abaixo da raiz, o caminho até uma folha NULL do 7 passa por quatro pretos (3, 6, 7 e a folha), e o caminho até a folha NULL à esquerda da raiz, por um só. Viola a propriedade 5. O nó novo é criado preto, e, sem nenhum vermelho, nenhum dos três testes de balanceia é verdadeiro: a árvore nunca é corrigida. Todo nó novo precisa ser vermelho:

novo->cor = VERMELHO;

Programa 4

llrb_remove.cC
Resposta

Depois de remover os quatro menores, o 12 aparece vermelho como filho direito do 10, e viola a propriedade 6. Em moveVermelhoParaEsquerda, falta a segunda inversão de cores, depois das duas rotações. Sem ela, o nó que subiu fica preto com os dois filhos vermelhos, e o antigo r, vermelho, fica com o filho esquerdo também vermelho (compare com o quadro (b5) da figura de moveVermelhoParaEsquerda). A descida continua por dois vermelhos seguidos, e a correção na volta da recursão não consegue restaurar todas as propriedades. O caso com rotações termina com uma nova inversão:

if (noEhVermelho(r->dir->esq)) {
    r->dir = rotacaoDireita(r->dir);
    r = rotacaoEsquerda(r);
    inverteCorNo(r);
}

Implemente

É uma LLRB? Implemente ehLLRB, que devolve true se a árvore é uma LLRB válida: uma árvore binária de busca (chaves estritamente crescentes na ordem simétrica), com a raiz preta, nenhum filho direito vermelho, nenhum vermelho com filho vermelho e o mesmo número de nós pretos em todos os caminhos da raiz até as folhas NULL. A árvore vazia é uma LLRB. A função deve fazer uma única passada pela árvore, em .

Nos testes, cada árvore é dada em pré-ordem, com cada chave seguida da sua cor (V ou P) e . para NULL: A 10P 5V . . . é a raiz preta 10 com o filho esquerdo vermelho 5. Uma linha G n s gera uma LLRB com chaves aleatórias (semente ), que pode ser alterada em seguida para deixar de ser LLRB.

Como conferir tudo numa passada só?

Uma função recursiva pode devolver um único número e, mesmo assim, conferir todas as propriedades. Que número?

Ver dica 1Esconder dica 1

Faça a função devolver a altura negra da subárvore, ou se alguma propriedade falhar lá dentro. No nó, compare as alturas negras devolvidas pelas duas subárvores.

Ver dica 2Esconder dica 2

Para a ordem, não basta comparar o nó com os filhos (lembre do exercício da ABB): passe para a recursão o intervalo de chaves permitido. A raiz preta é conferida uma vez, fora da recursão.

É uma LLRB?C
Executa o seu código uma vez, com os dados de entrada abaixo. Serve para experimentar; não conta como resposta.Executa o seu código em todos os casos de teste (os de exemplo e os ocultos) e mostra quais passaram.

Remover o maior. Implemente removeMaior, que remove o nó com a maior chave de uma subárvore não vazia e devolve a nova raiz da subárvore. As demais funções do TAD, incluindo moveVermelhoParaDireita e balanceia, estão prontas, e removeMaiorArvore chama a sua função e pinta a raiz de preto. Os testes conferem todas as propriedades da LLRB depois das remoções, inclusive removendo o maior até esvaziar árvores com dezenas de milhares de chaves.

Em que removeMaior difere de removeMenor?

O maior é o nó mais à direita, e a descida só vai para a direita. Mas os vermelhos ficam à esquerda. O que isso muda?

Ver dica 1Esconder dica 1

A estrutura é a de removeMenor, trocando os lados, com moveVermelhoParaDireita para preparar o caminho. A diferença está no primeiro passo de cada nó.

Ver dica 2Esconder dica 2

Um nó sem filho direito pode ter um filho esquerdo vermelho, e então não pode ser simplesmente liberado. Faça como em removeNo: se r->esq é vermelho, aplique antes uma rotação à direita em r. Depois disso, um nó sem filho direito é uma folha vermelha.

Remover o maiorC
Executa o seu código uma vez, com os dados de entrada abaixo. Serve para experimentar; não conta como resposta.Executa o seu código em todos os casos de teste (os de exemplo e os ocultos) e mostra quais passaram.

Referências

Nesta página