当前位置:首页 » 《随便一记》 » 正文

C++ | AVL树

25 人参与  2024年10月31日 12:07  分类 : 《随便一记》  评论

点击全文阅读


前言

本篇博客讲解c++中数据结构AVL树,看这篇博客之前请先去看:C++ | 二叉搜索树-CSDN博客

? 个人主页:普通young man-CSDN博客

⏩ 文章专栏:C++_普通young man的博客-CSDN博客

⏩ 本人giee:   普通小青年 (pu-tong-young-man) - Gitee.com

      若有问题 评论区见?

?欢迎大家点赞?收藏⭐文章
————————————————


目录

AVL树的概念

AVL树的实现

        AVL树的结构

AVL树的插入

平衡因子的更新原则

更新停止条件

插入结点及更新平衡因子的代码实现

代码结构

代码步骤

旋转

旋转的原则

旋转类型

AVL树的查找

AVL树的平衡检测

总结


AVL树的概念

概念描述
定义AVL树是最先发明的自平衡二叉查找树,是一棵空树或满足特定条件的二叉搜索树。
平衡条件左右子树均为AVL树,且左右子树高度差的绝对值不超过1。
命名来源以发明者G. M. Adelson-Velsky E. M. Landis 的名字命名,他们是前苏联的科学家。
发表时间1962年论文《An algorithm for the organization of information》。
平衡因子结点的一个属性,等于其右子树的高度减去左子树的高度,通常为 -1、0 或 1。
平衡因子作用用于方便观察和控制树的平衡性,如同风向标一样指示结点的平衡状态。
高度平衡原因虽然理论上高度差为0是最理想的平衡状态,但在某些节点数的情况下(如2个或4个节点),无法达到高度差为0。
效率

由于高度受限,AVL树的操作(增删查改)效率可以保持在对数级别,优于普通的二叉搜索树。


AVL树的实现

        AVL树的结构

//定义节点结构template<class k,class v>struct AVLTreeNode{//pair组合k,v数据pair<k, v> _kv;//三插链AVLTreeNode<k, v>* _left;AVLTreeNode<k, v>* _right;AVLTreeNode<k, v>* _parent;//平衡因子int   _bf;//构造函数AVLTreeNode(const pair<k, v>& kv) :_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}};//树类template<class K, class V>class AVLTree{ typedef AVLTreeNode<K, V> Node;public: //...private: Node* _root = nullptr;};


AVL树的插入

按照二叉搜索树的规则插入

如果插入的值小于当前节点的值,则递归地向左子树进行插入;如果插入的值大于当前节点的值,则递归地向右子树进行插入;如果插入的值等于当前节点的值(通常不允许重复值),则不进行插入。

更新高度和平衡因子

在完成插入操作之后,从插入节点开始向上回溯到根节点,更新每个祖先节点的高度和平衡因子。高度是节点的最大子树高度加一。平衡因子是左子树高度减去右子树高度。

检测并修正不平衡

如果在回溯过程中发现任何一个节点的平衡因子的绝对值大于1(即不平衡),则需要对该节点所在的子树进行旋转来恢复平衡。旋转包括单旋转(LL或RR)双旋转(LR或RL)

旋转类型

左旋(L):当父节点的平衡因子为正数(表示左子树较高),并且当前节点的平衡因子也为正数时,表示当前节点的左子树较高,需要进行左旋来平衡树。

右旋(R):当父节点的平衡因子为负数(表示右子树较高),并且当前节点的平衡因子也为负数时,表示当前节点的右子树较高,需要进行右旋来平衡树。

先右后左双旋(RL):当父节点的平衡因子为正数(左子树较高),但当前节点的平衡因子为负数(当前节点的右子树较高)时,首先需要对当前节点进行右旋,然后对其父节点进行左旋。

先左后右双旋(LR):当父节点的平衡因子为负数(右子树较高),但当前节点的平衡因子为正数(当前节点的左子树较高)时,首先需要对当前节点进行左旋,然后对其父节点进行右旋。

示例表格

父节点(parent)平衡因子当前节点(cur)平衡因子旋转类型
21左旋(L)
2-1先右后左双旋(RL)
-2-1右旋(R)
-21先左后右双旋(LR)

平衡因子的更新原则

计算公式: 平衡因子 = 右子树高度 - 左子树高度更新时机: 只有当子树的高度发生变化时,才会更新当前节点的平衡因子。插入节点的影响: 如果新节点插入到父节点的右子树中,父节点的平衡因子 +1。如果新节点插入到父节点的左子树中,父节点的平衡因子 -1。

更新停止条件

