该章节的所有源码均在gitee中开源:
AVL树和红黑树https://gitee.com/Ehundred/data-structure/tree/master/AVL%E6%A0%91+%E7%BA%A2%E9%BB%91%E6%A0%91
目录
二叉搜索树
二叉搜索树的性质
二叉搜索树的插入和查找
二叉搜索树的查找
二叉搜索树的插入
二叉搜索树的删除
叶子节点的删除
尾部节点的删除
中间节点的删除
二叉搜索树的中序遍历
AVL树
AVL树的诞生
AVL树的性质
AVL树的插入
平衡因子的调整
AVL树的旋转
最简单的情况
一般情况(黑盒思想)
代码实现
平衡因子的修改
红黑树
红黑树的诞生
编辑
红黑树的性质
红黑树的插入与调整
1.父节点与叔节点全为红
2.父节点为红且叔节点为黑
叔节点为空
3.父节点为黑结点
一个小问题
代码实现
AVL树和红黑树的验证
AVL树的验证:
红黑树的验证:
扩展阅读
二叉搜索树是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值的节点,这是后话以后再详细讲解,我们现在只考虑插入不同节点的值的情况
和二叉搜索树的查找一样,我们要先找到需要插入的路径具体位置,而查找的过程与以上原则十分相似
还是以示例二叉树为例。假设我们需要插入的节点值为28
用代码实现便是
//创建新节点
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
二叉搜索树虽然看着可行,但是其仍有一个很大的痛点:只有在理想情况下查找的时间复杂度才是O(logn)
接近满二叉树而如果二叉树的结构类似于链表,则时间复杂度骤升为O(N)
最差情况为了解决这一问题,有两位天才大佬发明了一棵无论如何都能让查找性能达到最优的树——AVL树
AVL树有以下的基本性质:
通过这个性质我们可以轻易想到,二叉搜索树杜绝了效率低下的情况,让所有的AVL树都接近于一个满二叉树,这样无论如何插入,其查找的效率都为理想状况
为了完成这一棵树,我们必须要加入一个新的概念:平衡因子
当一棵树的左子树高度增加时,平衡因子-1,代表左子树高度有所增加;当右子树高度增加的时候,平衡因子+1,代表右子树的高度有所增加,最终我们只需要访问每个节点的平衡因子,便可以知道是否满足AVL树的要求
template
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树在此基础上,增加了新的要求:在插入后调整平衡因子
这里我们通过枚举定义了一个新的变量: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的性质来看,分为以下几种情况:
用代码实现
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树在节点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
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 RBTree
{
public:
typedef RBTreeNode 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条原则,第一条是在插入时遵循的原则,是插入时便满足的,第二条是树的基本性质,是不会发生改变的,第五条我们无法操作空节点,所以也是任意时候都满足的,所以我们大部分时候只需要关注第三条和第四条,即
而在众多情况种,我们可以分为以下三大类
我们来看看是否违反了条件。
对于第三条,该节点为红且父节点为红,违反了条件,我们必须想办法修改其中一个节点为黑,才能满足条件
如果我们修改该节点为黑,那么该路径黑节点多出来了一个,而叔节点路径的黑节点没有发生变化,与第四条原则冲突了;而如果修改父节点为黑,我们也可以顺带着将叔节点也改为黑,然后将祖父节点改为红,这样每一条路径的黑色结点数目不会发生变化,且满足了第三条的要求
但是,调整以后,我们无法确保祖父节点往上是黑色节点还是红色节点,所以我们还要以祖父节点为下一步需要调整的节点继续往上调整
if (parent->_col == RED && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandparent->_col = RED;
cur = grandparent;
parent = cur->_parent;
continue;
}
这种情况下,我们无法再同时改变父节点和叔节点的值了,因为无论改变哪一个,都会对黑节点的数目产生影响,此时只能另辟蹊径
此时想一想,我们的目的是什么?我们的目的是消除两个相邻的红节点,那么我们就将父节点变为黑色;但是那样右边的路径就多出一个黑节点了啊?那我们就将祖父节点变为红色,但是那样左边又少了一个黑节点啊?那我们就将祖父左旋,让祖父变成父节点的左孩子
调整示例图调整之后,我们发现,新的黑盒图有了新的变化:
这便是旋转最巧妙的地方,即满足了条件,还结束了循环
而对于子节点的另一种情况,想必大家可以举一反三出来:
我们先通过父节点的右旋将其变为已经熟悉的情况,然后再通过已经熟悉的情况进行处理,便不多加说明
当叔节点为空时,我们不要忘了,空节点也为黑,所以我们完全可以将其当作黑结点来考虑
到了这种情况,只会有两种可能:
所以,我们无需考虑黑盒外的影响。而在黑盒内,当然满足原则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树和红黑树是否实现完成:
通过遍历判断每个树节点的左右子树高度差绝对值是否小于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);
}
红黑树比AVL高效在哪里?
AVL树可视化