【C++】AVL树
创始人
2024-05-13 12:18:59
0

​🌠 作者:@阿亮joy.
🎆专栏:《吃透西嘎嘎》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 👉AVL树👈
      • AVL树的概念
      • AVL树节点的定义
      • AVL树的插入
      • AVL树的旋转
      • AVL树的验证
      • AVL树的删除(了解)
      • AVL树的性能
      • AVL树的完整代码
    • 👉总结👈

👉AVL树👈

AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家 G.M.Adelson-Velskii 和 E.M.Landis 在 1962 年
发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

AVL 树也称为高度平衡二叉搜索树,其满足以下性质:

  • 它的左右子树都是 AVL 树(注:空树也是 AVL 树)
  • 左右子树的高度差(简称平衡因子)的绝对值不超过 1(-1 / 0 / 1)(注:无法保证左右子树的高度差永远为 0)
  • 平衡因子等于右子树高度减去左子树高度,平衡因子是非必须的,我们用的话方便实现 AVL 树。

在这里插入图片描述

如果一颗二叉搜索树是高度平衡的,它就是 AVL 树。如果它有 N 个节点,其高度可保持在O(log2N)O(log_2 N)O(log2​N) ,查找和插入的时间复杂度为O(log2Nlog_2 Nlog2​N) 。

AVL树节点的定义

template 
struct AVLTreeNode
{AVLTreeNode* _parent;	// 父节点AVLTreeNode* _left;	// 左孩子AVLTreeNode* _right;	// 右孩子pair _kv;	// 键值对int _bf;	// balance factor(平衡因子)AVLTreeNode(const pair& kv): _parent(nullptr), _left(nullptr), _right(nullptr), _kv(kv), _bf(0){}
};

AVL 树的节点是三叉链结构的,因为插入节点可能会影响父节点的平衡因子,有了指向父节点的指针会方便我们进行平衡因子的更新和旋转操作。新插入的节点的平衡因子都是 0。

AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

调整节点的平衡因子

在这里插入图片描述

在这里插入图片描述

bool Insert(const pair& kv)
{// 树为空,新插入的节点就是根节点if (_root == nullptr){_root = new Node(kv);return true;}// 按照二叉搜索树的方式插入节点Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else	// 树中已经存在Key{return false;}}// 插入节点cur = new Node(kv);// 判断在父节点的哪一边插入if (parent->_kv.first < kv.first)parent->_right = cur;elseparent->_left = cur;cur->_parent = parent;// 控制平衡并更新平衡因子// 更新平衡因子的最坏情况需要更新到根节点while (parent){// 新插入节点在父节点的右边,则父节点平衡因子自加一if (cur == parent->_left)--parent->_bf;else++parent->_bf;if (parent->_bf == 0)	// 不需要继续往上更新{break;}else if (abs(parent->_bf) == 1)	// 继续往上更新{parent = parent->_parent;cur = cur->_parent;}else if (abs(parent->_bf) == 2){// 说明parent所在的子树已经不平衡了,需要旋转处理if (parent->_bf == 2 && cur->_bf == 1)RotateL(parent);else if (parent->_bf == -2 && cur->_bf == -1)RotateR(parent);else if (parent->_bf == -2 && cur->_bf == 1)RotateLR(parent);else if (parent->_bf == 2 && cur->_bf == -1)RotateRL(parent);else	// 理论上不会走到这里assert(false);break;}else	// 理论上不会走到这里{assert(false);}}return true;
}

AVL树的旋转

如果在一棵原本是平衡的 AVL 树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL 树的旋转分为四种:左单旋、右单旋、左右双旋和右左双旋。注:旋转操作可能对整棵树做,也可能对子树做。旋转的原则是:旋转成平衡数并且符合二叉搜索树的规则。一次插入,最多一次旋转。

节点插入在较高右子树的右侧时,需要左单旋

在这里插入图片描述
注:当parent->_bf == 2 && cur->_bf == 1时,此时需要对parent所在的子树进行左单旋。parentsubR不可能为空,subRL可能为空。更新subR的父指针是需要需要parent是否是根节点。

void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)	// subRL不为空,其父指针且才能指向parentsubRL->_parent = parent;// 记录parent的父亲Node* ppNode = parent->_parent;subR->_left = parent;parent->_parent = subR;// parent就是根节点时,则需要将subR更新为根节点,subR的父指针指向nullptrif (parent == _root){_root = subR;subR->_parent = nullptr;}else{// 判断原父节点在左边还是右边if (ppNode->_left == parent)ppNode->_left = subR;elseppNode->_right = subR;subR->_parent = ppNode;}// 更新平衡因子parent->_bf = subR->_bf = 0;
}

