一、概念
若二叉查找树的左右不均衡(eg:左边的值很多,右边的高度很小,会导致查找效率低下),因此出现了平衡树。每一棵平衡树的左子树和右子树的高度差不超过1或者-1。
平衡树需要引入一个平衡因子的概念,每个结点都有一个平衡因子,任何结点的平衡因子=右子树的高度-左子树的高度(右 - 左),即任何结点的平衡因子等于 0 / -1 / 1 (AVL树并不是必须要有平衡因子,而是有了平衡因子的话就可以更方便我们去观察和控制树是否平衡,就像一个风向标一样)
- AVL树就接近完全二叉树了,高度为logN,增删查改的效率就为logN
二、AVL树的实现
1. 基本框架
#pragma once
template<class K, class V>
//平衡树结点的结构体
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; //balance factor
//构造函数
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
, _parent(nullptr) //AVL树中还需要有一个_parent指针,指向当前节点的父节点,后续进行更新平衡因子这一操作时我们就会用到
,_bf(0)
{}
};
//平衡树的结构体
template<class K,class V>
struct AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
private:
Node* _root = nullptr;
};
2. AVL树的插入
回顾一下搜索树的插入:从根结点开始比较,x比这个值大,往右走;小则往左走,直至没有值再可以比较。
插入
- 先进行插入操作:插入一个值按二叉搜索树的规则去进行插入操作。
#pragma once
#include<utility>
#include<stdio.h>
template<class K, class V>
//平衡树结点的结构体
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; //balance factor
//构造函数
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
, _parent(nullptr) //AVL树中还需要有一个_parent指针,指向当前节点的父节点,后续进行更新平衡因子这一操作时我们就会用到
,_bf(0)
{}
};
//平衡树的结构体
template<class K,class V>
struct AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
//到这里说明根结点不为空
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
//循环已经结束,此时parent是最后一个合适的结点,cur为空
cur = new Node(kv);
if (cur->_kv.first > parent->_kv.first)
parent->_right = cur;
else {
parent->_left = cur;
}
cur->_parent = parent;
//在插入之后,开始更新平衡因子
}
private:
Node* _root = nullptr;
};
更新
- 更新平衡因子:插入结点以后,可能会影响部分祖先结点的平衡因子,所以需要更新的平衡因子是从当前这个(新增结点到根结点)路径上的平衡因子,实际中最坏的情况下是必须要更新到根,有些情况就只需要更新到中间就可以停止了。
1.> 平衡因子=右子树的高度-左子树的高度。
2.> 只有子树高度变化才会影响当前节点的平衡因子。(二叉树的高度是指从根结点到最远叶子结点的最长路径上的节点数量,换言之,二叉树的高度就是二叉树的层数。)
3.>插入一个节点,若新增结点在A的右子树上时,A的平衡因子++,新增结点在A的左子树上时,A的平衡因子--。
4.> A所在子树的高度,是否变化决定了是否会继续往上更新。
- 如何判断高度是否变化呢?
首先要知晓:在插入之前,平衡因子是符合要求的:0,1,-1.更新之后,可能会不符合要求。
(1)更新后结点A的平衡因子是1或-1,说明更新前结点A的平衡因子为0,在A的右子树插入结点那A的_bf会变为1,在左子树插入结点_bf变为-1。【 _bf为0,说明这个结点的左右子树的高度一样,此时插入一个结点,无论插入在左还是右,势必会造成高度一边高,一边低,高度也一定会改变】A这棵二叉树的高度变了,那(以A为根结点)的树的高度也会变,所以需要继续向上更新_bf。
(2)更新后如果A的平衡因子等于0,说明更新前A的平衡因子为1或-1,就可以知道更新前A的子树是一边高一边低,新增的结点被插入在低的那边,插入后A所在的子树的高度不会变,不会影响A的父结点的左/右子树的高度,所以不会影响A的父结点的平衡因子,因此,更新结束。
(3)更新后A的平衡因子是2或-2,更新前平衡因子为1或-1,说明更新前以A为根结点的树的子树一边高一边低,新插入的那个节点被插在高的那边,这样就会使A所在的子树高的那边会变得更高,进而破坏了平衡,因而A所在的子树就会不符合平衡的要求,需要进行旋转处理,旋转的目标有2个:1.把parent子树旋转平衡。2.降低parent子树的高度。所以旋转后也不需要进行往上更新,插入结束。
先跟着思路写代码(新插入的结点叫cur,cur的父结点叫parent)
- 若cur是parent的右孩子,parent的_bf加加;若cur是parent的左孩子,parent的_bf减减
- 若加加减减之后,parent的_bf是0,则不继续向上更新_bf;parent的_bf是1或-1,则继续向上更新_bf;parent的_bf是2或-2,则进行旋转即可
需要思考一下,如果一直更新_bf,肯定是到根结点就停了,怎么判断到根了呢?此时的cur是根结点,那parent就为空了(根结点没有父亲结点),所以while(parnet)
//在插入之后,开始更新平衡因子
while (parent)
{
if (parent->_left == cur)
parent->_bf--;
else if (parent->_right == cur)
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 //防止真的不正常,大于2或小于-2
{
assert(false);
}
}
判断
- 判断平衡因子是否符合要求:更新平衡因子后,平衡因子符合0/-1/1,就说明插入结束。
旋转
- 平衡因子不符合要求则进行旋转:更新平衡因子过程中出现不平衡,对不平衡子树进行旋转操作,旋转这一步操作本质上是降低了子树的高度,不会再影响上一层,所以插入结束。
旋转的原则:
- 旋转结束之后,这棵树仍然是搜索树,可以按序搜索
- 不平衡变为平衡树,同时降低高度
左边高:右旋
右边高:左旋
提前总结:

在接下来的例子中,会发现分别在a,b,c三个部分进行新增结点。
- 在a的左子树新增结点,构成的是一条直线(蓝色,黄色,以及a的左,构成了直线),直线的话使用单旋即可。在c的右子树上新增结点,同样是直线
如果是直线的话,那就说明长的部分不仅是当前parent的左,还是根结点的左(不仅是当前parent的右,还是根结点的右)。如果是折线,那就是parent的左,根的右(或者parent的右,根的左)
在写代码的时候,如何判断它们是直线还是折线呢?(这将决定使用单旋还是双旋)。直线,parent的左高,根的左高,那右-左肯定是负数(两个平衡因子都是负数后者右高两个都是整数),所以直线(parent和根的_bf)是同号,折线是异号
- 在a的右子树新增结点,会发现构成的是折线(蓝,黄,a的右),不能使用单旋就完成旋转。
右单旋
右单旋是对根结点进行旋转。(没到根就旋转了,不算)
第一个图是最初始的状态,且a,b,c,三棵子树的高度不定,有可能是0,1,2,3,4…等等


(有一种左边太高了,将根结点往右下方压的感觉)
旋转时左边高:平衡因子为-2
写代码时,只需要注意旋转的时候谁的孩子结点/父结点变了,注意指向。以及注意各个指标:_parent,_left,_right;
void RotateR(Node* root) //右旋
{
//先将需要修改的结点找出来
Node* rootl = root->_left;
Node* rootlr = rootl->_right; //有为空的风险
//有可能root并不是真正的根结点,而是某个子树的根结点,所以有可能存在root->_parent,也将这个结点存储下来
Node* proot = root->_parent;
//修改
root->_left = rootlr;
root->_parent = rootl;
rootl->_right = root;
//不可以直接写rootlr->_parent = root;有可能rootlr为空
if (rootlr != nullptr)
rootlr->_parent = root;
//那修改之后,根节点是谁呢?分为两种情况,一种root为根,一种不为根
if (proot == nullptr)
{ //代表root为根
_root = rootl;
rootl->_parent = nullptr;
}
else //真正的根不是root
{
rootl->_parent = proot;
if (proot->_left == root)
proot->_left = rootl;
if (proot->_right == root)
proot->_right = rootl;
}
root->_bf = 0;
rootl->_bf = 0;
}
左单旋
结合以上理解:左单旋就是将这棵子树往左下方压。(右边太高,需要向左边压)
左单旋:用于右边纯粹的高【即:当前parent和根的右/左,而不是一个右,一个左】(在下图中,新增结点的那棵树是15的右,也是10的右)

