该章节的所有源码均在gitee中开源:
目录
二叉搜索树
二叉搜索树是AVL树和红黑树的基础,AVL树和红黑树是对二叉搜索树的改进
我们在学习二叉树的时候,只学习了二叉树的各种算法,却没有学习二叉树的应用场景,没有体现出二叉树存储的优势。而二叉搜索树是二叉树的典型应用场景之一,它可以把数据的插入和查找时间复杂度降为O(logn)
二叉搜索树的性质
二叉搜索树有以下的基本性质:
- 若左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若右子树不为空,则右子树上所有节点的值都小于根节点的值
- 左右子树也为二叉搜索树

而这些规矩并非无意义,我们在进行插入和查找的时候,便可以像二分查找一样更方便找到路径
为了方便学习和运用二叉搜索树,我们将常规的二叉树结构中加入了一个父亲节点,形成三叉链结构方便查找和遍历
struct Node
{
TreeNode(const K& key,const V& val)
:_val(val),
_key(key),
_left(nullptr),
_right(nullptr),
_parent(nullptr)
{}
K _key;//键值,用于定位查找
V _val;
Node* _left;//左孩子
Node* _right;//右孩子
Node* _parent;//父节点
};
二叉搜索树的插入和查找
二叉搜索树的插入和查找方法类似,都是要找到符合的路径,然后对路径上的节点或末端进行操作
这么说可能会比较抽象,我们来分别看两者方式如何实现
二叉搜索树的查找
Node* find(const K& key)
这个函数的要求是传入需要查找的值,返回查找到的节点的指针,如果没有查找到相应的值,则返回一个空指针
我们既然已知这是一个二叉搜索树,那么就可以从二叉搜索树的性质本身出发。以最开始的示例二叉树为例,如果我们要查找值20,最开始的根节点为21,我们应该怎么做?
显然易见,我们应该往左边去寻找。因为左边的值都是小于21的值,自然20肯定在左子树里
再下一步,到了19的节点,我们又应该往右寻找,因为右边都是大于19的值,20肯定在右子树里
最后,我们到了节点20,此时我们便找到了20这个节点,返回节点的位置便可以了
从上面的规律我们可以看出,如果需要查找的值大于当前节点的值,我们便向右子树查找;相反,如果小于当前节点的值,我们便向左子树查找,一直到查找到等于目标值的节点
倘若树中没有这个值,我们遍历到了空指针,最后便返回一个空指针,自然满足函数的要求
用代码实现便是
Node* find(const K& key)
{
return _find(_root, key);
}
//子函数实现
Node* _find(Node* root,const K& key)
{
if (root == nullptr)
return nullptr;
if (root->_key == key)
{
return root;
}
else if (root->_key > key)
{
return _find(root->_left, key);
}
else if (root->_key < key)
{
return _find(root->_right, key);
}
}
二叉搜索树的插入
bool insert(const K& key, const V& val)
这个函数的要求是插入一个键对值分别为key,val的节点(这里的val可以不用考虑,键对值的概念在map和set中会有讲解,其中只有key才有比较大小的功能),如果树中已经有该节点则返回false代表插入失败,如果插入成功则返回true
一般来说,二叉搜索树是不支持插入具有相同key值的节点的,因为在删除时会涉及到很多问题。但是在C++11新加的multimap中支持了插入相同key值的节点,这是后话以后再详细讲解,我们现在只考虑插入不同节点的值的情况
和二叉搜索树的查找一样,我们要先找到需要插入的路径具体位置,而查找的过程与以上原则十分相似
- 如果插入值大于当前节点的值,则往右子树继续查找具体位置
- 如果插入值小于当前节点的值,则往左子树继续查找具体位置
- 如果插入值等于当前节点的值,则插入失败,结束并返回false
- 如果当前节点为空指针,则表示找到了需要插入的位置,开始进行插入
还是以示例二叉树为例。假设我们需要插入的节点值为28
- 从根节点开始,节点为21,28>21,往右查找
- 到达23节点,28>23,往右查找
- 到达31节点,28<31,往左查找
- 到达空节点,便是需要插入的具体位置,将新节点插入到当前位置,返回true
用代码实现便是
//创建新节点
bool insert(const K& key, const V& val)
{
Node* ins = new Node(key, val);
return _insert(_root, ins);
}
//子函数实现
bool _insert(Node*& root,Node* ins)
/*传入参数一定要传指针的引用!!!!*/
{
if (root == nullptr)
{
root = ins;
return true;
}
if (root->_key == ins->_key)
{
return false;
}
else if (root->_key > ins->_key)
{
ins->_parent = root;
return _insert(root->_left, ins);
}
else if (root->_key < ins->_key)
{
ins->_parent = root;
return _insert(root->_right, ins);
}
}
二叉搜索树的删除
而与二叉搜索树的插入和查找不同的是,二叉搜索树的删除需要付出的代价就比较大。因为就算是插入这一改变了二叉树的结构的操作,也只是对尾部节点进行修改,而删除则会面临很多情况:
叶子节点的删除
尾部节点的删除
中间节点的删除
不同情况下的节点,其删除的复杂程度也不同,我们来以示例树为例分情况依次讨论
叶子节点的删除
我们把左右子树都为空的节点称为叶子节点,对于叶子节点,我们的操作方法很简单粗暴——直接删除就可以了。因为直接删除该节点不会对整棵树产生任何影响,其余部分仍是一棵平衡二叉树。
尾部节点的删除
我们把仅有一个孩子的节点称为尾部节点,尾部节点可不兴用直接删除的方法,因为如果直接删除,其下面的节点都会丢失,我们要想办法将删除节点的孩子和其父亲链接起来
而此时又用到了二叉树搜索的性质。比如我们要删除31,其父亲为23。31的所有孩子都在父亲23的右子树中,即31的无论左孩子还是右孩子都比父亲23要大,无论哪种情况都可以让孩子直接接在23的下面
同样依次样例,我们可以总结出以下规律:
- 如果尾部节点为其父节点的左孩子,则直接将尾部节点的无论左右孩子接在父节点的左孩子节点, 平衡性不受影响
- 如果尾部节点为其父节点的右孩子,则直接将尾部节点的无论左右孩子接在父节点的右孩子节点, 平衡性不受影响
(方框代表各种情况的抽象树)
中间节点的删除
我们把有两个孩子的节点称为中间节点, 中间节点的情况则更为复杂。虽然其仍满足尾部节点的条件,但是因为中间节点有两个孩子,所以无法直接接入,否则父节点就有了三个孩子。我们所有的操作采取的思想都是,在尽可能不影响其他结构的情况下,去改变这一节点,所以此时便有了一个特别巧妙的方案:去和左子树最大的节点或者右子树最小的节点交换值,然后再删除交换后的节点
这个逻辑的成立有两个原因:
- 交换值后,因为交换后的值为左子树最大或右子树最小,所以其左子树仍所有的值都小于该节点的值,右子树仍所有值都大于该节点的值,最终仍是一棵平衡的搜索二叉树
- 交换值后,需要删除的节点逐渐往尾部靠近,递归到最后可以套用叶子节点的删除或尾部节点的删除。
比如我们要删除节点23,按照以上思想,我们首先找到右子树最小的节点28,然后交换23和28,此时除去23,左子树的22小于28,右子树的31大于28,仍是一棵二叉搜索树,而在交换以后,23由中间节点变为了叶子节点,采取直接删除的方式即可。
将以上总结下来用代码实现便是
void erase(const K& key)
{
Node* erasement = find(key);
_erase(erasement);
}
Node* _find_max(Node* root)
{
if (root == nullptr)
return nullptr;
if (root->_right == nullptr)
return root;
return _find_max(root->_right);
}
Node* _find_min(Node* root)
{
if (root == nullptr)
return nullptr;
if (root->_left == nullptr)
return root;
return _find_min(root->_left);
}
void _erase(Node* erasement)
{
if (erasement == nullptr)
return;
if (erasement->is_leaf())
{
if (!erasement->_parent)
_root = nullptr;
else if (erasement->_parent->_left == erasement)
erasement->_parent->_left = nullptr;
else if (erasement->_parent->_right == erasement)
erasement->_parent->_right = nullptr;
delete erasement;
erasement = nullptr;
}
else if (erasement->_left == nullptr && erasement->_right)
{
if (!erasement->_parent)
{
_root = erasement->_right;
_root->_parent = nullptr;
}
else if (erasement->_parent->_left == erasement)
{
erasement->_parent->_left = erasement->_right;
erasement->_right->_parent = erasement->_parent;
}
else if (erasement->_parent->_right == erasement)
{
erasement->_parent->_right = erasement->_right;
erasement->_right->_parent = erasement->_parent;
}
delete erasement;
erasement = nullptr;
}
else if (erasement->_left && erasement->_right == nullptr)
{
if (!erasement->_parent)
{
_root = erasement->_left;
_root->_parent = nullptr;
}
else if (erasement->_parent->_left == erasement)
{
erasement->_parent->_left = erasement->_left;
erasement->_left->_parent = erasement->_parent;
}
else if (erasement->_parent->_right == erasement)
{
erasement->_parent->_right = erasement->_left;
erasement->_left->_parent = erasement->_parent;
}
delete erasement;
erasement = nullptr;
}
else
{
Node* tmp = _find_max(erasement->_left);
erasement->_key = tmp->_key;
_erase(tmp);
}
}
二叉搜索树的中序遍历
除了二叉搜索树的操作,还有一点我们需要注意:
二叉搜索树的中序遍历为升序遍历
这个性质可以很好得到,我们最先访问最左节点,然后一步步向右访问,一直到访问最右节点。恰好,二叉搜索树的最左节点为最小值,最右节点为最大值,其中序遍历便是从小到大升序遍历,这个性质通过简单分析便可以得到,在此便不再多做说明。
在这里分享一个可以将二叉搜索树操作可视化的小网站,有助于大家理解:
二叉搜索树可视化http://btv.melezinek.cz/binary-search-tree.html
AVL树
AVL树的诞生
二叉搜索树虽然看着可行,但是其仍有一个很大的痛点:只有在理想情况下查找的时间复杂度才是O(logn)

