目录
C++ 进阶: 本仓库存放一些较难C++代码https://gitee.com/j-jun-jie/c---advanced.git
引言:为啥设计红黑树?
我们以前讲了搜索二叉树但是如果插入的数据特别接近就会退化成单支树:
所以又出现了平衡搜索二叉树->AVL树。平衡二叉树保证了在最差的情况下,二叉树依然能够保持绝对的平衡,即左右两个子树的高度差的绝对值不超过1。但是这又会带来一个问题,那就是平衡二叉树的定义过于严格,导致每次插入或者删除一个元素之后,都要去维护二叉树整体的平衡,这样产生额外的代价又太大了。二叉搜索树可能退化成链表,而平衡二叉树维护平衡的代价开销又太大了。
所以我们采取中庸之道,就是把插入的定义改的宽松一些,可以让他不再那么高度必须最多差一层。维护平衡时的开销也减小一点。所以产生了红黑树。
红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须除了满足二叉搜索树的性质外,还要满足下面的性质:
- 性质1:每个节点要么是黑色,要么是红色。
- 性质2:根节点是黑色。
- 性质3:每个红色结点的两个子结点一定都是黑色。
- 性质4:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。(重要)
- 性质5:每个叶子节点(NIL)是黑色。(这个不重要)
红黑树确保没有一条路径会比其他路径长出俩倍(最长路径不超过最短路径的2倍),因而是接近平衡的。
最短路径:全部由黑色节点构成。
最长路径:由一黑一红构成,且红色节点数量等于黑色节点数量。
假设黑色节点有N个,最短路径长度为最长路径长度为2*。但是在红黑树中,不一定有最短路径和最长路径。
基本我们将插入的新节点改成红色,不改成黑色。
我们分析一下这两种情况;
如果插入的是红色,右边的情况不多,但是如果父节点是黑色就对了,红色可能错,这里他违反了的性质3.
但是如果插入的是黑色:
这个没有违反性质3,但是违反了性质4,任意一结点到每个叶子结点的路径都包含数量不相同的黑结点。违反性质3还可能对,但是违反性质4一定错,所以我们给插入的结点赋为红色。
红黑树可以说是AVL树的兄弟篇,所以我们可以利用一下它兄弟的代码。
这里我们仍旧采用KV模型和三叉链形式设计,但是用不到平衡因子而是用枚举来表示颜色(RED,BLACK)。
enum Colour { RED, // 0 BLACK, // 1 }; template<class K, class V> struct RBTreeNode { //三叉连结构 RBTreeNode* _left;//左孩子 RBTreeNode* _right;//右孩子 RBTreeNode* _parent;//父亲 pair_kv; Colour _cor; //构造函数 AVLTreeNode(const pair& kv) :_kv(kv) , _left(nullptr) , _right(nullptr) , _parent(nullptr) ,_cor(RED) {} };这里我们用颜色_cor代替平衡因子,给颜色初始化成红色(原因看特点最后一步)。
- //红黑树的类
- template <class K, class V>
- class RBTree
- {
- typedef RBTreeNode
Node; - public:
- //……
- private:
- Node* _root = nullptr;
- };
老生常谈,不说了,直接上狠活。
插入的四大件:
- 1.空节点: 开辟一个新节点,把kv给这个新节点。
- 开辟一个新节点。
- 手动让根节点的颜色给为黑色。
- 2,递归找合适的插入位置。
- 如果要插入的数大于结点值,往右走,递归。
- 如果要插入的数小于结点值,往左走,递归。
- 插入的值重复,返回false。
- 3.链接插入的结点和根节点。
- 插入的值小于父节点,链接在左边。
- 插入的值大于父节点,链接在右边。
- 把cur的_parent链接父节点。
- 4.进行一系列调整。
- 插入结点的父节点是黑色,不用调整。
- 插入结点的父节点是红色,调整。(3.2结点调整)
bool Insert(const pair& kv) { //1、一开始为空树,直接new新节点 if (_root == nullptr) { _root = new Node(kv); _root->_col = BLACK;//新插入的节点处理成黑色 return true; } //2、寻找插入的合适位置 Node* cur = _root; Node* parent = nullptr; 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;//插入的值 = 节点的值,数据冗余插入失败,返回false } } //3、找到了插入的位置,进行父亲与插入节点的链接 cur = new Node(kv); cur->_col = RED;//插入的节点处理成红色 if (parent->_kv.first < kv.first) { parent->_right = cur;//插入的值 > 父亲的值,链接在父亲的右边 } else { parent->_left = cur;//插入的值 < 父亲的值,链接在父亲的左边 } cur->_parent = parent;//三叉链,要双向链接只需把空节点的_bf改为颜色,这里因为是根节点,所以我们手动改为黑色。
接下来我们看一下插入后如何改变。
上面我们也介绍了,如果插入结点的父节点是黑色,我们就不要调整,
父节点是红色就需要调整,那么怎么调整呢?
情况一:
cur为红,parent,grandp为黑,uncle1结点存在为红, 如果说a,bcde都是子树,,我们先不在子树中插入。
如何调整:
- 父节点是不是根节点:
- 父节点是根节点:
把父节点的红色改成黑色:
- 父节点是子树结点
继续向上调整。
情况一不需要旋转,所以parent,uncle在左边还是右边不重要。cur在parent的那边也不重要。
情况2:
cur为红,parent,grandp为黑,uncle1结点不存在。
- 一条龙服务:
如果像图中所画,cur一定是新插入的结点,要不然,cur,p的颜色一定不一样。
那这个不就是AVL树的右单旋吗?(cur插入到较高子树的左侧) ?所以我们先来个旋转,在变更颜色。
如果说p在g的右边,就进行左单旋。方向正好相反。
- 闪电型分布:三个结点不在一条线上
总结情况:
- p为g的左,cur为p的左,则进行右单旋 + p变黑,g变红
- p为g的右,cur为p的右,则进行左单旋 + p变黑,g变红
- p是g的左,cur是p的右,则进行左右双旋 + cur变黑, g变红
- p是g的右,cur是p的左,则进行右左双旋 + cur变黑, g变红
情况3
cur为红,p为红,g为黑,u存在且为黑
- 当p是g的左子树,cur是p的右子树时,将p左单旋,g右单旋,cur变黑,g变红
- 当p是g的右子树,cur是p的左子树,p右单旋,g左单旋,p变黑,g变红。
其实情况2和情况3可以合并到一起,条件就是u不存在或者u存在但是为黑。
这里我们就不需要考虑u到底是存在还是颜色为黑就行了。
这里的插入分为两大类:
- parent在grandfather的左边
1.情况一,u存在且为红,cur为红。
- parent,uncle改变颜色为BLOCK,grandfather为RED
2.情况2+3,u不存在,u存在颜色为黑。
- 1. c在p的左边,g,p,c一条线,右单旋+变色(p黑,g红)。
- 2.c在p的右边 p左单旋+g右单旋+变色(cur黑,g红)
- parent在grandfather的右边:
1.情况一,u存在且为红。cur为红
- parent,uncle改变颜色为BLOCK,grandfather为RED
2.情况2+3,u不存在,u存在颜色为黑。
- 1. c在p的右边,g,p,c一条线,左单旋+变色(p黑,g红)。
- 2.c在p的左边 p右单旋+g左单旋+变色(cur黑,g红)
//4.进行修改和变色 while (parent&& parent->_cor == RED) { Node* grandfather = parent->_parent; assert(grandfather); assert(grandfather->_cor==BLACK); if (parent == grandfather->_left) //父亲在左,叔叔在右 { Node* uncle = grandfather->_right; //1.uncle存在为红,cur,parent是红,gfather黑 if(uncle&&uncle->_cor==RED) { //改色 parent->_cor = uncle->_cor = BLACK; grandfather->_cor = RED; cur = grandfather; //此时父亲是叶子节点 parent = cur->_parent; //继续往上走 } //情况2+3 uncle不存在+uncle存在且为红 else { // 情况2:右单旋+变色。大前提说了,p在g的zuobian // g // p (u) 可有可无 //c // if (cur == parent->_left) { RotateR(grandfather); cur->_cor = grandfather->_cor = RED; parent->_cor = BLACK; } // 情况3:左单旋+右单旋+变色。大前提说了,p在g的zuobian // g // p (u) 可有可无 // c // else { RotateL(parent); RotateR(grandfather); cur->_cor = BLACK; grandfather->_cor = RED; } break; } } else //(parent == grandfather->_right) //父亲在右,叔叔在左 { Node* uncle = grandfather->_left; //此时叔叔在左 //1.uncle存在为红,cur,parent是红,gfather黑 if (uncle&& uncle->_cor == RED) { //改色 parent->_cor = uncle->_cor = BLACK; grandfather->_cor = RED; cur = grandfather->_parent; //此时父亲是叶子节点 parent = cur->_parent; //继续往上走 } //情况2+情况3 else { // 情况2:左单旋+变色。大前提说了,p在g的zuobian // g // u p // c if (cur == parent->_right) { RotateL(grandfather); cur->_cor = grandfather->_cor = RED; parent->_cor = BLACK; } // 情况3:右单旋+左单旋+变色。大前提说了,p在g的zuobian // g // u p // c // else { RotateR(parent); RotateL(grandfather); cur->_cor = BLACK; grandfather->_cor = RED; } break; } } } _root->_cor = BLACK; return true; }
- //查找
- Node* FindR(const K& key)
- {
- Node* cur = _root;
- while (cur)
- {
- if (cur->_kv.first < key)//key比当前节点小,就向右查找
- {
- cur = cur->_right;
- }
- else if (cur->_kv.first > key)//key比当前节点大,就向左查找
- {
- cur = cur->_left;
- }
- else//找到了
- {
- return cur;
- }
- }
- return nullptr;//空树,直接返回
- }
这里我们仍旧用中序遍历打印。不写了,浪费地方。
其实检查的时候我们从5条定义入手:
- 1.根节点为空,正确,是红黑树
- 2.根节点颜色为红,错误,不是红黑树。
- 3.从根节点出发的任意一条路径上的黑色节点数一样,我们定义一个balckNum来专门记录。
bool IsBalance() { if (_root == nullptr) { return true; } if (_root->_cor == RED) { cout << "根节点不是黑色" << endl; return false; } PrevCheck(_root, 0); return true; } private: void PrevCheck(Node* root, int blackNum) { if (root == nullptr) { cout << blackNum << endl; return; } if (root->_cor == BLACK) { blackNum++; } PrevCheck(root->_left, blackNum); PrevCheck(root->_right, blackNum); }但是如果插入的数据过多。我们不想看,那能不能整合一下这帮伪军?
bool IsBalance() { if (_root == nullptr) { return true; } if (_root->_cor == RED) { cout << "根节点不是黑色" << endl; return false; } //选择任意一条路径的黑色节点数作为基准值 int Benchmark = 0; Node* cur = _root; while (cur) { if (cur->_cor == BLACK) { Benchmark++; } else cur=cur->_left; } return PrevCheck(_root, 0, Benchmark); } private: bool PrevCheck(Node* root, int blackNum, int Benchmark) { if (root == nullptr) { //cout << blackNum << endl; // return; if (blackNum != Benchmark) { cout << "某条黑色结点数量不相等"; return false; } else return true; } if (root->_cor==BLACK) { blackNum++; } if (root->_cor == RED && root->_parent->_cor == RED) { cout << "存在两个红色结点" << endl; return false; } return PrevCheck(root->_left, blackNum, Benchmark) &&PrevCheck(root->_right, blackNum, Benchmark); }
删除操作我们就不写了,有兴趣的老铁可以上网去搜一下。