节点插入在较高左子树的左侧时,需要右单旋。右单旋和左单旋是一致的。

在这里插入图片描述

注:当parent->_bf == -2 && cur->_bf == -1时,此时需要对parent所在的子树进行右单旋。parentsubL不可能为空,subLR可能为空。更新subL的父指针是需要需要parent是否是根节点。

void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;// subLR不为空时,其父指针才能指向parentif (subLR)subLR->_parent = parent;Node* ppNode = parent->_parent;subL->_right = parent;parent->_parent = subL;// parent就是根节点时,则需要将subR更新为根节点,subR的父指针指向nullptrif (parent == _root){_root = subL;subL->_parent = nullptr;}else{if (ppNode->_left == parent)ppNode->_left = subL;elseppNode->_right = subL;subL->_parent = ppNode;}// 更新平衡因子parent->_bf = subL->_bf = 0;
}

节点插入在较高左子树的右侧时,需要左右双旋。当parent->_bf == -2 && cur->_bf == 1时,需要进行左右双旋:先对subL进行左单旋,再对parent进行右单旋。

在这里插入图片描述
在这里插入图片描述

// 左右双旋
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;// 通过subLR的平衡因子来区分各种情况int bf = subLR->_bf;// 先左单旋再右单旋RotateL(subL);RotateR(parent);subLR->_bf = 0;if (bf == 1)	// 在c插入新节点{parent->_bf = 0;subL->_bf = -1;}else if (bf == 0)	// 新增节点就是自己{parent->_bf = 0;subL->_bf = 0;}else if (bf == -1)	// 在b插入新节点{parent->_bf = 1;subL->_bf = 0;}else	// 理论上不会走到这里{assert(false);}
}

节点插入在较高右子树的左侧时,需要右左双旋。当parent->_bf == 2 && cur->_bf == -1时,需要进行左右双旋:先对subR进行右单旋,再对parent进行左单旋。

在这里插入图片描述

// 右左双旋
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;// 通过subRL的平衡因子来区分各种情况int bf = subRL->_bf;// 先右单旋再左单旋RotateR(subR);RotateL(parent);subRL->_bf = 0;if (bf == 1)	// 在c插入新节点{parent->_bf = -1;subR->_bf = 0;}else if (bf == 0)	// 新增节点是自己{parent->_bf = 0;subR->_bf = 0;}else if (bf == -1)	// 在b插入新节点{parent->_bf = 0;subR->_bf = 1;}else	// 理论不会走到这里{assert(false);}
}

总结: 假如以 pParent 为根的子树不平衡,即 pParent 的平衡因子为 2 或者 -2,分以下情况考虑:

  1. pParent 的平衡因子为 2,说明 pParent 的右子树高,设 pParent 的右子树的根为 pSubR
    • 当 pSubR 的平衡因子为 1 时,执行左单旋
    • 当 pSubR 的平衡因子为 -1 时,执行右左双旋
  2. pParent 的平衡因子为 -2,说明 pParent 的左子树高,设 pParent 的左子树的根为 pSubL
    • 当 pSubL 的平衡因子为 -1 时,执行右单旋
    • 当 pSubL 的平衡因子为 1 时,执行左右双旋

旋转完成后,原以 pParent 为根的子树个高度降低,已经平衡,不需要再向上更新。

现在已经将 AVL 树的旋转写完了,那么现在就来验证一下写得对不对。

AVL树的验证

AVL 树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证 AVL 树,可以分两步:1、验证其为二叉搜索树:如果中序遍历可得到一个有序的序列,就说明为二叉搜索树。2、验证其为平衡树:节点的平衡因子是否计算正确,每个节点子树高度差的绝对值不超过 1(注意节点中如果没有平衡因子)。

public:// 判断是否是平衡树bool IsBalance(){return _IsBalance(_root);}// 中序遍历判断是否是二叉搜索树void InOrder(){return _InOrder(_root);}private:// 求二叉树的高度int Height(Node* parent){if (parent == nullptr)return 0;return max(Height(parent->_left), Height(parent->_right)) + 1;}bool _IsBalance(Node* root){if (root == nullptr)return true;int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);int diff = rightHeight - leftHeight;return abs(diff) < 2&& _IsBalance(root->_left)&& _IsBalance(root->_right);}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}

测试样例

void AVLTreeTest1()
{size_t N = 1000;srand(time(nullptr));AVLTree t;for (size_t i = 0; i < N; ++i){int x = rand();t.Insert(make_pair(x, i));}t.InOrder();cout << endl;cout << "IsBalance:" << t.IsBalance() << endl;
}void AVLTreeTest2()
{int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };  // 测试双旋平衡因子调节//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };AVLTree t1;for (auto e : a){t1.Insert(make_pair(e, e));}t1.InOrder();cout << "IsBalance:" << t1.IsBalance() << endl;
}