而如果二叉树的结构类似于链表,则时间复杂度骤升为O(N)

为了解决这一问题,有两位天才大佬发明了一棵无论如何都能让查找性能达到最优的树——AVL树
AVL树的性质
AVL树有以下的基本性质:
- AVL树是一棵二叉搜索树
- 左右子树都是AVL树
- 左右子树高度差(简称平衡因子)的绝对值不超过1

通过这个性质我们可以轻易想到,二叉搜索树杜绝了效率低下的情况,让所有的AVL树都接近于一个满二叉树,这样无论如何插入,其查找的效率都为理想状况
为了完成这一棵树,我们必须要加入一个新的概念:平衡因子
当一棵树的左子树高度增加时,平衡因子-1,代表左子树高度有所增加;当右子树高度增加的时候,平衡因子+1,代表右子树的高度有所增加,最终我们只需要访问每个节点的平衡因子,便可以知道是否满足AVL树的要求
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode(const K& key, const V& val)
:_val(val),
_key(key),
_left(nullptr),
_right(nullptr),
_parent(nullptr),
_bf(0)
{}
K _key;
V _val;
AVLTreeNode* _left;
AVLTreeNode* _right;
AVLTreeNode* _parent;
int _bf;//平衡因子
};
AVL树的插入
AVL树实际也是一棵二叉搜索树,其插入也相当与二叉搜索树的插入。只不过AVL树在此基础上,增加了新的要求:在插入后调整平衡因子
这里我们通过枚举定义了一个新的变量:direction来记录最新一步移动的路径
因为插入过程在二叉搜索树已经有了详细讲解,所以我们直接放出插入部分的代码,不做过多讲解
bool insert(const K& key,const V& val)
{
if (_root == nullptr)
{
_root = new Node(key, val);
return true;
}
Node* parent = _root;
Node* cur = _root;
direction dir;
while (cur)
{
if (key == cur->_key)
return false;
else if (key < cur->_key)
{
dir = LEFT;
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
dir = RIGHT;
parent = cur;
cur = cur->_right;
}
}
cur = new Node(key, val);
cur->_parent = parent;
if (dir == LEFT)
parent->_left = cur;
else if (dir == RIGHT)
parent->_right = cur;
//接下来调整平衡因子
...
}
平衡因子的调整
我们由AVL的性质来看,分为以下几种情况:
- 如果一个节点的子节点平衡因子bf由0变为了1或者-1,那么代表该子节点的高度发生了变化,此时该节点的平衡因子也要进行调整。如果变化的子节点为该节点的右孩子,则该节点的平衡因子+1;如果变化的子节点为左孩子,则该节点的平衡因子-1;
因为这种情况下,该节点的平衡因子发生了变化,所以还需要继续往上进行调整 - 如果一个节点的子节点平衡因子bf由-1或1变为了0,那么代表子节点的高度没有发生变化,也就是说对子节点往上的所有节点都没有影响,往上的所有节点平衡因子都不会发生变化,则此时调整结束
用代码实现
while (parent)
{
//修改平衡因子
if (cur == parent->_left)
parent->_bf--;
else if (cur == parent->_right)
parent->_bf++;
//判断平衡因子
if (parent->_bf == 0)
break;
else if (parent->_bf == 1 || parent->_bf == -1)
{
//继续向上层判断
cur = parent;
parent = parent->_parent;
}
}
此时会有一个问题:假如一个节点的平衡因子是1,子树插入后对该节点的影响是bf++,该节点的平衡因子变成了2,这种情况为什么没有出现在循环中呢?
别急,当出现这种情况时便说明了一个问题:我们的AVL树平衡出了问题,需要通过一定的修改来让这棵AVL树重新保持平衡
具体的修改是什么呢?这便是AVL树天才的地方:AVL树的旋转
AVL树的旋转
最简单的情况
我们先来考虑最简单的情况:单链表式结构
通过遍历我们可以发现,这棵AVL树在节点20处发生了平衡因子异常,我们需要调整这棵树来恢复AVL树的平衡,而最理想的情况自然是让这棵树变成满二叉树,那么30便自然成为了最佳的根节点选择,因为20比30小可以放在左子树,40比30大可以放在右子树,形成了一棵满二叉树

