一.AVL树的概念
AVL树是一颗特殊的二叉搜索树。二叉搜索树在有些极端情况下可能会出现单支的情况,这会影响其插入查找的效率。而AVL树是一个高度平衡的二叉搜索树,它要求任何的左右子树的高低差都小于等于1。它可以通过去控制左右子树的高度差来控制二叉树的平衡,让二叉树能一直保持近似完全二叉树的形态,加快了插入和查找的效率O(logN)。
AVL树的实现里,我们引入了一个平衡因子(balance factor)的概念 ,每个节点都有一个平衡因子,所有的平衡因子的值都是右子树高度-左子树的高度。所以再AVL树中,所有的平衡因子都在-1~1之间,如果超过了这个范围,说明这颗AVL树已经不平衡了,我们需要通过旋转来使其重新达到平衡。
二.AVL树的结构
AVL树也是key/value的场景,而且我们在树的节点中还加入了执行父亲节点的指针以及平衡因子,借助三叉链和平衡因子来控制AVL树的平衡。
template <typename K, typename V>
struct AVlTreeNode
{
pair<K, V> _kv;
AVlTreeNode<K, V>* _left;
AVlTreeNode<K, V>* _right;
AVlTreeNode<K, V>* _parent;
int _bf = 0;
AVlTreeNode(const pair<K,V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
};
template <typename K, typename V>
class AVLtree
{
typedef AVlTreeNode<K, V> Node;
private:
Node* _root = nullptr;
};
三.AVL树的插入
1.插入的步骤
1、首先按照二叉搜索树的插入逻辑,左右比较,找到要插入的位置,然后插入。
2、插入了一个节点之后,可能会影响这颗树的高度,也就是会影响这个节点的祖先节点的平衡因子,所以我们要沿着这个路径:新插入的节点->根节点,开始更新平衡因子,有可能更新到某个祖先就停止了,最坏的情况会更新到根节点。
3、更新过程中没有遇到问题则更新结束,插入动作完成。
4、更新平衡因子过程中如果遇到不平衡的情况:平衡因子绝对值大于1,此时就需要进行旋转,来使这棵树的高度降低,重新达到平衡,插入动作完成。
2.平衡因子的更新
2.1平衡因子的更新原则
- 平衡因子 = 右子树高度 - 左子树高度
- 新插入的节点的平衡因为 == 0,因为插入的都是叶子节点
- 新增节点,会引起树的高度变化,当在parent的左子树插入时,parent的平衡因子--,右子树插入平衡因子++
- parent所在的子树高度是否变化决定了是否继续向上更新平衡因子
分析:就如上面的插入实例来说,在14左边插入13节点,所以14的平衡因子--,变成-1,而对于14这颗子树来说,它的高度本来是1,现在变成了2,高度发生了变化,所以要继续向上更新平衡因子。
2.2更新平衡因子的结束条件
根据AVL树的要求,平衡因子其实只有三种取值,0、-1/1、2/-2。
当更新后平衡因子为0,停止更新。
平衡因子变成0,说明该节点原本的平衡因为为-1/1,即该节点原本只有一个孩子,插入了一个节点后,使左右平衡了,此时高度没有发生变化,所以更新停止。
当更新后平衡因子为-1/1,继续向祖先进行更新。
平衡因子变成-1/1,说明之前的平衡因子为0(不可能为2/-2,如果是2/-2的话,说明这棵树已经不平衡了,需要进行旋转),即这个节点的左右子树高度相同,但是插入一个之后,使其高度发生了变化,所以要继续向上更新。
当更新后平衡因子为2/-2,停止更新,进行旋转操作
平衡因子变为2/-2,此时已经不满足AVL树的要求——左右子树的高度差小于等于1.此时也要停止更新,进行旋转操作,旋转操作可以在保证AVL树的规则下,平衡该树使其高度降低,重新满足AVL树的要求。
3.节点的插入以及平衡因子的更新
当更新过程中,parent走到了nullptr,即parent已经使root本身了,root->_parent == nullptr,此时已经没有节点可以更新了,所以也要停止更新。
bbool Insert(const pair<K,V>& kv)
{
//树为空
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
//创建新节点,连接到树上,更新父亲节点的平衡因子
cur = new Node(kv);
if (parent->_kv.first > cur->_kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//更新平衡因子
while (parent)
{
if (parent->_left == cur)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
//高度不变,停止更新
if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)//高度发生改变,需要继续向上更新
{
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//旋转
break;
}
else
{
//一般情况不会进入这个else,如果进入则说明一定平衡因子_bf出错了
//这里直接断言报错,可以让我们将问题定位到_bf
assert(false);
}
}
return true;
}
四.旋转
1.旋转的原则
- 保持搜索树的规则
- 让旋转的树从不平衡变平衡,其次降低旋转树的高度
旋转分为两大类——单旋和双旋,而单旋有左单旋和右单旋,双旋有左右双旋和右左双旋。
2.右单旋
- 下面是以10为根节点的一棵抽象二叉树, 它有三棵抽象的子树a、b、c(都是AVL树),且高度都是h。但是因为5节点的存在,左子树的高度为h+1,右子树高度为h,所以10的平衡因子为-1.
- 当我们插入往a子树插入一个节点时,此时就要对平衡因子进行更新,更新到节点10时,它的平衡因子变成了-2,此时就要进行旋转。又因为对于10这个节点来说,它的左边高于右边;对于5节点来说,也是左边高于右边,所以要进行右单旋。
- 因为这是一个二叉搜索树,所以b子树上的所有结点的值都大于5小于10,所以我们将b变成10的左子树,接着将10变为5的右子树,最后让5成为新的根。此时这棵树的左右子树就重新恢复了平衡,且依旧满足二叉搜索树的规则。
- 最后一步就是重新更新这棵树里面的平衡因子,我们可以看到10的左右子树最后高度都是h,所以平衡因子为0,5的左右子树高度都为h+1,所以平衡因子也是0.
上面我们是将所有情况都抽象出来进行分析的,下面我们来分析一下一些具体情况:
当a/b/c的高度都是0时,此时高度不平衡,其满足右单旋的规则,所以我们将b->nullptr作为10的左子树,然后将10作为5的右子树,最后让5成为新的根。此时就完成了右单旋,平衡因子也更新成为0.
当a/b/c的高度为1时,我们依旧采取之前的原则,将b作为10的左子树,10作为5的右子树,然后5作为新的根,5和10的平衡因子更新都更新成为0.
- 需要注意的是,我们这里实现AVL树时使用的三叉链结构,所以当我们旋转时改变了左右子树的指向的同时也要改变_parent的指针指向,让其指向新的父亲。
- 还有就是这棵树有可能就是整个树,即10就是整个树的根,但是也有可能只是一颗子树而已。如果只是一个子树的话,我们就不能仅仅让5为了新的根了,要让其指向parent的parent。
3.1右单旋代码
我们前面说了满足右单旋是因为对于10来说,它的左子树高于右子树,对于4来说,它的左子树高于右子树。只有同时满足这两个要求,才可以进行右单旋。
将这句话转换为平衡因子也就是10的平衡因子为-2,5的平衡因子为-1时,进行右单旋。
//旋转
if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
void RotateR(const Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* pParent = parent->_parent;//为了避免10不是整棵树的根,所以要记录下parent的parent
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
subL->_right = parent;
parent->_parent = subL;
if (parent == _root)
{
subL = _root;
subL->_parent = nullptr;
}
else//如果parent不是根,那么要判断subL连接到pParent的左子树还是右子树
{
if (pParent->_left == parent)
{
pParent->_left = subL;
}
else
{
pParent->_right = subL;
}
subL->_parent = pParent;
}
//更新平衡因子
parent->_bf = 0;
subL->_bf = 0;
}
4.左单旋
左单旋和右单旋是非常相似的。
我们看下面这棵抽象的树,当我们在a这棵子树下面插入一个节点,导致这棵子树的高度变化,15的平衡因子就要变为-1,根据之前的更新规则,-1要继续向上更新。当我们更新到15这个节点时,发现平衡因为变为了2,此时就要进行旋转,那么进行那个旋转呢?还能进行右单旋嘛?
进行右单旋是因为对于整棵树左边是更高的,而对于下面这种情况,右边的子树高度更高,所以我们要进行左单旋,让右边的高度降低。
左单旋的操作与右单旋类似:我们将b作为10的右子树,10作为15的左子树,这样就可以达到我们的目的
4.1左单旋代码
//左单旋条件
else if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);
}
void RotateL(const Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* pParent = parent->_parent;
parent->_right = subRL;
if (subRL)
{
subRL->_parent = parent;
}
subR->_left = parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (pParent->_left == parent)
{
pParent->_left = subR;
}
else
{
pParent->_right = subR;
}
subR->_parent = pParent;
}
subR->_bf = 0;
parent->_bf = 0;
}
3.左右双旋
我们上面两种情况都是纯粹的一边高。所以可以采用单旋的方式来使其恢复平衡。
而当我们遇到下面这种情况的话,直接左单旋/右单旋是解决不了问题的:
当我们在5的右边插入一个节点,此时对于5这棵子树是右边高,对于10这棵子树是左边高,遇到这种情况,我们如果把他当作左边高,直接使用右单旋是达不到目的的。
对于上面这种情况,我们要采取双旋的方式来解决。
我们依旧采取将所有情况都抽象出来进行分析:
a/b/c如果不为空,那么也是AVL树,满足AVL树的规则。当我们将节点插入到b子树时,这就要采取双旋来解决问题:我们将b子树的左子树作为节点5的右子树,b子树的右子树作为10的左子树,然后b子树的根作为新的根,5和10分别作为新根的左右子树。
而上面的过程其实就是先对5那棵树进行一个左单旋,然后对10这棵树在进行一次右单旋,就完成了我们上面的过程。
但是插入节点在b子树的插入位置的不同,会影响旋转后平衡因子的更新不同。所以我们将b子树拆分开来,分析插入daob子树的不同位置,引起的不同的变化。
- 场景一:当h>=1时,新增节点在e子树,高度由h-1变为h,8、5、10的平衡因子都要进行更新,10的平衡因子变为-2,此时要进行旋转,根据上面的旋转规则,5的左右子树高度相同,平衡因子为0,10的平衡因为为1,而8的平衡因子为0
- 场景二:当h>=1时,新增节点在f子树,此时也要进行旋转,但是这种情况下,5的平衡因子变为-1,10的平衡因子变为0,8的平衡因子为0.
- 场景三:当h == 0时,新增节点的左右子树都为空,旋转后5、10的左右子树都为空,然后新增节点就是新的根,左右孩子分别为5,10.
3.1左右单旋代码
我们经过上面的分析,插入一个节点之后,对不同的两个节点来说,它们的高的子树位置不同,我们就要分别采用两次旋转来解决,如果下面的子树右边高,外面的子树左边高,此时就要先左旋后右旋来解决。
调用左右双旋的前提
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == -1)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else if(bf == 1)
{
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == 0)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
4.右左双旋
右左双旋与左右双旋类似,先右旋再左旋。
右左双旋说明对于内层的树来说,左子树高,对于外层的树来说,右子树高。所以我们对内层的根先右旋,在对外层的根左旋。接下来只需要分析插入位置不同导致的平衡因子的更新不同即可。
4.1右左双旋代码
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
旋转总结:
我们可以直接根据parent与cur的平衡因子的值来判断需要用什么旋转:
首先,当更新平衡因子后,出现2/-2才需要进行旋转
如果这个parent节点与cur节点同号则单旋,同负右单旋,同正左单旋;异号双旋,parent为正,右左双旋,parent为负,左右双旋。
五.AVL树的查找
AVL树的查找与二叉搜索树相同,比较查找:大于往右边找,小于往左边找。比较时只用key作为关键码来比较。
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
{
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
六.AVL树的测试
我们可以借助这些代码来测试你的AVL树是否正确,也可以测试AVL树查找的消耗
当查找的数确定在树中,其查找效率很高。
插入的效率之所以比查找的效率低主要是因为插入的过程需要开空间。
测试代码:
//测试代码
void TestAVLTree1()
{
AVLtree<int, int> t;
// 常规的测试用例
//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
// 特殊的带有双旋场景的测试用例
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a)
{
t.Insert({ e, e });
}
t.InOrder();
cout << t.IsBalanceTree() << endl;
}
// 插入一堆随机值,测试平衡,顺便测试一下高度和性能等
void TestAVLTree2()
{
const int N = 1000000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(rand() + i);
}
size_t begin2 = clock();
AVLtree<int, int> t;
for (auto e : v)
{
t.Insert(make_pair(e, e));
}
size_t end2 = clock();
cout << "Insert:" << end2 - begin2 << endl;
cout << t.IsBalanceTree() << endl;
cout << "Height:" << t.Height() << endl;
cout << "Size:" << t.Size() << endl;
size_t begin1 = clock();
// 确定在的值
for (auto e : v)
{
t.Find(e);
}
// 随机值
/*for (size_t i = 0; i < N; i++)
{
t.Find((rand() + i));
}*/
size_t end1 = clock();
cout << "Find:" << end1 - begin1 << endl;
}
测试所需代码:
//测试所需代码:
public:
void InOrder()
{
_InOrder(_root);
cout << endl;
}
int Height()
{
return _Height(_root);
}
int Size()
{
return _Size(_root);
}
bool IsBalanceTree()
{
return _IsBalanceTree(_root);
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
int _Size(Node* root)
{
if (root == nullptr)
return 0;
return _Size(root->_left) + _Size(root->_right) + 1;
}
bool _IsBalanceTree(Node* root)
{
// 空树也是AVL树
if (nullptr == root)
return true;
// 计算pRoot结点的平衡因子:即pRoot左右子树的高度差
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
int diff = rightHeight - leftHeight;
// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
// pRoot平衡因子的绝对值超过1,则一定不是AVL树
if (abs(diff) >= 2)
{
cout << root->_kv.first << "高度差异常" << endl;
return false;
}
if (root->_bf != diff)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
// pRoot的左和右如果都是AVL树,则该树一定是AVL树
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
完~