目录
AVL树
1.1AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树(如下图所示)
查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
1.它的左右子树都是AVL树
2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
平衡因子计算方法:当前节点的右子树与左子树的高度之差。
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O(log_2 n),搜索时间复杂度O(log_2 n)。
1.2 AVL树节点的定义:
如上图所示,我们每一个节点的结构体就长这样,我们使用了类模板来构建一个K_V模型,类里面有三个结构体指针,分别代表左孩子,右孩子,还有父亲节点,多个父亲节点是为了后期我们进行旋转操作的时候能控制住节点间的关系。除了这三个指针外我们还有一个标准库里的pair结构体和一个平衡因子,这个平衡因子就是保证我们的二叉搜索树是平衡二叉搜索树的关键,接下来就是初始化列表来对这些成员变量进行初始化。
1.3AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么 AVL树的插入过程可以分为两步:
1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子
//插入 bool Insert(const pair<K,value>& 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->_right; } else if (kv.first < cur->_kv.first) { parent = cur; cur = cur->_left; } else { return false; } } cur = new Node(kv); if (kv.first < parent->_kv.first) { parent->_left = cur; } else { parent->_right = cur; } cur->_parent = parent; //更新平衡因子 while (parent) { if (parent->_left == cur) { parent->_bf--; } else { 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) { RotateRL(parent); } else if (parent->_bf == -2 && cur->_bf == 1) { RotateLR(parent); } break; } else { //这种情况不可能出现 assert(false); } } return true; }
接下来我会对插入操作进行逐语句分析:
首先看我们的参数,我们传参传递的就是pair结构体,这是为了实际意义,如果只是一个简单的内置类型,我们也不需要AVL树这个数据结构了。
在分析完参数之后就是内部代码了,在看内部代码之前我有必要展示一下这个类的成员变量和一些自定义类型。
自定义类型:
我们的Node类型其实就是我们AVL树节点的定义,只不过我们对它进行了重命名。
成员变量:
成员变量也只有一个Node类型的_root,也就是我们这棵树的根节点。
在展示完这两部分之后我们就可以继续分析了;
前半部分几乎可以说是跟二叉搜索树一模一样,但我还是再说一遍:
1.我们的第一步是插入——我们先判断根节点是不是为空,是的话创建根节点,然后把要插入的数据内容直接放到这个根节点里就结束了。如果不为空,我们就往下遍历,如果要插入的key比当前节点小就往左子树放向走,如果大就往右子树方向走,如果值相等就插入失败。
定位完成后我们就要看看要插入的数据是比父亲节点小还是大,小往左,大往右,再调节一下指针的指向关系,我们的插入工作就算成功了。
2.定位完成之后我们就要进行第二部操作——更新平衡因子。
平衡因子的更新有以下几种情况:
a.插入父亲的左边,父亲的平衡因子--
b.插入父亲的右边,父亲的平衡因子++
c.父亲的平衡因子==0,父亲所在子树高度不变,不再继续往上更新,插入结束。
d.父亲平衡因子==1或==-1,父亲所在子树高度变了,继续往上更新。
e.父亲平衡因子==2或==-2,父亲所在子树已经不平衡了,需要旋转处理。
注意:更新中不可能出现其他值,插入之前树是AVL树,平衡因子要么是1,-1,0,++,--,最多就是cde三种情况。(下图可以辅助理解更新过程)
了解完平衡因子的更新过程之后我们就可以继续我们的思路了
一般情况下我们要一直往上更新平衡因子,直到出发c或e这两种情况,在讲完这些规则之后,我们的代码思路就非常清晰了,先对父亲的平衡因子进行加减操作,然后判断当前情况进行相应的操作,这些我相信大家也已经能清晰的看懂了,那么我就来详细说说e的情况下我们具体要怎么做。
旋转我们有4种:右单旋、左单旋、右左双旋、左右双旋
我们先来讲右单旋:
右单旋的条件是——新节点插入较高左子树的左侧:左左-右单旋
为了减少节点指向时发生的错误,我们先来两个指针subl表示的是值为30的节点,sublr表示的是b的矩形,然后将b变成60的左子树,如果b不为空,就把b的父亲指针指向60节点,再把值为60的节点变成30节点的右子树,再用ppnode记录下60节点的原父亲节点,再改变60节点的父亲节点指向30节点。如果60节点是这棵树的根节点,那么我们直接将_root设置成30节点,30节点的父亲节点指向空就可以了。否则我们再判断60节点是ppnode的左子树还是右子树,再改变对应的指向就可以了,最后不要忘了把30节点和60节点的平衡因子改为0就可以了。
接下来我们看看左单旋:
左单旋的条件是——新节点插入较高右子树的右侧:右右-左单旋
我们理解了右单旋的思路后就会发现左单旋就是换汤不换药,只不过是把指向关系稍作调整,我就不重述一遍了,我们来看接下来的两种。
左右双旋:
新节点插入较高左子树的右侧---左右:先左单旋再右单旋
我们按照我们花的一种图,可以复用左单旋和右单旋,这样指针指向就结束了,接下来就是更新平衡因子,基本有3种情况,就是60节点平衡因子为1还是-1还是0,我们所画的图是-1的情况,其他两种情况参照我所画的图简单修改以下就可以很快搞定了。
左右双旋搞定之后,右左双旋的思路也就是大差不差了,就是把步骤变换一下,平衡因子调整一下就可以了
右左双旋:
新节点插入较高右子树的左侧---右左:先右单旋再左单旋
同样的,平衡因子有三种情况,我们所画的图是bf为1的情况,其他情况只需把图稍作修改就能搞定。
1.4AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
1. 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确
为了验证其平衡性,我们封装了两组代码:上面的是求当前节点的高度:我们采用递归的思想去求得其高度。
bool _IsBalance(Node* root) { if (root == nullptr) { return true; } int leftHeight = _Height(root->_left); int rightHeight = _Height(root->_right); if (abs(leftHeight - rightHeight) >= 2) { cout << root->_kv.first << endl; return false; } if (rightHeight - leftHeight != root->_bf) { cout << root->_kv.first << endl; return false; } return _IsBalance(root->_left) && _IsBalance(root->_right); }
这组就是验证其平衡性,我们也是采用递归的思想,检查每个节点右子树高度与左子树高度之差,其绝对值超过2就证明它不是平衡的,我们也可以查一下平衡因子是否正确,不正确的话也给它返回一个false。
1.5 AVL树的删除(了解)
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的是,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。 具体实现学生们可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。
1.6AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2 (N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
05-08
890
![](https://csdnimg.cn/release/blogv2/dist/pc/img/readCountWhite.png)
05-02