而我们再来看这一过程,这个过程是让20变成了30的左子树节点,让30变成了根节点,整个过程是把最左的节点进行了旋转,所以我们把这个过程称为AVL树的左旋
同样,按此方式,我们也可以理解右旋
理解了之后,我们开始上一小点强度:如果不是三点共线的链式结构呢?
我们可以还是像分析三点共线一样思考,但是我们这里可以动用一个理科的基本思想:将未解决问题转化为已解决问题,然后再来解决,用代码的思想来体现就是一个词:复用
这句话怎么理解?用更通俗的话来说,我们需要想的办法由将其调整为一个平衡的AVL树变成将其调整为三点共线的模型,而最直观的方法就是将40的节点右旋
有人肯定要问,这只有两个节点,这怎么右旋?我们不要忽略了一个新的节点:30的左孩子是一个空节点
右旋以后,以上情况便还原成了三点共线时的情况,我们再对20进行左旋,便使AVL树恢复了平衡,而这一操作又被称为AVL树的右左旋
而另一种情况,便被称为AVL树的左右旋
是不是很简单?
一般情况(黑盒思想)
通过最简单的几种情况,我们了解了AVL树的单旋和双旋,但是实际情况远远比此复杂。在实际情况中,这三个节点不一定在根节点的位置,也不一定在叶子节点的位置,它可以是中间的任意一个节点(因为我们在调整平衡因子时会不停向上递归进行调整,所以可能会递归到中间的任意一个节点)

