1.AVL树是什么
AVL树是一种平衡二叉树,是二叉搜索树的优化版本。之前我们讨论过,二叉搜索树存在退化的情况,导致搜索的时间复杂度是O(N)。AVL树就能优化这种情况,使得对于一颗二叉搜索树来说,任何结点的左右子树的高度差不超过1,这就是最理想的二叉树。在AVL树中,搜索的时间复杂度就是O(logN),即高度次。
借助下面的图可以更深理解AVL树的结构特点
AVL树原则:保持二叉搜索树的规则 + 控制平衡,尽量降低高度
下面我会一步一步分析,结合代码帮助理解。
2.平衡因子、结点的定义
当树不符合AVL树的要求时,就会发生旋转。问题在于如何如何判断要不要旋转?怎么旋转?
平衡因子+三叉链(left、right、parent)解决了要不要旋转的问题。
所谓平衡因子,就是记录该节点右子树与左子树高度的差,当平衡因子为2或-2时,就会发生旋转。我们很容易理解,平衡因子只有-2、-1、0、1、2这五种情况,-2、2标志着需要旋转处理。平衡因子应当作为每个节点的标识之一,所以我们将它放进成员变量里。同时,我们要为计算平衡因子的时间复杂度考虑。根据以往二叉树的学习,计算一个结点的左右高度差可以很好的用递归来解决,但在AVL树里,我们要算所有节点的平衡因子,用递归的办法就过慢了,所以我们引入了第三个指针,除了left,right,还有一个parent,指向自己的父节点。父节点指针的存在使得旋转操作也变简单了,后续会一步一步验证的。
下面是节点的定义
其中,平衡因子用_bf来标识,在实现中,平衡因子的计算可以左树高度减右树高度,也可以右树减左树,随便选择,因为判断要不要旋转取决于平衡因子的绝对值,2和-2本质上没区别,但一定要保证整棵树的计算都要遵循这个规则。本文均采用右 - 左
3.insert、平衡因子的更新
平衡因子的更新是AVL树的难点,我们一定要理清楚逻辑,多练习
(1)空树处理
当一棵树为空树时,它的_root为nullptr,这时要特殊处理,涉及修改成员变量了。
(2)找到该插入的位置
要找到该插入的位置,我们的处理方式和搜索二叉树一样,让nodeC作为探路的,nodeP跟在nodeC后面,由于空树前面已经特殊处理过了,初始nodeC不可能为空,所以这里的while循环一定会进一次,nodeP一定不为空。因为在查找的时候就涉及到值的比较,在查找时顺便也把去重的问题解决了
(3)插入结点处平衡因子和parent的维护
这里就和搜索二叉树有了区别。
当插入在右边的时候除了让nodeP和新节点联系起来,我们还要维护好平衡因子和parent这两个成员变量。最好的思考方式是遍历,涉及两个结点,加起来8个成员变量,逐个考虑是否要修改。
很多人会疑惑平衡因子不是右子树减左子树高度吗?为什么直接++或--啊?
很多人会想:万一下面插入一个结点,上面的某个父节点高度不发生变化怎么办?就像下面这张图
这里我们就要注重理解直接父节点,至于祖先的情况,是后面着重要分析的。
4.平衡因子的逐级更新及旋转判断
平衡因子的维护很考验我们的思维严谨性。一个结点的平衡因子应当怎样维护?
1.当它的直接的子节点插入时要能正确++或--(刚刚已经解决了);
2.当它的非直接的子节点插入时要能正确判断和变化。
因为平衡因子每个结点都有,所以当我们新插入一个结点时,它的直接父亲只需要解决1就可以了,但对于它的其他节点而言,它们不需要解决1,需要解决2。当我们每插入一个结点,就对整棵树进行上面的维护操作,每次操作后所有结点的平衡因子都正确变化。由此一来,不论这棵树有多大,它的结点的平衡因子都可以维护。上面的逻辑一定要理解透彻。
接下来我们就讨论问题2,使得平衡因子的维护逻辑严密化
(1)除了直接父结点外,还有哪些结点会可能受到影响?哪些一定不会?
平衡因子计算的是右左子树高度的差,如果插入的结点都不在该节点的左右子树中,谈何影响?所以,当插入一个结点的时候,它的父亲以及父亲的父亲,顺着父辈一直到根节点都可能会受影响,其他节点一定不受影响,既然那些其它结点都不是插入结点的父辈结点,插入节点也必然不是它们的子树结点,必然不会受影响。
(2)什么时候向上更新?
如果上面的都理解了,我们自然也能理解每插入一个新的结点,对应父辈平衡因子的更新应当是从下往上走的,有的情况父辈会受影响,有的就不会,我们从直接父节点开始讨论。
a.当直接父节点的平衡因子在调整之后为0,说明不需要向上调整。
我们知道,插入只会使得平衡因子+1或-1,说明插入前只能是-1或1,意味着直接父结点的左边或右边已经有了一个节点了,新增的这个结点只是让它变平衡。这种情况下,在祖父看来,它的子树高度并没有变,所以就不需要调整了。
b.当直接父节点的平衡因子在调整之后为1或-1,说明需要向上调整。
在变化前是0或2或-2,为0说明左右什么都没有,随便加一个结点在祖父看来子树的高度就变了,所以要调整。调整的话和直接父节点的调整方式一致,只不过这个时候看的是子树,如果插入结点在祖父这里是右子树,就++,否则--
2和-2呢?事实上,对于直接父节点来说插入根本不会出现2或-2的情况
c.当在向祖先调整时,祖先的平衡因子调整后为0,也不需要向上调整
d.当在向祖先调整时,祖先的平衡因子调整后为1或-1,也需要向上调整(和上面理解方式一样)
e.当在向祖先调整时,祖先的平衡因子调整后为2或-2,需要旋转处理,且不再向上调整了
第5点我们先记住,下面我会重点讲解如何旋转处理,不再向上调整的原因。我们先把整体代码实现了
整体逻辑
向上调整逻辑
向上调整也是一个节点一个结点地调整,有可能调整完nodeP平衡因子变成2或-2,所以接下来就需要旋转。
仔细捋捋逻辑就知道,直接父节点不会出现平衡因子为-2/2的情况,只有在向上调整的过程中某个nodeP可能变成2/-2,所以其中一个if判断可以省略
这里需要反复体会,大逻辑比旋转处理方式重要多了,带入每个结点,体会这样处理的严谨性。
5.旋转处理
(1)单旋
下面以右单旋为例子分析
这张抽象图的某些部分容易让人疑惑,要不断假设,推翻才能更好理解
在讨论时我们要重点关注h = 0,只有这个时候才会出现空指针,其余所有情况都算作一种,因为真正受到影响的就只有两个结点,nodeC和nodeP
注意相对位置,nodeC和nodeP在旋转函数中不会修改指向的对象。
左单旋也是如此,自己画画图就很简单了
(2)双旋
以左右双旋为例
同样的,插入节点之前树的高度为h + 2,插入后还是h + 2。
注意处理h = 0和h = num这两种情况
右左双旋处理也类似
左单旋、右单旋、左右双旋、右左双旋涵盖了AVL树所有旋转情况。单旋解决一边倒问题(类似于直链),双旋解决折线问题(一边高,但高的那边并不是直链)
除这两种以外就没有第三种旋转方式了,逻辑的严密性需要体会
6.单旋函数
接下来就是两种单旋函数的实现了,单旋会影响nodeC、nodeRL(nodeLR)、nodeP以及nodeP->parent这四个结点,共16个指针,只要遍历式考虑所有情况就能很好地写出代码了。
特别注意当h = 0时nodeRL(nodeLR)为空,nodeP为根时nodeP->parent为空
至此,我们已经实现了整个AVL树的insert操作,重点是大逻辑以及两种旋转的理解。
7.所有代码
#include <iostream>
#include <utility>
#include <string>
#include <assert.h>
using namespace std;
namespace my_AVLTree
{
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode(const pair<K, V>& p)
:_val(p)
, _bf(0)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
int _bf;//右 - 左
pair<K, V> _val;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
};
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree() = default;
AVLTree(const AVLTree<K, V>& t)
{
CreateTree(*this, t._root);
}
void CreateTree(AVLTree<K, V>& newt, Node* t)
{
if (t == nullptr)
return;
newt.insert(t->_val);
CreateTree(newt, t->_left);
CreateTree(newt, t->_right);
}
AVLTree& operator=(AVLTree t)
{
std::swap(_root, t._root);
return *this;
}
~AVLTree()
{
Destroy(_root);
_root = nullptr;
}
void Destroy(Node* cur)
{
if (cur == nullptr)
return;
Destroy(cur->_left);
Destroy(cur->_right);
delete cur;
}
//不能修改nodeP指向的对象
void RotateL(Node* const nodeP)
{
//受影响的
Node* const nodeC = nodeP->_right;
Node* const nodeRL = nodeC->_left;
Node* nodePP = nodeP->_parent;
nodeC->_left = nodeP, nodeC->_parent = nodeP->_parent;
nodeP->_parent = nodeC;
if (nodeRL == nullptr)
nodeP->_right = nullptr;
else
nodeP->_right = nodeRL, nodeRL->_parent = nodeP;
if (nodeP == _root)
_root = nodeC;
else
{
if (nodePP->_left == nodeP)
nodePP->_left = nodeC;
else
nodePP->_right = nodeC;
}
}
void RotateR(Node* const nodeP)
{
Node* const nodeC = nodeP->_left;
Node* const nodeLR = nodeC->_right;
Node* nodePP = nodeP->_parent;
nodeC->_right = nodeP, nodeC->_parent = nodeP->_parent;
nodeP->_parent = nodeC;
if (nodeLR == nullptr)
nodeP->_left = nullptr;
else
nodeP->_left = nodeLR, nodeLR->_parent = nodeP;
if (nodeP == _root)
_root = nodeC;
else
{
if (nodePP->_left == nodeP)
nodePP->_left = nodeC;
else
nodePP->_right = nodeC;
}
}
bool insert(const pair<K, V>& val)
{
//空树处理
if (_root == nullptr)
{
_root = new Node(val);
return true;
}
//找到该插入的结点的位置
Node* nodeP = _root, * nodeC = _root;//nodeP也可以是nullptr,至少被nodeC赋值一次
while (nodeC)
{
//如果key相等就不插入
if (nodeC->_val.first == val.first)
return false;
else if (nodeC->_val.first < val.first)
{
nodeP = nodeC;
nodeC = nodeC->_right;
}
else
{
nodeP = nodeC;
nodeC = nodeC->_left;
}
}
//在右边插入新节点
if (nodeP->_val.first < val.first)
{
nodeP->_right = new Node(val);
nodeP->_right->_parent = nodeP;
nodeP->_bf++;//平衡因子++
}
//在左边插入新的结点
else
{
nodeP->_left = new Node(val);
nodeP->_left->_parent = nodeP;
nodeP->_bf--;//平衡因子--
}
//向上更新平衡因子
while (nodeP)
{
//平衡因子为0就不向上更新了
if (nodeP->_bf == 0)
break;
else
{
//向上调整
nodeC = nodeP, nodeP = nodeP->_parent;
if (nodeP)
{
if (nodeP->_right == nodeC)
nodeP->_bf++;
else
nodeP->_bf--;
}
//旋转处理
if (nodeP && (nodeP->_bf == 2 || nodeP->_bf == -2))
{
if (nodeP->_bf == 2)
{
//左单旋
if (nodeP->_right->_bf == 1)
{
RotateL(nodeP);
//更新平衡因子
nodeP->_bf = nodeP->_parent->_bf = 0;
}
//先右后左,右左双旋
else
{
int subRL = nodeP->_right->_left->_bf;
RotateR(nodeP->_right);
RotateL(nodeP);
//更新平衡因子
nodeP->_parent->_bf = 0;
if (subRL == 0)
{
nodeP->_bf = 0;
nodeP->_parent->_right->_bf = 0;
}
else if (subRL == -1)
{
nodeP->_bf = 0;
nodeP->_parent->_right->_bf = 1;
}
else
{
nodeP->_bf = -1;
nodeP->_parent->_right->_bf = 0;
}
}
}
else
{
//右单旋
if (nodeP->_left->_bf == -1)
{
RotateR(nodeP);
//更新平衡因子
nodeP->_bf = nodeP->_parent->_bf = 0;
}
//先左后右,左右双旋
else
{
//记录nodeLR的平衡因子,判断之后平衡因子的变化
int subLR = nodeP->_left->_right->_bf;
RotateL(nodeP->_left);
RotateR(nodeP);
//更新平衡因子
nodeP->_parent->_bf = 0;
if (subLR == 0)//当h = 0时新插入的结点就是nodeLR,这时要特殊处理
{
nodeP->_bf = 0;
nodeP->_parent->_left->_bf = 0;
}
else if (subLR == -1)
{
nodeP->_bf = 1;
nodeP->_parent->_left->_bf = 0;
}
else
{
nodeP->_bf = 0;
nodeP->_parent->_left->_bf = -1;
}
}
}
break;
}
}
}
return true;
}
size_t TreeHeight(Node* cur)
{
if (cur == nullptr)
return 0;
size_t left = TreeHeight(cur->_left);
size_t right = TreeHeight(cur->_right);
return left > right ? left + 1 : right + 1;
}
bool IsAVLTree(const AVLTree<K, V>& t)
{
bool ret = _IsAVLTree(t._root);
if (ret)
cout << "IsAVLTree" << endl;
else
assert(0);
return false;
}
void InOrder()
{
_InOrder(_root);
}
private:
bool _IsAVLTree(Node* cur)
{
if (cur == nullptr)
return true;
size_t leftH = TreeHeight(cur->_left);
size_t rightH = TreeHeight(cur->_right);
bool isAVLTree = false;
if (rightH - leftH <= 1 || leftH - rightH <= 1)
isAVLTree = true;
return isAVLTree && _IsAVLTree(cur->_left) && _IsAVLTree(cur->_right);
}
void _InOrder(Node* cur)
{
if (cur == nullptr)
return;
_InOrder(cur->_left);
cout << cur->_val.first << " " << cur->_val.second << endl;
_InOrder(cur->_right);
}
private:
Node* _root = nullptr;
};
}