Um artigo leva você a entender as árvores rubro-negras e simulá-las

Insira a descrição da imagem aqui

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 Redou 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.

Insira a descrição da imagem aqui

2. Natureza

  1. Cada nó é vermelho ou preto.
  2. O nó raiz é preto.
  3. Se um nó for vermelho, ambos os nós filhos serão pretos.
  4. 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".
  5. 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 pParentpara o nó raiz da árvore rubro-preta, pLefto domínio aponta para o menor nó da árvore rubro-preta e _pRighto 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 pParentaponta para o nó raiz da árvore rubro-preta, pLeftaponta para o menor nó da árvore rubro-preta e pRightaponta 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:

  1. 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.
  2. Acesse rapidamente os nós menores e maiores : o nó principal pLeftaponta para o menor nó da árvore rubro-negra e pRightaponta 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.
  3. 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 pParentpara 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 headervariá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;
};
  1. Tipo de enumeraçãoColour : um tipo de enumeração é definido aqui, incluindo dois membros REDe BLACK. 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ó.

  2. EstruturaRBTreeNode<K, V> : esta estrutura define a estrutura do nó da árvore rubro-negra, que contém as seguintes variáveis ​​​​de membro:

    • _lefte _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 ser REDou BLACK.

    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 _kve cores _col.

  3. EstruturaRBTree<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 é definido Nodepara 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

Insira a descrição da imagem aqui

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

Insira a descrição da imagem aqui

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

Insira a descrição da imagem aqui

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

  1. Se _rootestiver vazio, indicando que a árvore rubro-preta está vazia, um novo nó será criado _roote 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.
  2. Se _rootnã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 correto parente o local onde o novo nó deve ser inserido cur.
  3. Se a chave a ser inserida já existir na árvore, ela será retornada falseporque não são permitidas chaves duplicadas.
  4. Crie um novo nó cure defina sua cor como vermelho. A cor do novo nó é inicialmente definida como vermelho para satisfazer a natureza de uma árvore rubro-preta.
  5. 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 parenta cor do nó. Se parentfor vermelho, será necessário processamento adicional para manter as propriedades da árvore vermelho-preto.
    • O código então verifica unclea cor do nó, que é parentirmão de . De acordo com unclea 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.

4. Insira efeitos dinâmicos

1. Construa uma árvore rubro-negra inserindo em ordem crescente

Insira a descrição da imagem aqui

2. Construa uma árvore rubro-negra inserindo em ordem decrescente

Insira a descrição da imagem aqui

3. Inserção aleatória para construir uma árvore rubro-negra

Insira a descrição da imagem aqui

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);
}
  1. Verificação de uniformidade de altura preta : a função usa recursão para percorrer os nós na árvore e mantém uma blackNumvariá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.
  2. 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.
  3. 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ário true.

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:

  1. Se o rootnó de entrada estiver vazio (ou seja, a árvore estiver vazia), ele retornará diretamente sem realizar nenhuma operação.
  2. Caso contrário, a função faz o seguinte recursivamente:
    • Primeiro chame _InOrdera 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 _InOrderpara percorrer a subárvore direita (nó filho direito).

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;
    }

}
  1. Primeiro, salve parento nó filho direito do nó subRe subRo nó filho esquerdo do nó, respectivamente. se tornará o novo nó raiz e se tornará o filho certo do nó.subRLsubRsubRLparent
  2. Em seguida, aponte parento ponteiro do nó filho direito do nó , certificando-se de que o ponteiro pai do nó aponte para . Esta etapa é conectar- se com ._rightsubRLsubRL_parentparentsubRLparent
  3. Salve parento ponteiro do nó pai do nó ppNode. ppNodeIsso é para atualizar para onde o filho esquerdo ou direito aponta após a rotação subR, para garantir que a árvore esteja conectada corretamente.
  4. Aponte o ponteiro do subRnó filho esquerdo e aponte o ponteiro do nó pai de . Esta etapa consiste em girar para a posição de ._leftparentparent_parentsubRparentsubR
  5. A seguir, trate do caso do nó raiz. Se _rootestava apontando , parentagora _rootdeve apontar para subR, então _rootatualize para subRe subRdefina o ponteiro pai para nullptr.
  6. Se _rootnão apontar para parent, você precisa atualizar o nó filho esquerdo ou direito apontando ppNodede acordo com a situação para garantir que toda a árvore esteja conectada corretamente.ppNodesubR

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;
    }

}
  1. Primeiro, salve parento nó filho esquerdo do nó subLe subLo nó filho direito do nó, respectivamente. se tornará o novo nó raiz e se tornará o filho esquerdo do nó.subLRsubLsubLRparent
  2. Em seguida, aponte parento ponteiro do nó filho esquerdo do nó e certifique-se de que o ponteiro pai do nó aponte para . Esta etapa é conectar- se com ._leftsubLRsubLR_parentparentsubLRparent
  3. Salve parento ponteiro do nó pai do nó ppNode. ppNodeIsso é para atualizar para onde o filho esquerdo ou direito aponta após a rotação subL, para garantir que a árvore esteja conectada corretamente.
  4. Aponte subLo ponteiro _rightdo nó filho à direita parente aponte parento ponteiro do nó pai de . Esta etapa consiste em girar para a posição de ._parentsubLparentsubL
  5. A seguir, trate do caso do nó raiz. Se _rootestava apontando , parentagora _rootdeve apontar para subL, então _rootatualize para subLe subLdefina o ponteiro pai para nullptr.
  6. Se _rootnão apontar para parent, você precisa atualizar o nó filho esquerdo ou direito apontando ppNodede acordo com a situação para garantir que toda a árvore esteja conectada corretamente.ppNodesubL

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:

  1. std::mapSoma em C++ STLstd::setstd::map : Soma na Biblioteca de Modelos Padrão C++ (STL) std::setgeralmente é 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Acho que você gosta

Origin blog.csdn.net/kingxzq/article/details/132830388
Recomendado
Clasificación