其中方框为抽象二叉树,可以代表任意一种情况。(当所有的抽象二叉树都为空时,就是最简单的情况)
这种表现方式可以类比成我们电路图中的黑盒,无论外部是如何,我们需要改变的永远是黑盒中的部分,然后与外部的接口进行交换,外部没有任何改变

而当我们改变时,也只是对内部结构进行操作,外部的接口并没有发生变化

我们对黑盒中的节点进行了旋转,然后重连接口,外部的接口没有发生变化,内部只有节点和接口发生了变化,并且接口的变化极易理解,只需要找到最近的接口,这种方法大大简化了我们的理解难度,并且也让我们的代码实现更加直观。
代码实现
我们以黑盒模型图为例来实现左旋
第一步,我们找到所有可能会改变的节点,一般的旋转是只定义少量节点换来换去,但是初学者方便理解,我们可以直接将所有节点定义出来,避免了交换顺序的问题
其中因为平衡因子异常的节点才会产生旋转,所以我们需要传入的root是平衡因子异常的节点
//root为平衡因子异常的节点
//节点1的父节点
Node* grandparent = root->_parent;
//节点1的节点
Node* parent = root;
//节点1的左孩子——也为节点2的兄弟节点
Node* brother;
//节点2的节点
Node* cur = root->_left;
//节点2的左孩子
Node* childL = cur->_left;
//节点2的右孩子——即为节点3的节点
Node* childR = cur->_right;
//节点3的孩子节点
Node* childRC;
此时我们观察黑盒改变前后图片,我们发现,有两个接口并没有产生改变:
分别是1的左子树和3的孩子节点,它们并没有涉及到修改操作,所以我们没有必要去定义这两个节点
//剩下的节点
//节点1的父节点
Node* grandparent = root->_parent;
//节点1的节点
Node* parent = root;
//节点2的节点
Node* cur = root->_left;
//节点2的左孩子
Node* childL = cur->_left;
//节点2的右孩子——即为节点3的节点
Node* childR = cur->_right;
第二步,我们分别对每个节点进行接口修改,但是这里要注意一点,首先要判断1是其父节点的左孩子还是右孩子,不然修改完成之后就无法判断了
修改祖先节点的接口:
if (grandparent)
{
如果祖父节点不为空,则让祖父节点的孩子指向cur
if (grandparent->_left == root)
{
grandparent->_left = cur;
}
else if (grandparent->_right == root)
{
grandparent->_right = cur;
}
}
else
{
//如果祖父节点为空,则代表cur就是根节点
_root = cur;
}
修改cur节点的接口:
cur->_parent = grandparent;
cur->_left = parent;
修改parent节点的接口:
parent->_parent = cur;
parent->_left = childR;
修改2的左孩子的接口
//如果左孩子为空,则无法改变
//如果左孩子不为空,则让左孩子的父亲指向节点2
if(childL)
childL->_parent = parent;
而剩下没有修改的接口,即为没有发生变化的节点与接口,我们在此便省略掉了
平衡因子的修改
此时,便剩下了最后一个问题——旋转完成之后的平衡因子应该如何变化?
如果只有几个节点还好说,节点数量一旦上去,我们便没有办法轻易找到高度差了,此时,我们还是可以用黑盒思想轻易找到平衡因子
以平衡因子210模型为例,我们假设3的孩子节点高度为n,因为2的平衡因子为1,所以我们可以轻易推出2的左子树比右子树高度低1,因为右子树的高度为n+节点3的1=n+1,所以2的左子树高度为n+1-1=n
而1的平衡因子是2,则表示1的左子树比右子树高度低2,因为右子树的高度为n+1+1,所以左子树的高度为n+1+1-2=n,即为图中所表示
而黑盒内部的改变不会对外部产生影响,所以黑盒外部的三个部件高度不会发生变化仍为n,我们再来反推出平衡因子
改变后,1的左子树右子树高度均为n,所以平衡因子是0;3的节点没有发生变化,所以平衡因子也不会产生变化,2的左子树右子树高度均为n+1,所以平衡因子是0;而祖先的黑盒以下的节点,因为2的平衡因子为0,结束了循环,故不需要继续往上进行调整,也就是说,只要产生一次旋转过后插入过程便结束了。
parent->_bf = cur->_bf = 0;
合起来的总代码便是:
void RotateL(Node* root)
{
Node* grandparent = root->_parent;
Node* parent = root;
Node* cur = root->_right;
Node* childL = cur->_left;
//因为整个过程cur的右孩子即3并没有发生任何修改,所以直接忽略该节点
//Node* childR = cur->_right;
if (grandparent)
{
if (grandparent->_left == root)
{
grandparent->_left = cur;
}
else if (grandparent->_right == root)
{
grandparent->_right = cur;
}
}
else
{
_root = cur;
}
cur->_parent = grandparent;
cur->_left = parent;
parent->_parent = cur;
parent->_right = childL;
if(childL)
childL->_parent = parent;
parent->_bf = cur->_bf = 0;
}
而剩下的几种情况包括AVL树删除分析方法与其完全相同,便不再赘述,具体可以看实现后的代码
红黑树
红黑树的诞生
如果说,AVL树是由天才发明的,那么红黑树就是由上帝发明的。原本应该是AVL树占据整个市场,在红黑树诞生后,其有近似于AVL树的查找效率和远超AVL树的插入和删除的效率,彻底取代了AVL树的地位,成为了map&set实现的底层容器
红黑树的性质
红黑树也是一棵二叉搜索树,在二叉搜索树的基本结构之上,红黑树对每一个节点加入了一个新的元素:颜色。颜色只能为红色或者黑色,并且红黑树必须满足以下基本原则:
- 红黑树是一棵二叉搜索树
- 红黑树每一个节点不是红色就是黑色,并且根节点必须为黑色
- 如果一个节点为红色,那么他的孩子节点必须为黑色
- 对于每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
- 每个叶子节点都是黑色(此处的叶子节点指的是空节点)
而满足了以上的性质之后,红黑树便有了一个很重要的性质——红黑树最长路径不会超过最短路径的两倍

