AVL树

  • 从BST的角度看AVL
  • AVL的定义及性质
  • AVL树的结构定义
  • AVL树的旋转算法
    • 左左情况---右旋
    • 右右情况---左旋
    • 左右情况---左右旋
    • 右左情况---右左旋
  • AVL树的遍历操作
  • AVL树的查询操作
  • AVL树的插入操作
  • AVL树的删除操作
  • AVL树的总结

2019年9月13日10:47:49
在学习AVL树,之前需要先行学习 树的存储结构、 树与二叉树基础知识大全解、 二叉树的四种遍历方式(递归与非递归实现)、 BST树。AVL树比较复杂,所以必须要有一个强大的树、二叉树,尤其是BST树的基础作为支撑。本节将会介绍AVL树的定义、性质、结构定义、最重要的 旋转算法、和AVL树的插入删除等。预计将会花费较多时间,还是那句话:苟利国家生死以,岂因祸福避趋之。漫漫清华路,且行且珍惜。

从BST的角度看AVL

回顾一下 上一篇的BST树:即二叉查找树,也称为有序二叉查找树。它满足二叉查找树的一般性质,指的是一棵非空树具有如下性质:

  1. 任意节点左子树不为空,则左子树的值均小于根节点的值.
  2. 任意节点右子树不为空,则右子树的值均大于于根节点的值.
  3. 任意节点的左右子树也分别是BST.
  4. 没有键值相等的节点.
  5. 查找最好时间复杂度O(logN),最坏时间复杂度O(N)。插入删除操作算法简单,时间复杂度与查找差不多
  6. 删除时,第一种情况,要删除的是叶子节点(左右孩子为nullptr);第二种情况,要删除结点只有一个孩子;第三种情况,要删除的结点同时存在左右孩子。那么对于前两种情况,我们只需要将当前待删除结点的左或右孩子赋值给其父结点的左或右孩子即可。但是对于第三种情况,我们这里统一使用的是前驱结点来代替该删除结点的值。然后将第三种退化到第一种、第二种上 进行统一的处理。

我们在上篇的BST的插入中,若是一直持续插入连续小于根节点的值;或者删除时,连续删除节点小的,或者大的。就会造成我们的原来正常的BST树(右边)成了左边的单支树的样子:

如果我们的根节点选择是最小或者最大的数,那么BST树就完全退化成了线性结构。那么我们设计的BST树基于折半查找思想实现的这种数据结构的查找性能将大打折扣,退化成O(N)。即使BST树的确能在很大程度上提高查找的效率,然而,我们不能保证二叉查找树一直处于这种左右平衡的状态,因此当二叉查找树退化成是单支树时,其搜索查找效率将下降到为 O(N),此时的BST将退化成为一条链表,而且另一个结点域也是空值 还造成了空间浪费。

因此,造成上面问题的原因就在于BST树的平衡性,于是第一个平衡二叉搜索树,即AVL树就诞生了。虽然在BST树的基础上,出现了AVL树,红黑树,它们两个都是基于二叉查找树,但是只是在二叉查找树的基础上又对其做了些限制。(注:BST树非常重要,还请见我的博客BST树)。AVL树是对子树过高的情况进行了优化,这里面有个平衡因子的概念:当前节点的平衡因子=左子树高度-右子树高度,AVL树的每一个节点的平衡因子的绝对值都是 < 2 的,也就是说 -1,0,1都是可以的。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。(注:我说的是高度平衡,而不是完全平衡)查找、插入和删除在平均和最坏情况下都是O(log N)。但是增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。(注:通过旋转来保持平衡,然而旋转是非常耗时的,于是AVL树适合用于插入删除次数比较少,而查找多的情况)

AVL的定义及性质

AVL树是在BST树的基础上进行的,其定义为:一棵AVL树可以是空树;也可以是具有以下性质的BST树:

  1. 作为BST树,是要满足:若它的左子树不为空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不为空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为BST树。
  2. 左子树和右子树都是AVL树
  3. 左子树和右子树的高度之差的绝对值不超过 1。也即,AVL树本质上是带了平衡功能的BST树。或者说:每个节点都有一个平衡因子(balance factor),任意一个节点的平衡因子是-1,0,1。

下图就是一棵AVL树:任意一个节点的左右子树的平衡因子都不会超过1

但是由于维护这种高度平衡所付出的代价 有时候会比从中获得的快速查找的效率收益还大,故而实际的应用不多。更多的地方是用追求局部而不是非常严格整体平衡的RB树。(红黑树我们下节讨论)当然,如果应用场景中对插入删除不频繁,只是对查找要求较高,那么AVL还是较优于红黑树的。正如上面所说:结点的平衡因子是它的左子树的高度减去它的右子树的高度(当然反过来也是可以的)。带有平衡因子 1、0 或 -1 的结点就是平衡的,而平衡因子的绝对值大于 1 的结点被认为是不平衡的,并需要重新平衡这个树。(注:平衡因子可以直接存储在每个结点中,或从可能存储在结点中的子树高度计算出来)通常对于拥有n个结点的AVL树,其高度可保持在 ⌊logN⌋+1左右,因此其查找、插入和删除在平均和最坏情况下都是 O(logN)。 但是增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。因此如何保证AVL树的平衡,或者说保证结点的平衡因子的绝对值不大于 1 ?

好的,开始今天的手撕代码!!!

AVL树的结构定义

暂停一下,吃个药 马上回来
2019年9月13日11:57:04
注:学习的时候,一定要保重身体 学不成算了(当然各位还是想学好的),身体要紧。我每隔一段时间,就得去医生那里报次到。这很不好!不要老提醒自己的女朋友多喝热水,完了特么自己不喝。喝点热水,中国的热水 就像开机重启一样,是可以解决掉大多数问题的。下次当某人说 包治百病的时候,还是要劝她 多喝热水。

