前言
在学习AVL树的底层之前,先回顾一下二叉搜索树,我们知道二叉搜索树在极端场景是会形成单支树的,如下图:
在退化成单支树后,查找的效率就会降到O(n),所以为了解决退化成单支树的情况,AVL树就诞生了,在学习AVL树的模拟实现之前,先要明白AVL是如何避免单支树的产生的。
一、AVL树解决单支树问题的原理
在搜索某个节点时,本质上是要搜索这个节点的高度次,但当形成单支树或接近单支树,就会使时间复杂度提升至O(n),所以为了提高搜索的效率,就要把高度降低。这也是AVL树解决单支树问题的关键!
原理:
当向二叉搜索树中插入新结点后,必须保证每个结点的左右子树高度之差的绝对值不超过1,即可降低树的高度,从而减少平均搜索长度。
所以根据原理,我们可以设计一个简单的AVL树,只需要在树的节点中增添一个成员变量来记录每个节点的左右子树的高度差,在插入节点时,来判断节点的左右子树高度差的绝对值是否超过1,再进行后续操作。
二、AVL树的模拟实现(源代码+解析)
本次只实现中序遍历为升序,无重复值,且无删除操作的AVL树 (后续会完善功能)
1. 创造AVL树节点的类(AVLTreeNode)
创造树节点的类时,需要考虑里面应该包含什么成员
成员变量:
指向左右子树的节点指针 | 【 _left、 _right 】 |
指向父亲节点的指针(方便找父亲) | 【 _parent 】 |
记录每个节点左右子树高度差 平衡因子(balance factor) | 【 _bf 】 |
该节点包含的数据 | 【 _data 】 |
成员函数:
- 构造函数
//我们在创造AVL树之前,先要把AVL树节点创造出来,要明确的是在AVLTreeNode类外
//我们还是需要使用到节点里面的内容,所以这里定义为struct类,默认权限为public
template <class T> //模版,T可以为任何类型
struct AVLTreeNode
{AVLTreeNode(const T& data): _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0), _data(data){}AVLTreeNode<T>* _left;AVLTreeNode<T>* _right;AVLTreeNode<T>* _parent;int _bf; //用_bf来记录树节点的左右子树高度差,bf = high(right) - high(left)T _data;
};
2. 创造AVL树的类(AVLTree)
成员变量:
- 根节点指针 AVLTreeNode*
成员函数:
- 插入操作(intsert)
- 验证是否为AVL树
- 其余函数请自行添加
//创造完AVLTreeNode后,可以开始创造AVLTree
//需要注意的是,我们在日常使用AVL树时,只需要使用其成员函数,并不需要用成员变量
//所以设定为class类,成员函数权限为public,成员函数权限为private
template <class T>
class AVLTree
{
public:
成员函数//插入bool insert(const T& data);//判断是否为AVLbool _IsBalance(Node* root, int& height);
private:typedef AVLTreeNode<T> Node;Node* _root = nullptr; //等价于在初始化列表初始化,在没插入之前,默认为空树
};
3. 成员函数:插入【insert】
在插入时,其他步骤均与二叉搜索树一致,都是小的去左子树,大的去右子树,唯一多出来的步骤是:更新平衡因子
4. 更新平衡因子【重点】
因为我们AVL树的规定为 每个节点的左右子树高度差不能超过1
那bf 只能为 -1、0、1;
所以在插入之前,parent节点的平衡因子值只能为-1、0、1三种情况;
而在更新的过程中,因为bf = high(right) - high(left)
所以当在parent的左子树插入时,默认是增加左子树的高度,需要--bf;反之是增加右子树的高度,++bf;
在parent的平衡因子更新之后,bf结果只能为-2,-1,0,1,2这五种情况
我们后续就要根据这五种情况来判断,是否更新完成,更新完成直接退出,反之继续向上更新
(这里的更新是否完成是指:是否会影响上层节点的平衡因子bf的值,不影响就更新完成,影响就继续向上更新)
(1)更新后:parent->_bf == 0
当更新完之后parent的平衡因子为0时,我们认为更新之前的parent平衡因子只有1或-1两种情况;
我们通过抽象图可以看出,不会影响parent以上的节点的平衡因子
所以在parent->_bf == 0时,平衡因子的更新结束!
结论:当parent->_bf == 0时,不会影响上层节点的bf,不需要继续更新,更新结束!
(2)更新后:parent->_bf == -1
下图,我将更新之前,分为三种情况(有很多情况,不一列举)
因为无论怎么插入节点,已有节点的平衡因子,也无非是-1,0,1;所以我将parent的父亲节点的平衡因子分为0,-1,1三种情况;
以便探索在更新parent的平衡因子之后,是否还需要继续向上更新
更新之后是否还满足AVL的规则( | bf | ≤ 1 )
所以我们通过图中可以看出,无论是哪一种情况,当parent的bf == -1时,都会影响上层的节点的平衡因子,都要继续向上更新
结论:当parent->_bf == -1时,需要继续向上更新
(3)更新后:parent->_bf == 1
我们发现,当更新后parent的平衡因子bf == 1时,也会影响上层的节点的平衡因子,所以仍需要继续向上更新
结论:当parent->_bf == 1时,需要继续向上更新!
通过(1)(2)可以得到当parent的平衡因子 bf == 正负1 时,都需要继续向上更新
(4)更新后:parent->_bf == 2 或 parent->_bf == -2
当更新后的平衡因子超过1的时候,已经违反AVL树的规则,此时要进行旋转
旋转的目的就是为了让高度降低,那如何来旋转请看下图:
通过图中可以看出在违背规则时,一共有4种不同的旋转方式可以解决
第一种:左旋转
旋转之后,不会影响上层节点的平衡因子,更新结束
第二种:右旋转
旋转之后,不会影响上层节点的平衡因子,更新结束
第三种:先左旋转,后右旋转
这里需要进行两次旋转,因为最开始是右边高,要左旋,后来是左边高,再右旋
但是在平衡因子的更新就需要分情况讨论了
我们在cur的子树的左右侧分别插入,会导致最后的平衡因子为不同值,而最开始导致不同的原因是cright的bf的不同,所以在旋转结束后,更新平衡因子时以cright的bf不同值做分情况讨论
情况1,当h不存在,cright->_bf == 0
情况2,h存在,且在右侧插入,cright->_bf == 1
情况3,h存在且在左子树插入,cright->_bf == -1;
第四种:先右旋转,后左旋转
这里与上面的采用统一思想
情况1:当h == 0时,插入的节点cleft->_bf == 0
情况2:在cur的左子树的左侧插入,cleft->_bf == -1
情况3:在cur的左子树的右侧插入,cleft->_bf == 1
【insert代码实现】
bool insert(const T& data){//这里插入规则与二叉搜索树一致//空树if(_root == nullptr){_root = new Node(data);//创建失败if(_root == nullptr){perror("new error");return false;}return true;}//不为空树else{Node* cur = _root; //遍历找到插入的位置Node* parent = _root; //记录插入位置的父亲节点位置while(cur){if(cur->_data > data) //插入值小于节点值:去左子树插入{parent = cur;cur = cur->_left;}else if(cur->_data < data) //插入值大于节点值:去右子树插入{parent = cur;cur = cur->_right;}else //插入值等于节点值:插入失败{perror("insert a same data");return false;}}//找到插入位置了,创造新节点Node* newnode = new Node(data);if(parent->_data > data) //新节点的值比父亲的值小,插入父亲的左侧{parent->_left = newnode;}else //新节点的值比父亲的值大,插入父亲的右侧{parent->_right = newnode;}newnode->_parent = parent;//更新平衡因子bfUpdateBF(newnode, parent);return true;}}
//左旋转void RotationL(Node* parent, Node* cur){Node* cleft = cur->_left;parent->_right = cleft;//cleft不为空,则需要链接其父亲,为空,则不用链接if(cleft)cleft->_parent = parent;cur->_left = parent;//Node* pparent = parent->_parent;parent->_parent = cur;if(parent == _root) //当parent就是根,旋转之后,cur就为根{_root = cur;cur->_parent = nullptr;}else //如果parent不是根,则需要判断parent在其父亲的左节点还是右节点{if(pparent->_left == parent){pparent->_left = cur;}else{pparent->_right = cur;}cur->_parent = pparent;}//旋转之后,parent和cur的平衡因子都为0,不影响上层节点parent->_bf = 0;cur->_bf = 0;}//右旋转void RotationR(Node* parent, Node* cur){Node* cright = cur->_right;parent->_left = cright;//cright存在,则链接其父亲到parent,不存在,不链接if(cright)cright->_parent = parent;cur->_right = parent;Node* pparent = parent->_parent;parent->_parent = cur;if(parent == _root) //当parent就是根,旋转之后,cur就为根{_root = cur;cur->_parent = nullptr;}else //如果parent不是根,则需要判断parent在其父亲的左节点还是右节点{if(pparent->_left == parent){pparent->_left = cur;}else{pparent->_right = cur;}cur->_parent = pparent;}//旋转之后,parent和cur的平衡因子都为0,不影响上层节点parent->_bf = 0;cur->_bf = 0;}//左右旋转void RotationLR(Node* parent, Node* cur){Node* cright = cur->_right;int bf = cright->_bf;RotationL(cur, cright);RotationR(parent, cright);if(bf == 1){parent->_bf = 0;cright->_bf = 0;cur->_bf = -1;}else if(bf == -1){parent->_bf = 1;cur->_bf = 0;cright->_bf = 0;}else{parent->_bf = 0;cur->_bf = 0;cright->_bf = 0;}}//右左旋转void RotationRL(Node* parent, Node* cur){Node* cleft = cur->_left;int bf = cleft->_bf;RotationR(cur, cleft);RotationL(parent, cleft);if(bf == 1){parent->_bf = -1;cleft->_bf = 0;cur->_bf = 0;}else if(bf == -1){parent->_bf = 0;cur->_bf = 1;cleft->_bf = 0;}else{parent->_bf = 0;cur->_bf = 0;cleft->_bf = 0;}}//更新平衡因子void UpdateBF(Node*& cur, Node*& parent){while(parent){//我们在插入之前,parent的平衡因子可能为三种情况// 1、0、-1if(parent->_left == cur) //插入的节点在父亲的左边,bf--{--parent->_bf;}else //插入的节点在父亲的右边,bf++{++parent->_bf;}//更新完平衡因子之后,parent的bf值可能为:2,1,0,-1,-2if(parent->_bf == 0)//说明parent在插入之前的bf为1或-1,以parent为根的左右子树高度差1//新增节点之后,也并不会影响整颗树的bf,只会让以parent为根的左右子树高度差更新为0{break;}else if(parent->_bf == -1 || parent->_bf == 1)//说明parent在插入之前的bf == 0,是AVL树,但是插入之后,会影响整体的高度//所以要依次向上更新{cur = parent;parent = cur->_parent;}else if(parent->_bf == 2 || parent->_bf == -2)//parent的平衡因子超过1,破坏了AVL树的规则,需要进行旋转//但是有4种旋转的情况{//情况1:左旋转if(parent->_bf == 2 && cur->_bf == 1){RotationL(parent, cur);}//情况2:右左旋转else if(parent->_bf == 2 && cur->_bf == -1){RotationRL(parent, cur);}//情况3:右旋转else if(parent->_bf == -2 && cur->_bf == -1){RotationR(parent, cur);}//情况4:左右旋转else if(parent->_bf == -2 && cur->_bf == 1){RotationLR(parent, cur);}else;//旋转结束、不需要更新break;}else{//平衡因子有问题assert(false);}}}
5. 判断是否为AVL树
要想验证我们上面插入之后,是否为AVL树,可以写一个函数去判断一下,根据AVL树的性质:任意一个节点的左右子树的高度差的绝对值不超过1,所以我们可以根据这一性质来编写代码:
//判断是否为AVL树bool IsBalance(){int height = 0;return _IsBalance(_root, height);}//判断是否是AVL树bool _IsBalance(Node* root, int& height){if (root == nullptr){height = 0;return true;}int leftHeight = 0, rightHeight = 0;if (!_IsBalance(root->_left, leftHeight)|| !_IsBalance(root->_right, rightHeight)){return false;}if (abs(rightHeight - leftHeight) >= 2){cout <<root->_data<<"不平衡" << endl;return false;}if (rightHeight - leftHeight != root->_bf){cout << root->_data <<"平衡因子异常" << endl;return false;}height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;return true;}
这里建议可以把不想向外部展露的函数放在private里,避免对自身调用时的影响
三、代码汇总
#include <iostream>
#include <cassert>
using namespace std;
//
//我们在创造AVL树之前,先要把AVL树节点创造出来,要明确的是在AVLTreeNode类外
//我们还是需要使用到节点里面的内容,所以这里定义为struct类,默认权限为public
template <class T>
struct AVLTreeNode
{AVLTreeNode(const T& data): _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0), _data(data){}AVLTreeNode<T>* _left;AVLTreeNode<T>* _right;AVLTreeNode<T>* _parent;int _bf; //用_bf来记录树节点的左右子树高度差T _data;
};
///
//创造完AVLTreeNode后,可以开始创造AVLTree
//需要注意的是,我们在日常使用AVL树时,只需要使用其成员函数,并不需要用成员变量
//所以设定为class类,成员函数权限为public,成员函数权限为private
template <class T>
class AVLTree
{
public://成员函数//插入函数bool insert(const T& data){//这里插入规则与二叉搜索树一致//空树if(_root == nullptr){_root = new Node(data);//创建失败if(_root == nullptr){perror("new error");return false;}return true;}//不为空树else{Node* cur = _root; //遍历找到插入的位置Node* parent = _root; //记录插入位置的父亲节点位置while(cur){if(cur->_data > data) //插入值小于节点值:去左子树插入{parent = cur;cur = cur->_left;}else if(cur->_data < data) //插入值大于节点值:去右子树插入{parent = cur;cur = cur->_right;}else //插入值等于节点值:插入失败{perror("insert a same data");return false;}}//找到插入位置了,创造新节点Node* newnode = new Node(data);if(parent->_data > data) //新节点的值比父亲的值小,插入父亲的左侧{parent->_left = newnode;}else //新节点的值比父亲的值大,插入父亲的右侧{parent->_right = newnode;}newnode->_parent = parent;//更新平衡因子bfUpdateBF(newnode, parent);return true;}}//遍历void InOrder(){_InOrder(_root);}//判断是否为AVL树bool IsBalance(){int height = 0;return _IsBalance(_root, height);}
private:typedef AVLTreeNode<T> Node;Node* _root = nullptr;//以下成员函数不希望被外部看到,所以设为private//判断是否是AVL树bool _IsBalance(Node* root, int& height){if (root == nullptr){height = 0;return true;}int leftHeight = 0, rightHeight = 0;if (!_IsBalance(root->_left, leftHeight)|| !_IsBalance(root->_right, rightHeight)){return false;}if (abs(rightHeight - leftHeight) >= 2){cout <<root->_data<<"不平衡" << endl;return false;}if (rightHeight - leftHeight != root->_bf){cout << root->_data <<"平衡因子异常" << endl;return false;}height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;return true;}//遍历void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_data << " ";_InOrder(root->_right);}//左旋转void RotationL(Node* parent, Node* cur){Node* cleft = cur->_left;parent->_right = cleft;//cleft不为空,则需要链接其父亲,为空,则不用链接if(cleft)cleft->_parent = parent;cur->_left = parent;//Node* pparent = parent->_parent;parent->_parent = cur;if(parent == _root) //当parent就是根,旋转之后,cur就为根{_root = cur;cur->_parent = nullptr;}else //如果parent不是根,则需要判断parent在其父亲的左节点还是右节点{if(pparent->_left == parent){pparent->_left = cur;}else{pparent->_right = cur;}cur->_parent = pparent;}//旋转之后,parent和cur的平衡因子都为0,不影响上层节点parent->_bf = 0;cur->_bf = 0;}//右旋转void RotationR(Node* parent, Node* cur){Node* cright = cur->_right;parent->_left = cright;//cright存在,则链接其父亲到parent,不存在,不链接if(cright)cright->_parent = parent;cur->_right = parent;Node* pparent = parent->_parent;parent->_parent = cur;if(parent == _root) //当parent就是根,旋转之后,cur就为根{_root = cur;cur->_parent = nullptr;}else //如果parent不是根,则需要判断parent在其父亲的左节点还是右节点{if(pparent->_left == parent){pparent->_left = cur;}else{pparent->_right = cur;}cur->_parent = pparent;}//旋转之后,parent和cur的平衡因子都为0,不影响上层节点parent->_bf = 0;cur->_bf = 0;}//左右旋转void RotationLR(Node* parent, Node* cur){Node* cright = cur->_right;int bf = cright->_bf;RotationL(cur, cright);RotationR(parent, cright);if(bf == 1){parent->_bf = 0;cright->_bf = 0;cur->_bf = -1;}else if(bf == -1){parent->_bf = 1;cur->_bf = 0;cright->_bf = 0;}else{parent->_bf = 0;cur->_bf = 0;cright->_bf = 0;}}//右左旋转void RotationRL(Node* parent, Node* cur){Node* cleft = cur->_left;int bf = cleft->_bf;RotationR(cur, cleft);RotationL(parent, cleft);if(bf == 1){parent->_bf = -1;cleft->_bf = 0;cur->_bf = 0;}else if(bf == -1){parent->_bf = 0;cur->_bf = 1;cleft->_bf = 0;}else{parent->_bf = 0;cur->_bf = 0;cleft->_bf = 0;}}//更新平衡因子void UpdateBF(Node*& cur, Node*& parent){while(parent){//我们在插入之前,parent的平衡因子可能为三种情况// 1、0、-1if(parent->_left == cur) //插入的节点在父亲的左边,bf--{--parent->_bf;}else //插入的节点在父亲的右边,bf++{++parent->_bf;}//更新完平衡因子之后,parent的bf值可能为:2,1,0,-1,-2if(parent->_bf == 0)//说明parent在插入之前的bf为1或-1,以parent为根的左右子树高度差1//新增节点之后,也并不会影响整颗树的bf,只会让以parent为根的左右子树高度差更新为0{break;}else if(parent->_bf == -1 || parent->_bf == 1)//说明parent在插入之前的bf == 0,是AVL树,但是插入之后,会影响整体的高度//所以要依次向上更新{cur = parent;parent = cur->_parent;}else if(parent->_bf == 2 || parent->_bf == -2)//parent的平衡因子超过1,破坏了AVL树的规则,需要进行旋转//但是有4种旋转的情况{//情况1:左旋转if(parent->_bf == 2 && cur->_bf == 1){RotationL(parent, cur);}//情况2:右左旋转else if(parent->_bf == 2 && cur->_bf == -1){RotationRL(parent, cur);}//情况3:右旋转else if(parent->_bf == -2 && cur->_bf == -1){RotationR(parent, cur);}//情况4:左右旋转else if(parent->_bf == -2 && cur->_bf == 1){RotationLR(parent, cur);}else;//旋转结束、不需要更新break;}else{//平衡因子有问题assert(false);}}}
};
四、总结与完善
- 希望大家可以自行写出遍历操作、求树的高度、求树的节点个数等成员函数,需要注意的是,利用递归来完成的,都需要写个子函数,然后封装一下子函数.
- 大家感兴趣,可以自行查阅删除操作
- 后续会实现红黑树,红黑树那里会模拟实现一下map和set