所以,我们可以将红黑树的节点定义为:
enum color
{
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
RBTreeNode(const K& key = K(), const V& val = V())
:_val(val),
_key(key),
_left(nullptr),
_right(nullptr),
_parent(nullptr),
_col(RED)
{}
K _key;
V _val;
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
color _col;
};
同时为了方便封装set和map,我们不再在红黑树里存储根节点root来作为红黑树的开始,而是存储根节点root的父亲来作为红黑树的开始
template<class K,class V>
class RBTree
{
public:
typedef RBTreeNode<K, V> Node;
//父节点初始时指向自己代表空树
RBTree()
{
_pHead = new Node;
_pHead->_left = _pHead;
_pHead->_right = _pHead;
_pHead->_col = BLACK;
}
//析构函数
~RBTree()
{
if (_pHead->_left == _pHead)
{
delete _pHead;
}
else
{
_Destroy(_pHead->_left);
delete _pHead;
}
}
private:
//根节点的父亲
Node* _pHead;
//析构函数的子函数
void _Destroy(Node* root)
{
if (root == nullptr)
return;
_Destroy(root->_left);
_Destroy(root->_right);
delete root;
}
}
红黑树的插入与调整
初步插入与二叉搜索树和AVL树的插入相同,在此不多做赘述
bool insert(const K& key,const V& val)
{
//空树情况
if (_pHead->_left == _pHead)
{
Node* ins = new Node(key, val);
ins->_col = BLACK;
_pHead->_left = _pHead->_right = ins;
ins->_parent = _pHead;
return true;
}
//非空树情况
Node* cur = _pHead->_left;
Node* parent = _pHead;
direction dir;
while (cur)
{
if (cur->_key == key)
return false;
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
dir = RIGHT;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
dir = LEFT;
}
}
cur = new Node(key, val);
cur->_parent = parent;
if (dir == RIGHT)
parent->_right = cur;
else if (dir == LEFT)
parent->_left = cur;
//...
}
此时,我们要注意一点:红黑树节点的默认构造函数为红色。为什么不是黑色?因为如果是黑色,那么会对所有路径上的黑色节点数目都产生影响,整个树都要发生变动,而如果是红色,只会对当前路径产生影响,便于我们进行调整
在插入完成后,我们便开始对红黑树进行修改,这里也需要我们采用黑盒思想来便于理解
红黑树的修改,便是找到违反红黑树原则的那一条,然后根据违反的条件来调整红黑树,从而让调整后的新的红黑树符合要求。同时,在调整时,我们要尽可能减少调整的复杂度,提高调整的效率
同样,红黑树的修改也分为很多种情况,但是那5条原则,第一条是在插入时遵循的原则,是插入时便满足的,第二条是树的基本性质,是不会发生改变的,第五条我们无法操作空节点,所以也是任意时候都满足的,所以我们大部分时候只需要关注第三条和第四条,即
- 是否出现了两个相邻的红节点?
- 每条路径的黑色节点数目是否发生了改变?
而在众多情况种,我们可以分为以下三大类
1.父节点与叔节点全为红
我们来看看是否违反了条件。
对于第三条,该节点为红且父节点为红,违反了条件,我们必须想办法修改其中一个节点为黑,才能满足条件
如果我们修改该节点为黑,那么该路径黑节点多出来了一个,而叔节点路径的黑节点没有发生变化,与第四条原则冲突了;而如果修改父节点为黑,我们也可以顺带着将叔节点也改为黑,然后将祖父节点改为红,这样每一条路径的黑色结点数目不会发生变化,且满足了第三条的要求
但是,调整以后,我们无法确保祖父节点往上是黑色节点还是红色节点,所以我们还要以祖父节点为下一步需要调整的节点继续往上调整
if (parent->_col == RED && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandparent->_col = RED;
cur = grandparent;
parent = cur->_parent;
continue;
}
2.父节点为红且叔节点为黑
这种情况下,我们无法再同时改变父节点和叔节点的值了,因为无论改变哪一个,都会对黑节点的数目产生影响,此时只能另辟蹊径
此时想一想,我们的目的是什么?我们的目的是消除两个相邻的红节点,那么我们就将父节点变为黑色;但是那样右边的路径就多出一个黑节点了啊?那我们就将祖父节点变为红色,但是那样左边又少了一个黑节点啊?那我们就将祖父左旋,让祖父变成父节点的左孩子