把10往左下压,15成为新的根结点。那10的右是谁呢?需要找一个比10大的。
15成为根之后,有10,b,a这三棵树(二叉树只有左右孩子),将b舍弃,同时b这棵树干好大于10小于15,刚好可以作为10的右子树。
15的左子树需要满足:比15小(10以及它的左子树满足,所以可以成为15的左子树。)
b这棵子树已经满足,每一个值都比15小,但它们是10的右子树,所以肯定大于10.(10<15的左子树<15),b可以成为10的右子树。
void RotateL(Node* root)
{
Node* rootr = root->_right;
Node* rootrl = rootr->_left;
Node* proot = root->_parent;
root->_parent = rootr;
root->_right = rootrl;
rootr->_left = root;
if (proot == nullptr)
{
_root = rootr;
rootr->_parent = nullptr;
}
else
{
rootr->_parent = proot;
if (proot->_left == root)
proot->_left = rootr;
if (proot->_right == root)
proot->_right = rootr;
}
if (rootrl != = nullptr)
rootrl->_parent = root;
root->_bf = 0;
rootr->_bf = 0;
}
左右双旋

在b新增结点,(蓝色,黄色,和b子树,构成了折线),需要使用双旋。
可以将b分开,

共有三种插入情况:(我们需要记录rootlr插入后(旋转前)的平衡因子,判断插入的位置)若插入之后_bf是1,说明在右边插入的,即f;插入之后_bf是-1,说明在e插入的
(1)在b的子树插入之后,rootlr的平衡因子是0

(2)在b新增结点:在它的左子树e插入(先对小的左,再对大的右),subLR的平衡因子为-1

(3)在b新增结点:在它的右子树f插入(先对小的左,再对大的右),subLR的平衡因子为1

由图我们可以看到,其实对于左右双旋来说,插在e还是f点对结果的影响就是subL与parent的平衡因子不同,旋转的步骤是一模一样的。
所以我们不管是插入在e还是f,对于左边双旋的步骤都为:1.左旋subL 2.右旋parent 3.记录subLR旋转前的平衡因子,判断插入的位置,根据插入的位置来更新平衡因子
代码:需要哪几个变量呢?左右双旋就是先左单旋,再右单旋,直接调用之前的函数即可,只需要传参即可,参数是当前旋转的子树的根,这便是双旋需要的变量。(parent和subL),但是我们还需要利用subLR的平衡因子判断插入位置,所以它也是变量
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
//先左再右
RotateL(subL);
RotateR(subL);
//两个单旋之后开始修改平衡因子
if (bf == 0) //例图一
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1) //例图一
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == -1) //例图一
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
右左双旋

