当前位置: 首页 > news >正文

CD76.【C++ Dev】AVL的模拟实现(1) 以左单旋为切口,分析旋转规律

目录

1.知识回顾

2.节点类

3.insert函数(限于篇幅,只分析左单旋)

插入节点

更新平衡因子

从1变0

从0变1

平衡因子的向上调整算法

从-1变-2和从1变2(AVL树失衡,需要调整)

★★以左单旋为例,深度剖析调整规律(全文重点)★★

先向上调整

更新平衡因子+向上找到平衡因子越界的节点的代码

平衡因子为2,已经越界,停止向上调整值,需要调整AVL树,进行左单旋

从模块的角度讨论左单旋

前置知识:判断二叉搜索树的节点的大小关系

从简单的情况逐步演变,推出规律

旋转的提示

RotateLeft函数编写

★总结平衡因子更新策略

4.二叉树可视化在线网站


1.知识回顾

之前在C++ Contest专栏讲过AVL树:

CC40.【C++ Cont】二叉搜索树和平衡二叉树

本文深入讲解模拟实现

2.节点类

使用三叉链的写法,存3个指针

template<class K,class V>
class AVLTreeNode
{
public:AVLTreeNode(const std::pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr),_pack(kv),_bf(0){ }AVLTreeNode<K,V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;int8_t _bf;std::pair<K, V> _pack;
};

注:1.三叉链:比一般的二叉树的节点类要多用一个指针指向父节点,这样就方便向上找根节点

2._bf是Balance Factor平衡因子的缩写,一开始插入这个节点的平衡因子为0,因为这个节点没有左子树或右子树

3.insert函数(限于篇幅,只分析左单旋)

class AVLTree
{typedef AVLTreeNode<K, V> Node;
public://......
private:Node* _root;
};

插入节点

一开始是个空树,直接修改_root为插入的节点

Node* node = new Node(kv);
if (_root == nullptr)
{_root = node;return true;
}

然后借用之前的二叉搜索树的代码:

while (cur)
{if (cur->_pack.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_pack.first > kv.first){parent = cur;cur = cur->_left;}else//cur->_pack.first == kv.first{return false;}
}

利用parent指向的节点存储的的左右指针来确定node的位置:

如果parent->_pack.first > kv.first,那么:

如果parent->_pack.first < kv.first,那么:

//找到了需要修改指针的节点,设置指针
if (parent->_pack.first < kv.first)parent->_right = node;
else//parent->_pack.first > kv.firstparent->_left = node;
cur = node;
node->_parent = parent;

更新平衡因子

设平衡因子的大小==子树的高度-子树的高度

枚举平衡因子大小变化的情况:

注: 一般情况下,平衡因子不可能绝对值为3,但程序可能出bug,建议代码中额外判断一下

更新平衡因子后,某些情况下插入节点会导致AVL树不平衡,那么判断AVL树是否平衡需要看平衡因子

平衡因子的值在[-1,1]之间变化是正常的,如果从-1到-2或者-2到-1,需要将不平衡的树调整为AVL树

下面看6种变化情况的示意图:

下面分析这棵标注了平衡因子的AVL树

从1变0

树仍然是平衡的,不需要调整

cur指向7.5,parent指向8,那么parent的平衡因子的变化为parent->_bf--,但parent的祖先节点们的平衡因子是不用调整的,因为平衡因子变成0说明以parent指向的根节点对应的树高度不变

平衡因子从0变成1从0变成-1,树高度

平衡因子从1变成0从-1变成0,树高度不变

从0变1

cur指向6.5,parent指向8,那么parent的平衡因子的变化为parent->_bf++,但parent的祖先节点们的平衡因子是需要向上调整的,因为平衡因子变成0说明以parent指向的根节点对应的树高度变了,会影响上面节点对应的树的高度

平衡因子的向上调整算法

需要向上调整上图parent指向节点的平衡因子和和的祖先节点们的平衡因子

向上调整即沿着祖先路径向上更新,而且向上更新有parent指针操作会很简单

沿着祖先路径更新平衡因子,如果平衡因子更新为0,就不用向上继续更新,因为平衡因子变成0说明\根节点对应的树高度不变

