目录
第一节:AVL树的特征
第二节:实现思路
2-1.插入
2-1-1.右单旋
2-1-2.左单旋
2-1-3.左右双旋
2-1-4.右左双旋
2-1-5.总结
2-2.删除
第三节:代码实现
3-1.Node类
3-2.AVLTree类
3-2-1.Insert函数
3-2-2.Height函数
3-2-3.Balance函数
3-2-4.其他函数
3-2-5.clear函数
第四节:测试
第五节:优化方案
gitee:AVL树 · 转调/C++ - 码云 - 开源中国
第六章:总结
下期预告:
第一节:AVL树的特征
如果一个搜索二叉树的结构如下:
那么当我想找a的时候,时间复杂度就从 log₂n 退化成 n 了。
究其原因,搜索二叉树没有一个机制让树的整体高度变得更低。
所以就出现了AVL树,AVL树通过保证每个节点的左右子树高度差不超过[-1,1],保证整棵树的平衡。
将左右子树高度差抽象成平衡因子,平衡因子=右子树高度-左子树高度,每个节点都要维护自己的平衡因子不超过1。
第二节:实现思路
2-1.插入
1.首先按照搜索二叉树的规则进行插入
2.插入后更新其父亲节点的平衡因子
a.插入父亲节点的左子树,父亲节点平衡因子--
b.插入父亲节点的右子树,父亲节点平衡因子++
3.如果父亲节点平衡因子是-1或者1,说明父亲所在子树的高度改变了,继续更新父亲的父亲节点的平衡因子,若父亲的父亲节点的平衡因子也为-1或者1,继续向上更新,直到平衡因子不为-1或者1;
如果某个祖先节点的平衡因子更新后为0,说明祖先节点所在子树高度不变了,对祖先之上的节点也没有影响了,不再往上更新;
如果某个祖先节点的平衡因子为-2或者2,就需要进行旋转处理了:
2-1-1.右单旋
当前节点平衡因子为-2 && 其左孩子的平衡因子为-1 进行右单旋
用长方形表示子树高度,b、c的高度都为h,a的高度为h+1,红色方块为节点新增位置,蓝色字体为节点的平衡因子:
60的平衡因子为-2,并且其左孩子30的平衡因子为-1,进行右单旋:
具体的方法是将60的左孩子变成30的右孩子,30的右孩子被"抢走了",就把60变成30的右孩子,最后把60的父亲变成30的父亲。
这样做之后30和60的平衡因子都变成了0,就不需要再向上更新了。
可以发现30向右旋转把60顶替了,60向右旋转屈居30,a、b、c从左向右的顺序依然不变。
2-1-2.左单旋
当前节点平衡因子为+2 && 其右孩子的平衡因子为+1
30的平衡因子为+2,并且其右孩子60的平衡因子为+1,进行左单旋:
这样做之后30和60的平衡因子都变成了0,就不需要再向上更新了。
可以发现60向左旋转把30顶替了,30向左旋转屈居30,a、b、c从左向右的顺序依然不变。
2-1-3.左右双旋
当前节点平衡因子为-2 && 其左孩子的平衡因子为+1时进行左右双旋。
左右双旋有三种不同的情况,它们的旋转逻辑是一样的,但是最后的平衡因子不同。
旋转逻辑:
先不看它们的平衡因子,只看节点和子树的交换关系,此时a=h,d=h,b和c有一个等于h-1,另一个等于h,才能保证90的平衡因子为-2,30的平衡因子为1。
其实就是孙子变成了它爸爸和爷爷共同的父亲,然后仍然保持a、b、c、d的左右顺序
(1)如果60自己就是新增的节点
3个节点的平衡因子都为0,不再向上更新。
为了便于理解,我使用绿色字母表示新增节点后a、b、c、d的高度。
(2)如果60的左子树新增节点
此时60的平衡因子为0,不再向上更新,30的平衡因子为0,90的平衡因子为1
(3)如果60的右子树新增节点
此时60的平衡因子为0,不再向上更新,30的平衡因子为-1,90的平衡因子为0
2-1-4.右左双旋
当前节点平衡因子为+2 && 其右孩子的平衡因子为-1时进行左右双旋。
左右双旋也有三种不同的情况,它们的旋转逻辑是一样的,但是最后的平衡因子不同。
旋转逻辑:
![]()
也是孙子变成了它爸爸和爷爷共同的父亲,然后仍然保持a、b、c、d的左右顺序 ,这和左右双旋的结果一致。
所以如果是60的左子树b新增节点,旋转后,30、60、90的平衡因子分别是0、0、1;
如果是60的右子树c新增节点,旋转后,30、60、90的平衡因子分别是-1、0、0;
如果60本身是新增的节点,旋转后,30、60、90的平衡因子都是0。
2-1-5.总结
只要进行旋转后,"辈分"最高的节点的平衡因子都变成了0,说明只要进行过旋转,就不需要再往上更新了。
第三节:代码实现
将以上思路整理成代码。
3-1.Node类
首先创建一个节点类:
它除了节点的连接和保存的值之外,还需要一个变量_bf来保存平衡因子。
template<class T> class Node { public: Node<T>* _left = nullptr; Node<T>* _right = nullptr; Node<T>* _parent = nullptr; T _val; short _bf = 0; };
3-2.AVLTree类
它是AVL树的具体实现。
class AVLTree
{
private:
Node<T>* _root = nullptr; // 保存根节点
};
3-2-1.Insert函数
首先完成插入函数,它传入一个值,然后按照搜索二叉树的思路插入节点:
void Insert(const T& val) { // 如果根节点为空,赋值根节点即可 if (_root == nullptr) { _root = new Node<T>; _root->_val = val; return; } Node<T>* cur = _root; Node<T>* parent = nullptr; while (cur) { if (cur->_val < val) { parent = cur; cur = cur->_right; } else if (cur->_val > val) { parent = cur; cur = cur->_left; } } cur = new Node<T>; cur->_val = val; if (parent->_val < val) { parent->_right = cur; } else { parent->_left = cur; } cur->_parent = parent; // 更新祖先节点的平衡因子 while (parent) { if (cur == parent->_left) { parent->_bf--; } else { parent->_bf++; } // 继续更新 if (abs(parent->_bf) == 1) { cur = parent; parent = parent->_parent; continue; } // 不再更新 if (parent->_bf == 0) { break; } // 如果失衡,进行旋转 if (abs(parent->_bf) > 1) { Rotate(parent); break; } } }
3-2-2.Height函数
它获取某个节点的高度,获得某个节点左右孩子的高度后,就可以计算_bf:
size_t _Height(Node<T>* root) { if (root == nullptr) return 0; return std::max(_Height(root->_left), _Height(root->_right)) + 1; } size_t Height(Node<T>* root) { return _Height(root); }
3-2-3.Balance函数
AVL树的核心函数,它用来更新节点的平衡因子:
void Rotate(Node<T>* cur) { // 进行旋转处理,旋转完也不需要继续更新了 if (cur->_bf == -2 && cur->_left->_bf == -1) // 右单旋 { Node<T>* P = cur; // 父亲 Node<T>* S = P->_left; // 儿子 P->_left = S->_right; if (S->_right) S->_right->_parent = P; S->_right = P; if (P != _root) { if (P == P->_parent->_left) { P->_parent->_left = S; } else if (P == P->_parent->_right) { P->_parent->_right = S; } } else { _root = S; } S->_parent = P->_parent; P->_parent = S; P->_bf = 0; S->_bf = 0; } else if (cur->_bf == 2 && cur->_right->_bf == 1) // 左单旋 { Node<T>* P = cur; Node<T>* S = P->_right; P->_right = S->_left; if (S->_left) S->_left->_parent = P; S->_left = P; if (P != _root) { if (P == P->_parent->_left) { P->_parent->_left = S; } else if (P == P->_parent->_right) { P->_parent->_right = S; } } else { _root = S; } S->_parent = P->_parent; P->_parent = S; P->_bf = 0; S->_bf = 0; } else if (cur->_bf == -2 && cur->_left->_bf == 1) // 左右双旋 { Node<T>* P = cur; // 父亲 Node<T>* S = P->_left; // 儿子 Node<T>* G = S->_right;// 孙子 P->_left = G->_right; if (G->_right) G->_right->_parent = P; G->_right = P; S->_right = G->_left; if (G->_left) G->_left->_parent = S; G->_left = S; if (P != _root) { if (P == P->_parent->_left) { P->_parent->_left = G; } else if (P == P->_parent->_right) { P->_parent->_right = G; } } else { _root = G; } G->_parent = P->_parent; P->_parent = G; S->_parent = G; if (G->_bf == 1) { S->_bf = -1; P->_bf = 0; } else if (G->_bf == -1) { S->_bf = 0; P->_bf = 1; } else if (G->_bf == 0) { S->_bf = P->_bf = 0; } G->_bf = 0; } else if (cur->_bf == 2 && cur->_right->_bf == -1) // 右左双旋 { Node<T>* P = cur; Node<T>* S = P->_right; Node<T>* G = S->_left; P->_right = G->_left; if (G->_left) G->_left->_parent = P; G->_left = P; S->_left = G->_right; if (G->_right) G->_right->_parent = S; G->_right = S; if (P != _root) { if (P == P->_parent->_left) { P->_parent->_left = G; } else if (P == P->_parent->_right) { P->_parent->_right = G; } } else { _root = G; } G->_parent = P->_parent; P->_parent = G; S->_parent = G; if (G->_bf == 1) { S->_bf = 0; P->_bf = -1; } else if (G->_bf == -1) { S->_bf = 1; P->_bf = 0; } else if (G->_bf == 0) { S->_bf = P->_bf = 0; } G->_bf = 0; } }
3-2-4.其他函数
Print 和 IsBalance 用来按升序打印内容和判断每个节点是否都平衡:
bool IsBalance() { return _IsBalance(_root); } void Print() { _Print(_root); } bool _IsBalance(Node<T>* root) { if (root == nullptr) return true; if (abs(root->_bf) > 1) return false; return _IsBalance(root->_left) && _IsBalance(root->_right); } void _Print(Node<T>* root) { if (root == nullptr) return; _Print(root->_left); std::cout << root->_val << " "; _Print(root->_right); }
3-2-5.clear函数
因为节点都是new出来的,所以使用清理函数释放空间,析构函数也要调用这个函数:
void _clear(Node<T>* root) { if (root == nullptr) return; _clear(root->_left); _clear(root->_right); delete root; } void clear() { _clear(_root); } ~AVLTree() { clear(); }
第四节:测试
插入多个随机数,然后调用 Print 和 IsBalance。
#include <stdio.h> #include <stdlib.h> #include <time.h> #include "AVLTree.hpp" int main() { zd::AVLTree<int> tree; srand(time(NULL)); int i = 100; while (i--) { int x = rand(); tree.Insert(x); } tree.Print(); if (tree.IsBalance()) { printf("true\n"); } else { printf("false\n"); } return 0; }
观察打印结果和最后是否为true即可。
第五节:优化方案
实际上左右双旋和右左双旋就是左单旋和右单旋的组合,所以可以将左单旋、右单旋封装成函数,供左右单旋和右左单旋调用。
但是封装的时候G的_bf需要提前保存,因为调用两次单旋之后,G的_bf就改变了:
void RotateR(Node<T>* cur) // 右单旋 { Node<T>* P = cur; Node<T>* S = P->_left; P->_left = S->_right; if (S->_right) S->_right->_parent = P; S->_right = P; if (P != _root) { if (P == P->_parent->_left) { P->_parent->_left = S; } else if (P == P->_parent->_right) { P->_parent->_right = S; } } else { _root = S; } S->_parent = P->_parent; P->_parent = S; P->_bf = 0; S->_bf = 0; } void RotateL(Node<T>* cur) // 左单旋 { Node<T>* P = cur; Node<T>* S = P->_right; P->_right = S->_left; if (S->_left) S->_left->_parent = P; S->_left = P; if (P != _root) { if (P == P->_parent->_left) { P->_parent->_left = S; } else if (P == P->_parent->_right) { P->_parent->_right = S; } } else { _root = S; } S->_parent = P->_parent; P->_parent = S; P->_bf = 0; S->_bf = 0; } void RotateLR(Node<T>* cur) // 左右双旋 { Node<T>* P = cur; Node<T>* S = P->_left; Node<T>* G = S->_right; short bf = G->_bf; RotateL(S); RotateR(P); if (bf == 1) { S->_bf = -1; P->_bf = 0; } else if (bf == -1) { S->_bf = 0; P->_bf = 1; } else if (bf == 0) { S->_bf = P->_bf = 0; } G->_bf = 0; } void RotateRL(Node<T>* cur) // 右左双旋 { Node<T>* P = cur; Node<T>* S = P->_right; Node<T>* G = S->_left; short bf = G->_bf; RotateR(S); RotateL(P); if (bf == 1) { S->_bf = 0; P->_bf = -1; } else if (bf == -1) { S->_bf = 1; P->_bf = 0; } else if (bf == 0) { S->_bf = P->_bf = 0; } G->_bf = 0; }
用以上接口替换Rotate中的对应代码即可。
Gitee:AVL树 · 转调/C++ - 码云 - 开源中国
第六章:总结
单旋时,向左会被旋转到向右,向右会被旋转到向左,但是a、b、c的顺序不变
双旋时,两种双旋的结果一样,a、b、c、d的顺序也不变,而且双旋可以拆分成两次单旋:S先旋、P后旋。
负二左负右单旋,反之正一左右旋;正二右正左单旋,反之负一右左旋;两个双旋子先动;a、b、c、d顺序齐。
下期预告:
第十三章将介绍另外一种搜索二叉树——红黑树。