C++:红黑树的实现

前言:

  • 在吃透AVL树 依靠高度差维护严格平衡的原理后,我们不难发现它的短板 :严苛的平衡判定会带来频繁的旋转操作,拉高了插入、删除场景的性能开销。而红黑树 作为工程界更常用的平衡二叉树方案,并没有沿用 AVL 树的高度管控思路,而是借助结点染色规则实现宽松的近似平衡。
  • 本篇就基于已掌握的 AVL 树知识体系,一步步拆解红黑树的设计思想、约束规则与底层运行逻辑。

一、红黑树的概念

红黑树是一棵二叉搜索树,它的每个结点增加一个存储位来表示结点的颜色,可以是红色或者黑色。通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是接近平衡的。

(1)、红黑树的规则

  • 规则一:每个结点不是红色就是黑色
  • 规则二:根结点是黑色的
  • 规则三:如果一个结点是红色的,则它的两个孩子结点必须是黑色的,也就是说任意一条路径不会有连续的红色结点。
  • 规则四:对于任意一个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色结点

《算法导论》等书籍上补充了一条每个叶子结点 (NIL) 都是黑色的规则。这里所指的叶子结点不是传统的意义上的叶子结点,而是空结点,部分书籍中将 NIL 称作外部结点。NIL 用于精准标识所有路径,《算法导论》在讲解具体实现时会忽略 NIL 结点,了解该概念即可。

(2)、红黑树最长路径≤最短路径2倍的原理

  1. 最短路径推导

    依据规则4,根到所有NULL结点的黑色结点数量一致。最短路径为全部由黑色结点构成,路径长度记为bh(黑高)

  2. 最长路径推导

    依据规则2、规则3,树中不存在连续红色结点。极端最长路径为黑、红结点交替排布,路径长度最大值为2*bh

  3. 路径长度区间结论

    全黑最短路径、红黑交替最长路径仅为理论极值,并非所有红黑树都会出现。设根到NULL的任意路径高度为hhh,满足:

    bh≤h≤2bh\boldsymbol{bh \le h \le 2bh}bh≤h≤2bh

(3)、红黑树的效率

1、时间复杂度推导

设:NNN = 红黑树结点总数,hhh = 最短路径长度(黑高)

关系式:2h−1≤N<22h−12^h -1 \le N < 2^{2h}-12h−1≤N<22h−1

可推导出:h≈log⁡Nh \approx \log Nh≈logN

最坏情况下操作需要遍历最长路径 2log⁡N2\log N2logN,增删查改时间复杂度依旧为 O(log⁡N)O(\log N)O(logN)

2、红黑树与AVL树对比

平衡实现方式

  • AVL树:依靠结点高度差,直观、严格管控平衡
  • 红黑树:依靠四条颜色规则约束,间接达成近似平衡

运行开销

  • 二者时间复杂度处于同一层级;红黑树平衡约束更宽松,插入结点时所需旋转次数更少,修改性能更优。

二、红黑树的实现

(1)、红黑树的结构

AVL树结构的基础上增加了颜色枚举 Colour 和结点新增成员:Colour _col,去掉了平衡因子。

  • 颜色枚举定义了 RED、BLACK 两种颜色,用来依靠颜色规则维护平衡,AVL 依靠平衡因子,没有颜色字段。
  • 每个红黑树节点自带颜色属性,是红黑树维持弱平衡的核心标识,AVL 不存在该成员。
cpp 复制代码
// 枚举值表示颜色
enum Colour
{
    RED,
    BLACK
};

// 这里我们默认按key/value结构实现
template<class K, class V>
struct RBTreeNode
{
    // 这里更新控制平衡也要加入parent指针
    pair<K, V> _kv;
    RBTreeNode<K, V>* _left;
    RBTreeNode<K, V>* _right;
    RBTreeNode<K, V>* _parent;
    Colour _col;

    RBTreeNode(const pair<K, V>& kv)
        :_kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
    {}
};