在这里插入图片描述
在这里插入图片描述

AVL树的删除(了解)

因为 AVL 树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子。最差情况下,需要一直要更新到根节点的位置。

具体实现大家可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。

AVL树的性能

AVL 树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过 1,这样可以保证查找高效的时间复杂度,即 log2Nlog_2 Nlog2​N。但是如果要对 AVL 树做一些结构修改的操作,性能非常低下。比如:插入时要维护其绝对平衡,旋转的次数比较多。更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑 AVL 树,但一个结构经常修改,就不太适合。

在这里插入图片描述

AVL树的完整代码

#pragma oncetemplate 
struct AVLTreeNode
{AVLTreeNode* _parent;AVLTreeNode* _left;AVLTreeNode* _right;pair _kv;int _bf;	// balance factorAVLTreeNode(const pair& kv): _parent(nullptr), _left(nullptr), _right(nullptr), _kv(kv), _bf(0){}
};template 
class AVLTree
{typedef AVLTreeNode Node;
public:bool Insert(const pair& kv){if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else{return false;}}// 插入节点cur = new Node(kv);// 判断在父节点的哪一边插入if (parent->_kv.first < kv.first)parent->_right = cur;elseparent->_left = cur;cur->_parent = parent;// 控制平衡并更新平衡因子while (parent){// 新插入节点在父节点的右边,则父节点平衡因子自加一if (cur == parent->_left)--parent->_bf;else++parent->_bf;if (parent->_bf == 0){break;}else if (abs(parent->_bf) == 1){//cur = parent;//parent = parent->_parent;parent = parent->_parent;cur = cur->_parent;}else if (abs(parent->_bf) == 2){// 说明parent所在的子树已经不平衡了,需要旋转处理if (parent->_bf == 2 && cur->_bf == 1)RotateL(parent);else if (parent->_bf == -2 && cur->_bf == -1)RotateR(parent);else if (parent->_bf == -2 && cur->_bf == 1)RotateLR(parent);else if (parent->_bf == 2 && cur->_bf == -1)RotateRL(parent);elseassert(false);break;}else{assert(false);}}return true;}bool IsBalance(){return _IsBalance(_root);}void InOrder(){return _InOrder(_root);}private:void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node* ppNode = parent->_parent;subR->_left = parent;parent->_parent = subR;if (parent == _root){_root = subR;subR->_parent = nullptr;}else{// 判断原父节点在左边还是右边if (ppNode->_left == parent)ppNode->_left = subR;elseppNode->_right = subR;subR->_parent = ppNode;}parent->_bf = subR->_bf = 0;}void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* ppNode = parent->_parent;subL->_right = parent;parent->_parent = subL;if (parent == _root){_root = subL;subL->_parent = nullptr;}else{if (ppNode->_left == parent)ppNode->_left = subL;elseppNode->_right = subL;subL->_parent = ppNode;}parent->_bf = subL->_bf = 0;}// 左右双旋void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;// 通过subLR的平衡因子来区分各种情况int bf = subLR->_bf;// 先左单旋再右单旋RotateL(subL);RotateR(parent);subLR->_bf = 0;if (bf == 1)	// 在c插入新节点{parent->_bf = 0;subL->_bf = -1;}else if (bf == 0)	// 新增节点就是自己{parent->_bf = 0;subL->_bf = 0;}else if (bf == -1)	// 在b插入新节点{parent->_bf = 1;subL->_bf = 0;}else{assert(false);}}// 右左双旋void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;// 通过subRL的平衡因子来区分各种情况int bf = subRL->_bf;// 先右单旋再左单旋RotateR(subR);RotateL(parent);subRL->_bf = 0;if (bf == 1)	// 在c插入新节点{parent->_bf = -1;subR->_bf = 0;}else if (bf == 0)	// 新增节点是自己{parent->_bf = 0;subR->_bf = 0;}else if (bf == -1)	// 在b插入新节点{parent->_bf = 0;subR->_bf = 1;}else{assert(false);}}int Height(Node* parent){if (parent == nullptr)return 0;return max(Height(parent->_left), Height(parent->_right)) + 1;}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}bool _IsBalance(Node* root){if (root == nullptr)return true;int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);int diff = rightHeight - leftHeight;return abs(diff) < 2&& _IsBalance(root->_left)&& _IsBalance(root->_right);}private:Node* _root = nullptr;
};

👉总结👈

本篇博客主要讲解了什么是 AVL 树、AVL 树的插入、旋转、验证等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...