二叉搜索树
一、AVL 平衡搜索树
二叉搜索树虽可以缩短查找的效率,但如果数据极端,退化为链表,搜索时间复杂度为O(N),所以引入了ALV树,在二叉搜索树的基础加入平衡因子
平衡因子:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1
1、节点定义
template<class T>
struct AVLTreeNode{
AVLTreeNode(const T& data)
: _pLeft(nullptr)
, _pRight(nullptr)
, _pParent(nullptr)
, _data(data)
, _bf(0)//平衡因子,初始值为0,默认已经平衡
{}
AVLTreeNode<T>* _pLeft; // 该节点的左孩子
AVLTreeNode<T>* _pRight; // 该节点的右孩子
AVLTreeNode<T>* _pParent; // 该节点的双亲
T _data;
int _bf; // 该节点的平衡因子
};
2、AVL 树旋转分类
2.1. 右单旋

45节点 的存在与否都不重要,如果40 没有右子树,那么它依然要进行单旋。上面的案列 50 没有双亲节点,如果有父节点,那该怎么调整呢?
就拿上列来说,我们假设 50 还有双亲节点
void RotateRight1(Node* pParent){
// pParent 代表的是 50 节点
// pSubL: pParent的左孩子 40
// pSubLR: pParent左孩子的右孩子 45
Node* pSubL = pParent->_pLeft;
Node* pSubLR = pSubL->_pRight;
// 旋转完成之后,40 的右孩子作为双亲的左孩子
pParent->_pLeft = pSubLR;
// 如果 50 的左子树的右孩子存在,更新亲双亲
// 使 45 的双亲指向 50
if (pSubLR) {
pSubLR->_pParent = pParent;
}
// 50 作为 40 的右孩子
pSubL->_pRight = pParent;
// 因为 50 可能是棵子树,因此在更新其双亲前必须先保存 50 的双亲
Node* pPParent = pParent->_pParent;
// 更新 50 的双亲
pParent->_pParent = pSubL;
// 更新 40 的双亲
pSubL->_pParent = pPParent;
// 如果 50 是根节点,根新指向根节点的指针
if (nullptr == pPParent){
pRoot = pSubL;
pSubL->_pParent = nullptr;
}
else{
// 如果 50 是子树,可能是其双亲的左子树,也可能是右子树
if (pPParent->_pLeft == pParent) {
pPParent->_pLeft = pSubL;
}
else {
pPParent->_pRight = pSubL;
}
}
// 根据调整后的结构更新部分节点的平衡因子
pParent->_bf = pSubL->_bf = 0;
}
2.2 左单旋
和右单旋正好相反
2.3. 双旋 — 先左单旋 再 右单旋

2.4. 双旋 — 先右单旋 再 左单旋
和上面的正好相反
2.5. 总结

旋转完成后,原 pParent 为根的子树个高度降低,已经平衡,不需要再向上更新
3、AVL树的特点与性能分析
-
需要通过多次的旋转来保证搜索性能
-
左右子树都是AVL树
-
左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
-
搜索时间复杂度O(log2N )
4、AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
-
验证其为二叉搜索树,如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
-
验证其为平衡树,判断每个节点的左子树高度和右子树高度,如果全部都在 -1 ~ 1 的范围,则说明平衡
//计算深度
int Height(Node* pRoot){
if (root == NULL){
return 0;
}
if (root->left == NULL && root->right == NULL){
return 1;
}
int left = GetHeight(root->left);
int right = GetHeight(root->right);
return max(left,right)+1;
}
//判断是否平衡
bool IsBalanceTree(Node* pRoot){
if (nullptr == pRoot) {
return true;
}
// 计算pRoot节点的平衡因子:即pRoot左右子树的高度差
int leftHeight = Height(pRoot->_pLeft);
int rightHeight = Height(pRoot->_pRight);
int diff = abs(rightHeight - leftHeight);
if (diff > 1)){
return false;
}
return IsBalanceTree(pRoot->_pLeft) && IsBalanceTree(pRoot->_pRight);
}
二、红黑树
1、为什么引入红黑树?
红黑树和AVL树都是高效的平衡二叉树,搜索时间复杂度都是O(log2N ),只不过AVL树旋转的次数太多,红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优 而且红黑树实现比较简单,所以实际运用中红黑树更多
2、红黑树特性

- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的(不能有连在一起的红色节点)
- 对于每个结点,从该结点到所有后代叶结点的简单路径上,黑色节点相同
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
只要满足上述性质,可以保证红黑树最长路径不会超过最短路径的两倍
3、RBTree 是如何插入?
插入和AVL树一样,而且不需要进行平衡因子的判断,相对来说比较简单
4、RBTree 如何旋转的?
4.1情况1
cur 是红色,p 也是红色,且 u 节点存在也为红

正确操作:把 p 和 u 改为黑色(如果 g 为根节点)

如果 g 不是根节点,上述把双亲节点和叔叔节点变为黑树就违反了红黑树的性质,因为右子树分别多了一个黑色节点
正确做法:把 g 变为红色节点

当然对于 g 双亲节点的颜色,如果是黑色的,则不用调整了,但是如果是红色节点,则还需要进行调整
分析:对于 cur 节点有两种场景
- 新插入的节点
- 本来是黑色,但是子树不满足性质,把 cur 变为了红色
4.2情况2
cur为红,p为红,g为黑,u不存在或者 u 存在一定为黑
根据 u 对 cur 节点进行分析
- 如果 u 不存在,则 cur 一定是插入的节点
- 如果 u 存在且为黑色,则 cur 本来的颜色就是黑色(这种情况就是 4.1情况中的 g 双亲节点为红色)

这种情况时候无论怎么变换颜色,都会破坏红黑树性质,所以
如果 p 为 g 的左孩子,cur 为 p 的左孩子,则进行右单旋
如果 p 为 g 的右孩子,cur 为 p 的右孩子,则进行左单旋

最后 p ,g 变色

4.3情况3
cur为红,p为红,g为黑,u不存在/u为黑

和情况2特别相似,我们只要对 p 做左单旋就会发现

又变回了情况2,所以按照情况2的处理方式进行处理就可以了
5、红黑树验证
1、验证根节点颜色
2、验证任意路径黑色节点的个数
3、验证红色节点的左右节点是否是红色
6、红黑树应用
C++ STL 库,作为 map ,set 的底层结构
Java 库
Linux 库
…
本文详细介绍了AVL树的节点定义、旋转分类及其特点与性能分析,包括右单旋、左单旋和双旋等。接着探讨了红黑树的引入原因、特性、插入操作及旋转调整,强调了红黑树在实际应用中的优势。内容涵盖两种平衡二叉搜索树的验证方法和应用场景。

361


被折叠的 条评论
为什么被折叠?



