在之前的二叉搜索树中提到 最坏的情况下每一层只有一个节点或者近似于这样情况的时候 插入查找删除的复杂度会退化为O(N) 而AVL树可以解决这样的问题 让时间复杂度稳定在O(logN)
它的名字取决于发明者G.M.Adelson-Velsky和E.M.Landis两个前苏联的科学家
在AVL树中 对于每一个节点 要求它的左右子树的高度差的绝对值不超过1 (不是0是因为 如果有偶数个节点的话不可能保证高度一样)
AVL树实现这里我们引入平衡因子的概念 每一个结点都有⼀个平衡因子 平衡因子就等于右子树的高度减去左子树的高度 那么如果这棵树满足AVL树 则每一个结点的平衡因子只有0/1/-1这三种情况 (AVL树不是一定要有平衡因子的概念 实现AVL树也可以用其他的方式 在本文用平衡因子的方式来实现)
AVL树的实现
还是先把基本的结构搭好
和二叉搜索树那里差不多 只不过节点类型多了一个parent指针和_bf(平衡因子)之前K类型的key和V类型的value变成了一个pair类型的对象 这个pair就是之前set map那里的pair类型 里面的first就是key second就是value 其实就像结构体一样 里面存着key和value两个变量


template<class K ,class V >
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent; //在平衡二叉树的基础上加了一个patent指针
int _bf; //平衡因子 右子树高度-左子树高度
AVLTreeNode(const pair<K, V>& x)
: _val(x)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
,_bf(0)
{
}
};
template<class K,class V>
class AVLTree
{
public:
typedef AVLTreeNode<K,V> Node;
private:
Node* _root=nullptr;
};
插入
对于新节点的插入 首先按照正常二叉搜索树的规则把这个新的节点插入到正确的位置上
这个新节点插入后可能有多种情况 要根据不同的情况进行不同的处理 有可能只影响了新节点的父节点的高度和平衡因子 也可能影响了从新节点到根节点这一路径上的部分节点 或者是全部节点
接下来就对具体可能的情况进行分析
首先 在插入一个新节点之前 这个树一定是符合AVL树的 那么每一个节点的平衡因子就只有0 -1 1这三种情况
分析一下插入新节点后这棵树平衡因子可能受到的影响
新插入节点的平衡因子一定是0 而对于它的父节点平衡因子的影响有两种情况
①插入之前父节点的左右节点都是空的 插入后变为1(插入在右边)或者-1(插入在左边)
②之前就有一个节点 插入后变为0