略去从-1变0和从0变-1,画法和平衡因子的向上调整算法同上,不再赘述

从-1变-2和从1变2(AVL树失衡,需要调整)

从-1变-2和从1变2都会时平衡因子越界,导致AVL树失衡,需要调整

★★以左单旋为例,深度剖析调整规律(全文重点)★★

将新节点插入到最小不平衡子树的根的右边,即CC40.【C++ Cont】二叉搜索树和平衡二叉树文章提到的RR型

先向上调整

更新平衡因子+向上找到平衡因子越界的节点的代码

循环结束后,cur指向新插入的节点,parent指向新插入节点的父节点

//向上更新平衡因子
while (parent)
{if (parent->_left == cur)parent->_bf--;else//parent->_right == curparent->_bf++;if (parent->_bf == 0)break;else if (abs(parent->_bf) == 1){//继续向上更新cur = parent;parent = parent->_parent;}else if (abs(parent->_bf) == 2){//需要调整}else//防止出现异常情况{cout << "Balance Factor out of range!" << endl;assert(false);}
}
平衡因子为2,已经越界,停止向上调整值,需要调整AVL树,进行左单旋
从模块的角度讨论左单旋
前置知识:判断二叉搜索树的节点的大小关系

对于以下的二叉搜索树,问a、b、c、d节点对应的值的大小关系

解:

显然b<a<c,d<c,那么d和a是什么大小关系?

明确节点的插入顺序:a、b、c节点比d先插入,当d插入时要沿着二叉树去找可以存放的位置:

d插入到c的左子树,那么由二叉搜索树的性质可知一定有d>a

那么答案为: b<a<d<c

从简单的情况逐步演变,推出规律
旋转的提示

1.保持是搜索树

2.变成平衡树且降低子树的高度  

★从最简单的情况看起,然后一步一步拓展★

注: 下面讨论的几种方法虽然能帮助考试快速写出正确代码,但是讨论不严谨,情况不全,之后会单独讲树插入的所有情况的枚举

情况A.最简单的情况

设c为新增节点:

节点a的平衡因子为2,需要旋转,根据前置知识,节点的大小关系为a<b<c

根据a<b<c旋转为AVL树:"a<b<c"的b处于不等式的中间,所以b为根

那么有:

就有旋转这一说法了:右边高,往左边旋转; 左边高,往右边旋转

情况B.拓展最简单的情况,未旋转前,如果a上面有根,然后插入c

向上检测到a的平衡因子为2,需要旋转:

得出a、b、c都小于d,将a、b、c旋转后,d的right指针要设置为b:

情况C.再拓展情况,未旋转前,如果a有左子树,然后插入c

向上检测到d的平衡因子为2,需要旋转:

[1,1,1,null,null,1,1,null,null,null,null,null,null,1]

节点的大小关系为:e<d<a<b<c,且一定有f>d和f<a成立,那么:

e<d<f<a<b<c

由前置知识得出:

方法1: 如果f做根节点,那么就将临近的3个节点a<b<c排成完全二叉搜索树:

方法2: 如果a做根节点,那么就将临近的3个节点e<d<f排成完全二叉搜索树:

这两种做法都可以,但修改步骤最少的是方法2

方法1:

//不考虑parent成员变量
f->left=d;
f->right=b;
a->left=nullptr;
a->right=nullptr;
d->right=nullptr;
b->left=nullptr;

方法2:

//不考虑parent成员变量
a->left=d;
d->right=f;

显然方法2步骤更少,效率更高!

情况D.再拓展情况,未旋转前,如果d上面有根且然后插入c

旋转上方和情况C一样,只需要设置上方的指针(......对应的节点的左指针和右指针)

左单旋(含设置平衡因子)之后,停止向上设置平衡因子,因为树已经平衡,且满足二叉搜索树的性质,插入函数结束

RotateLeft函数编写

注意既要考虑一般情况(上方的情况D),也要考虑特殊情况(上方的情况A、B和C),需要分类讨论

左旋的条件:从CC40.【C++ Cont】二叉搜索树和平衡二叉树文章得知,插在最小不平衡子树的根的孩子的子树上,如果从平衡因子来看,显然满足的条件为:

parent指向平衡因子为2的节点:

if (parent->_bf == 2 && cur->_bf == 1)
{RotateLeft(parent);break;//调整后就停止向上查找
}

私有成员函数RotateLeft只需要改动这几个指针:

一般情况下(上方讲的情况D):

先定义3个指针:

void RotateLeft(Node* parent)
{Node* cur = parent->_right;Node* cur_left = cur->_left;Node* grandparent = parent->_parent;//parent的父节点//......
}

修改指针成员变量:需要修改3个节点的_parent指针:

cur->_parent = grandparent;
parent->_parent = cur;
cur_left->_parent = parent;

再修改两个节点的_left和_right指针:

parent->_right = cur_left;
cur->_left = parent;

还要设置grandparent的_left或_right指向新节点(不容易考虑到)

if (grandparent->_left == parent)
{grandparent->_left = cur;
}
else//grandparent->_right == parent
{grandparent->_right = cur;
}

第一版代码:

void RotateLeft(Node* parent)
{Node* cur = parent->_right;Node* cur_left = cur->_left;Node* grandparent = parent->_parent;//parent的父节点cur->_parent = grandparent;parent->_parent = cur;cur_left->_parent = parent;parent->_right = cur_left;cur->_left = parent;if (grandparent->_left == parent){grandparent->_left = cur;}else//grandparent->_right == parent{grandparent->_right == cur;}
}

情况D退化到情况C,需要修改第一版的代码

情况C有一个很明显的标志:parent == _root且grandparent ==  nullptr 

需要对第一版代码添加判断条件,分出情况C和情况D

可以这样做:

if (parent == _root)
{//......
}
else
{//......
}

第二版代码:

void RotateLeft(Node* parent)
{Node* cur = parent->_right;Node* cur_left = cur->_left;Node* grandparent = parent->_parent;//parent的父节点//如果是情况C,那么因为有构造函数,grandparent为nullptr,cur->_parent值是正确的cur->_parent = grandparent;parent->_parent = cur;cur_left->_parent = parent;parent->_right = cur_left;cur->_left = parent;if (parent == _root){_root = cur;}else//grandparent!=nullptr{if (grandparent->_left == parent){grandparent->_left = cur;}else//grandparent->_right == parent{grandparent->_right == cur;}}
}

情况C退化到情况B,需要修改第二版的代码

为了分出情况B和情况C,需要判断cur_left是否为空

那么执行cur_left->_parent = parent前需要添加判断,否则可能出现nullptr->_parent = parent错误

第三版代码

void RotateLeft(Node* parent)
{Node* cur = parent->_right;Node* cur_left = cur->_left;Node* grandparent = parent->_parent;//parent的父节点//如果是情况C,那么因为有构造函数,grandparent为nullptr,cur->_parent值是正确的cur->_parent = grandparent;parent->_parent = cur;if (cur_left){cur_left->_parent = parent;}parent->_right = cur_left;//如果cur_left为nullptr,那么parent->_rightt为nullptrcur->_left = parent;if (parent == _root){_root = cur;}else//grandparent!=nullptr{if (grandparent->_left == parent){grandparent->_left = cur;}else//grandparent->_right == parent{grandparent->_right == cur;}}
}

情况B退化到情况A,但不需要修改第三版的代码

最后再设置平衡因子

注:下图的数字表示平衡因子,其大小定义为右子树的高度-左子树的高度

如果是情况A:

如果是情况B:

如果是情况C:

如果是情况D:

向上更新平衡因子时已经做过一部分平衡因子的调整内容,这里仅需要调整parent和cur的平衡因子

在RotateLeft的结尾写上:

parent->_bf = cur->_bf = 0;

结论:旋转只操作子树,而且会降低这个子树的高度

★总结平衡因子更新策略

平衡因子的作用: 检测树是否平衡