void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//仍然是保存一个根结点
Node* proot = parent->_parent;
int bf = subRL->_bf;
//右左双旋
RotateR(subR);
RotateL(parent);
if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 1;
subRL->_bf = 0;
}
else
assert(false);
}
查找
AVL树的查找与搜索二叉树的搜索无异。(AVL树是以搜索二叉树为底,然后加入了平衡因子而已)
查找的话是给k来查找,所以函数的参数是const K& key
Node* AVLFind(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_kv.first)
cur = cur->_right;
else if (key < cur->_kv.first)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
高度&&个数&&中序遍历
void _InOrder(Node* root) //中序遍历:左根右
{
//先写递归的结束条件
if (root == nullptr)
return;
else
{
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
}
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftcount = _Height(root->_left);
int rightcount = _Height(root->_right);
return leftcount > rightcount ? leftcount + 1 : rightcount + 1;
}
int _Size(Node* root)
{
if (root == nullptr)
return 0;
return _Size(root->_left) + _Size(root->_right) + 1;
}
全部代码
#pragma once
#include<iostream>
#include<utility>
#include<stdio.h>
#include<assert.h>
#include <utility>
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;
int _bf; //balance factor
//构造函数
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
, _parent(nullptr) //AVL树中还需要有一个_parent指针,指向当前节点的父节点,后续进行更新平衡因子这一操作时我们就会用到
,_bf(0)
{}
};
//平衡树的结构体
template<class K,class V>
struct AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
//到这里说明根结点不为空
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
//循环已经结束,此时parent是最后一个合适的结点,cur为空
cur = new Node(kv);
if (cur->_kv.first > parent->_kv.first)
parent->_right = cur;
else {
parent->_left = cur;
}
cur->_parent = parent;
//在插入之后,开始更新平衡因子
while (parent)
{
if (parent->_left == cur)
parent->_bf--;
else if (parent->_right == cur)
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)
{
//旋转
if (parent->_bf == -2 && cur->_bf == -1)
RotateR(parent);
else if (parent->_bf == 2 && cur->_bf == 1)
RotateL(parent);
else if (parent->_bf == -2 && cur->_bf == 1)
RotateLR(parent);
else if(parent->_bf == 2 && cur->_bf == -1)
RotateRL(parent);
}
else //防止真的不正常,大于2或小于-2
{
assert(false);
}
}
return true;
}
int Size()
{
return _size(_root);
}
int Height()
{
return _Height(_root);
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
Node* _root = nullptr;
void RotateR(Node* root) //右旋
{
//先将需要修改的结点找出来
Node* rootl = root->_left;
Node* rootlr = rootl->_right; //有为空的风险
//有可能root并不是真正的根结点,而是某个子树的根结点,所以有可能存在root->_parent,也将这个结点存储下来
Node* proot = root->_parent;
//修改
root->_left = rootlr;
root->_parent = rootl;
rootl->_right = root;
//不可以直接写rootlr->_parent = root;
//因为a,b,c的h可能为0,所以rootlr有可能为空
if (rootlr != nullptr)
rootlr->_parent = root;
//那修改之后,根节点是谁呢?分为两种情况,一种root为根,一种不为根
if (proot == nullptr)
{ //代表root为根
_root = rootl;
rootl->_parent = nullptr;
}
else //真正的根不是root
{
rootl->_parent = proot;
if (proot->_left == root)
proot->_left = rootl;
if (proot->_right == root)
proot->_right = rootl;
}
root->_bf = 0;
rootl->_bf = 0;
}
void RotateL(Node* root)
{
Node* rootr = root->_right;
Node* rootrl = rootr->_left;
Node* proot = root->_parent;
root->_parent = rootr;
root->_right = rootrl;
rootr->_left = root;
if (proot == nullptr)
{
_root = rootr;
rootr->_parent = nullptr;
}
else
{
rootr->_parent = proot;
if (proot->_left == root)
proot->_left = rootr;
if (proot->_right == root)
proot->_right = rootr;
}
if (rootrl != nullptr)
rootrl->_parent = root;
root->_bf = 0;
rootr->_bf = 0;
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
//先左再右
RotateL(subL);
RotateR(parent);
//两个单旋之后开始修改平衡因子
if (bf == 0) //例图一
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1) //例图一
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == -1) //例图一
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//仍然是保存一个根结点
Node* proot = parent->_parent;
int bf = subRL->_bf;
//右左双旋
RotateR(subR);
RotateL(parent);
if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 1;
subRL->_bf = 0;
}
else
assert(false);
}
Node* AVLFind(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_kv.first)
cur = cur->_right;
else if (key < cur->_kv.first)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
void _InOrder(Node* root) //中序遍历:左根右
{
//先写递归的结束条件
if (root == nullptr)
return;
else
{
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
}
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftcount = _Height(root->_left);
int rightcount = _Height(root->_right);
return leftcount > rightcount ? leftcount + 1 : rightcount + 1;
}
int _Size(Node* root)
{
if (root == nullptr)
return 0;
return _Size(root->_left) + _Size(root->_right) + 1;
}
};
测试用例:

2961

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



