Entenda as árvores rubro-negras e simule-as
- O conceito e as propriedades das árvores rubro-negras
- A estrutura de uma árvore rubro-negra
- Definição de nó da árvore rubro-preta e definição de membro da estrutura da árvore rubro-preta
- Inserção de árvores rubro-negras
- Verificação de árvores rubro-negras
- Saída de passagem de árvore vermelho-preta e rotação esquerda-direita
- A simulação de árvore rubro-negra implementa todos os códigos
- Aplicações de árvores rubro-negras
- Comparação de árvores rubro-negras e árvores AVL
O conceito e as propriedades das árvores rubro-negras
1. Conceito
Uma árvore rubro-preta é uma árvore de busca binária , mas um bit de armazenamento é adicionado a cada nó para representar a cor do nó, que pode ser Red
ou Black
. Ao restringir a coloração de cada nó em qualquer caminho da raiz até a folha, a árvore rubro-negra garante que nenhum caminho tenha o dobro do comprimento de qualquer outro caminho e, portanto, esteja próximo do equilíbrio.
2. Natureza
- Cada nó é vermelho ou preto.
- O nó raiz é preto.
- Se um nó for vermelho, ambos os nós filhos serão pretos.
- Cada caminho de qualquer nó para cada um de seus nós folha contém o mesmo número de nós pretos, o que é chamado de "igualdade de altura preta".
- Cada nó folha (nó NIL, geralmente representado como um nó vazio) é preto.
Essas propriedades da árvore rubro-negra garantem o equilíbrio da árvore, garantindo assim que a altura da árvore esteja dentro da faixa logarítmica, de modo que a complexidade temporal das operações básicas permaneça dentro do O(log n)
nível
A estrutura de uma árvore rubro-negra
A fim de simplificar a implementação subsequente de contêineres associativos, um nó principal é adicionado à implementação da árvore vermelha e preta.Como o nó seguinte deve ser preto, para distingui-lo do nó raiz, o nó principal é colorido em preto e o domínio do nó principal aponta pParent
para o nó raiz da árvore rubro-preta, pLeft
o domínio aponta para o menor nó da árvore rubro-preta e _pRight
o domínio aponta para o maior nó da árvore rubro-preta.
Ao adicionar um nó principal à implementação de uma árvore vermelha e preta, o objetivo é simplificar as operações e lidar com casos extremos sem ter que escrever código separado para casos especiais. Este nó principal é geralmente um nó preto localizado acima do nó raiz da árvore rubro-preta.Ele pParent
aponta para o nó raiz da árvore rubro-preta, pLeft
aponta para o menor nó da árvore rubro-preta e pRight
aponta para o maior nó. nó na árvore rubro-negra.
A seguir está uma explicação da função e dos detalhes de implementação do nó principal:
- Processamento simplificado de condições de limite : a existência do nó principal torna o nó raiz sempre preto, porque o nó raiz é o nó filho direito do nó principal. Desta forma, em operações como inserção e exclusão, não há necessidade de lidar com a cor do nó raiz e a existência do nó pai em casos especiais.
- Acesse rapidamente os nós menores e maiores : o nó principal
pLeft
aponta para o menor nó da árvore rubro-negra epRight
aponta para o nó maior. Isso significa que você pode encontrar os menores e maiores nós da árvore em tempo O(1) sem ter que percorrer. - Acesso unificado ao nó raiz : seja inserção, exclusão ou outras operações, você sempre pode iniciar a operação a partir do nó principal, porque o nó principal aponta
pParent
para o nó raiz real. Isso simplifica a lógica do código porque você não precisa lidar especialmente com o caso do nó raiz.
A seguir está um exemplo de implementação de um nó de cabeçalho:
struct Node {
int data;
Color color;
Node* left;
Node* right;
Node* parent;
};
class RedBlackTree {
private:
Node* root; // 实际的根节点
Node* header; // 头结点
// ...其他成员函数和辅助函数...
public:
RedBlackTree() : root(nullptr), header(new Node) {
header->color = BLACK;
header->left = nullptr;
header->right = nullptr;
header->parent = nullptr;
}
// ...其他公共成员函数...
};
Neste exemplo, adicionamos uma header
variável de membro chamada que é um ponteiro para o nó principal. A inicialização do nó principal é concluída no construtor e garante que ele esteja sempre preto e que o nó raiz esteja localizado no centro do nó principal pParent
. A existência do nó principal simplificará a operação e o processamento de casos extremos de árvores rubro-negras.
Este é um método de implementação, mas não o implementaremos desta forma posteriormente. Claro, você também pode escolher este método para implementá-lo, que é mais simples. O processo de implementação da árvore rubro-negra é apenas para nos familiarizar mais com a sua realidade subjacente. Existem muito poucos cenários que precisamos implementar .
Definição de nó da árvore rubro-preta e definição de membro da estrutura da árvore rubro-preta
Aqui nossa implementação adota a forma de uma cadeia de três pontas. Mencionaremos os benefícios desse método de escrita mais tarde.
Usamos modelos de pares de valores-chave aqui para facilitar a implementação posterior de simulação de mapa e conjunto.
enum Colour
{
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
{
}
};
template<class K, class V>
struct RBTree
{
typedef RBTreeNode<K,V> Node;
private:
Node* _root = nullptr;
};
-
Tipo de enumeração
Colour
: um tipo de enumeração é definido aqui, incluindo dois membrosRED
eBLACK
. Este tipo de enumeração é usado para representar a cor dos nós em uma árvore vermelho-preta. Em uma árvore rubro-preta, os nós podem ser vermelhos ou pretos, e é uma prática comum usar esse tipo de enumeração para representar a cor do nó. -
Estrutura
RBTreeNode<K, V>
: esta estrutura define a estrutura do nó da árvore rubro-negra, que contém as seguintes variáveis de membro:_left
e_right
: ponteiros para os nós filhos esquerdo e direito do nó, respectivamente._parent
: ponteiro para o nó pai do nó._kv
: variável de membro usada para armazenar pares de valores-chave. Este par chave-valor geralmente é usado para armazenar os dados do nó._col
: Indica a cor do nó, que pode serRED
ouBLACK
.
O construtor
RBTreeNode(const pair<K, V>& kv)
é usado para inicializar o objeto do nó e atribuir valores iniciais a cada variável membro, incluindo pares de valores-chave_kv
e cores_col
. -
Estrutura
RBTree<K, V>
: Esta estrutura define a estrutura de toda a árvore rubro-negra, que contém as seguintes variáveis de membro e um alias:_root
: Ponteiro para o nó raiz da árvore rubro-negra. O nó raiz é o ponto inicial da árvore rubro-negra e todas as operações começam a partir do nó raiz.
typedef RBTreeNode<K,V> Node;
Um alias é definidoNode
para representar o tipo de nó da árvore vermelho-preto. Isso simplifica seu código e facilita a referência a tipos de nós em seu código.
Inserção de árvores rubro-negras
1. Insira novos nós de acordo com as regras da árvore de pesquisa binária
bool Insert(const pair<K,V>& kv)
{
if (_root == nullptr)//为空直接插入
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)//同搜索树规则找到插入位置
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
cur->_col = RED;//新插入节点即为红节点,不懂结合性质和我下面的讲解
if (parent->_kv.first < kv.first)//同搜索树规则先直接插入
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;//更新新插入节点的父指针
while (parent && parent->_col == RED)
{
Node* grandfater = parent->_parent;
assert(grandfater);
assert(grandfater->_col == BLACK);
if (parent == grandfater->_left)//具体看下面讲解
{
Node* uncle = grandfater->_right;
// 情况一 : uncle存在且为红,变色+继续往上处理
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfater->_col = RED;
// 继续往上处理
cur = grandfater;
parent = cur->_parent;
}// 情况二+三:uncle不存在 + 存在且为黑
else
{
// 情况二:右单旋+变色
if (cur == parent->_left)
{
RotateR(grandfater);
parent->_col = BLACK;
grandfater->_col = RED;
}
else
{
// 情况三:左右单旋+变色
RotateL(parent);
RotateR(grandfater);
cur->_col = BLACK;
grandfater->_col = RED;
}
break;
}
}
else // (parent == grandfater->_right)
{
Node* uncle = grandfater->_left;
// 情况一
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfater->_col = RED;
// 继续往上处理
cur = grandfater;
parent = cur->_parent;
}
else
{
// 情况二:左单旋+变色
if (cur == parent->_right)
{
RotateL(grandfater);
parent->_col = BLACK;
grandfater->_col = RED;
}
else
{
// 情况三:右左单旋+变色
RotateR(parent);
RotateL(grandfater);
cur->_col = BLACK;
grandfater->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
Na verdade, o código anterior é semelhante à inserção da árvore AVL de que falamos no artigo anterior, exceto que as árvores vermelhas e pretas aqui não possuem fator de equilíbrio e tornam-se cores.
A seguir olhamos para a análise de cada situação
2. Verifique se as propriedades da árvore rubro-negra causam danos após a inserção do novo nó.
Como a cor padrão de um novo nó é vermelho, portanto: se a cor de seu nó pai for preta, ela não violará nenhuma propriedade da árvore vermelho-preta e nenhum ajuste será necessário; mas quando a cor do nó pai do nó recém-inserido é vermelho, viola as propriedades Três não podem ter nós vermelhos conectados entre si. Neste momento, precisamos discutir a situação das árvores vermelhas e pretas:
Convenção: cur é o nó atual, p é o nó pai, g é o nó avó e u é o nó tio.
Caso 1: cur é vermelho, p é vermelho, g é preto, u existe e é vermelho
Cur e p são ambos vermelhos. Solução: mude p e u para preto, g para vermelho, depois trate g como cur e continue a ajustar para cima.
Caso 2: cur é vermelho, p é vermelho, g é preto, u não existe/u existe e é preto
Se p for o filho esquerdo de g e cur for o filho esquerdo de p, será realizada uma rotação única à direita. Pelo contrário, se p for o filho direito de g e cur for o filho direito de p, será realizada uma rotação única à esquerda. será executado. p e g mudarão de cor – p ficará preto. , g ficará vermelho
Caso 3: cur é vermelho, p é vermelho, g é preto, u não existe/u existe e é preto
p é o filho esquerdo de g, e cur é o filho direito de p, então execute uma rotação única para a esquerda em p; pelo contrário, se p for o filho direito de g, cur é o filho esquerdo de p, então execute um rotação única à direita em p, então ela é convertida para o Caso 2
3. Insira análise de código
- Se
_root
estiver vazio, indicando que a árvore rubro-preta está vazia, um novo nó será criado_root
e sua cor será definida como preta. Este é o nó raiz e, seguindo as regras da árvore vermelha e preta, o nó raiz deve ser preto. - Se
_root
não estiver vazio, o código procura o local correto na árvore para inserir o novo nó. Ao percorrer a árvore, encontre o nó pai corretoparent
e o local onde o novo nó deve ser inseridocur
. - Se a chave a ser inserida já existir na árvore, ela será retornada
false
porque não são permitidas chaves duplicadas. - Crie um novo nó
cur
e defina sua cor como vermelho. A cor do novo nó é inicialmente definida como vermelho para satisfazer a natureza de uma árvore rubro-preta. - Em seguida, o código entra em um loop, cujo objetivo é garantir que as propriedades de uma árvore rubro-negra ainda sejam satisfeitas após a inserção de um novo nó. Esta é uma parte crítica da operação de inserção da árvore rubro-negra.
- No loop, primeiro verifique
parent
a cor do nó. Separent
for vermelho, será necessário processamento adicional para manter as propriedades da árvore vermelho-preto. - O código então verifica
uncle
a cor do nó, que éparent
irmão de . De acordo comuncle
a cor, é dividido em caso um, caso dois e caso três. - Finalmente, após sair do loop, defina a cor do nó raiz como preto para garantir que o nó raiz atenda às propriedades de uma árvore vermelho-preta.
- No loop, primeiro verifique
4. Insira efeitos dinâmicos
1. Construa uma árvore rubro-negra inserindo em ordem crescente
2. Construa uma árvore rubro-negra inserindo em ordem decrescente
3. Inserção aleatória para construir uma árvore rubro-negra
Verificação de árvores rubro-negras
bool IsBalance()
{
if (_root == nullptr)
{
return true;
}
if (_root->_col == RED)//检查根节点是否为黑
{
cout << "根节点不是黑色" << endl;
return false;
}
// 黑色节点数量基准值
int benchmark = 0;
return PrevCheck(_root, 0, benchmark);
}
Implementação da função de valor de retorno PrevCheck
bool PrevCheck(Node* root, int blackNum, int& benchmark)
{
if (root == nullptr)
{
if (benchmark == 0)
{
benchmark = blackNum;
return true;
}
if (blackNum != benchmark)
{
cout << "某条黑色节点的数量不相等" << endl;
return false;
}
else
{
return true;
}
}
if (root->_col == BLACK)
{
++blackNum;
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "存在连续的红色节点" << endl;
return false;
}
return PrevCheck(root->_left, blackNum, benchmark)
&& PrevCheck(root->_right, blackNum, benchmark);
}
- Verificação de uniformidade de altura preta : a função usa recursão para percorrer os nós na árvore e mantém uma
blackNum
variável para rastrear o número de nós pretos passados do nó raiz para o nó atual. Durante o percurso, ele verifica se o número de nós pretos em cada caminho da raiz ao nó folha é o mesmo. Se o número de nós pretos no caminho for diferente, significa que as propriedades da árvore rubro-preta foram violadas. - Verificação contínua do nó vermelho : durante o processo de travessia, o código também verifica se há nós vermelhos consecutivos. Em uma árvore rubro-negra, não é permitida a existência de dois nós vermelhos adjacentes. Se houver nós vermelhos contínuos, isso também viola as propriedades das árvores rubro-negras.
- Valor de retorno : a função retorna um valor booleano para indicar se a árvore rubro-negra satisfaz as propriedades. A função retornará se alguma propriedade for quebrada durante a travessia
false
, caso contráriotrue
.
Esta função é um método comum usado para verificar árvores rubro-negras e é usada para verificar se a árvore mantém as propriedades de uma árvore rubro-preta, incluindo nós vermelhos com a mesma altura preta e nós descontínuos. Se a função retornar true
, significa que a árvore é uma árvore rubro-preta válida; caso contrário, significa que a estrutura da árvore viola as propriedades de uma árvore rubro-preta.
Saída de passagem de árvore vermelho-preta e rotação esquerda-direita
1. Percorra a saída
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
A travessia aqui é um método de percorrer uma árvore de pesquisa binária (BST) na ordem do valor do nó. Aqui, a árvore vermelha e preta também é um BST, portanto, a travessia em ordem pode ser usada para gerar os nós da árvore na ordem das chaves.
A lógica principal da função inclui:
- Se o
root
nó de entrada estiver vazio (ou seja, a árvore estiver vazia), ele retornará diretamente sem realizar nenhuma operação. - Caso contrário, a função faz o seguinte recursivamente:
- Primeiro chame
_InOrder
a função recursivamente para percorrer a subárvore esquerda (nó filho esquerdo). - Produza o par chave-valor do nó atual (suponha aqui que a chave é um número inteiro e o valor é um número inteiro, ajuste o formato de saída conforme necessário).
- Finalmente, a função é chamada recursivamente
_InOrder
para percorrer a subárvore direita (nó filho direito).
- Primeiro chame
2. Rotação única à esquerda
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
Node* ppNode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subR;
}
else
{
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
}
- Primeiro, salve
parent
o nó filho direito do nósubR
esubR
o nó filho esquerdo do nó, respectivamente. se tornará o novo nó raiz e se tornará o filho certo do nó.subRL
subR
subRL
parent
- Em seguida, aponte
parent
o ponteiro do nó filho direito do nó , certificando-se de que o ponteiro pai do nó aponte para . Esta etapa é conectar- se com ._right
subRL
subRL
_parent
parent
subRL
parent
- Salve
parent
o ponteiro do nó pai do nóppNode
.ppNode
Isso é para atualizar para onde o filho esquerdo ou direito aponta após a rotaçãosubR
, para garantir que a árvore esteja conectada corretamente. - Aponte o ponteiro do
subR
nó filho esquerdo e aponte o ponteiro do nó pai de . Esta etapa consiste em girar para a posição de ._left
parent
parent
_parent
subR
parent
subR
- A seguir, trate do caso do nó raiz. Se
_root
estava apontando ,parent
agora_root
deve apontar parasubR
, então_root
atualize parasubR
esubR
defina o ponteiro pai paranullptr
. - Se
_root
não apontar paraparent
, você precisa atualizar o nó filho esquerdo ou direito apontandoppNode
de acordo com a situação para garantir que toda a árvore esteja conectada corretamente.ppNode
subR
A rotação única à esquerda e a rotação única à direita são basicamente iguais à nossa implementação anterior da árvore AVL. Se você não entende, leia o artigo anterior.
3. Rotação única para a direita
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
}
- Primeiro, salve
parent
o nó filho esquerdo do nósubL
esubL
o nó filho direito do nó, respectivamente. se tornará o novo nó raiz e se tornará o filho esquerdo do nó.subLR
subL
subLR
parent
- Em seguida, aponte
parent
o ponteiro do nó filho esquerdo do nó e certifique-se de que o ponteiro pai do nó aponte para . Esta etapa é conectar- se com ._left
subLR
subLR
_parent
parent
subLR
parent
- Salve
parent
o ponteiro do nó pai do nóppNode
.ppNode
Isso é para atualizar para onde o filho esquerdo ou direito aponta após a rotaçãosubL
, para garantir que a árvore esteja conectada corretamente. - Aponte
subL
o ponteiro_right
do nó filho à direitaparent
e aponteparent
o ponteiro do nó pai de . Esta etapa consiste em girar para a posição de ._parent
subL
parent
subL
- A seguir, trate do caso do nó raiz. Se
_root
estava apontando ,parent
agora_root
deve apontar parasubL
, então_root
atualize parasubL
esubL
defina o ponteiro pai paranullptr
. - Se
_root
não apontar paraparent
, você precisa atualizar o nó filho esquerdo ou direito apontandoppNode
de acordo com a situação para garantir que toda a árvore esteja conectada corretamente.ppNode
subL
A simulação de árvore rubro-negra implementa todos os códigos
#pragma once
#include<iostream>
#include<assert.h>
#include<time.h>
using namespace std;
enum Colour
{
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
{
}
};
template<class K, class V>
struct RBTree
{
typedef RBTreeNode<K,V> Node;
public:
bool Insert(const pair<K,V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
cur->_col = RED;
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
while (parent && parent->_col == RED)
{
Node* grandfater = parent->_parent;
assert(grandfater);
assert(grandfater->_col == BLACK);
if (parent == grandfater->_left)
{
Node* uncle = grandfater->_right;
// 情况一 : uncle存在且为红,变色+继续往上处理
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfater->_col = RED;
// 继续往上处理
cur = grandfater;
parent = cur->_parent;
}// 情况二+三:uncle不存在 + 存在且为黑
else
{
// 情况二:右单旋+变色
if (cur == parent->_left)
{
RotateR(grandfater);
parent->_col = BLACK;
grandfater->_col = RED;
}
else
{
// 情况三:左右单旋+变色
RotateL(parent);
RotateR(grandfater);
cur->_col = BLACK;
grandfater->_col = RED;
}
break;
}
}
else // (parent == grandfater->_right)
{
Node* uncle = grandfater->_left;
// 情况一
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfater->_col = RED;
// 继续往上处理
cur = grandfater;
parent = cur->_parent;
}
else
{
// 情况二:左单旋+变色
if (cur == parent->_right)
{
RotateL(grandfater);
parent->_col = BLACK;
grandfater->_col = RED;
}
else
{
// 情况三:右左单旋+变色
RotateR(parent);
RotateL(grandfater);
cur->_col = BLACK;
grandfater->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
bool IsBalance()
{
if (_root == nullptr)
{
return true;
}
if (_root->_col == RED)
{
cout << "根节点不是黑色" << endl;
return false;
}
// 黑色节点数量基准值
int benchmark = 0;
return PrevCheck(_root, 0, benchmark);
}
private:
bool PrevCheck(Node* root, int blackNum, int& benchmark)
{
if (root == nullptr)
{
if (benchmark == 0)
{
benchmark = blackNum;
return true;
}
if (blackNum != benchmark)
{
cout << "某条黑色节点的数量不相等" << endl;
return false;
}
else
{
return true;
}
}
if (root->_col == BLACK)
{
++blackNum;
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "存在连续的红色节点" << endl;
return false;
}
return PrevCheck(root->_left, blackNum, benchmark)
&& PrevCheck(root->_right, blackNum, benchmark);
}
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
Node* ppNode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subR;
}
else
{
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
}
private:
Node* _root = nullptr;
};
Aplicações de árvores rubro-negras
A árvore vermelha e preta é uma árvore de pesquisa binária com autoequilíbrio amplamente utilizada em vários campos da ciência da computação e engenharia de software devido ao seu equilíbrio e desempenho eficiente. Aqui estão algumas aplicações comuns de árvores rubro-negras:
std::map
Soma em C++ STLstd::set
std::map
: Soma na Biblioteca de Modelos Padrão C++ (STL)std::set
geralmente é implementada usando uma árvore vermelha e preta. Eles são usados para implementar contêineres associativos ordenados nos quais os pares de valores-chave são automaticamente classificados e balanceados para suportar operações eficientes de pesquisa, inserção e exclusão.- Sistema de banco de dados : as árvores rubro-negras são frequentemente usadas em estruturas de índice em sistemas de banco de dados, como a implementação de árvores B+. Os índices em bancos de dados precisam oferecer suporte eficiente a operações como pesquisas, consultas de intervalo e classificação, e as árvores rubro-negras são uma boa opção para desempenho.
- Sistema operacional : a árvore vermelho-preta é usada no sistema operacional para agendamento de processos, gerenciamento de memória virtual e implementação de sistema de arquivos. Nestes cenários, são necessárias estruturas de dados eficientes para gerir e operar vários recursos do sistema.
- Tabelas de roteamento de rede : árvores vermelho-pretas são amplamente utilizadas para implementação de tabelas de roteamento para oferecer suporte à pesquisa de rotas eficiente para dispositivos de rede, como roteadores e switches de rede.
- Compilador : O compilador pode usar árvores vermelho-pretas para gerenciar tabelas de símbolos para análise de sintaxe, verificação de tipo e geração de código.
- Computação gráfica : Na computação gráfica, árvores rubro-negras podem ser usadas na implementação de estruturas de dados de particionamento espacial, como octrees e quadtrees, para suportar renderização gráfica eficiente e detecção de colisão.
- Bibliotecas e estruturas de alto desempenho : As árvores rubro-negras são frequentemente usadas como estruturas de dados centrais em bibliotecas e estruturas de alto desempenho para fornecer funções eficientes de gerenciamento e operação de dados.
- Sistemas em tempo real : Os sistemas em tempo real requerem estruturas de dados eficientes para gerenciar tarefas e eventos.Árvores rubro-negras podem ser usadas para implementar temporizadores e agendadores.
Em geral, a árvore rubro-negra é uma estrutura de dados versátil, adequada para qualquer campo que exija árvores binárias de busca binárias com auto-equilíbrio eficientes, especialmente cenários que exijam operações eficientes de inserção, exclusão e busca. Seu equilíbrio e desempenho o tornam uma ferramenta importante em diversas aplicações.
Comparação de árvores rubro-negras e árvores AVL
Red-Black Tree (Red-Black Tree) e AVL Tree (Adelson-Velsky e Landis Tree) são árvores de pesquisa binária com auto-equilíbrio. Elas têm muitas semelhanças na manutenção do equilíbrio e no suporte a operações eficientes de inserção, exclusão e pesquisa. , mas há são algumas diferenças importantes. Aqui está uma comparação entre árvores rubro-negras e árvores AVL:
- Requisitos de equilíbrio :
- Árvore rubro-negra: A árvore rubro-negra relaxa os requisitos de equilíbrio, o que garante que a altura da árvore seja no máximo 2 vezes.
- Árvore AVL: A árvore AVL possui requisitos mais rígidos, garantindo que a diferença de altura das árvores não ultrapasse 1, tornando a árvore AVL mais equilibrada.
- Desempenho :
- Árvores rubro-negras: Devido aos requisitos de balanceamento relativamente baixos, as operações de inserção e exclusão podem ser, em média, mais rápidas do que as árvores AVL; portanto, nos cenários que exigem operações frequentes de inserção e exclusão, as árvores rubro-negras podem ser mais adequadas.
- Árvore AVL: Uma árvore AVL pode ser um pouco mais rápida nas operações de pesquisa porque é mais balanceada, mas pode ser mais lenta nas operações de inserção e exclusão porque precisa executar operações de rotação com mais frequência para manter o equilíbrio.
- Operação de rotação :
- Árvores rubro-negras: As árvores rubro-negras requerem relativamente poucas rotações porque relaxam os requisitos de equilíbrio. Normalmente, as árvores vermelho-pretas requerem menos operações de rotação, mas requerem mais operações de transformação de cores para manter o equilíbrio.
- Árvore AVL: as operações de rotação da árvore AVL são mais frequentes porque requerem um equilíbrio mais rígido. Isso pode resultar na execução de mais rotações durante as operações de inserção e exclusão.
- Consumo de memória :
- Árvores rubro-negras: como as árvores rubro-negras têm requisitos de balanceamento mais baixos, elas normalmente consomem menos memória porque nenhum fator de balanceamento adicional é necessário para controlar as diferenças de altura dos nós.
- Árvore AVL: a árvore AVL precisa armazenar fatores de balanceamento para cada nó, o que pode resultar em mais consumo de memória.
- Cenários de aplicação :
std::map
Árvore vermelho-preto: Adequado para cenários que exigem operações eficientes de inserção e exclusão, mas têm requisitos de desempenho relativamente baixos para operações de pesquisa, como soma no STL do C++std::set
.- Árvore AVL: adequada para cenários que possuem requisitos de desempenho mais elevados para operações de pesquisa, mas podem tolerar operações mais lentas de inserção e exclusão, como índices de banco de dados.
Em geral, a escolha de usar uma árvore vermelho-preta ou uma árvore AVL depende das necessidades do aplicativo e dos requisitos de desempenho.Na implementação do mapa e conjunto C++, a camada inferior chama a árvore vermelho-preta, mas em entrevistas e trabalhos reais , são usadas árvores rubro-negras. As árvores pretas podem ter aplicações mais amplas.