1. 新增在左,parent平衡因子--
2. 新增在右,parent平衡因子++
3. 更新后parent平衡因子==0,说明parent所在的子树的高度不变,不会再影响祖先,不用再继续沿着到root的路径往上更新,回到第1步继续调整,如果整棵树的根的平衡因子调整完了,插入函数结束
4. 更新后parent平衡因子==1或-1,说明parent所在的子树的高度变化,会再影响祖先,需要继续沿着到root的路径往上更新,回到第1步继续调整,如果整棵树的根的平衡因子调整完了,插入函数结束

5. 更新后parent平衡因子==2或 -2,说明parent所在的子树的高度变化且不平衡,需要对parent所在子树进行旋转,让树平衡,插入函数结束

4.测试代码

测试含测试单关键字的AVL树:

#include "AVLTree.h"
using namespace std;
int main()
{AVLTree<int, nullptr_t> tree;//测试单关键字tree.insert(make_pair(1,nullptr));tree.insert(make_pair(2, nullptr));tree.insert(make_pair(3, nullptr));return 0;
}

运行结果:

继续添加节点:

tree.insert(make_pair(4, nullptr));
tree.insert(make_pair(5, nullptr));

运行结果:

继续添加节点:

测试的结果都没有问题

5.二叉树可视化在线网站

https://www.cs.usfca.edu/~galles/visualization/AVLtree.html可动态展示AVL树的增删查改


文章转载自:

http://wjqx3qju.rnytd.cn
http://VBcn1oxj.rnytd.cn
http://v72ictT1.rnytd.cn
http://TenICwAI.rnytd.cn
http://7C5l5zIl.rnytd.cn
http://UBeli6E9.rnytd.cn
http://cstuFwoe.rnytd.cn
http://Tu0ERDpB.rnytd.cn
http://yJSAWC5i.rnytd.cn
http://UGDOSrFY.rnytd.cn
http://YznH7ZTQ.rnytd.cn
http://N705sYSG.rnytd.cn
http://ipIq9lt5.rnytd.cn
http://Nb5Kmg1A.rnytd.cn
http://1wsH9xVm.rnytd.cn
http://SCZyzPRB.rnytd.cn
http://xWHOFd5a.rnytd.cn
http://bxvZPLNm.rnytd.cn
http://cAInK1Jy.rnytd.cn
http://H4Av4C1l.rnytd.cn
http://QZOYyXEk.rnytd.cn
http://Eatt2SmZ.rnytd.cn
http://OgQ3WNEt.rnytd.cn
http://LxTtUCT7.rnytd.cn
http://SPC5aai1.rnytd.cn
http://WBdVb4sL.rnytd.cn
http://8QqejOBv.rnytd.cn
http://Js525GaX.rnytd.cn
http://Jcd47L7I.rnytd.cn
http://f9YP08f2.rnytd.cn
http://www.dtcms.com/a/371744.html

相关文章:

  • 中国计算机发展史
  • LeetCode刷题记录----20.有效的括号(Easy)
  • 从voice和练习发声谈起
  • 5.python——数字
  • 数据化运营的工作流程
  • llama_factory 安装以及大模型微调
  • Linux | i.MX6ULL 搭建 Web 服务器(第二十章)
  • 量子電腦組裝之三
  • 适配器详细
  • GD32自学笔记:5.定时器中断
  • 前端三件套简单学习:HTML篇1
  • Android --- SystemUI 导入Android Studio及debug
  • 服务器为什么会选择暴雨?
  • Spring Boot + Apache Tika 从文件或文件流中提取文本内容
  • day26|学习前端之算法学习
  • 数据结构之二叉树(2)
  • Mac设置中的安全性缺少“任何来源”
  • 样式化你的 Next.js 应用:CSS 模块、Tailwind CSS 和全局样式
  • Qwen2.5-VL技术详解
  • Claude code 使用笔记
  • FPGA学习笔记——SDR SDRAM的读写(不调用IP核版)
  • C++ 常见面试题汇总
  • cifar10分类对比:使用PyTorch卷积神经网络和SVM
  • 2025算法八股——机器学习——SVM损失函数
  • kafka特性和原理
  • webpack和vite优化方案都有哪些
  • Unity UI 中最干净的点击区域实现:RaycastZone 完整实战讲解
  • Java开发环境配置入门指南
  • lua中table键类型及lua中table的初始化有几种方式
  • 【CMake】缓存变量