之前几篇对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个共同点是:
其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
不知道大家还记不记得, 之前说map和set都是用红黑树实现的, 我们今天先不看红黑树, 先来看看高度平衡的AVL树
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
因此, 平衡二叉树(AVL树)诞生了
一棵AVL树或者是空树, 或者是具有以下性质的二叉搜索树:
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O( l o g 2 n {log_2{n}} log2n ) ,搜索时间复杂度O( l o g 2 n {log_2{n}} log2n )。
AVL树在二叉搜索树的基础上新增了平衡因子, 平衡因子的绝对值要<=1
平衡因子: 右子树的高度 - 左子树高度
AVL树采用 数据 + 三叉链 + 平衡因子的结构
//平衡二叉树结点
template<class T>
struct AVLTreeNode {
//数据
T _data;
//左右孩子指针
AVLTreeNode<T>* _left;
AVLTreeNode<T>* _right;
//父节点指针
AVLTreeNode<T>* _parent;
//平衡因子
int _bf;
AVLTreeNode(const T& val = T())
: _data(val)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
AVL树的重中之重也就是它的插入操作了, 接下来我们一起来看
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。
那么AVL树的插入过程可以分为两步:
与之前二叉搜索树的插入一样, 也是搜索+插入的过程, 只不过要多连接一个parent指针
bool insert(const T& val) {
//空树, 创建根节点
if (_root == nullptr) {
_root = new Node(val);
return true;
}
//搜索: 不包含重复数据, 左子树小于根, 右子树大于根
Node* cur = _root;
Node* parent = nullptr;
while (cur) {
parent = cur;
if (val == cur->_data)
return false;
else if (val < cur->_data)
cur = cur->_left;
else
cur = cur->_right;
}
//循环走完之后, cur走到nullptr, 而parent就是要插入位置的父节点
//创建新节点, 判断在父节点的哪边, 然后插入
cur = new Node(val);
if (val < parent->_data)
parent->_left = cur;
else
parent->_right = cur;
//最后连接parent指针
cur->_parent = parent;
}
我们先把这部分写出代码, 如下 :
//更新平衡因子
//更新范围: 从parent一直到root, 这条路径上的祖先结点
while (parent) {
if (parent->_left == cur) {
//左子树插入, 左边高度+1, 平衡因子-1
parent->_bf--;
}
else {
//右子树插入, 右边高度+1, 平衡因子+1
parent->_bf++;
}
//判断parent的平衡因子
if (parent->_bf == 0) {
//parent结点某一个子树补齐
//parent的高度并没有发生变化, 所以对上层没有影响, 结束循环
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) {
//说明插入之前parent的平衡因子是0
//插入之后+1/-1, 高度发生了变化
//要继续向上更新
cur = parent;
parent = parent->_parent;
}
else {
//平衡因子为2/-2, 要调整avl树, 重新达到平衡
}
下面给出代码:
//右旋
void rotateR(Node* parent) {
//左子树的根结点
Node* subL = parent->_left;
//左子树的右子树的根节点
Node* subLR = subL->_right;
//parent的父结点
Node* grandp = parent->_parent;
//修改6个链接
//一对一对的写, 比较容易写出来
subL->_right = parent;
parent->_parent = subL;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
//如果parent为根节点, 需要更新根节点
if (parent == _root) {
_root = subL;
subL->_parent = nullptr;
}
else {
//链接grandp
if (grandp->_left == parent)
grandp->_left = subL;
else
grandp->_right = subL;
subL->_parent = grandp;
}
//更新平衡因子
parent->_bf = subL->_bf = 0;
}
可以对着代码看图, 更容易理解
左旋代码
//左旋
void rotateL(Node* parent) {
//右子树的根节点
Node* subR = parent->_right;
//右子树的左子树的根节点
Node* subRL = subR->_left;
//parent的父节点
Node* grandp = parent->_parent;
//修改6个指针链接
subR->_left = parent;
parent->_parent = subR;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
//判断parent是否为根节点
if (parent == _root) {
_root = subR;
subR->_parent = nullptr;
}
else {
//链接grandp
if (grandp->_left == parent)
grandp->_left = subR;
else
grandp->_right = subR;
subR->_parent = grandp;
}
//更新平衡因子
parent->_bf = subR->_bf = 0;
}
代码如下 :
else if (parent->_bf == -2 && cur->_bf == 1) {
//记录subLR的平衡因子
Node* subLR = cur->_right;
int bf = subLR->_bf;
//左边的右边高: 左右双旋
rotateL(cur);
rotateR(parent);
// cur subLR p
//双旋之后, subLR是新的根, subLR的左子树放在cur的右边, 右子树放在p的左边
if (bf == -1) {
//subLR的左边高
cur->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1) {
//subLR的右边高
parent->_bf = 0;
cur->_bf = -1;
}
}
代码如下 :
else if (parent->_bf == 2 && cur->_bf == -1) {
Node* subRL = cur->_left;
int bf = subRL->_bf;
//右边的左边高: 右左双旋
rotateR(cur);
rotateL(parent);
// p subRL cur
//双旋之后, subRL是新的根, subRL的左子树放在p的右边, 右子树放在cur的左边
if (bf == -1) {
//subRL的左边高
parent->_bf = 0;
cur->_bf = 1;
}
else if (bf == 1) {
//subRL的右边高
cur->_bf = 0;
parent->_bf = -1;
}
}
到这里插入接口就结束了, 下面给出完整的插入代码
bool insert(const T& val) {
//空树, 创建根节点
if (_root == nullptr) {
_root = new Node(val);
return true;
}
//搜索: 不包含重复数据, 左子树小于根, 右子树大于根
Node* cur = _root;
Node* parent = nullptr;
while (cur) {
parent = cur;
if (val == cur->_data)
return false;
else if (val < cur->_data)
cur = cur->_left;
else
cur = cur->_right;
}
cur = new Node(val);
if (val < parent->_data)
parent->_left = cur;
else
parent->_right = cur;
//连接parent指针
cur->_parent = parent;
//更新平衡因子
//更新范围: 从parent一直到root, 这条路径上的祖先结点
while (parent) {
if (parent->_left == cur) {
//左子树插入, 左边高度+1, 平衡因子-1
parent->_bf--;
}
else {
//右子树插入, 右边高度+1, 平衡因子+1
parent->_bf++;
}
//判断parent的平衡因子
if (parent->_bf == 0) {
//parent结点某一个子树补齐
//parent的高度并没有发生变化, 所以对上层没有影响, 结束循环
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) {
//说明插入之前parent的平衡因子是0
//插入之后+1/-1, 高度发生了变化
//要继续向上更新
cur = parent;
parent = parent->_parent;
}
else {
//平衡因子为2/-2, 要调整avl树, 重新达到平衡
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) {
//记录subLR的平衡因子
Node* subLR = cur->_right;
int bf = subLR->_bf;
//左边的右边高: 左右双旋
rotateL(cur);
rotateR(parent);
// cur subLR p
//双旋之后, subLR是新的根, subLR的左子树放在cur的右边, 右子树放在p的左边
if (bf == -1) {
//subLR的左边高
cur->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1) {
//subLR的右边高
parent->_bf = 0;
cur->_bf = -1;
}
}
else if (parent->_bf == 2 && cur->_bf == -1) {
Node* subRL = cur->_left;
int bf = subRL->_bf;
//右边的左边高: 右左双旋
rotateR(cur);
rotateL(parent);
// p subRL cur
//双旋之后, subRL是新的根, subRL的左子树放在p的右边, 右子树放在cur的左边
if (bf == -1) {
//subRL的左边高
parent->_bf = 0;
cur->_bf = 1;
}
else if (bf == 1) {
//subRL的右边高
cur->_bf = 0;
parent->_bf = -1;
}
}
break;
}
}
return true;
}
//右旋
void rotateR(Node* parent) {
//左子树的根结点
Node* subL = parent->_left;
//左子树的右子树的根节点
Node* subLR = subL->_right;
//parent的父结点
Node* grandp = parent->_parent;
//修改6个链接
subL->_right = parent;
parent->_parent = subL;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
//如果parent为根节点, 需要更新根节点
if (parent == _root) {
_root = subL;
subL->_parent = nullptr;
}
else {
//链接grandp
if (grandp->_left == parent)
grandp->_left = subL;
else
grandp->_right = subL;
subL->_parent = grandp;
}
//更新平衡因子
parent->_bf = subL->_bf = 0;
}
//左旋
void rotateL(Node* parent) {
//右子树的根节点
Node* subR = parent->_right;
//右子树的左子树的根节点
Node* subRL = subR->_left;
//parent的父节点
Node* grandp = parent->_parent;
//修改6个指针链接
subR->_left = parent;
parent->_parent = subR;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
//判断parent是否为根节点
if (parent == _root) {
_root = subR;
subR->_parent = nullptr;
}
else {
//链接grandp
if (grandp->_left == parent)
grandp->_left = subR;
else
grandp->_right = subR;
subR->_parent = grandp;
}
//更新平衡因子
parent->_bf = subR->_bf = 0;
}
老生常谈了, 直接上代码吧
//中序遍历
void inoder() {
_inorder(_root);
cout << endl;
}
void _inorder(Node* root) {
if (root == nullptr)
return;
_inorder(root->_left);
cout << root->_data << " ";
_inorder(root->_right);
}
遍历整棵树, 如果每个结点的平衡因子都在[-1, 1]区间, 则是AVL树, 否则不是
为了检查平衡因子更新错误的问题, 在遍历每一个结点的时候先计算并判断当前结点的平衡因子是否正确, 如果不正确, 输出结点数据信息并退出, 正确则继续遍历
//判断平衡二叉树
bool isBalance() {
return _isBalance(_root);
}
bool _isBalance(Node* root) {
if (root == nullptr)
return true;
int rightHeight = getHeight(root->_right);
int leftHeight = getHeight(root->_left);
//判断平衡因子的值是否正确
//bf = right - left
if (root->_bf != rightHeight - leftHeight) {
//平衡因子更新有问题
cout << "当前位置: " << root->_data << " bf: " << root->_bf << endl;
cout << "left: " << leftHeight << " right: "<< rightHeight << endl;
return false;
}
return abs(root->_bf) < 2
&& _isBalance(root->_left)
&& _isBalance(root->_right);
}
//获取高度
int getHeight(Node* root) {
if (root == nullptr)
return 0;
int left = getHeight(root->_left);
int right = getHeight(root->_right);
return left > right ? left + 1 : right + 1;
}