因为AVL树是在BST树的基础上进行的限制,因此还是沿用之前的二叉链表的方式进行结构的定义。只是与普通的BST树不同的是:需要为每个结点额外再添加一个结点高度值的域,专门负责记录该结点的实时高度

template <typename T>
class AVLTree
{public:struct AVLNode;AVLTree(){_root = nullptr;}private:struct AVLNode{AVLNode(T val=T()) :_data(val), _left(nullptr),       _right(nullptr),_height(1){}T _data;AVLNode* _left;AVLNode* _right;int _height;//存储这个结点的实时高度};//提供负责返回一个结点的高度  去计算该结点左右子树的高度差int height_of_node(AVLNode* node)const//返回这个节点的高度 {return node == nullptr ? 0 : node->_height;//不是空节点 返回高度}//返回左右子树最高的层数int maxlevel_of_child(AVLNode* left_node,AVLNode* right_node)const{int left_level = height_of_node(left_node);int right_level = height_of_node(right_node);return left_level > right_level ? left_level : right_level;}AVLNode* _root;
};

AVL树的旋转算法

保证AVL树平衡的基本思想就是:当在AVL树中插入一个节点时,首先检查是否因插入而破坏了平衡,若 破坏,则找出其中的最小不平衡二叉树,在保持二叉排序树特性的情况下,调整最小不平衡子树中节点之间的关系,以达到新的平衡。所谓最小不平衡子树 指的是:离插入节点最近且其平衡因子的绝对值大于1的节点作为根的子树。

作为一种平衡的BST树,AVL树的基本操作与普通的BST树是相似的,在我们进行结点的插入和删除时,有可能会破坏AVL树的平衡性,实时的保持这种平衡性非常重要。通常可以通过调整树结构,使之保持平衡,这种用以进行平衡化的操作被称为旋转 。如下所示:
在向AVL树插入一个元素的时候,可能会导致这个平衡状态下的AVL树,发生某节点的不平衡,如下:

分析:如上图所示,向一棵平衡状态下的AVL树,插入一个新的叶子节点。插入一个val为6的节点,其插入位置为7的左孩子。但是当给 结点7 插入左孩子时,这棵AVL树的平衡特性就被破坏,由于 节点8 失衡了,即 节点8 的左右子树高度差为 2,不满足平衡条件。此时的节点8也就被称为是最小不平衡子树。又或者将 结点1从AVL树删除,此时左边的AVL树的平衡特性也被破坏了,节点2失衡。

所以为了平衡特性,或者说实时的保持这种平衡性,需要进行旋转操作,以达到平衡。而旋转的目的就是为了降低高度
分析如下:

  1. 插入数据以后,父节点的平衡因子必然会被改变!首先判断父节点的平衡因子是否满足性质,如果满足,则要回溯向上检查插入该节点是否影响了其它节点的平衡因子值!
  2. 当父节点的平衡因子等于0时,父节点所在的子树已经平衡,不会影响其他节点的平衡因子了。
  3. 当父节点的平衡因子等于1或者-1时,需要继续向上回溯一层,检验祖父节点的平衡因子是否满足条件(把父节点给当前节点)。
  4. 当父节点的平衡因子等于2或者-2时,不满足性质的话(该节点失衡了),这时需要进行旋转 来降低高度。

而通常向一棵平衡的AVL树中插入一个新的结点,就可能造成节点的失衡归结为以下四种可能:

  1. LL:插入一个新节点到根节点的左子树(Left)的左子树(Left),导致根节点的平衡因子由1变为2。
  2. RR:插入一个新节点到根节点的右子树(Right)的右子树(Right),导致根节点的平衡因子由-1变为-2。
  3. LR:插入一个新节点到根节点的左子树(Left)的右子树(Right),导致根节点的平衡因子由1变为2。
  4. RL:插入一个新节点到根节点的右子树(Righ)的左子树(Left),导致根节点的平衡因子由-1变为-2。

如下:向AVL树中插入一个新节点时,树中有些结点的平衡状态就可能会发生变化。因此,在插入一个新节点只后,随之就需要从插入位置沿回到根节点的路径进行向上回溯,检查路径上各结点的左右子树高度差(也即检测一下:其平衡因子)。如果在某一点发现高度不平衡,则停止回溯。然后从这个发生不平衡的节点起,沿刚才回溯上来的路径取直接下两层的节点。此时也即3个节点,为最小不平衡子树的节点。

正如上图所示:这四棵树的根节点都发生了失衡情况(其平衡因子达到了2),也可以称此节点为最小不平衡子树的根节点。
于是针对上面四种失衡状态,旋转方式主要是分为:左旋和右旋、先左旋后右旋和先右旋后左旋四种。判断是采用单旋 还是 双旋的标准是:如上图所示 左边两个树 三个节点处在同一条直线上,那么采用单旋进行平衡化;若这三个节点不处于同一条直线上(如右边两个图),那么采用双旋进行平衡化。假设距离新插入节点最近的失衡节点为 t,它是最小不平衡子树的根节点。 ( t 的平衡因子的绝对值达到了2,且距离新节点最近)

节点插入 旋转处理
LL型:新节点在 t 的左孩子的左子树上 右旋
RR型:新节点在 t 的右孩子的右子树上 左旋
LR型:新节点在 t 的左孩子的右子树上 左右旋
RL型:新节点在 t 的右孩子的左子树上 右左旋

左左情况—右旋

左左情况:由于向t节点的左子树中插入一个左孩子导致该结点失衡(也即:节点的左子树的高度本来就大,插入点还在左子树的左孩子处),需要使用右旋操作来维护AVL树的平衡。
具体分析如下图:
第一种:parent既有左孩子又有右孩子:插入c之前处于平衡态,插入c之后parent的平衡因子变为-2,这时要以parent的左孩子为轴进行旋转。

第二种:parent只有一个孩子:插入a之前处于平衡状态,插入之后subL与parent的平衡因子被改变,需要以parent的左孩子为轴进行旋转

右旋的实现:将这3个成直线排列的节点中的的中间节点为轴,进行顺时针旋转。该中间节点的原父节点变成该节点的右子节点,该中间节点的右子树则变成其原父节点的左子树。实现如下:

 //LL插入,右旋处理AVLNode* rightRotation(AVLNode* node)//node 就是发生不平衡的根节点{AVLNode* new_node = node->_left;//这个是最后返回的新的根节点//该新根节点的右子树则变成其原父节点的左子树node->_left = new_node->_right;new_node->_right = node;//该新根节点的原父节点变成其右子节点//更新节点高度node->_height = maxlevel_of_child(node->_left,node->_right) + 1;new_node->_height = maxlevel_of_child(new_node->_left, new_node->_right) + 1;return new_node;//返回旋转后的根节点}

右右情况—左旋

左左情况:由于向t节点的右子树中插入一个右孩子导致该结点失衡,需要使用左旋操作来维护AVL树的平衡。
具体分析如下图:
第一种:.parent有两个孩子:没有插入节点c之前处于平衡状态,插入c之后,平衡被破坏,向上回溯检验祖父节点的平衡因子,当其平衡因子=2 时,以此节点的右孩子为轴进行左旋

第二种:.parent有一个孩子:没有插入节点a之前处于平衡状态,插入节点a之后,parent节点的平衡因子=2不满足AVL树的性质,要以parent的右孩子为轴进行左旋

左旋的实现:将这3个成直线排列的节点的中间节点为轴,进行逆时针旋转。该中间节点的原父节点变成该节点的左子节点,该中间节点的左子树则变成其原父节点的右子树。实现如下:

//RR插入,左旋处理AVLNode* leftRotation(AVLNode* node)//node 就是发生不平衡的根节点{AVLNode* new_node = node->_right;//这个是最后返回的新的根节点//该新根节点的左子树则变成其原父节点的右子树node->_right = new_node->_left;new_node->_left = node;//该新根节点的原父节点变成其左子节点//更新节点高度node->_height = maxlevel_of_child(node->_left,node->_right) + 1;new_node->_height = maxlevel_of_child(new_node->_left, new_node->_right) + 1;return new_node;//返回旋转后的根节点}

注:上面虽然介绍了AVL树的两种单向旋转方式。那么接下来,首先看一下如下图所示的这类情况。下图经过一次单旋转(左旋 或者 右旋)后,无论是X或者W作为根结点都无法符合AVL树的性质,此时就需要用双旋转算法来实现了。

由于子树Y是在插入某个结点后导致X结点的左右子树失去平衡,那么就说明子树Y肯定是非空的,因此为了易于理解,我们可以把子树Y看作一个根结点和两棵子树,如下图所示:
那么此时对上图 进行左旋 或者 右旋都是不能解决问题的。

左右情况—左右旋

由于向某节点的左子树中插入一个右孩子导致该结点失衡,我们需要使用先左后右双向旋转操作来维护AVL树的平衡。
具体分析如下图:
第一种:parent只有一个孩子:在插入节点sunLR之前,AVL树处于平衡状态,左右子树高度差的绝对值不超过1。由于插入了节点subLR导致grandfather的平衡因子变为-2,平衡树失衡,所以需要利用旋转来降低高度!首先以subL的右孩子为轴,将subLR向上提(左旋),将grandfather、parent和subL旋转至一条直线上;再以parent的左孩子为轴将之前的subLR向上提(右旋),左树的高度降1,grandfather的平衡因子加1后变为-1,恢复平衡状态。双旋完成后将parent、subL的平衡因子置为0即可,左右双旋也就完成啦!

第二种:parent有两个孩子:没有插入subRL或subRR之前的AVL树一定是处于平衡状态的,并且满足AVL树的性质。正是由于插入了节点subRL或者subRR,导致其祖先节点的平衡因子被改变,grandfather的平衡因子变为-2,平衡态比打破,需要进行旋转来降低高度!首先parent的右孩子为轴将subR节点往上提至原parent的位置(左旋),将grandfather、parent 和 subR旋至一条直线上;再以grandfather的左孩子为轴将subR往上提至grandfather的位置(右旋),此时以subR为根的左右子树的高度相同,恢复了平衡态!

parent有两个孩子时,要看插入的节点是subR的右孩子还是左孩子,双旋后对平衡因子的修改分两种情况:subR的平衡因子为1,即subR有右孩子无左孩子(有subRR但无subRL),双旋之后将grandfather的平衡因子置为0,将parent的平衡因子置为-1;subR的平衡因子为-1,即subR有左孩子无右孩子(有subRL但无subRR),双旋之后将grandfather的平衡因子置为1,将parent的平衡因子置为0;

左右旋的实现:先左后右双旋转的处理方法是以3个成折线排列的节点中的末节点为轴,进行逆时针旋转(左旋),使得末节点代替中间节点的位置,也就是让末节点成为原中间节点的父节点,而末节点的左子树变为原中间节点的右子树。这时,这3个节点将成一直线排列,原来的末节点变成了3条直线的中间节点,而原来的中间节点变成了3条直线中的末节点。这时再以新的中间节点为旋转轴做右单旋转,即可完成平衡操作。一言以蔽之:首先对原失衡节点的左子树进行左旋操作,再对原失衡节点做右旋操作即可。

//LR插入 左平衡  左-右旋转AVLNode* leftbalance(AVLNode* node)//node 就是发生不平衡的根节点{//第一步:对node->left  原失衡节点的左子树进行左旋操作node->_left = leftRotation(node->_left);//变成一条线//第二步:对原失衡节点做右旋操作return rightRotation(node);}

右左情况—右左旋

由于向某节点的右子树中插入一个左孩子导致该结点失衡,我们需要使用先右后左双向旋转操作来维护AVL树的平衡。
具体分析如下图:
第一种:parent只有一个孩子:由于节点subRL的插入破坏了AVL树的平衡,parent的平衡因子变为2,需要利用旋转来降低高度!首先,以subR的左孩子为轴,将subRL提上去(右旋),保证parent、subR 和 subRL在一条直线上;以parent的右孩子为轴,将上一步标记为subRL的节点向上升(左旋),这样达到了降低高度的目的;双旋之后,parent和subR的平衡因子都要置为0。

第二种:parent有两个孩子:没有插入subLL或者subLR之前的AVL树一定是处于平衡状态的,并且满足AVL树的性质。正是由于插入了节点subLL或者subLR,导致其祖先节点的平衡因子被改变,grandfather的平衡因子变为2,平衡态比打破,需要进行旋转来降低高度!首先parent的左孩子为轴将subL节点往上提至原parent的位置(右旋),将grandfather、parent 和 subL旋至一条直线上;再以grandfather的右孩子为轴将subL往上提至grandfather的位置(左旋),此时以subL为根的左右子树的高度相同,恢复了平衡态!

parent有两个孩子时,要看插入的节点是subL的右孩子还是左孩子,双旋后对平衡因子的修改分两种情况:subL的平衡因子为1,即subL有右孩子无左孩子(有subLR但无subLL),双旋之后将grandfather的平衡因子置为-1,将parent的平衡因子置为0;
subL的平衡因子为-1,即subL有左孩子无右孩子(有subLL但无subLR),双旋之后将grandfather的平衡因子置为0,将parent的平衡因子置为1;

右左旋的实现:先右后左双旋转的处理方法是以3个成折线排列的节点中的末节点为轴,进行顺时针旋转(右旋),使得末节点代替中间节点的位置,也就是让末节点成为原中间节点的父节点,而末节点的右子树变为原中间节点的左子树。这时,这3个节点将成一直线排列,原来的末节点变成了3条直线的中间节点,而原来的中间节点变成了3条直线中的末节点。这时再以新的中间节点为旋转轴做左单旋转,即可完成平衡操作。一言以蔽之:首先对原失衡节点的右子树进行右旋操作,再对原失衡节点做左旋操作即可。

//RL插入 右平衡  右-左旋转AVLNode* rightbalance(AVLNode* node)//node 就是发生不平衡的根节点{//第一步:对node->right  原失衡节点的右子树进右旋操作node->_right = rightRotation(node->_right);//变成一条线//第二步:对原失衡节点做左旋操作return leftRotation(node);}

AVL树的遍历操作

请见我的博客:二叉树的四种遍历方式(递归与非递归实现)

AVL树的查询操作

请见我的博客:BST树的查询

AVL树的插入操作

AVL树的插入操作与普通的BST树基本类似,区别就是在每次插入了新节点后会导致AVL树失去平衡特性,我们使用递归回溯的特性,每当创建好新节点回溯到父节点插入后,判断该结点的左右子树高度差,然后进行相应操作。具体的实现是我们给某结点的左子树插入新节点后,那么该结点的左子树高度便增加1,此时有可能失衡,但是我们需要判断是由于向左子树插入左孩子导致失衡(需要右旋)还是向左子树插入右孩子导致失衡(需要左-右旋转)。判断方法就是判断当前结点与插入的val的大小关系,大于当前结点肯定是插到了当前结点的右边,小于当前结点肯定是插入到了当前结点的左边,然后进行相应的旋转操作即可。那么对于在某节点的右子树插入新节点的情况与上边的分析过程类似。

void insert(const T& val){_root = inser_Operator(root, val);//接收调整之后的新的树的根节点}AVLNode* insert_Operator(AVLNode* this_node, const T& val){if (this_node == nullptr)//到达插入点return new AVLNode(val);if (val < this_node->_data)//往左走   是插在左子树上{// 回溯到父节点插入//每次都回到这this_node->_left = insert_Operator(this_node->_left, val);//在插入成功之后,会依次向父节点本层插入  回退//在每一层中 就可以做很多事情 比如调整节点高度,以及旋转//插入之后,回退一层 看一下新插入的叶子节点的父节点的左右子树的高度if (height_of_node(this_node->_left) - height_of_node(this_node->_right) > 1){if (height_of_node(this_node->_left->_left) >= height_of_node(this_node->_left->_right)){//左子树的左孩子太高  LL方式插入this_node = rightRotation(this_node);}else{//左孩子的右孩子太高  LR方式插入  左平衡this_node = leftbalance(this_node);}}//上面if没有进去 说明此时高度查(平衡因子)没问题}else if (this_node->_data < val)//往右走   是插在右子树上{// 回溯到父节点插入//每次都回到这this_node->_right = insert_Operator(this_node->_right, val);//在插入成功之后,会依次向父节点本层插入  回退//在每一层中 就可以做很多事情 比如调整节点高度,以及旋转//插入之后,回退一层 看一下新插入的叶子节点的父节点的左右子树的高度if (height_of_node(this_node->_right) -  height_of_node(this_node->_left) > 1){if (height_of_node(this_node->_right->_right) >= height_of_node(this_node->_right->_left)){//右子树的右孩子太高  RR方式插入this_node = leftRotation(this_node);}else{//右孩子的左孩子太高  RL方式插入 右平衡this_node = rightbalance(this_node);}}//上面if没有进去 说明此时高度查(平衡因子)没问题}else{//这特么是val重了}//这一层旋转结束之后  需要重新调整当前新被调整上去的节点//(一系列  路径上的根节点)的高度值this_node->_height = maxlevel_of_child(this_node->_left, this_node->_right) + 1;return this_node;}

AVL树的删除操作

删除比插入更加复杂,但是有着插入节点积累的经验,理解删除反而更加容易。
删除节点首先面对的问题是平衡因子从哪个节点开始变化的?这就要说到二叉查找树的删除逻辑了,在请见我的博客:BST树有着详细的介绍。这里简单回顾一下,删除叶子节点或者度为1的节点,平衡因子从被删除节点的父节点开始变化。删除度为2的节点需要使用复制删除的技巧,使用被删除节点的直接前驱或者直接后继来替换被删除节点,然后删除直接前驱或者直接后继,那么,平衡因子就是从直接前驱或者直接后继的父节点开始变化的。
既然知道了平衡因子开始变化的节点,就可以从下到上的更新平衡因子的变化。
在探讨插入节点引起的二叉树的变化时,我们总结出了RR、RL、LL、LR四种情况,删除节点会引起怎样的变化呢?
删除和插入有两个不同点: 这两种是BST删除的第三种中的情况

  1. 删除不仅包含以上四种变化,还多出两种,当然这两种也是对称的多出来的两种是当前节点的父节点的平衡因子等于+2或者-2时,当前节点的平衡因子等于0。插入节点之所以没有这两种变化:是因为插入节点使当前节点平衡因子变成0,说明当前节点的高度没有变化,更加不会引起祖先节点的平衡因子变化了。而删除节点时,如果当前节点平衡因子变成0,那么当前节点的高度其实变小了,可能引起祖先节点的平衡因子变化。怎么处理这种情况呢?如果父节点平衡因子等于+2,那就对当前节点进行右旋。如果父节点平衡因子等于-2,那就对当前节点进行左旋即可。
  2. 重新平衡过程没有在发现平衡因子为+2或者-2的第一个节点P后停止,而是追踪到根节点在插入节点的重新平衡过程中,我们在意的是第一个平衡因子为+2或者-2的节点,原因在于我们执行左旋或者右旋之后,子树不仅重新平衡并且高度也和没有插入节点之前保持一致,因此,祖先节点的平衡因子就不会发生变化了。但是删除节点发生了变化,重新平衡之后子树的高度变小了。为什么重新平衡之后,插入节点高度不变而删除节点高度变小呢?可以思考下平衡的过程,假设平衡因子变化为-2的节点为P,它的左子树高度为h,右子树高度必然为h + 1,只有在右子树插入新的节点,P节点的平衡因子才会变化为-2,重新平衡之后该树的左右子树高度为h + 1,因此该树的高度h + 2没有变化。但是对于删除又有不同,假设P节点左子树高度为h,右子树高度必然为h + 1,只有在左子树删除节点之后,P节点的平衡因子才会变化为-2,重新平衡之后该树的左右子树的高度为h,那么该树的高度h + 1比原来就减少了1。因为上述原因,删除节点之后重新平衡的逻辑不能只执行一次,而是从平衡因子发生变化的节点开始往上一直追踪到根节点,发现不平衡的节点都要执行平衡逻辑。

为了避免上面的复杂情况,我们的做法是:由于AVL树的删除操作基本框架和普通的二叉查找树也是类似的,我们在分析BST树的删除情况时分了三种情况,在AVL树中同样是适用的,但是对于第三种情况,也就是待删除结点同时拥有左右孩子的情况,我们之前讲解的是可以使用前驱或后继节点来替代当前节点并将其前驱或后继删除,我们只采用了其中一种方法,但是在AVL树的删除中,有可能导致失衡从而需要使用旋转操作来维护平衡,但是在这里直接判断当前待删除结点的左右子树高度,若左子树高我们删除前驱,若右子树高或者高度相同我们删除后继,这样就不用进行旋转操作,简化编程流程。但是对于普通的情况,即待删除结点只有一个孩子的情况,我们就必须要进行相应判断和旋转操作来维护AVL树的平衡特性。如果我们待删除的结点在左子树中找到,那么删除后,左子树高度降低,那么失衡肯定是由于右子树的高度过高导致的。因此我们直接判断右子树的右孩子与右子树的左孩子的高度大小,若右子树的右孩子高度更高,那么我们进行左旋操作维护平衡,否则我们进行右-左双旋转维护平衡。如果我们待删除的结点在右子树中找到,分析过程与上述类似。

void remove(const T& val){_root = remove_Operator(_root, val);}AVLNode* remove_Operator(AVLNode* this_node, const T& val){if (this_node == nullptr) return nullptr;if (val < this_node->_data)//往左走 {//删除之后 同样回到这里this_node->_left = remove_Operator(this_node->_left, val);//删除左子树上面的节点  得看看右边是不是多了if (height_of_node(this_node->_right) -height_of_node(this_node->_left) > 1){//右子树确实高了//右子树的右子树高了if (height_of_node(this_node->_right->_right) >= height_of_node(this_node->_right->_left))//来一次 左旋转{this_node = leftRotation(this_node);}else//右子树的左子树高了,右平衡{this_node = rightbalance(this_node);}}}else if (this_node->_data < val)//往右走{//删除之后 同样回到这里this_node->_right = remove_Operator(this_node->_right, val);//删除右子树上面的节点  得看看左边是不是多了if (height_of_node(this_node->_left) - height_of_node(this_node->_right) > 1){//左子树确实高了//左子树的左子树高了//来一次 右旋转if (height_of_node(this_node->_left->_left)>= height_of_node(this_node->_left->_right)){this_node = rightRotation(this_node);}else//左子树的右子树高了,左平衡{this_node = leftbalance(this_node);}}}else//找到 删除的val了{//第三种情况if (this_node->_left != nullptr && this_node->_right != nullptr){//这个节点的左子树 比 右子树高if (height_of_node(this_node->_left) > height_of_node(this_node->_right)){//所以这里选择 交换前驱,删除前驱AVLNode* pre_node = this_node->_left;while (pre_node->_right != nullptr)//寻找前驱{pre_node = pre_node->_right;}this_node->_data = pre_node->_data;//下面是去删除 这个前驱节点 把第三种情况统一到前两种this_node->_left = remove_Operator(this_node->_left, pre_node->_data);}else{//所以这里选择 交换后继,删除后继AVLNode* next_node = this_node->_right;while (next_node->_left != nullptr){next_node = next_node->_left;}this_node->_data = next_node->_data;//下面是去删除 这个后继节点 把第三种情况统一到前两种this_node->_right = remove_Operator(this_node->_right, next_node->_data);}}else if (this_node->_left!=nullptr){AVLNode* child = this_node->_left;delete this_node;return child;}else if (this_node->_right!=nullptr){AVLNode* child = this_node->_right;delete this_node;return child;}else{delete this_node;return nullptr;}this_node->_height = maxlevel_of_child(this_node->_left,this_node->_right) + 1;return this_node;}}

本节源代码如下:

/**══════════════════════════════════╗
*作    者:songjinzhou                                                 ║
*CSND地址:https://blog.csdn.net/weixin_43949535                       ║
**GitHub:https://github.com/TsinghuaLucky912/My_own_C-_study_and_blog║
*═══════════════════════════════════╣
*创建时间:2019年9月13日12:07:00
*功能描述:
*
*
*═══════════════════════════════════╣
*结束时间: 2019年9月13日21:58:21
*═══════════════════════════════════╝
//                .-~~~~~~~~~-._       _.-~~~~~~~~~-.
//            __.'              ~.   .~              `.__
//          .'//              西南\./联大               \\`.
//        .'//                     |                     \\`.
//      .'// .-~"""""""~~~~-._     |     _,-~~~~"""""""~-. \\`.
//    .'//.-"                 `-.  |  .-'                 "-.\\`.
//  .'//______.============-..   \ | /   ..-============.______\\`.
//.'______________________________\|/______________________________`.
*/
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <algorithm>using namespace std;template <typename T>
class AVLTree
{public:struct AVLNode;AVLTree(){_root = nullptr;}//LL插入,右旋处理AVLNode* rightRotation(AVLNode* node)//node 就是发生不平衡的根节点{AVLNode* new_node = node->_left;//这个是最后返回的新的根节点node->_left = new_node->_right;//该新根节点的右子树则变成其原父节点的左子树new_node->_right = node;//该新根节点的原父节点变成其右子节点//更新节点高度node->_height = maxlevel_of_child(node->_left, node->_right) + 1;new_node->_height = maxlevel_of_child(new_node->_left, new_node->_right) + 1;return new_node;//返回旋转后的根节点}//RR插入,左旋处理AVLNode* leftRotation(AVLNode* node)//node 就是发生不平衡的根节点{AVLNode* new_node = node->_right;//这个是最后返回的新的根节点node->_right = new_node->_left;//该新根节点的左子树则变成其原父节点的右子树new_node->_left = node;//该新根节点的原父节点变成其左子节点//更新节点高度node->_height = maxlevel_of_child(node->_left, node->_right) + 1;new_node->_height = maxlevel_of_child(new_node->_left, new_node->_right) + 1;return new_node;//返回旋转后的根节点}//LR插入 左平衡  左-右旋转AVLNode* leftbalance(AVLNode* node)//node 就是发生不平衡的根节点{//第一步:对node->left  原失衡节点的左子树进行左旋操作node->_left = leftRotation(node->_left);//变成一条线//第二步:对原失衡节点做右旋操作return rightRotation(node);}//RL插入 右平衡  右-左旋转AVLNode* rightbalance(AVLNode* node)//node 就是发生不平衡的根节点{//第一步:对node->right  原失衡节点的右子树进右旋操作node->_right = rightRotation(node->_right);//变成一条线//第二步:对原失衡节点做左旋操作return leftRotation(node);}void insert(const T& val){_root = insert_Operator(_root, val);//接收调整之后的新的树的根节点}AVLNode* insert_Operator(AVLNode* this_node, const T& val){if (this_node == nullptr)//到达插入点return new AVLNode(val);if (val < this_node->_data)//往左走   是插在左子树上{// 回溯到父节点插入this_node->_left = insert_Operator(this_node->_left, val);//每次都回到这//在插入成功之后,会依次向父节点本层插入  回退//在每一层中 就可以做很多事情 比如调整节点高度,以及旋转//插入之后,回退一层 看一下新插入的叶子节点的父节点的左右子树的高度if (height_of_node(this_node->_left) - height_of_node(this_node->_right) > 1){if (height_of_node(this_node->_left->_left) >= height_of_node(this_node->_left->_right)){//左子树的左孩子太高  LL方式插入this_node = rightRotation(this_node);}else{//左孩子的右孩子太高  LR方式插入  左平衡this_node = leftbalance(this_node);}}//上面if没有进去 说明此时高度查(平衡因子)没问题}else if (this_node->_data < val)//往右走   是插在右子树上{// 回溯到父节点插入this_node->_right = insert_Operator(this_node->_right, val);//每次都回到这//在插入成功之后,会依次向父节点本层插入  回退//在每一层中 就可以做很多事情 比如调整节点高度,以及旋转//插入之后,回退一层 看一下新插入的叶子节点的父节点的左右子树的高度if (height_of_node(this_node->_right) - height_of_node(this_node->_left) > 1){if (height_of_node(this_node->_right->_right) >= height_of_node(this_node->_right->_left)){//右子树的右孩子太高  RR方式插入this_node = leftRotation(this_node);}else{//右孩子的左孩子太高  RL方式插入 右平衡this_node = rightbalance(this_node);}}//上面if没有进去 说明此时高度查(平衡因子)没问题}else{//这特么是val重了}//这一层旋转结束之后  需要重新调整当前新被调整上去的节点(一系列  路径上的根节点)的高度值this_node->_height = maxlevel_of_child(this_node->_left, this_node->_right) + 1;return this_node;}int height_of_tree()//整棵树的高度 下面测试用{return height_of_tree(_root);}int height_of_tree(AVLNode* node){if (node == nullptr) return 0;int left = height_of_tree(node->_left);int right = height_of_tree(node->_right);return (left > right ? left : right) + 1;}void remove(const T& val){_root = remove_Operator(_root, val);}AVLNode* remove_Operator(AVLNode* this_node, const T& val){if (this_node == nullptr) return nullptr;if (val < this_node->_data)//往左走 {//删除之后 同样回到这里this_node->_left = remove_Operator(this_node->_left, val);//删除左子树上面的节点  得看看右边是不是多了if (height_of_node(this_node->_right) - height_of_node(this_node->_left) > 1){//右子树确实高了if (height_of_node(this_node->_right->_right) //右子树的右子树高了>= height_of_node(this_node->_right->_left))//来一次 左旋转{this_node = leftRotation(this_node);}else//右子树的左子树高了,右平衡{this_node = rightbalance(this_node);}}}else if (this_node->_data < val)//往右走{//删除之后 同样回到这里this_node->_right = remove_Operator(this_node->_right, val);//删除右子树上面的节点  得看看左边是不是多了if (height_of_node(this_node->_left) - height_of_node(this_node->_right) > 1){//左子树确实高了if (height_of_node(this_node->_left->_left) //左子树的左子树高了>= height_of_node(this_node->_left->_right))//来一次 右旋转{this_node = rightRotation(this_node);}else//左子树的右子树高了,左平衡{this_node = leftbalance(this_node);}}}else//找到 删除的val了{//第三种情况if (this_node->_left != nullptr && this_node->_right != nullptr){//这个节点的左子树 比 右子树高if (height_of_node(this_node->_left) > height_of_node(this_node->_right)){//所以这里选择 交换前驱,删除前驱AVLNode* pre_node = this_node->_left;while (pre_node->_right != nullptr)//寻找前驱{pre_node = pre_node->_right;}this_node->_data = pre_node->_data;//下面是去删除 这个前驱节点 把第三种情况统一到前两种this_node->_left = remove_Operator(this_node->_left, pre_node->_data);}else{//所以这里选择 交换后继,删除后继AVLNode* next_node = this_node->_right;while (next_node->_left != nullptr){next_node = next_node->_left;}this_node->_data = next_node->_data;//下面是去删除 这个后继节点 把第三种情况统一到前两种this_node->_right = remove_Operator(this_node->_right, next_node->_data);}}else if (this_node->_left!=nullptr){AVLNode* child = this_node->_left;delete this_node;return child;}else if (this_node->_right!=nullptr){AVLNode* child = this_node->_right;delete this_node;return child;}else{delete this_node;return nullptr;}this_node->_height = maxlevel_of_child(this_node->_left, this_node->_right) + 1;return this_node;}}//层序遍历 把顺序保存在一个数组中 非递归实现vector<T>nonlevelOrder(){return nonlevelOrder(_root);}vector<T>nonlevelOrder(AVLNode* root){vector<T>myvec;if (root == nullptr)return myvec;queue<AVLNode*>myque;myque.push(root);while (!myque.empty()){AVLNode* head_node = myque.front();myvec.push_back(head_node->_data);myque.pop();if (head_node->_left != nullptr)myque.push(head_node->_left);if (head_node->_right != nullptr)myque.push(head_node->_right);}return myvec;}private:struct AVLNode{AVLNode(T val=T()) :_data(val), _left(nullptr), _right(nullptr),_height(1){}T _data;AVLNode* _left;AVLNode* _right;int _height;//存储这个结点的实时高度};//提供负责返回一个结点的高度  去计算该结点左右子树的高度差int height_of_node(AVLNode* node)const//返回这个节点的高度 {return node == nullptr ? 0 : node->_height;//不是空节点 返回高度}int maxlevel_of_child(AVLNode* left_node,AVLNode* right_node)const//返回左右子树最高的层数{int left_level = height_of_node(left_node);int right_level = height_of_node(right_node);return left_level > right_level ? left_level : right_level;}AVLNode* _root;
};int main()
{AVLTree<int> mytree;mytree.insert(1);cout << mytree.height_of_tree() << endl;mytree.insert(2);mytree.insert(3);cout << mytree.height_of_tree() << endl;cout << "层序遍历当前树:";for (int val : mytree.nonlevelOrder()){cout << val << " ";}cout << endl;mytree.insert(4);mytree.insert(5);cout << mytree.height_of_tree() << endl;mytree.insert(6);cout << mytree.height_of_tree() << endl;mytree.insert(7);mytree.insert(8);mytree.insert(9);cout << mytree.height_of_tree() << endl;cout << "层序遍历当前树:";//预测结果:4 2 6 1 3 5 8 7 9for (int val : mytree.nonlevelOrder()){cout << val << " ";}cout << endl;//4是根节点 删掉mytree.remove(4);cout << "层序遍历当前树:";//预测结果:因为换的是后继 5 2 8 1 3 6 9 7for (int val : mytree.nonlevelOrder()){cout << val << " ";}cout << endl;return 0;
}
/**
*备用注释:
*
*
*
*/

测试打印如下:

AVL树的总结

最小二叉平衡树的节点的公式如下 F(n)=F(n-1)+F(n-2)+1 这个类似于一个递归的数列,可以参考Fibonacci数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。

此外虽然平衡二叉树的性能优势:很显然,平衡二叉树的优势在于不会出现普通二叉查找树的最差情况。其查找的时间复杂度为O(logN)。但是为了保证高度平衡,动态插入和删除的代价也随之增加。所有二叉查找树结构的查找代价都与树高是紧密相关的,能否通过减少树高来进一步降低查找代价呢。我们可以通过多路查找树的结构来做到这一点。在大数据量查找环境下(比如说系统磁盘里的文件目录,数据库中的记录查询 等),所有的二叉查找树结构(BST、AVL、RBT)都不合适。如此大规模的数据量(几G数据),全部组织成平衡二叉树放在内存中是不可能做到的。那么把这棵树放在磁盘中吧。问题就来了:假如构造的平衡二叉树深度有1W层。那么从根节点出发到叶子节点很可能就需要1W次的硬盘IO读写。大家都知道,硬盘的机械部件读写数据的速度远远赶不上纯电子媒体的内存。 查找效率在IO读写过程中将会付出巨大的代价。在大规模数据查询这样一个实际应用背景下,平衡二叉树的效率就很成问题了。

所以为了解决这个问题,就引入了 RB树。
2019年9月13日22:14:43

到此为止,AVL树中添加和删除节点的逻辑已经探讨完毕。

DSA 经典数据结构与算法 学习心得和知识总结(四) | AVL树相关推荐

  1. 【数据结构与算法学习心得】

    重建二叉树 1.根据前序遍历和中序遍历构造二叉树:如:前序和中序遍历序列分别为[1,2,4,7,3,5,6,8],[4,7,2,1,5,3,8,6].要构造出如下的二叉树: 思路:1.要构造出二叉树首 ...

  2. 数据结构与算法学习④(哈夫曼树 图 分治回溯和递归)

    数据结构与算法学习④(哈夫曼树 图 回溯和递归 数据结构与算法学习④ 1.哈夫曼树 1.1.相关概念 1.2.哈夫曼树的构建 1.3.哈夫曼编码 1.4.面试题 2.图 2.1.图的相关概念 2.2. ...

  3. 数据结构与算法学习⑥(动态规划 题解 背包和打家劫舍问题)

    数据结构与算法学习⑥(动态规划 动态规划 1.初识动态规划 1.1.从贪心说起 1.1.1.贪心的特点 1.1.2.贪心的局限性 1.1.3.贪心失效后怎么办 1.1.4.从最优化问题到递归 1.2. ...

  4. 20个经典数据结构与算法,300多幅算法手绘图解,带你领略算法之美

    一些经典的数据结构和算法图书,偏重理论,读者学起来可能感觉比较枯燥.一些趣谈类的数据结构和算法图书,虽然容易读懂,但往往内容不够全面.另外,很多数据结构和算法图书缺少真实的开发场景,读者很难将理论和实 ...

  5. 数据结构与算法学习路线

    文章目录 一.入门 二.基础 三.进阶 四.实战 五.面试 六.课外阅读 可视化展现 这部分知识相当于C语言的进阶知识啦,而且这些知识对所有语言是通用的,把它比作编程语言的灵魂毫不为过. 一.入门 & ...

  6. Surf算法学习心得(一)——算法原理

    Surf算法学习心得(一)--算法原理 写在前面的话: Surf算法是对Sift算法的一种改进,主要是在算法的执行效率上,比Sift算法来讲运行更快!由于我也是初学者,刚刚才开始研究这个算法,然而网上 ...

  7. 分享一下字符串匹配BM算法学习心得。

    字符串匹配BM(Boyer-Moore)算法学习心得 BM算法 是 Boyer-Moore算法 的缩写,是一种基于后缀比较的模式串匹配算法.BM算法在最坏情况下可以做到线性的,平均情况下是亚线性的(即 ...

  8. 数据结构与算法学习笔记之 从0编号的数组

    数据结构与算法学习笔记之 从0编号的数组 前言 数组看似简单,但掌握精髓的却没有多少:他既是编程语言中的数据类型,又是最基础的数据结构: 一个小问题: 为什么数据要从0开始编号,而不是 从1开始呢? ...

  9. python leetcode_leetcode 介绍和 python 数据结构与算法学习资料

    for (刚入门的编程)的高中 or 大学生 leetcode 介绍 leetcode 可以说是 cs 最核心的一门"课程"了,虽然不是大学开设的,但基本上每一个现代的高水平的程序 ...

最新文章

  1. Python 位运算符号
  2. jQuery 阻止冒泡和默认事件
  3. HTML第一章:初始HTML
  4. Codeforces Round #324 (Div. 2) B. Kolya and Tanya
  5. requestfacade 这个是什么类?_Java 的大 Class 到底是什么?
  6. 从0基础学Python:装饰器及练习(基础)
  7. 常见面试题整理--Python概念篇
  8. 设计模式-设计原则-迪米特法则
  9. 计算机函数left的用法,excel的left函数的用法
  10. 用什么软件免费查重呢?4款比较靠谱的论文查重软件值得一试
  11. 奇瑞QQ序列首款新能源汽车QQ冰淇淋上市;上海嘉定集中发展氢燃料电池和ICV | 能动...
  12. lnmp单独安装php,lnmp 环境,再单独安装php7.2 的版本,多版本php 同时运行
  13. 计算机网络五层体系结构各层协议
  14. 为什么DCIM在中国市场不给力?
  15. MD5工具类,提供字符串MD5加密(校验)、文件MD5值获取(校验)功能
  16. Mac卸载程序清除残留文件
  17. flash特效原理:图片切换滚动
  18. nyoj325 zb的生日(DFS)
  19. js和html5实现扫描条形码
  20. 电商平台-RBAC系统权限的设计与架构

热门文章

  1. JAVA全栈面试题(一)
  2. Flash AIR文件操作:AIR文件基础
  3. 今日头条、UC头条(大鱼号)、企鹅号文章分类、推送、拉取业务实现及接口api说明文档
  4. IT经典:一个枪手的自白
  5. ROS与Arduino:用Twist消息+用键盘+用Xbox one手柄 控制小车
  6. AccessDatabaseEngine 2007 Office system 驱动程序:数据连接组件
  7. 因为我是科研废物所以每天翘课摸鱼准备去打工这件事
  8. 2022软考成绩能不能复核?
  9. 关于拟认定为杨浦区第八批区块链企业的名单公示
  10. Windows下载mingw安装器安装gcc/make组件