引言
学习了map/multimap/set/multiset等容器之后,我们知道这几个容器的底层实现都是按照二叉搜索树来实现的,但是二叉搜索树也有着自身的缺陷,比如我们如果往树中插入的元素有序或者接近有序的话,二叉搜索树就会退化成单支树,查找元素就相当于在顺序表中查找元素,时间复杂度会退化成O(N),因此map,set等关联式容器的底层结构都是对二叉树进行了平衡处理,即由此引出了平衡二叉树。
一、AVL树的概念
AVL树是于1962年由两位俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis发明的一种解决二叉搜索树退化成单支树的方法。该方法为:在我们向二叉搜索树中插入新结点后,如果我们能保证每个结点的左右子树高度之差的绝对值不超过1(不能为0,考虑偶数个结点),则可以降低树的高度,从而减少我们的平均搜索长度。
一颗AVL树要么是空树,要么有着以下的特点
- 它的左右子树都是AVL树
- 它的左右子树的高度之差(右子树的高度-左子树的高度)的绝对值不超过2
二、AVL树的简单实现
AVL树的结点定义
首先我们要实现的是关联式容器,与序列式容器不同,其里面存储的是<key,value>的结构的键值对,这种存储在数据检索时比序列式容器的效率更高。
其次,我们构造一个三叉链,除了定义两个孩子结点,还需要一个指向父亲的结点。
接着,我们还需要一个平衡因子,方便我们监视和调整结点的子树的高度以满足平衡。
所以我们给出下列类模板
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;//三叉链
int _bf;//balance factor平衡因子
pair<K, V> _kv;//存储的键值对
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
, _kv(kv)
{}
};
AVL树的插入
对于AVL树的插入,我们考虑如下步骤
- 先将该结点按照二叉搜索树的规则插入到树中
- 插入完成后更新平衡因子
- 对插入结点的父亲结点不同的的平衡因子做出不同的处理
- 当父亲结点的平衡因子为0时
- 当父亲结点的平衡因子为-1或1时
- 当父亲结点的平衡因子为-2或者2时,这里需要考虑旋转
插入
从头结点开始,我们使用键值对中的first来比较大小,遇到大的向左走,反之向右走。
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
parent = cur;
if (cur->_kv.first > kv.first)
cur = cur->_left;
else if (cur->_kv.first < kv.first)
cur = cur->_right;
else
return false;
}
cur = new Node(kv);
if (parent->_kv.first<kv.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
更新平衡因子
这里不用考虑该结点的插入位置,每次插入都是该树的叶子结点,无论是哪个叶子结点首先影响的是该结点的父亲结点,当结点插入到右边时则父亲结点的平衡因子加一,否则减一。
if (cur == parent->_right)
parent->_bf++;
else
parent->_bf--;
分析平衡因子的不同对树的影响
经过分析我们知道在我们插入一个结点时,其影响的只会是其祖先的平衡因子,其他结点并不会受到影响,且并不是该点的所有祖先都会被它影响,所以这里我们将父亲结点的平衡因子分为三种情况来讨论:
1、更新后父亲结点的平衡因子是0
在插入新结点更新后的父亲的平衡因子为0,说明在插入前父亲的平衡因子为1或者-1,即该结点只有一个左叶子结点或者一个右叶子结点。此时再插入一个恰好使其平衡因子为0,说明新结点插入到了没有叶子结点的一边,此时父亲结点的高度并没有变化,所以它并不会影响到之后的祖先。
if (parent->_bf == 0)
{
break;
}
2、更新后父亲结点的平衡因子是-1/1
在插入新结点更新后的父亲的平衡因子为-1或1,说明在插入前父亲的平衡因子为0,说明父亲结点原来没有叶子结点,此时新插入的结点会影响到父亲节点及之后祖先的高度,这里需要不断向上更新祖先结点的平衡因子,观察祖先结点的平衡因子是否有问题。
else if(parent->_bf==1||parent->_bf==-1)
{
cur = parent;
parent=parent->_parent;
}
3、更新后父亲结点的平衡因子为2/-2
如果出现平衡因子为2或-2的情况,说明该结点不平衡,需要做旋转处理。根据新结点插入的位置不同,我们将其分成四种情况来讨论,对于这四种情况分别使用的不同的方法来解决问题。
1)新结点插入到较高左子树的左侧
当在较高左子树的左侧插入新结点后,该点失去平衡,需要调整。此时我们需要将左子树高度减一,右子树的高度增加一, 由于我们本身就是以二叉搜索树建树,所以满足每个结点的左子树都比其小,右子树都比其大,所以我们考虑将y作为6结点的左子树,6作为4结点的右子树,如下图所示
我们对平衡因子为-2 的点进行右旋转处理如下
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* pparent = parent->_parent;
parent->_left = subLR;
if(subLR)
subLR->_parent = parent;
subL->_right = parent;
parent->_parent = subL;
//分类讨论
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
pparent->_left = subL;
else
pparent->_right = subL;
subL->_parent = pparent;
}
subL->_bf = parent->_bf = 0;
}
2)新结点插入到较高右子树的右侧
该情况与上一个情况类似,只不过是进行左单旋处理
void RotateL( Node* parent)
{
Node* pparent = parent->_parent;//记录parent的父亲结点
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if(subRL)
subRL->_parent = parent;
subR->_left = parent;
parent->_parent = subR;
if (_root == parent)//分情况讨论,如果parent是头节点
{
_root = subR;
subR->_parent = nullptr;
}
else//parent是非头节点
{
if (parent == pparent->_left)
pparent->_left = subR;
else
pparent->_right = subR;
subR->_parent = pparent;
}
parent->_bf = subR->_bf = 0;
}
3)新结点插入到较高左子树的右侧
这里我们采用先对30进行左单旋,接着再对90进行右单旋。但是我们在这里需要注意的是在b点和在c点插入新结点或者考虑h为0的情况都会导致平衡树的高度变化,三种情况下旋转之后的结构是差不多的,但是其结点的平衡因子会因为情况的不同而不同。
所以我们考虑通过控制不同的平衡因子来区分不同的情况。上图是第一种情况,第二和第三种情况如下
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
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 if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
}
4)新结点插入到较高右子树的左侧
与上一小节讨论类似,不过这里我们采用的是先右单旋再左单旋的方法,同样是分三种情况来讨论,这里过程我们省略直接上代码
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(subR);
RotateL(parent);
if (bf == -1)
{
parent->_bf=0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
parent->_bf = -1;
subRL->_bf = 0;
}
else if (bf==0)
{
subR->_bf = 0;
parent->_bf = 0;
subRL->_bf = 0;
}
}
小结
到此,我们的针对平衡树的所有情况的旋转已经完成了,我们可以看到当插入的结点和其 父亲结点及父亲结点的父亲结点在一条直线上时,我们只需要单旋,如果是条折线的话我们则需要双旋。
中序遍历打印
由于其是一颗二叉搜索树,当按照中序遍历打印时,刚好是按照从小到大的顺序排出来的。
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first<< ":" << root->_kv.second << " ";
_Inorder(root->_right);
}
void Inorder()
{
_Inorder(_root);
cout << endl;
}
由于类中的成员变量_root是私有的,不能在类外面直接传参,所以我们使用一个函数来调用另一个函数的子函数来解决这个问题。
判断是否为平衡二叉树
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 _IsBalance(Node* root)
{
if (root == nullptr)
return true;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return abs(leftHeight - rightHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
bool IsBalance()
{
return _IsBalance(_root);
}
完整代码展示
如下
#pragma once
#include<iostream>
using namespace std;
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;//三叉链
int _bf;//balance factor平衡因子
pair<K, V> _kv;//存储的键值对
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
, _kv(kv)
{}
};
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree()
:_root(nullptr)
{}
/*~AVLTree()
{
Destory(_root);
}*/
bool Insert(const pair<K,V>& kv)
{
//先按搜索树的规则插入
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
parent = cur;
if (cur->_kv.first > kv.first)
cur = cur->_left;
else if (cur->_kv.first <= kv.first)//multi版本的
cur = cur->_right;
}
//找到了
cur = new Node(kv);
if (parent->_kv.first<kv.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
//这里插入完成,准备更新平衡因子,插入一个平衡因子可能会影响到根
while (parent)
{
if (cur == parent->_right)//节点插入到左边则平衡因子加一,否则减一
parent->_bf++;
else
parent->_bf--;
//判断平衡因子经过插入结点 后是否受影响
if (parent->_bf == 0)//插入节点影响的只是他的祖先,且不一定所有祖先都会被影响,
//如果插入之后该插入节点的父亲结点的平衡因子为0,
// 则说明插入节点后父亲节点的高度没有变化,则它就不会对之后的祖先产生影响
{
break;
}
else if(parent->_bf==1||parent->_bf==-1)//说明之前parent节点的因子为0.再插入一个cur后才会变成1或-1,这样继续往上更新
{
cur = parent;
parent=parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)//说明该结点不平衡,需要做旋转处理
{
//旋转处理
if (parent->_bf == 2)
{
if (cur->_bf == 1)
{
//左旋
RotateL(parent);
}
else if (cur->_bf == -1)
{
//右左双旋
RotateRL(parent);
}
}
if (parent->_bf == -2)
{
if (cur->_bf == -1)
{
RotateR(parent);
}
else if(cur->_bf==1)
{
//左右双旋
RotateLR(parent);
}
}
break;
}
}
return true;
}
//旋转处理的左单旋
void RotateL( Node* parent)
{
Node* pparent = parent->_parent;//记录parent的父亲结点
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if(subRL)
subRL->_parent = parent;
subR->_left = parent;
parent->_parent = subR;
if (_root == parent)//分情况讨论,如果parent是头节点
{
_root = subR;
subR->_parent = nullptr;
}
else//parent是非头节点
{
if (parent == pparent->_left)
pparent->_left = subR;
else
pparent->_right = subR;
subR->_parent = pparent;
}
parent->_bf = subR->_bf = 0;
}
//右旋转
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* pparent = parent->_parent;
parent->_left = subLR;
if(subLR)
subLR->_parent = parent;
subL->_right = parent;
parent->_parent = subL;
//分类讨论
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
pparent->_left = subL;
else
pparent->_right = subL;
subL->_parent = pparent;
}
subL->_bf = parent->_bf = 0;
}
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(subR);
RotateL(parent);
if (bf == -1)
{
parent->_bf=0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
parent->_bf = -1;
subRL->_bf = 0;
}
else if (bf==0)
{
subR->_bf = 0;
parent->_bf = 0;
subRL->_bf = 0;
}
}
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first<< ":" << root->_kv.second << " ";
_Inorder(root->_right);
}
void Inorder()
{
_Inorder(_root);
cout << endl;
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
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 if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
}
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 _IsBalance(Node* root)
{
if (root == nullptr)
return true;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return abs(leftHeight - rightHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
bool IsBalance()
{
return _IsBalance(_root);
}
private:
Node* _root;
};
void Test_AVLTree()
{
int a[] = { 16,3,7,11,9,26,18,14,15 };
AVLTree<int, int> t;
for (auto e : a)
{
t.Insert(make_pair(e,e));
}
t.Inorder();
cout << t.IsBalance() << endl;
}