template<class K, class V>
class RBTree
{
    typedef RBTreeNode<K, V> Node;
public:
private:
    Node* _root = nullptr;
};

(2)、红黑树的插入

红黑树插入整体流程

节点代号定义:
  • c(cur):最新插入的当前节点
  • p(parent):c 的父节点
  • g(grandfather):c 的祖父节点
  • u(uncle):p 的兄弟节点(祖父的另一个孩子)
  1. 先依照二叉搜索树BST规则完成节点插入,插入完毕后校验红黑树四条约束规则。
  2. 新节点颜色规定
    • 空树首次插入:新节点设为黑色(根节点强制黑色)
    • 非空树插入:新节点固定为红色;若插入黑节点会破坏「每条路径黑色节点数量相等」的规则4,修复成本极高。
  3. 插入后父节点为黑色:不会触发任何规则冲突,插入流程直接结束。
  4. 插入后父节点为红色 :违反规则3(不能存在连续红色节点)。
    此时:当前节点c(红)、父节点p(红) → 祖父节点g一定是黑色;最终修复方案由叔叔节点u的状态区分处理。

① 情况1:仅变色不旋转

总模型:

  • 触发条件:c(当前节点)红、p(父节点)红、g(祖父节点)黑,叔叔u存在且为红色。

处理操作:

  1. 父节点p、叔叔u修改为黑色
  2. 祖父节点g修改为红色
  3. 将g当作新的cur节点,向上循环校验修复
  4. 若g为整棵树根节点,最后强制把g改回黑色

分析:

因为p和u都是红色,g是黑色,把p和u变黑,左边子树路径各增加一个黑色结点,g再变红,相当于保持g所在子树的黑色结点的数量不变,同时解决了c和p连续红色结点的问题,需要继续往上更新是因为,g是红色,如果g的父亲还是红色,那么就还需要继续处理;如果g的父亲是黑色,则处理结束了;如果g就是整棵树的根,再把g变回黑色。

② 情况2:单旋+变色

  • 触发条件:c为红,p为红,g为黑,u不存在或者u存在且为黑。

  • u不存在,则c一定是新增结点;

  • u存在且为黑,则c一定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。

  • 分析:p必须变黑,才能解决连续红色结点的问题,u不存在或者是黑色的,这里单纯的变色无法解决问题,插在p的左面 ,需要旋转+变色。

③ 情况3:双旋+变色

  • 触发条件:c为红,p为红,g为黑,u不存在或者u存在且为黑。

  • u不存在,则c一定是新增结点。

  • u存在且为黑,则c一定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。

  • 分析:p必须变黑,才能解决连续红色结点的问题,u不存在或者是黑色的,这里单纯的变色无法解决问题,插在p的右面 ,需要双旋+变色

完整代码:

cpp 复制代码
// 旋转代码的实现跟AVL树是一样的,只是不需要更新平衡因子
bool Insert(const pair<K, V>& kv)
{
    // 树为空,直接创建根节点,根强制黑色
    if (_root == nullptr)
    {
        _root = new Node(kv);
        _root->_col = BLACK;
        return true;
    }

    // 1、二叉搜索树标准查找插入位置
    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
        {
            // key重复,插入失败
            return false;
        }
    }

    // 2、新建节点,新增节点默认红色
    cur = new Node(kv);
    cur->_col = RED;
    // 挂到父节点左/右孩子
    if (parent->_kv.first < kv.first)
    {
        parent->_right = cur;
    }
    else
    {
        parent->_left = cur;
    }
    cur->_parent = parent;

    // 3、红黑树核心:向上调整颜色+旋转(父节点为红才需要修正)
    while (parent && parent->_col == RED)
    {
        Node* grandfather = parent->_parent;
        // 分支1:父亲是祖父的左孩子
        if (parent == grandfather->_left)
        {
            Node* uncle = grandfather->_right;
            // 情况1:叔叔存在且红色 ------ 仅变色,向上迭代
            if (uncle && uncle->_col == RED)
            {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;
                cur = grandfather;
                parent = cur->_parent;
            }
            else
            {
                // 叔叔为空 / 叔叔黑色:需要旋转+变色
                if (cur == parent->_left)
                {
                    // 左左:右旋祖父(单旋)
                    RotateR(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                else
                {
                    // 左右:先左旋父亲、再右旋祖父(双旋)
                    RotateL(parent);
                    RotateR(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break; // 旋转处理完毕,不用继续向上
            }
        }
        else
        {
            // 分支2:父亲是祖父的右孩子
            Node* uncle = grandfather->_left;
            // 情况1:叔叔存在且红,只变色上浮
            if (uncle && uncle->_col == RED)
            {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;
                cur = grandfather;
                parent = cur->_parent;
            }
            else
            {
                // 叔叔黑/不存在,旋转处理
                if (cur == parent->_right)
                {
                    // 右右:左旋祖父(单旋)
                    RotateL(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                else
                {
                    // 右左:先右旋父亲、再左旋祖父(双旋)
                    RotateR(parent);
                    RotateL(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;
            }
        }
    }
    // 兜底:根节点强制为黑色
    _root->_col = BLACK;
    return true;
}

代码讲解:

①模块1:空树处理

树没有节点时,新建节点作为根,根节点必须设置黑色(红黑树硬性规则)。

②模块2:BST插入逻辑

按照二叉搜索树规则遍历寻找空位,key重复直接返回false;找到位置后新建节点默认红色

新增节点选红色原因:红色只会破坏「不能连续红节点」规则;黑色会破坏「每条路径黑色数量一致」,修复成本更高。

③模块3:while循环 颜色&旋转修正(核心)

触发条件:父节点是红色 → 出现红红相连,违反红黑树规则,必须修正

grandfather 一定存在:父是红,父不可能是根,必然存在祖父。

分为两大对称分支:父在祖父左、父在祖父右

分支A:parent 是 grandfather 的左孩子

(1)叔叔uncle(祖父右孩子)存在且红色(情况1)

  • 父、叔叔变黑,祖父变红
  • 把祖父当作新cur向上循环,继续检查上层是否红红相连
  • 只变色,不旋转

(2)叔叔为空 / 叔叔黑色(情况2、3)

  • 形态【左左】:cur在parent左 → 对祖父单次右旋,父变黑、祖父变红
  • 形态【左右】:cur在parent右 → 先左旋父,再右旋祖父(双旋),cur变黑、祖父变红
  • 旋转完成直接break,结构平衡无需继续上浮
分支B:parent 是 grandfather 的右孩子(和左边镜像对称)

(1)叔叔(祖父左孩子)红色:同样变色上浮

(2)叔叔黑色/空

  • 右右形态:祖父左旋(单旋)
  • 右左形态:先右旋父,再左旋祖父(双旋)
  • 处理完break退出循环
④模块4:收尾

无论中间怎么变色上浮,最终根节点强制赋值黑色,防止根被改成红色,保证规则永久成立。


(3)、红黑树的查找

红黑树本质是二叉搜索树,查找逻辑和普通BST完全一致:

cpp 复制代码
Node* Find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first < key)
        {
            cur = cur->_right;
        }
        else if (cur->_kv.first > key)
        {
            cur = cur->_left;
        }
        else
        {
            return cur;
        }
    }
    return nullptr;
}

(4)、红黑树的验证

这里获取最长路径和最短路径,检查最长路径不超过最短路径的2倍是不可行的,因为就算满足这个条件,红黑树也可能颜色不满足规则,当前暂时没出问题,后续继续插入还是会出问题的。所以我们还是去检查4点规则,满足这4点规则,一定能保证最长路径不超过最短路径的2倍。

  1. 规则1枚举颜色类型,天然实现保证了颜色不是黑色就是红色。
  2. 规则2直接检查根即可
  3. 规则3前序遍历检查,遇到红色结点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了。
  4. 规则4前序遍历,遍历过程中用形参记录跟到当前结点的blackNum(黑色结点数量),前序遍历遇到黑色结点就++blackNum,走到空就计算出了一条路径的黑色结点数量。再任意一条路径黑色结点数量作为参考值,依次比较即可。

代码展示:

cpp 复制代码
bool Check(Node* root, int blackNum, const int refNum)
{
    if (root == nullptr)
    {
        // 走到空节点,代表一条路径走完
        if (refNum != blackNum)
        {
            cout << "存在黑色节点数量不相等的路径" << endl;
            return false;
        }
        return true;
    }

    // 校验规则:红色节点的父亲不能是红色(不能连续红)
    if (root->_col == RED && root->_parent->_col == RED)
    {
        cout << root->_kv.first << "存在连续的红色节点" << endl;
        return false;
    }

    // 当前节点是黑色,黑色计数+1
    if (root->_col == BLACK)
    {
        blackNum++;
    }

    // 递归校验左子树 && 右子树
    return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}

bool IsBalance()
{
    // 空树合法
    if (_root == nullptr)
        return true;

    // 规则2:根节点必须是黑色
    if (_root->_col == RED)
        return false;

    // 选取最左侧一条路径的黑节点数作为标准参考值 refNum
    int refNum = 0;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_col == BLACK)
        {
            ++refNum;
        }
        cur = cur->_left;
    }

    // 递归整棵树校验
    return Check(_root, 0, refNum);
}

讲解:

1、外层入口函数:IsBalance()

作用:对外提供接口,整体校验红黑树是否合法

  • 空树直接判定合法。
  • 校验规则2:根节点颜色必须为黑色,根为红直接返回false。
  • 拿到基准黑点数refNum:一路走到最左叶子的空位置,统计这条路径所有黑色节点数量,当作统一标准。
  • 调用递归函数Check,从头遍历整树校验。

2、递归校验函数:Check

参数说明

  • root:当前遍历节点
  • blackNum:走到当前节点累计的黑色数目
  • refNum:全局统一的标准黑色数量(最左路径算出)
① 递归终止条件(root == nullptr)

一条从根到空叶子的完整路径遍历完成

对比本条路径的blackNum 和标准refNum,不一致 → 违反规则4(所有路径黑色节点数目相同),报错返回false;相等则正常返回true。

② 校验规则3:禁止连续红色节点

如果当前节点是红色,同时其父节点也是红色,出现红红相连,红黑树非法,打印错误信息并返回false。

设计思路:不用判断孩子节点,只看自身+父节点,写法更加简洁。

③ 累加黑色计数

当前节点是黑色,blackNum += 1;红色不计数。

④ 左右子树递归校验

左、右子树必须全部校验通过(&&关系),整棵树才算合法。


三、完整代码

RBTree.h

cpp 复制代码
#pragma once
#include<iostream>
using namespace std;


enum Colour
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
	pair< K , V> _kv;
	RBTreeNode<K,V>* _parent;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	Colour _col;

	RBTreeNode(const pair<K,V>& kv)
		:_kv(kv)
		,_parent(nullptr)
		, _left(nullptr)
		, _right(nullptr)
	{ }
};

template<class K, class V>
class 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* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kv.first < cur->_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* grandfather = parent->_parent;
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_left)
					{
						//        g
						//    p      u
						//  c
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//        g
						//    p      u
						//      c
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_right)
					{
						//        g
						//    u      p
						//             c
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//        g
						//    u      p
						//         c
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;
	}
	//右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		Node* parentP = parent->_parent;
		subL->_right = parent;
		parent->_parent = subL;

		if (parent == _root)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (parentP->_left == parent)
			{
				parentP->_left = subL;
			}
			else
			{
				parentP->_right = subL;
			}
			subL->_parent = parentP;
		}
		
	}

	//左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		Node* parentP = parent->_parent;
		subR->_left = parent;
		parent->_parent = subR;
		if (parentP == nullptr)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parent == parentP->_left)
			{
				parentP->_left = subR;
			}
			else
			{
				parentP->_right = subR;
			}
			subR->_parent = parentP;
		}
	}

	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	int Height()
	{
		return _Height(_root);
	}

	int Size()
	{
		return _Size(_root);
	}

	bool IsBalanceTree()
	{
		// 空树合法
		if (_root == nullptr)
			return true;

		// 规则2:根节点必须是黑色
		if (_root->_col == RED)
			return false;

		// 选取最左侧一条路径的黑节点数作为标准参考值 refNum
		int refNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				++refNum;
			}
			cur = cur->_left;
		}

		// 递归整棵树校验
		return Check(_root, 0, refNum);
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	int _Size(Node* root)
	{
		if (root == nullptr)
			return 0;

		return _Size(root->_left) + _Size(root->_right) + 1;
	}

	bool Check(Node* root, int blackNum, const int refNum)
	{
		if (root == nullptr)
		{
			// 走到空节点,代表一条路径走完
			if (refNum != blackNum)
			{
				cout << "存在黑色节点数量不相等的路径" << endl;
				return false;
			}
			return true;
		}

		// 校验规则:红色节点的父亲不能是红色(不能连续红)
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << root->_kv.first << "存在连续的红色节点" << endl;
			return false;
		}

		// 当前节点是黑色,黑色计数+1
		if (root->_col == BLACK)
		{
			blackNum++;
		}

		// 递归校验左子树 && 右子树
		return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
	}