如果是② 父节点p的平衡因子虽然改变了 但是它的高度没有改变 那么从它到根节点的路径上所有节点的高度和平衡因子都不会发生改变 这种情况不需要处理
如果是①的情况 p的平衡因子由0变为了1或者-1 对于父节点p它的高度增加了1 那么这个父节点的父节点sp的平衡因子一定也发生了改变
对于这个sp在p发生改变之前它的平衡因子同样只有三种情况0 1 -1 如果p是它的右孩子节点 那么它的平衡因子需要加一 如果p是他的左节点 那么它的平衡因子需要-1
所以在p节点平衡因子由0变为1或者-1之后 sp的平衡因子改变后的情况有三种
①由0变为1或者-1
②1或者-1变为0
③由1变为2或者-1变为了-2
可以看到对于sp平衡因子①②的情况和p平衡因子的情况一样 那么所做的处理也一样
如果是②就不需要处理了
如果是①就需要再处理sp的父节点 而对于sp的父节点也一定会是这三种的情况 如果一直是①的情况 一直到了根节点 根节点是①②的情况都会直接结束
如果是③的情况 需要进行旋转的处理
先对平衡因子这里的代码实现一下 首先需要插入一个新节点 这里的方式和二叉搜索树那里一模一样 不过这里用到了pair类型 需要改变一下细节
bool insert(const pair<K,V>& kv)
{
if (_root == nullptr) //对空树的处理
_root = new Node(kv);
Node* cur = _root;
Node* curparent = cur;
while (cur != nullptr) //直到为空了 就是要插入的位置
{
if (kv.first > cur->_kv.first) //要插入的数大于根的值就往右
{
curparent = cur; //cur里面存的是cur的上一个位置 cur改变之前先把它的位置存到curparent中
cur = cur->_right;
}
else if (kv.first < cur->_kv.first) //小于根的值就往左
{
curparent = cur;
cur = cur->_left;
}
else
return false; //不支持插入重复的元素 插入失败 返回false
}
//如果正常出了循环 那么此时cur的位置就是新插入节点的位置 那么此时为新节点开空间 然后让它的父节点指向它
cur= new Node(kv);
if (curparent->_kv.first < kv.first)
curparent->_right = cur; //此时还需要判断 cur位置的节点是父节点的右孩子还是左孩子
else
curparent->_left = cur;
return true; //插入成功 返回true
}
对于平衡因子的处理只需要在上面代码 return true前面加下面的代码就可以了
cur->_parent = curparent; //处理节点的parent指针
//根据curparent平衡因子改变后的结果做处理
while (curparent) //刚开始的curparent就是新插入节点的parent
{
//对平衡因子做处理
if (cur == curparent->_left)
curparent->_bf--;
else
curparent->_bf++;
//根据父节点平衡因子的情况做不同处理
if (curparent->_bf == 0) //从1或者-1到0的情况
return true;
else if (curparent->_bf == 1 || curparent->_bf == -1) //从0到1或者到-1的情况 继续向上处理
{
cur = curparent;
curparent = curparent->_parent;
}
else if (curparent->_bf == 2 || curparent->_bf == -2) //从1到2或者从-1到-2的情况 需要旋转处理
{
//旋转处理
break;
}
else //其实正常情况的话这种情况就不会发生 但是可能出现错误 这样就会更容易发现
assert(false);
}
旋转
接下来就是考虑③的情况 即当某一个节点的平衡因子变为2或者-2要做的旋转处理
我们要保证在旋转之后搜索树的规则不能改变 即任意节点的左右子树高度差始终小于1且要处理好旋转后平衡因子的变化
旋转总共分为四种 左单旋/右单旋/左右双旋/右左双旋
右单旋
如下图 a树里面底层某一个新节点的增加对于它的父节点发生①情况 需要向上传递 然后上面的节点也一直是①的情况(如果发生②就结束了 如果是③在那里就要旋转处理了) 直到了找到第一个平衡因子变为2或者-2的节点 (也就是这里的10)该节点就是为旋转点 然后记录一下它的左节点5 (因为之后要对旋转点的左节点处理)
对于5的平衡因子必须是0->-1才是它的左树a树中变化引起的 才是这里右单旋的情况 对于它右树b树改变了5平衡因子0->1是之后左右双旋的情况
接下来用图文来解释里面的一些细节

旋转的前后如下图

接下来拿具体的h为0的情况分析

拿h为1的情况分析

右旋的代码
只看图可能觉得只需要让subl的右给了parent的左 然后parent变成subl的右 但实际的代码处理要注意很多 要处理以下
subl(parent的左)的right指针指向parent
parent的left指针指向subl的右节点
sublR(sunl的右)的parent指针指向parent(如果subR存在的话)
pparent(parent的parent指针指向的节点)指向subl节点(如果pparent存在的话 即parent不是根节点)
subl的parent指向pparent节点
此外如果pparent不存在的时候 也就是旋转前parent为根节点 在旋转之后要更新根节点root为subl
右旋后parent和subl的平衡因子都一定会为0
void RotateR(Node* parent)
{
Node* subl=parent->_left;
Node* sublR = subl->_right;
parent->_left = sublR; //先将subl的右给了parent的左
subl->_right = parent; //然后parent变为subl的右
//还需要处理parent和subl的平衡因子和parent指针的指向问题
parent->_bf = subl->_bf = 0;
if (sublR)
{
sublR->_parent = parent;
}
Node* pparent = parent->_parent; //在改变parent的parent指针之前先存一下
parent->_parent = subl; //还需要改变parent的parent指针
//处理pparent的指向问题
if (pparent) //pparent不为空就让pparent指向subl
{
if (pparent->_left == parent)
{
pparent->_left = subl;
}
else
{
pparent->_right = subl;
}
}
else //如果pparent为空 说明parent就是根节点 那么直接更新根节点为parent
{
_root = subl;
}
subl->_parent = pparent;
}
左单旋
左单旋和右单旋逻辑是一模一样的 就不赘述了

