目录
前言:
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序时,二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度,即AVL树(一种平衡二叉搜索树)。
一,AVL树认识
AVL树是具有此性质的二叉搜索树:任何左右子树高度之差(简称平衡因子)的绝对值不超过1,即平衡因子大小为-1/0/1。这里全部将右子树高度减去左子树高度为平衡因子,如下图:
由此可看出,如果一棵AVL树有n个结点,其高度可保持在 O(log n),搜索时间复杂度在O(log n)
注意:AVL树虽也是平衡二叉搜索树,但它不是红黑树。红黑树和AVL树虽都是自平衡的二叉搜索树,但它们之间有一些关键的区别。
二,AVL树的实现
2-1,AVL树节点的定义
这里我们定义的AVL树来实现存储pair键值对型的数据,具体AVL树节点定义如下:
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left; //左子树
AVLTreeNode<K, V>* _right; //右子树
AVLTreeNode<K, V>* _parent; //双亲节点
pair<K, V> _kv; //存放数据
int _bf; //平衡因子AVLTreeNode(const pair<K, V>& kv)
: _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{ }
};
2-2,AVL树的插入框架
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:1. 按照二叉搜索树的方式插入新节点 2. 调整节点的平衡因子
这里要注意,插入新节点后,可能会出现不平衡现象,若插入后不平衡,这时就需要进行旋转处理,将不平衡转变为平衡且要满足搜素规则。
插入大致框架思路设计:若插入节点为根节点root,这里可直接结束;若插入节点不为根节点root,双亲结点的平衡因子需做出调正,这里我们将平衡因子设置为右子树减去左子树,即若插入结点在双亲结点 parent 的左边,parent->_bf--;若插入在双亲结点 parent 的右边,p->_bf++。这里要注意,插入后的结点也可能会影响爷爷结点或更高结点的平衡因子,至于是否影响还需看parent的平衡因子。不难发现,插入新节点后,若 parent->_bf == 0,说明parent所在的子树高度不变,在插入前,parent的_bf是1或-1,插入的结点在parent矮的那边,使得左右子树均匀,不影响爷爷结点或更高结点的平衡因子,这种情况直接可结束。若 parent->_bf == -1或1,说明parent所在的子树高度发生变化,会影响爷爷结点或更高结点的平衡因子,这里就要往上依次检查各个爷爷结点的平衡因子,若存在结点的平衡因子变为2或-2,这里就要进行旋转处理,(旋转的逻辑下面会具体分析,这里先明白逻辑框架)。这里旋转后会让平衡因子变为2或-2的回到插入之前,不会对上层有影响,因此旋转后可直接结束。代码设计如下:
bool Insert(const pair<K, V>& kv)
{
//插入节点
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = _root->_parent;
while (cur)
{
if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
if (parent->_kv.first > kv.first)
{
parent->_left = new Node(kv);
cur = parent->_left;
}
else
{
parent->_right = new Node(kv);
cur = parent->_right;
}
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) //平衡被破坏,旋转处理
{
//这里的代码进行旋转
}
else //表示原本的AVL树就出错
{
assert(false);
}
}
return true;
}
2-3,AVL树的旋转
旋转发生在插入结点后平衡失调的情况下。在以上代码中,一旦往上发现parent平衡失调就要开始旋转,旋转的目的就是要将不平衡变为平衡,即回到插入之前。旋转的方式跟插入结点的位置有关。插入结点共有四种方式:插入在parent左子树的左侧、插入在parent右子树的右侧、插入在parent左子树的右侧、插入在parent右子树的左侧。因此,对应的旋转方式也有四种。
1,左单旋
当插入的结点在parent右子树的右侧而发生的旋转叫做左单旋。这里右子树的右侧表示右侧子树中的左右都可以。这里对应的表现形式如下图:
有些人可能比较疑惑,为什么插入前的原图中a、b、c子树结构的高度都为h,只要满足平衡条件高度差不是可以随意吗?确实是这样,至于为什么会这样我们来往下分析情况。
首先,我们要明白,上图中结构在插入前是一颗AVL树,插入后要想满足左单选的条件,上面插入前的原图中数据为30结点的平衡因子必须为1,插入后变为2,即c树中插入结点后高度必加1且插入后数据为60结点的高度满足平衡条件,因为我们在往上排查中发现的是最近一个结点的平衡失调,而原图中30所对应的结点是parent结点。至于其它的信息暂时不明确,我们往下分析。将a子树高度设为h(h >= 0),那么b和c两颗子树对应的高度只能有三种情况。
1,b子树高度为h,c子树高度为h - 1。这种情况显然是不满足要求的,因为我们往c树中插入结点后,c树高度加1,变为h - 1 + 1 = h,parent对应的是数据为30的结点,parent->_bf = 1,满足平衡条件,根本不用旋转,所以这种情况根本不可能出现。
2,b子树高度为h - 1,c子树高度为h。这时往c结构插入后,c树高度变为h + 1,数据为60的结点对应的平衡因子为h + 1 - (h - 1) = 2,不满足平衡条件,所以这种情况也不可能出现。
3,b子树高度为h,c子树高度为h。这时插入后,60对应结点的平衡因子为1,parent->_bf == 2,满足所有条件。
综上所述,发生左单旋时,a、b、c三课子树的高度只能都是h(h >= 0)。
出现以上情况的旋转很简单,只要将parent右子树高度减1,左子树高度增1,这样一来就满足了插入之前的平衡模样。具体的左单旋旋转规则是这样的,令cur为上图中60对应的结点,将parent的右孩子指向b,cur的左孩子指向parent,这里要注意里面双亲结点的重新连接,最后更新平衡因子即可。
以上逻辑的实现有两个坑:1,h为0的情况,这里要注意b子树结构,a和c不影响。2,parent是根节点和不是根节点的情况。
//左单旋: 插入在右子树的右侧(右侧:右侧子树中的左右都可以)
void RotateL(Node* parent)
{
Node* cur = parent->_right;
Node* curL = cur->_left;
Node* pparent = parent->_parent;//旋转
parent->_right = curL;
if (curL) //子树高度h不等于0,即为空树时的连接情况
curL->_parent = parent;//更新节点之间的关联
if (parent == _root)
{
_root = cur;
_root->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = cur;
cur->_parent = pparent;
}
else
{
pparent->_right = cur;
cur->_parent = pparent;
}
}//旋转
cur->_left = parent;
parent->_parent = cur;//更新平衡因子,如上图,parent和cur的平衡因子都为0
parent->_bf = 0;
cur->_bf = 0;
}
2,右单旋
当插入的结点在parent左子树的左侧而发生的旋转叫做右单旋。这里左子树的左侧表示左侧子树中的左右都可以。
右单旋的逻辑思想以及结构跟左单旋基本相似,只不过这里是将parent左子树高度减1,右子树高度增1,这样一来就满足了插入之前的平衡模样。右单旋旋转规则是这样的,令cur为上图中30对应的结点,将parent的左孩子指向b,cur的右孩子指向parent,这里要注意里面双亲结点的重新连接,最后更新平衡因子即可。右单旋的注意事项要素与左单旋一样,这里不再说明,对应的表现形式如下图:
//右单旋: 插入在左子树的左侧(左侧:左侧子树中的左右都可以)
void RotateR(Node* parent)
{
Node* cur = parent->_left;
Node* curR = cur->_right;
Node* pparent = parent->_parent;//旋转
parent->_left = curR;
if (curR)
curR->_parent = parent;//更新节点之间的关联
if (parent == _root)
{
_root = cur;
_root->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = cur;
cur->_parent = pparent;
}
else
{
pparent->_right = cur;
cur->_parent = pparent;
}
}//旋转
cur->_right = parent;
parent->_parent = cur;//更新平衡因子
parent->_bf = 0;
cur->_bf = 0;
}
3,左右旋
当插入在parent左子树的右侧时,要进行的旋转是左右旋,即先左单旋再右单旋。这里树结构的原理跟左单旋的原理一样——a高度h,b高度h - 1,c高度h - 1,d高度h(h >= 1)。但是这里存在特殊情况,即当a、b、c、d为空树时照样满足旋转条件,这时60为新插入的节点。
这种情况的旋转将不能简单的进行上面那种单旋转,若只进行上面那种单旋,你会发现平衡依旧被破坏。这里左右旋的旋转规则是进行两次旋转,将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新。
这里的旋转问题不大,只需调用左右两次单旋即可,但问题是平衡因子的更新,由于插入情况是在左子树的右侧,所以插入的位置可能会是上图中的b子树或c子树(图中插入在b子树展示)。虽然无论插入在哪里都不会影响旋转后的目的,但是通过上图中可发现,平衡后30和90的平衡因子会因此出现不同的变化,60的平衡因子为0。平衡前90的平衡因子固定为-1,30的平衡因子固定为0,但是60的平衡因子可能为-1(新节点插入在b树结构中)也可能为1(新节点插入在c树结构中),且还有a、b、c、d都为空树时的情况。
当插入后60的平衡因子为-1时(插入在b树结构),这里的情况如上图般,旋转后可看出30的平衡因子为0,90的平衡因子为1。
当插入后60的平衡因子为1时(插入在c树结构),此时上图中b树高度应改为h - 1,c树高度应改为h,此时旋转后30的平衡因子为-1,90的平衡因子为0。
当a、b、c、d都为空树时,此时就没有a、b、c、d结构,新插入的节点对应上图中的60,此时通过上图可发现,30,60,90的平衡因子都为0。
//左右旋: 插入在左子树的右侧,先左单旋再右单旋
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curR = cur->_right;
int curRbf = curR->_bf;//先左再右两次旋转
RotateL(cur);
RotateR(parent);//更新平衡因子
if (curRbf == -1) //插入在b树,即curR的左边
{
curR->_bf = 0;
cur->_bf = 0;
parent->_bf = 1;
}
else if (curRbf == 1) //插入在c树,即curR的右边
{
curR->_bf = 0;
cur->_bf = -1;
parent->_bf = 0;
}
else if (curRbf == 0)
{
curR->_bf = 0;
cur->_bf = 0;
parent->_bf = 0;
}
else //这里表示原本结构就出错的情况
{
assert(false);
}
}
4,右左旋
当插入在parent右子树的左侧时,要进行的旋转是右左旋,即先右单旋再左单旋。这种情况跟左右旋类似,结构如下图(图中演示插入在c树结构中的情况)。
类比左右单旋,这里也分为三种情况:1,插入在b树中。2,插入在c树中。3,60为插入的新节点。通过上图分析不难发现,无论那种情况60所对应的高度旋转后都为0。
当插入在b树中,此时上图中b树高度为h,c树高度为h - 1。30所对应的高度为0,90所对应的高度为1。
当插入在c树中,此时如上图般,30所对应的高度为-1,90所对应的高度为0。
当60为新插入的节点时,此时情况基本与左右旋一样,30和90所对应的高度都为0。
//右左旋:插入在右子树的左侧,先右单旋再左单旋
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curL = cur->_left;
int curLbf = curL->_bf;//先右再左两次旋转
RotateR(cur);
RotateL(parent);//更新平衡因子
if (curLbf == -1) //插入b树中,即curL的左边
{
curL->_bf = 0;
cur->_bf = 1;
parent->_bf = 0;
}
else if (curLbf == 1) //插入c树中,即curL的右边
{
curL->_bf = 0;
cur->_bf = 0;
parent->_bf = -1;
}
else if (curLbf == 0)
{
curL->_bf = 0;
cur->_bf = 0;
parent->_bf = 0;
}
else //这里表示原本结构就出错的情况
{
assert(false);
}
}
2-4,AVL树的总设计
旋转的方式一共对应插入在parent左子树的左侧、右子树的右侧、左子树的右侧、右子树的左侧四种,这时在以上代码框架中所对应的parent和cur的平衡因子会发生不同的变化,我们可以根据此变化来实现对应的旋转。
........ //这里对应的是上面框架中的代码
else if (parent->_bf == -2 || parent->_bf == 2) //平衡被破坏,旋转处理
{
if (parent->_bf == 2 && cur->_bf == 1) //左单旋
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1) //右单旋
{
//注意: 以下代码中p只是变量parent的别名,而变量Node* parent中存储的是节点地址
//这里的p不是节点空间地址的别名,p的改变只会影响parent变量的数值,不会影响节点本身
/*Node* pparent = parent->_parent;
Node*& p = parent;
p = 0;*/ //这里parent的指向也为0,但节点本身的数据和指向不会改变
RotateR(parent); //所以这里不能使用指针引用,以下同理
}
else if (parent->_bf == -2 && cur->_bf == 1) //左右旋
{
RotateLR(parent);
}
else //右左旋
{
RotateRL(parent);
}
break; //因为旋转完毕之后平衡恢复如初,可直接退出
}.......... //这里对应的是上面框架中的代码
2-5,AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,即平衡因子的限制,因此要验证AVL树,可以分两步:
1. 验证其为二叉搜索树——如果中序遍历可得到一个从小到大的有序序列,就说明为二叉搜索树。此种方式只需调用中序遍历算法查看即可,不用专门验证。
2. 验证其为平衡树——平衡树的验证分为两步:第一步,每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)。第二步,节点的平衡因子是否计算正确。
注意:这里AVL树的验证不能直接使用平衡因子_bf,因为若是平衡因子如若出错将导致失败。这里应在原树结构上入手,可使用左右子树的高度差来验证平衡。
bool IsBalance()
{
if (!_root)
assert(false);
return _IsBalance(_root);
}bool _IsBalance(const Node* root)
{
if (!root)
return true;
int leftheight = _Height(root->_left);
int rightheight = _Height(root->_right);
if (abs(rightheight - leftheight) >= 2) //平衡失调
return false;
if (rightheight - leftheight != root->_bf) //平衡因子出错
return false;
return _IsBalance(root->_left) && _IsBalance(root->_right);
}
不难发现,此种判断是一种前序的判断,会多次重复计算下层的高度,效率太低。我们可以使用后序法进行检查。后序判断法不会多次重复计算下层高度,效率得到很大的提高。
bool _IsBalance(const Node* root, int& height)
{
if (!root)
return true;
int leftheight = 0, rightheight = 0;
if (!_IsBalance(root->_left, leftheight) || !_IsBalance(root->_right, rightheight))
{
return false;
}
if (abs(rightheight - leftheight) >= 2) //平衡失调
return false;
if (rightheight - leftheight != root->_bf) //平衡因子出错
return false;
height = leftheight > rightheight : leftheight + 1 ? rightheight + 1;
return true;
}
2-6,AVL树的测试
在AVL树的测试这一点上,如若发现出错进行一步步调试可发现比较艰难。这里可先在几个比较敏感的地方进行输出,然后自己设置条件断点(注意:有些编译器上可能没有条件断点的设置,这里可自己编写),总代码如下:
#pragma once
#include <iostream>
#include <vector>
#include <assert.h>
#include <utility>
using namespace std;
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left; //左子树
AVLTreeNode<K, V>* _right; //右子树
AVLTreeNode<K, V>* _parent; //双亲节点
pair<K, V> _kv; //存放数据
int _bf; //平衡因子
AVLTreeNode(const pair<K, V>& kv)
: _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{ }
};template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree(): _root(nullptr){ }.......//AVL树的插入算法,这里不在写入,如上
.......//AVL树的四个旋转算法,这里不在写入,如上
//AVL树高度
int Height()
{
if (!_root) return 0;
return _Height(_root);
}
int _Height(const Node* root)
{
if (!root) return 0;
int leftheight = _Height(root->_left);
int rightheight = _Height(root->_right);
return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}//平衡检查
bool IsBalance()
{
if (!_root) assert(false);int height = 0;
return _IsBalance(_root,height);
}
//此方法依赖于平衡因子,一旦平衡因子出问题将导致失败
//bool _IsBalance(const Node* root)
//{
// if (!root)
// return true;
// if (root->_bf >= 2 || root->_bf <= -2)
// return false;
// bool leftcheck = _IsBalance(root->_left);
// if (leftcheck == false)
// return false;
// bool rightcheck = _IsBalance(root->_right);
// if (rightcheck == false)
// return false;
// return true;
//}
//此种判断是一种前序的判断,会多次重复计算下层的高度,效率太低
//bool _IsBalance(const Node* root)
//{
// if (!root)
// return true;
// int leftheight = _Height(root->_left);
// int rightheight = _Height(root->_right);
// if (abs(rightheight - leftheight) >= 2) //平衡失调
// return false;
// if (rightheight - leftheight != root->_bf) //平衡因子出错
// return false;
// return _IsBalance(root->_left) && _IsBalance(root->_right);
//}//后序判断方法,不会多次重复计算下层高度,效率较高
bool _IsBalance(const Node* root, int& height)
{
if (!root) return true;
int leftheight = 0, rightheight = 0;//后序遍历计算
if (!_IsBalance(root->_left, leftheight) || !_IsBalance(root->_right, rightheight))
return false;
if (abs(rightheight - leftheight) >= 2) return false; //平衡失调
if (rightheight - leftheight != root->_bf) return false; //平衡因子出错
height = leftheight > rightheight ? leftheight + 1 : rightheight + 1;
return true;
}//AVL树的节点数量
size_t Size()
{
return _Size(_root);
}
size_t _Size(const Node* root)
{
if (!root) return 0;
return _Size(root->_left) + _Size(root->_right) + 1;
}
//AVL的查找
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first > key) cur = cur->_left;
else if (cur->_kv.first < key) cur = cur->_right;
else return cur;
}
return nullptr;
}//中序输出数据(从小到大输出)
void Inorder()
{
cout << "中序节点数据: ";
_Inorder(_root);
cout << endl;
}
void _Inorder(const Node* root)
{
if (!root) return;
_Inorder(root->_left);
cout << "(" << root->_kv.first << ", " << root->_kv.second << ")" << " ";
_Inorder(root->_right);
}//中序输出各节点平衡因子
void InorderBf()
{
cout << "中序平衡因子: ";
_InorderBf(_root);
cout << endl;
}
void _InorderBf(const Node* root)
{
if (!root) return;
_InorderBf(root->_left);
cout << root->_bf << " ";
_InorderBf(root->_right);
}//测试样例
void test1()
{
vector<int> v1 = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
vector<int> v2 = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int, int> a;
for (auto e : v2)
{
//调试方法技巧,先在某个地方输出必要数据,然后打出条件断点分析,即自己编写设置条件断点
//if (e == 14) //因为断点无法设置到空白地方,这里以e等于14为例,
//{ //这里设置的目的是为了打条件断点,画出在插入14前的树,单步跟踪,分析细节原因
// int x = 0;
//}
a.Insert(make_pair(e, 0));
}
a.Inorder(); //中序遍历数据first
a.InorderBf(); //中序遍历平衡因子cout << a.Height() << endl; //AVL高度
if (a.IsBalance()) cout << "平衡" << endl;
else cout << "不平衡" << endl;cout << a.Size() << endl;
AVLTreeNode<int, int>* p = a.Find(16);
if (p == nullptr) cout << "不存在" << endl;
else cout << "存在此节点" << endl;
}
private:
Node* _root;
};