平衡因子变为0: 当父节点的平衡因子由非0变为0时(例如从-1变为0或从1变为0),这意味着之前父节点的一侧较高,而新插入的节点恰好在较低的一侧,因此插入后整个子树的高度未变,不影响其父节点的平衡因子,此时可以停止更新。 平衡因子变为±1: 当父节点的平衡因子由0变为±1时(例如从0变为1或从0变为-1),这意味着新插入的节点使两侧的高度不再相等,但由于高度仅增加了1,因此仍符合AVL树的平衡要求,但需要继续向上更新,直到找到需要旋转的节点或满足其他停止条件。 平衡因子变为±2: 当父节点的平衡因子由±1变为±2时(例如从1变为2或从-1变为-2),这表示新插入的节点使较高一侧的高度进一步增加,破坏了平衡。此时需要进行旋转操作来重新平衡该子树,并降低子树的整体高度。旋转完成后,由于子树高度已恢复至插入前的状态,因此不需要继续向上更新,插入操作结束。
插入前平衡因子插入后平衡因子插入位置更新原则停止条件
0±1右子树+1继续更新
0±1左子树-1继续更新
±10左子树-1停止更新
±10右子树+1停止更新
±1±2右子树+1进行旋转
±1±2左子树-1进行旋转
00任意位置无变化

停止更新


插入13这个节点,平衡因子向上更新,更新到10这个节点的时候我们发现10平衡因子变成了2,代表了它的右子树高于左子树,这边需要做一个双旋

没更新到底部,这个树只需要更新平衡因子,不需要做旋转处理,因为平衡,从这里就可以看出树的平衡是看平衡因子

更新到最底部:算最坏的情况


插入结点及更新平衡因子的代码实现