private:
	Node* _root = nullptr;
};

test.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"RBTree.h"
#include<vector>

// 测试代码
void TestAVLTree1()
{
    RBTree<int, int> t;
    // 常规的测试用例
    //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
    // 特殊的带有双旋场景的测试用例
    int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
    for (auto e : a)
    {
        t.Insert({ e, e });
    }

    t.InOrder();
    cout << t.IsBalanceTree() << endl;
}

// 插入一堆随机值,测试平衡,顺便测试一下高度和性能等
void TestAVLTree2()
{
    const int N = 100000;
    vector<int> v;
    v.reserve(N);
    srand(time(0));

    for (size_t i = 0; i < N; i++)
    {
        v.push_back(rand() + i);
    }

    size_t begin2 = clock();
    RBTree<int, int> t;
    for (auto e : v)
    {
        t.Insert(make_pair(e, e));
    }
    size_t end2 = clock();

    cout << "Insert:" << end2 - begin2 << endl;
    cout << t.IsBalanceTree() << endl;
    cout << "Height:" << t.Height() << endl;
    cout << "Size:" << t.Size() << endl;

    size_t begin1 = clock();
    //确定在的值
    /*for (auto e : v)
    {
        t.Find(e);
    }*/
    // 随机值
    for (size_t i = 0; i < N; i++)
    {
        t.Find((rand() + i));
    }

    size_t end1 = clock();
    cout << "Find:" << end1 - begin1 << endl;
}
int main()
{
    //TestAVLTree1();
    TestAVLTree2();
    return 0;
}
相关推荐
_wyt0011 小时前
拓扑排序:有向无环图的排队艺术
c++·拓扑排序·队列
皓月斯语1 小时前
B2118 验证子串
c++·题解
乐观勇敢坚强的老彭2 小时前
C++信奥:开关门、开关灯问题
开发语言·c++·算法
冻柠檬飞冰走茶2 小时前
PTA基础编程题目集 7-31 字符串循环左移(C语言实现)
c语言·开发语言·数据结构·算法
a1117763 小时前
中文优先的企业 RAG 知识库 开源项目
开发语言·开源·kotlin
2301_777998343 小时前
C/C++:预处理详解
c语言·c++
破z晓3 小时前
javascript 导出excel表
开发语言·javascript·excel
t-think3 小时前
C++类和对象详解(一)
开发语言·c++
西门啐血3 小时前
上位机开发之假装有设备,使用 C# 模拟串口设备
开发语言·mongodb·c#