调整之后,我们发现,新的黑盒图有了新的变化:
- 没有相邻的红节点
- 每条路径的黑结点数量和调整前相同,也就是说调整之后对其他路径不会产生任何影响
- 最上层结点变为了黑结点,也就是说无论祖先是红是黑都能符合要求,调整结束
这便是旋转最巧妙的地方,即满足了条件,还结束了循环
而对于子节点的另一种情况,想必大家可以举一反三出来:
我们先通过父节点的右旋将其变为已经熟悉的情况,然后再通过已经熟悉的情况进行处理,便不多加说明
叔节点为空
当叔节点为空时,我们不要忘了,空节点也为黑,所以我们完全可以将其当作黑结点来考虑
3.父节点为黑结点
到了这种情况,只会有两种可能:
- 该节点为新插入节点,子节点为空节点黑色
- 该节点为第一种情况递归上来的节点,子节点均为黑色
所以,我们无需考虑黑盒外的影响。而在黑盒内,当然满足原则3,对于原则4,因为我们新插入的是红色节点,不会对任何路径产生影响,所以也满足原则4,此时可以直接结束循环。
一个小问题
当我们一直往上递归的时候,可能产生一种情况:将根节点递归为了黑色。
(因为root的父节点pHead也为黑色,所以循环一定会终止)
此时,便违反了原则2——根节点必须为黑色,我们应该如何解决呢?
最好的解决方案就是暴力解决——每一次插入都将根节点重设为黑色。因为根节点是所有路径的共享节点,所以根节点的颜色变化不会对原则4产生任何影响
代码实现
bool insert(const K& key,const V& val)
{
if (_pHead->_left == _pHead)
{
Node* ins = new Node(key, val);
ins->_col = BLACK;
_pHead->_left = _pHead->_right = ins;
ins->_parent = _pHead;
return true;
}
Node* cur = _pHead->_left;
Node* parent = _pHead;
direction dir;
while (cur)
{
if (cur->_key == key)
return false;
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
dir = RIGHT;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
dir = LEFT;
}
}
cur = new Node(key, val);
cur->_parent = parent;
if (dir == RIGHT)
parent->_right = cur;
else if (dir == LEFT)
parent->_left = cur;
//调整颜色
while (cur)
{
if (parent->_col == BLACK)
break;
else if (parent->_col == RED)
{
Node* uncle = get_uncle(cur);
Node* grandparent = parent->_parent;
if (uncle == nullptr|| uncle->_col == BLACK)
{
//如果uncle为空 则说明只有一种情况:
//cur就是叶子节点
//而uncle为黑和uncle为空处理情况是一样的:旋转加变色
//三点一线的情况:单旋
if (grandparent->_left == parent && parent->_left == cur)
{
RotateR(grandparent);
parent->_col = BLACK;
grandparent->_col = cur->_col = RED;
}
else if (grandparent->_right == parent && parent->_right == cur)
{
RotateL(grandparent);
parent->_col = BLACK;
grandparent->_col = cur->_col = RED;
}
//三点不共线的情况:双旋
else if (grandparent->_left == parent && parent->_right == cur)
{
RotateL(parent);
RotateR(grandparent);
cur->_col = BLACK;
grandparent->_col = parent->_col = RED;
}
else if (grandparent->_right == parent && parent->_left == cur)
{
RotateR(parent);
RotateL(grandparent);
cur->_col = BLACK;
grandparent->_col = parent->_col = RED;
}
break;
}
else if (uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandparent->_col = RED;
cur = grandparent;
parent = cur->_parent;
continue;
}
}
}
//重设根节点
_pHead->_left->_col = BLACK;
_pHead->_col = BLACK;
return true;
}
对于红黑树的删除,原则与其重复度极高,但复杂程度远高于插入,所以不做讲解
AVL树和红黑树的验证
以下代码供大家验证AVL树和红黑树是否实现完成:
AVL树的验证:
通过遍历判断每个树节点的左右子树高度差绝对值是否小于1
bool is_balance()
{
return _is_blance(_root);
}
//子函数
bool _is_blance(Node* root)
{
if (root == nullptr)
return true;
size_t left = Height(root->_left);
size_t right = Height(root->_right);
return ((left - right <= 1) || (left - right >= -1)) && _is_blance(root->_left) && _is_blance(root->_right);
}
红黑树的验证:
判断是否有相邻的红节点与每条路径的黑结点数目是否相同
bool is_balance()
{
if (_pHead->_left == _pHead)
return true;
Node* cur = _pHead->_left;
int adjust = 0;
while (cur)
{
if (cur->_col == BLACK)
adjust++;
cur = cur->_left;
}
return _is_blance(_pHead->_left, 0, adjust);
}
//子函数
bool _is_blance(Node* root,int i,const int adjust)
{
if (root == nullptr && i == adjust)
return true;
if (root->_col == RED && root->_parent->_col == RED)
return false;
if (root->_col == BLACK)
i++;
return _is_blance(root->_left, i, adjust)&&_is_blance(root->_right,i,adjust);
}
扩展阅读