左单旋的代码
void RotateL(Node* parent)
{
Node* subl = parent->_right;
Node* sublL = subl->_left;
parent->_right = sublL;
subl->_left = parent;
if (sublL)
{
sublL->_parent = parent;
}
parent->_bf = subl->_bf = 0;
Node* pparent = parent->_parent;
parent->_parent = subl;
if (pparent)
{
if (pparent->_left == parent)
{
pparent->_left = subl;
}
else
pparent->_right = subl;
}
else
{
_root = subl;
}
subl->_parent = pparent;
}
接下来就是双旋了
左右双旋

而我们要对5进行左单旋 就需要先把b树拆一下(5的左单旋要和里面的节点有细节关系)情况见下图文

对于h>=1的时候和上面的情况一样 需要看新插入的节点是在subR的左还是右来决定最后的parent和subl的平衡因子
但是对于下面这种h为0的情况是特殊的情况 parent和subl平衡因子都会变为0

左右双旋代码
在对subl左旋 和对parent右旋之后 subl sublR和parent的平衡因子都会变为0
所以要在旋转之前先用con存sublR的平衡因子 在旋转后根据con来处理他们的平衡因子 虽然正常情况下不会发生else的情况 但是这样处理之后如果出了什么问题我们就能及时发现(比如在后面测试时候 就发现报了这块的错误了 然后检查发现没有写con==0的情况也就是刚开始h为0的特殊情况 然后很快就找到解决了)
void RotateLR(Node* parent)
{
Node* subl = parent->_left;
Node* sublR = subl->_right;
int cou = sublR->_bf; //先存一下该节点的平衡因子 如果是1那就是右树高度增加 最终parent平衡因子为0subl为-1
//如果是-1就是左树高度增加 最终parent平衡因子为subl为0
RotateL(subl); //先对subl进行左单旋 再对parent进行右单旋
RotateR(parent);
if (cou==1) //往sublR的右插
{
parent->_bf = 0;
sublR->_bf = 0; //其实这两行不用先 在经过上面单旋双旋之后 他们平衡因子都为0了
subl->_bf = -1;
}
else if (cou == -1) //往sublR的左插
{
sublR->_bf = 0;
subl->_bf = 0;
parent->_bf = 1;
}
else if (cou == 0) //p //刚开始h为0的特殊情况
{ // subl
sublR->_bf = 0; // sublR新插入的
subl->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
右左双旋
同样的 右左双旋和和左右单旋的逻辑一模一样 也不在重复
右左双旋代码
void RotateRL(Node* parent)
{
Node* subl= parent->_right;
Node* sublL = subl->_left;
int con=sublL->_bf;
RotateR(subl);
RotateL(parent);
if (con == 1)
{
subl->_bf = 0;
sublL->_bf = 0;
parent->_bf = -1;
}
else if (con == -1)
{
subl->_bf = 1;
sublL->_bf = 0;
parent->_bf = 0;
}
else if (con == 0)
{
subl->_bf = 0;
sublL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
查找
查找和二叉搜索树那里一样 因为AVL树的规则 所以查找的时间复杂度我们可以稳定在O(logN)
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树之后 我们怎么能保证我们实现的就是正确的呢 所以我们需要来检测一下
我们先实现判断高度的函数 节点个数的函数 然后这里用到了前序遍历的方式对每一个节点都判断了平衡因子及高度是否合规(只要有一个节点左右子树高度差大于1了或者是平衡因子不对就会返回0) 然后实现中序遍历
int Height()
{
return _Height(_root);
}
int size()
{
return _size(_root);
}
int _size(Node* root)
{
if (root == nullptr)
return 0;
return _size(root->_left) + _size(root->_right)+1;
}
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;
}
bool IsBalanceTree()
{
return _IsBalanceTree(_root);
}
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);
}
void Midbl() //中序遍历的形参类型需要为节点 但是我们创建的对象是BStree类型 且里面的root根节点为私有
{ //所以 我们可以提供一个返回根节点的函数 或者像之前实现归并非递归那样做一层封装
Midbl1(_root);
}
void Midbl1(Node* root) //中序遍历 先左再中再右 对搜索二叉树来说也就是从小到大的顺序打印
{
if (root == nullptr)
{
return;
}
Midbl1(root->_left);
cout << root->_kv.first << " ";
Midbl1(root->_right);
}
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.Midbl();
cout << endl;
cout << t.IsBalanceTree() << endl;
}
如下 可以看到对于两组测试用例 中序遍历的结果和判断每一个节点的高度和平衡因子的结果都是正确的