bool insert(const pair<k, v>& kv) {    // 如果树为空,则创建根节点    if (_root == nullptr) {        _root = new Node(kv);        return true;    }    // 定义父节点和当前节点指针    Node* parent = nullptr;    Node* cur = _root;    // 寻找插入位置    while (cur != nullptr) {        if (cur->_kv.first > kv.first) {            parent = cur; // 当前节点大于新节点,向下移动到左子树            cur = cur->_left;        } else if (cur->_kv.first < kv.first) {            parent = cur; // 当前节点小于新节点,向下移动到右子树            cur = cur->_right;        } else {            // 如果键已经存在,则返回false            return false;        }    }    // 创建新的节点并插入    cur = new Node(kv);    // 将新节点连接到父节点上    if (parent->_kv.first > kv.first) {        parent->_left = cur; // 新节点成为父节点的左子节点    } else if (parent->_kv.first < kv.first) {        parent->_right = cur; // 新节点成为父节点的右子节点    }    // 设置新节点的父节点指针    cur->_parent = parent;    // 更新平衡因子并检查是否需要旋转来维持平衡    while (parent != nullptr) {        // 根据插入方向调整平衡因子        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) {                Rotati_l(parent); // 左旋            } else if (parent->_bf == -2 && cur->_bf == -1) {                Rotati_r(parent); // 右旋            } else if (parent->_bf == -2 && cur->_bf == 1) {                Rotati_LR(parent); // 先左后右双旋            } else if (parent->_bf == 2 && cur->_bf == -1) {                Rotati_RL(parent); // 先右后左双旋            } else {                assert(false); // 不应该达到此情况            }            break;        } else {            assert(false); // 不应该达到此情况,因为平衡因子应介于-2到2之间        }    }    return true;}

代码解析:

代码结构

这段代码是一个用于向 AVL 树中插入一个键值对 (pair<k, v>) 的函数 insert。AVL 树是一种自平衡的二叉查找树,其特点是任意节点的左右子树的高度差不超过 1。

代码步骤

检查根节点

if (_root == nullptr) {    _root = new Node(kv);    return true;}
首先检查根节点 _root 是否为空,如果为空,则直接创建一个新的节点作为根节点,并返回 true 表示插入成功。

寻找插入位置

Node* parent = nullptr;Node* cur = _root;while (cur) {    if (cur->_kv.first > kv.first) {        parent = cur;        cur = cur->_left;    } else if (cur->_kv.first < kv.first) {        parent = cur;        cur = cur->_right;    } else {        return false;    }}
定义两个指针 parent 和 cur 分别指向当前节点的父节点和当前节点。通过循环遍历树,找到合适的位置插入新节点。如果当前节点的键大于要插入的键,则向左子树移动;如果当前节点的键小于要插入的键,则向右子树移动。如果找到了键相同的节点,则返回 false 表示插入失败(AVL 树不允许键重复)。

插入新节点

cur = new Node(kv);if (parent->_kv.first > kv.first) {    parent->_left = cur;} else if (parent->_kv.first < kv.first) {    parent->_right = cur;}cur->_parent = parent;
创建一个新的节点 cur 并将键值对 kv 存入其中。根据父节点与新节点的关系,将新节点作为父节点的左子节点或右子节点。设置新节点的父节点指针。

更新平衡因子并进行必要的旋转

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) {        // 旋转操作        break;    } else {        assert(false);    }}

从新插入的节点开始向上遍历,更新每个节点的平衡因子。

检查每个节点的平衡因子: 如果平衡因子为 0,说明树已经恢复平衡,跳出循环。如果平衡因子为 ±1,表示轻微不平衡,继续向上遍历。如果平衡因子为 ±2,表示严重不平衡,需要进行旋转操作来重新平衡树。如果平衡因子不在上述范围内,则表示出现了不应该发生的情况。

旋转操作

根据具体情况选择适当的旋转操作:单左旋、单右旋或者双旋(先左后右或先右后左)。

旋转

这里可能会比较抽象,因为在我以前博客中二叉树并没有这样的操作,大家可以配合画图来进行阅读,这样会比较好理解。

旋转的原则

保持搜索树的规则:旋转后的树必须仍然是有效的二叉搜索树,即对于任意节点 NN,所有左子树中的节点的键值都小于 NN 的键值,所有右子树中的节点的键值都大于 NN 的键值。

让旋转的树从不平衡变为平衡,并且尽可能降低旋转树的高度:通过旋转操作,使树恢复到平衡状态,并尽量减少树的高度,从而提高树的操作效率。


旋转类型

AVL树中常见的旋转类型有四种:

1. 左单旋(Left Rotation)

适用场景:当新增节点导致节点 AA 的右子树 BB 的高度比左子树高 2 个单位时,并且 BB 的左子树高度小于等于其右子树高度。效果:将 BB 旋转到 AA 的位置,AA 成为 BB 的左子树。判断:如果失衡节点是2,代表该节点的左树低,右树高做左单旋

我们再看一个图:

这边插入parent->subR->right,15的平衡因子变成1,10的平衡因子变成了2,证明右树高于左树,需要左单旋。这里有一个规律:subR->left 给了parent->right,然后subR->left变成了parent.

        特殊情况:

parent不是root,那就是说parent还有一个父亲,这里需要判断处理。

        实现代码:

// 左旋函数定义void Rotate_l(Node* parent) {    Node* subR = parent->_right; // 获取父节点的右子节点    Node* subRL = subR->_left; // 获取右子节点的左子节点    // 旋转过程开始    parent->_right = subRL; // 将父节点的右子节点设置为右子节点的左子节点    if (subRL) { // 检查新的右子节点是否为空,避免野指针        subRL->_parent = parent; // 设置新右子节点的父节点为当前父节点    }    // 存储父节点的父节点,方便后续链接    Node* parentParent = parent->_parent;    subR->_left = parent; // 将右子节点的左子节点设置为当前父节点    parent->_parent = subR; // 设置当前父节点的父节点为右子节点    // 判断是否是根节点还是局部旋转,以便正确地链接上下节点    if (parentParent == nullptr) { // 如果当前节点是根节点        _root = subR; // 新的根节点是右子节点        subR->_parent = nullptr; // 根节点的父节点应该为 null    } else { // 如果不是根节点,则根据其位置进行相应的调整        if (parent == parentParent->_left) { // 当前节点是其父节点的左子节点            parentParent->_left = subR; // 将父节点的左子节点设置为右子节点        } else { // 当前节点是其父节点的右子节点            parentParent->_right = subR; // 将父节点的右子节点设置为右子节点        }        subR->_parent = parentParent; // 右子节点的父节点设置为其原来的父节点    }    // 更新旋转后节点的平衡因子    parent->_bf = subR->_bf = 0; // 假设每次旋转之后,这两个节点的平衡因子都变为0}

2. 右单旋(Right Rotation

适用场景:当新增节点导致节点 AA 的左子树 BB 的高度比右子树高 2 个单位时,并且 BB 的右子树高度小于等于其左子树高度。

效果:将 BB 旋转到 AA 的位置,AA 成为 BB 的右子树。

还是一样,上面这个图是给你展示是如何旋转的,下面我来介绍旋转的过程

这边插入parent->subL->left,5的平衡因子变成-1,10的平衡因子变成了-2,证明左树高于右树,需要右单旋。这里有一个规律:subL->right给了parent->left,然后subL->left变成了parent.

        特殊情况:

parent不是root,那就是说parent还有一个父亲,这里需要判断处理。

这三种何况都是一个概括,图三也计算了有多少种组合。

代码实现

// 右旋函数定义void Rotate_r(Node* parent) { // 注意:这里的参数名已从 Rotate_r 改为 Rotate_r 并且 Node* 类型前没有 const,因为我们会修改这些节点    Node* subL = parent->_left; // 获取父节点的左子节点    Node* sublr = subL->_right; // 获取左子节点的右子节点        // 旋转过程开始    parent->_left = sublr; // 将父节点的左子节点设置为左子节点的右子节点    if (sublr) { // 检查新的左子节点是否为空,避免野指针        sublr->_parent = parent; // 设置新左子节点的父节点为当前父节点    }        // 存储父节点的父节点,方便后续链接    Node* Pparent = parent->_parent;    subL->_right = parent; // 将左子节点的右子节点设置为当前父节点    parent->_parent = subL; // 设置当前父节点的父节点为左子节点        // 判断是否是根节点还是局部旋转,以便正确地链接上下节点    if (parent == _root) { // 如果当前节点是根节点        _root = subL; // 新的根节点是左子节点        subL->_parent = nullptr; // 根节点的父节点应该为 null    } else { // 如果不是根节点,则根据其位置进行相应的调整        if (Pparent->_left == parent) { // 当前节点是其父节点的左子节点            Pparent->_left = subL; // 将父节点的左子节点设置为左子节点        } else if (Pparent->_right == parent) { // 当前节点是其父节点的右子节点            Pparent->_right = subL; // 将父节点的右子节点设置为左子节点        }        subL->_parent = Pparent; // 左子节点的父节点设置为其原来的父节点    }        // 更新旋转后节点的平衡因子    subL->_bf = parent->_bf = 0; // 假设每次旋转之后,这两个节点的平衡因子都变为0}
 

3. 左右双旋(Left-Right Double Rotation)

适用场景:当新增节点导致节点 AA 的右子树 BB 的高度比左子树高 2 个单位时,并且 BB 的左子树高度大于其右子树高度。过程:首先对 BB 进行左旋,然后对 AA 进行右旋。效果:先通过左旋使 BB 的左子树成为新的右子树,再通过右旋使新的右子树成为 AA 的新位置。

这两个图插入之后都变的不平衡,第一个图,对他进行了一个右旋,会发现他只是对称了一下,出现这个问题的原因是插入位置的不同,左旋和右旋都是插入在最大,最小的位置。而这里插入的是小中最大的位置。如何判断是这个位置:平衡因子

大家可以边看代码边画图自己分析一下就懂了

// 左右旋转调整(LR旋转)函数// 参数parent是需要进行旋转操作的节点的父节点void Rotate_LR(Node* parent) {    // 获取parent的左子节点    Node* subl = parent->_left;    // 获取subl的右子节点,这将是旋转的核心节点    Node* sublr = subl->_right;        // 存储sublr的平衡因子    int bf = sublr->_bf;    // 首先执行左旋操作来调整subl节点    Rotate_L(parent->_left);        // 然后执行右旋操作来调整parent节点    Rotate_R(parent);        // 根据sublr的平衡因子更新相关节点的平衡因子    if (bf == 0) {        // 如果sublr的平衡因子为0,则所有相关节点的平衡因子都设为0        subl->_bf = 0;        sublr->_bf = 0;        parent->_bf = 0;    }    else if (bf == 1) {        // 如果sublr的平衡因子为1,则设置parent的平衡因子为0,subl的为-1,sublr的为0        parent->_bf = 0;        subl->_bf = -1;        sublr->_bf = 0;    }    else if (bf == -1) {        // 如果sublr的平衡因子为-1,则设置subl的平衡因子为0,sublr的为0,parent的为1        subl->_bf = 0;        sublr->_bf = 0;        parent->_bf = 1;    } else {        // 如果平衡因子不是以上三种情况中的任何一种,则断言失败        assert(false);    }}

4. 右左双旋(Right-Left Double Rotation)

适用场景:当新增节点导致节点 AA 的左子树 BB 的高度比右子树高 2 个单位时,并且 BB 的右子树高度大于其左子树高度。过程:首先对 BB 进行右旋,然后对 AA 进行左旋。效果:先通过右旋使 BB 的右子树成为新的左子树,再通过左旋使新的左子树成为 AA 的新位置。

这个和左右旋是一个相反的概念,就不做讲解

//右左旋void Rotati_RL(Node* parent) {Node* subr = parent->_right;Node* subrl = subr->_left;//存储subrl的平衡因子int bf = subrl->_bf;//旋转Rotati_r(parent->_right);Rotati_l(parent);//更新平衡因子if (bf == 0) {subr->_bf = 0;subrl->_bf = 0;parent->_bf = 0;}else if (bf == 1){parent->_bf = -1;subrl->_bf = 0;subr->_bf = 0;}else if (bf == -1){parent->_bf = 0;subrl->_bf = 0;subr->_bf = 1;}else{assert(false);}}

AVL树的查找

这个二叉树的时候经常用:

Node* Find(const k& key){Node* cur = _root;while (cur){if (cur->_kv.first < key){cur = cur->_right;}else if (cur->_kv.first > key){cur = cur->_left;}else{return cur;}}return nullptr;}

AVL树的平衡检测

我们是如何判断AVL树是否平衡?

我们可以通过检查左右子树高度差的的程序进行反向验证,同时检查⼀下结点的平衡因子更新是否出现了问题。

直接代码:

如果右递归不是很好的朋友,可以画递归展开图来帮助自己理解

//计算高度int _Height(Node* root) {if (root == nullptr)return 0;int LeftHeight = _Height(root->_left);int RightHeight = _Height(root->_right);return LeftHeight > RightHeight ? LeftHeight + 1 : RightHeight + 1;}//判断是否是否平衡bool _IsBalanceTree(Node* root) {//空树也是AVL树if (root == nullptr) return true;//计算高度差int LeftHeight = _Height(root->_left);int RightHeight = _Height(root->_right);int diff =RightHeight - LeftHeight;//判断if (abs(diff) >= 2){cout << root->_kv.first << "⾼度差异常" << endl;assert(false);return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因⼦异常" << endl;return false;}// pRoot的左和右如果都是AVL树,则该树⼀定是AVL树 return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);}

总结

先附上我的git:

AVL树/AVL树 · 普通小青年/c++学习 - 码云 - 开源中国 (gitee.com)

AVL树通过维护每个节点的平衡因子来保持树的平衡状态。当插入,如果发现某节点的平衡因子不在{-1, 0, 1}范围内,则需要根据具体情况选择适当的旋转策略来进行调整,以恢复树的平衡性。

每个节点都有一个平衡因子(balance factor, BF),它定义为该节点的左子树高度减去右子树高度: BF=height(left subtree)−height(right subtree)BF=height(left subtree)−height(right subtree)

平衡因子的可能值为 -1、0 或 1。如果平衡因子的绝对值大于1,则表示树不平衡,需要进行旋转调整。


​​​​​​​


点击全文阅读


本文链接:http://m.zhangshiyu.com/post/180411.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

最新文章

  • 林语熙周晏京(离婚后,老公天天跪求复合全集阅读)最新章节免费在线阅读_《离婚后,老公天天跪求复合全集阅读》最新热门小说 -
  • 顾绫雪嬴政《被始皇读心后,文武百官卷疯了!完结版阅读》完整版免费在线阅读_(顾绫雪嬴政)全集免费阅读 -
  • 情深意长皆成空完整版阅读(沈卿林砚辞)抖音热文_《情深意长皆成空完整版阅读》最新章节免费在线阅读 -
  • 姐姐为暴富重伤傅家金孙后火葬场了全集阅读小说(傅延江瑶)全文免费阅读无弹窗大结局_(姐姐为暴富重伤傅家金孙后火葬场了全集阅读免费阅读全文大结局)最新章节列表_笔趣阁(姐姐为暴富重伤傅家金孙后火葬场了全集阅读) -
  • 乔以诺萧瑾淮(不是你好是我好全集阅读)精彩试读_《不是你好是我好全集阅读》全本阅读 -
  • 白月光双双《重生后我成全老公和白月光,他却急了全集》全文免费阅读无弹窗大结局_(白月光双双)最新章节免费在线阅读 -
  • 苏小枫苏末小说免费笔趣阁_苏小枫苏末小说全章完本大结局
  • 死遁后他疯了最新小说全文阅读_最新免费小说沈轻洲林梦江之雪_完本小说(死遁后他疯了)
  • 免费小说《顾里宋婷婷小岳欧颖倩》已完结(顾里宋婷婷小岳欧颖倩)热门小说大结局全文阅读笔趣阁
  • 侄子为求富贵,变性后全家后悔最新小说_免费小说全文阅读(苏小枫苏末)_侄子为求富贵,变性后全家后悔苏小枫苏末小说推荐完结
  • 最新《沈轻洲林梦江之雪》小说(全集完整新上小说大结局(沈轻洲林梦江之雪))全文阅读笔趣阁
  • 拒绝嫁给姐夫后,我在八零年代暴富了(陈小棠沈正韩)阅读 -

    关于我们 | 我要投稿 | 免责申明

    Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1