一、红黑树的概念
红黑树是一颗二叉搜索树,它的节点增加一个存储位置来表示节点的颜色,可以是红色或黑色。
需达成的约束:确保没有一条路径会比其他路径长出2倍,因此接近平衡。
二、红黑树的规则
1.节点颜色不是红色就是黑色。
2.根节点是黑色的。
3.当前节点是红色的,两个孩子必须是黑色的,任何一条路径不会有连续的红色节点。
4.对于任⼀结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色结点。
因此,从根开始每条路径(走到空)黑色结点数量相等。
约束解析:
极端场景:
假设每条路径有x个黑色结点:
最短路径:全都是黑色结点,路径长度为x
最长情况:一黑一红(因为规则2限制没有连续红色节点),路径长度为2x
达成了约束,没有一条路径会比其他路径长出2倍。
红黑树的效率:
假设N是红黑树中结点数量,h最短路径的长度,那么 2^h-1<=N<2^(2h)-1,h约等于logN,最坏情况要走2*logN,数量级还是logN,因此红黑树增删查改的效率是O(logN)。
三、红黑树的插入
红黑树的情况比较难于直接分类讨论,但只要符合规则就能保证一棵树是红黑树,因此分类时根据规则来分情况讨论。
红黑树插入的情况分析:
1)对规则1,红黑树颜色是枚举值,非红即黑,符合
2)对规则2,只需对根节点特殊处理,将根节点颜色置成黑色,易解决
3)对规则3和规则4:
规则4首先要保证任一节点走到空的黑色结点数量一致,就要使插入的节点必须是红色节点,如果是黑色结点,会使从根走到空的其他路径黑色结点数量不一样。
规则3要验证每一个红色节点的孩子都是黑色结点不好判断,自身对子树判断有四种情况(左右孩子都有,只有左孩子,只有右孩子,左右孩子都没有),因而传化成:判断是否有连续的红色节点(即判断新增节点的父节点是否为红色),只要没有了连续的红色节点,插入就完成了,因为保证了四条规则。
因而现在只需要判断是否有连续的红色节点及其处理就行了。
对于判断是否连续的红色节点,可以拆分出以下几种情况
1)新增节点的父节点是黑色,不需要调整,没有了连续的红色节点。
2)新增节点的父节点是红色,此时需要调整。
注:新增节点的父节点为空,新增节点为新的根,在insert最开始处理,不计入调整。
新增节点的父节点是红色的情况分析:
当前可确定的情况如上图所示,即cur为红色,parent为红色,grandparent为黑色。
原因:cur为新增节点,必为红色(为了满足规则4);现在条件是parent为红色;插入前是红黑树,grandparent必为黑色(为了满足规则3)。
此时,不确定的情况为uncle节点,有三种子情况:
1)uncle存在且为红色
2)uncle不存在
3)uncle存在且为黑色
对uncle的三种情况进行分类讨论:
情况1,uncle存在且为红色
解决方法:变色
分析:首先parent一定要变为黑色,因为要同时满足规则3和规则4。但这样做以grandparent为根,经过parent的走到空的路径的黑色节点数量会比grandparent的右子树路径多一个。
因此要将grandparent变为红色节点,同时uncle变为黑色结点,这样grandparent左右子树黑色节点就一样多了,parent左右子树的黑色节点也一样多。
形象描述:grandparent的黑色下放给parent和uncle,grandparent变为红色。
注意有特殊情况:
若grandparent为根节点,处理完后cur变为根节点,parent为空指针,由于最外层循环条件为parent->_col为红色,要处理空指针解引用的问题,以及cur此时为根节点,根节点为红色的问题。
解决方法:循环条件改为while(parent && parent->_col==RED);出循环后直接将根节点的颜色置为黑色,这种做法成本最低。
总结处理方法:将parent和uncle变为黑色,grandparent变为红色。grandparent赋值给cur,cur的parent赋值给parent,继续向上调整。
情况2:uncle不存在或者uncle存在且为黑色,且cur与parent的关系与grandparent与parent的关系一致(即同左同右,单纯的一边高)
解决方法:单旋加变色
cur在parent左边,uncle存在且为黑色情况
分析:uncle不能分担grandparent的黑色了,因此右单旋,grandparent变红,parent变黑,达到走uncle路径的黑色节点数量不变,同时走cur路径的黑色节点数量也不变。
cur为parent的左边,uncle不存在情况:
分析:若uncle为空,那么cur的左右孩子都为空,cur就是新增节点。parent的右孩子也必是空。
原因:cur的左右孩子若不是空,不能是黑色节点否则就违背规则4;不能是红色节点否则就违背规则3;但既不能是红色节点又不能是黑色节点就违背了规则1,因此只能是空。parent同理。
可以发现,uncle存在且为黑和uncle为空的处理方式一模一样,因此可以把它们归为一种情况。
总结处理方法:对grandparent进行右单旋,parent变为黑色,grandparent变为红色,调整结束。
情况3:uncle不存在或者uncle存在且为黑色,且cur与parent的关系与grandparent与parent的关系不一致(即非同左同右,不是单纯的一边高)
解决方法:双旋加变色
parent在grandparent左边,但cur在parent右边
总结处理方法:对parent进行左单旋,转化成一边高,再对grandparent进行右单旋。cur变为黑,grandparent变为红。
插入代码实现:
bool insert(const pair<K,V>& data) { if (_root == nullptr) { _root = new Node(data); _root->_col = BLACK; return true; } Node* parent = nullptr; Node* cur = _root; while (cur) { //比cur小,往左走 if (cur->_kv.first > data.first) { parent = cur; cur = parent->_left; } //比cur大,往右走 else if (cur->_kv.first < data.first) { parent = cur; cur = parent->_right; } else { return false; } } cur = new Node(data); //比parent小,链接在左边,否则链接在右边 if (data.first < parent->_kv.first) { parent->_left = cur; } else { parent->_right = cur; } cur->_parent = parent; //调整部分 //parent不为空 且 parent的颜色为红色就还要继续调整 while (parent && parent->_col==RED) { //grandparent一定非NULL, // 因为如果parent如果是根节点,parent就是黑色的 //parent是黑色的进不来循环 Node* grandparent = parent->_parent; if (parent == grandparent->_left) { // g g // p u 或 p u // c c Node* uncle = grandparent->_right; //无论cur在左还是右都不影响 //uncle存在且为红 if (uncle && uncle->_col == RED) { parent->_col = uncle->_col = BLACK; grandparent->_col = RED; //还要向上处理 cur = grandparent; parent = cur->_parent; } //uncle不存在 或者 uncle存在且为黑 else { // g // p u // c //cur为parent的左 if (cur == parent->_left) { rotateR(grandparent); parent->_col = BLACK; grandparent->_col = RED; //旧的parent现在的grandparent // 已经为黑色了,不需要向上处理 break; } //cur为parent的右,不是单纯一边高 //双旋加变色 else { // g // p u // c rotateL(parent); rotateR(grandparent); cur->_col = BLACK; grandparent->_col = RED; //旧的cur现在的grandparent //已经变为黑色了,不需要向上处理 break; } } } else { // g // u p // c Node* grandparent = parent->_parent; Node* uncle = grandparent->_left; //无论cur在左还是右,不影响 if (uncle && uncle->_col == RED) { grandparent->_col = RED; uncle->_col = parent->_col = BLACK; //此时grandparent为红色,需要向上更新 cur = grandparent; parent = cur->_parent; } // uncle存在且为黑 或者 uncle为空 else { // g // u p // c //单纯一边高,单旋加变色 if (cur == parent->_right) { rotateL(grandparent); parent->_col = BLACK; grandparent->_col = RED; //旧parent新grandparent //此时已经为黑,停止更新 break; } // g // u p // c //不是单纯一边高,双旋加变色 else { rotateR(parent); rotateL(grandparent); cur->_col = BLACK; grandparent->_col = RED; //旧cur新grandparent //此时已经为黑,停止更新 break; } } } } _root->_col = BLACK; return true; }
判断一颗树是否为红黑树
根据规则来走:
规则1,满足,枚举类型非红即黑。
规则2,将根特殊处理就行了
规则3,如果当前结点是红色,左右孩子只能是黑色,即不能有连续的红色节点。判断当前节点不好判断,四种情况,改为判断若当前节点是红色,那么父节点不能是红色。
规则4,前序遍历,从根走到空,每次走到空时和基准值(遍历一遍最左路径,记录黑色节点的数量)比较,若相等则是,反之则不是。
bool IsBalance() { if (_root == nullptr) return true; if (_root->_col == RED) return false; // 参考值 int refNum = 0; Node* cur = _root; while (cur) { if (cur->_col == BLACK) { ++refNum; } cur = cur->_left; } return Check(_root, 0, refNum); } bool Check(Node* root, int blackNum, const int refNum) { if (root == nullptr) { // 前序遍历走到空时,意味着一条路径就走完了 //cout << blackNum << endl; if (refNum != blackNum) { cout << "存在黑色结点的数量不相等的路径" << endl; return false; } return true; } // 检查孩子不太方便,因为孩子有两个,且不?定存在,反过来检查父亲就方便多了 if (root->_col == RED && root->_parent->_col == RED) { cout << root->_kv.first << "存在连续的红色结点" << endl; return false; } if (root->_col == BLACK) { blackNum++; } return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum); }