再拿随机数来测试
void TestAVLTree2()
{
const int N = 100000;
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({ 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;
}


随机值插入后 InBalanceTree函数的判断结果是1 也是正确的
可以发现插入的速度要比查找的速度慢一些 因为插入一定需要到叶子节点才能插入 而查找可能在中途就结束了 而且插入还需要new开空间 这也是有损耗的 具体值的查找快于随机值的查找也是因为具体值的查找更有可能中途就找到
AVLTree.h完整代码
# include <iostream>
# include <assert.h>
# include <vector>
using namespace std;
template<class K ,class V >
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent; //在平衡二叉树的基础上加了一个patent指针
int _bf; //平衡因子 右子树高度-左子树高度
AVLTreeNode(const pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
,_bf(0)
{
}
};
template<class K,class V>
class AVLTree
{
public:
typedef AVLTreeNode<K,V> Node;
void RotateR(Node* parent)
{
Node* subl=parent->_left;
Node* sublR = subl->_right;
parent->_left = sublR; //先将subl的右给了parent的左
subl->_right = parent; //然后parent变为subl的右
//还需要处理parent和subl的平衡因子和parent指针的指向问题
parent->_bf = subl->_bf = 0;
if (sublR)
{
sublR->_parent = parent;
}
Node* pparent = parent->_parent; //在改变parent的parent指针之前先存一下
parent->_parent = subl; //还需要改变parent的parent指针
//处理pparent的指向问题
if (pparent) //pparent不为空就让pparent指向subl
{
if (pparent->_left == parent)
{
pparent->_left = subl;
}
else
{
pparent->_right = subl;
}
}
else //如果pparent为空 说明parent就是根节点 那么直接更新根节点为parent
{
_root = subl;
}
subl->_parent = pparent;
}
void RotateL(Node* parent)
{
Node* subl = parent->_right;
Node* sublL = subl->_left;
parent->_right = sublL;
subl->_left = parent;
if (sublL)
{
sublL->_parent = parent;
}
parent->_bf = subl->_bf = 0;
Node* pparent = parent->_parent;
parent->_parent = subl;
if (pparent)
{
if (pparent->_left == parent)
{
pparent->_left = subl;
}
else
pparent->_right = subl;
}
else
{
_root = subl;
}
subl->_parent = pparent;
}
void RotateLR(Node* parent)
{
Node* subl = parent->_left;
Node* sublR = subl->_right;
int cou = sublR->_bf; //先存一下该节点的平衡因子 如果是1那就是右树高度增加 最终parent平衡因子为0subl为-1
//如果是-1就是左树高度增加 最终parent平衡因子为subl为0
RotateL(subl); //先对subl进行左单旋 再对parent进行右单旋
RotateR(parent);
if (cou==1) //往sublR的右插
{
parent->_bf = 0;
sublR->_bf = 0; //其实这两行不用先 在经过上面单旋双旋之后 他们平衡因子都为0了
subl->_bf = -1;
}
else if (cou == -1) //往sublR的左插
{
sublR->_bf = 0;
subl->_bf = 0;
parent->_bf = 1;
}
else if (cou == 0) //p //刚开始h为0的特殊情况
{ // subl
sublR->_bf = 0; // sublR新插入的
subl->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
void RotateRL(Node* parent)
{
Node* subl= parent->_right;
Node* sublL = subl->_left;
int con=sublL->_bf;
RotateR(subl);
RotateL(parent);
if (con == 1)
{
subl->_bf = 0;
sublL->_bf = 0;
parent->_bf = -1;
}
else if (con == -1)
{
subl->_bf = 1;
sublL->_bf = 0;
parent->_bf = 0;
}
else if (con == 0)
{
subl->_bf = 0;
sublL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
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;
}
bool insert(const pair<K,V>& kv)
{
if (_root == nullptr) //对空树的处理
_root = new Node(kv);
Node* cur = _root;
Node* curparent = cur;
while (cur != nullptr) //直到为空了 就是要插入的位置
{
if (kv.first > cur->_kv.first) //要插入的数大于根的值就往右
{
curparent = cur; //cur里面存的是cur的上一个位置 cur改变之前先把它的位置存到curparent中
cur = cur->_right;
}
else if (kv.first < cur->_kv.first) //小于根的值就往左
{
curparent = cur;
cur = cur->_left;
}
else
return false; //不支持插入重复的元素 插入失败 返回false
}
//如果正常出了循环 那么此时cur的位置就是新插入节点的位置 那么此时为新节点开空间 然后让它的父节点指向它
cur= new Node(kv);
if (curparent->_kv.first < kv.first)
curparent->_right = cur; //此时还需要判断 cur位置的节点是父节点的右孩子还是左孩子
else
curparent->_left = cur;
cur->_parent = curparent; //处理节点的parent指针
//根据curparent平衡因子改变后的结果做处理
while (curparent) //刚开始的curparent就是新插入节点的parent
{
//对平衡因子做处理
if (cur == curparent->_left)
curparent->_bf--;
else
curparent->_bf++;
//根据父节点平衡因子的情况做不同处理
if (curparent->_bf == 0) //从1或者-1到0的情况
return true;
else if (curparent->_bf == 1 || curparent->_bf == -1) //从0到1或者到-1的情况 继续向上处理
{
cur = curparent;
curparent = curparent->_parent;
}
else if (curparent->_bf == 2 || curparent->_bf == -2) //从1到2或者从-1到-2的情况 需要旋转处理
{
//旋转处理
if (curparent->_bf == -2 && cur->_bf == -1) //右单旋
RotateR(curparent);
else if (curparent->_bf == 2 && cur->_bf == 1) //左单旋
RotateL(curparent);
else if (curparent->_bf == -2 && cur->_bf == 1) //左右双旋
RotateLR(curparent);
else if (curparent->_bf == 2 && cur->_bf == -1) //右左双旋
RotateRL(curparent);
break;
}
else //其实正常情况的话这种情况就不会发生 但是可能出现错误 这样就会更容易发现
assert(false);
}
return true; //插入成功 返回true
}
int Height()
{
return _Height(_root);
}
int size()
{
return _size(_root);
}
int _size(Node* root)
{
if (root == nullptr)
return 0;
return _size(root->_left) + _size(root->_right)+1;
}
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;
}
bool IsBalanceTree()
{
return _IsBalanceTree(_root);
}
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);
}
void Midbl() //中序遍历的形参类型需要为节点 但是我们创建的对象是BStree类型 且里面的root根节点为私有
{ //所以 我们可以提供一个返回根节点的函数 或者像之前实现归并非递归那样做一层封装
Midbl1(_root);
}
void Midbl1(Node* root) //中序遍历 先左再中再右 对搜索二叉树来说也就是从小到大的顺序打印
{
if (root == nullptr)
{
return;
}
Midbl1(root->_left);
cout << root->_kv.first << " ";
Midbl1(root->_right);
}
private:
Node* _root=nullptr;
};
3673

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



