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

【C++】平衡二叉树(AVL树)迭代版

目录

前言:

一:判断一棵树是否为平衡二叉树

二:明确思路 

1.为什么使用平衡二叉树

2.旋转

2.1 左旋 

2.2 右旋

3.冲突节点

4.平衡因子

5.双旋

5.1 左右双旋(LR)

5.2 右左双旋(RL)

6.平衡因子的更新

7.冲突节点问题补充

三:创建平衡二叉树

1.定义树节点

2.定义树 

3.插入节点

3.1 平衡因子的迭代调整 

3.2 左单旋方法(RR类型)

3.3 右单旋方法(LL类型) 

3.4 RL双旋抽象图

①当 h = 0 时: 

②当 h = 1 时: 

③当 h = 2 时:  

RL双旋总结: 

3.5 LR双旋抽象图 

①当 h = 0 时: 

②当 h = 1 时: 

LR双旋总结: 

4.其他方法

四:AVL树迭代方法全部代码

总结:


前言:

本篇先进行劝退,需要一些C++语法基础,之后你必须掌握二叉搜索树才能看懂这篇文章。

平衡二叉树非常重要,它是由三个大佬发明的,也称AVL树,它是学习红黑树的基础。本篇主要使用C++的迭代方法实现,当然也会提供递归版本。如果你压根知道什么是平衡二叉树或者只是对它的一些旋转操作有些模糊,请你去B站看看up 蓝不过海呀 的平衡二叉树章节(平衡二叉树(AVL树)_哔哩哔哩_bilibili),看完之后对接下来的学习有帮助。那么接下来,开始学习之旅吧。

一:判断一棵树是否为平衡二叉树

在开始讲解红黑树之前,我们先来做一道题目:力扣-110.平衡二叉树。

这里先说平衡二叉树的定义是什么:左右两棵子树高度差绝对值不超过1。

我们先传入根节点,之后获取左子树高度和右子树高度,并递归判断是否为平衡二叉树,因为本篇重点为如何构建平衡二叉树,所以这里不详细解释只给出代码(伪代码):

int _GetHeight(Node* root)
{if (root == nullptr)return 0;//max (左子树高度, 右子树高度) + 1int leftH = _GetHeight(root->_left);int rightH = _GetHeight(root->_right);return leftH > rightH ? leftH + 1 : rightH + 1;
}bool _IsBalanceTree(Node* root)
{if (root == nullptr)return true;//看左子树高度和右子树高度差int leftH = _GetHeight(root->_left);int rightH = _GetHeight(root->_right);//获取高度差int diff = rightH - leftH;//为了方便调试 我们这里异常就打印出来if (abs(diff) > 1){return false;}return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

二:明确思路 

一个好的程序员其实敲代码时间不会很长,最长的的是在敲代码前苦思冥想的时间。

我们这里使用非递归方法,会比递归好想一些,但是鱼与熊掌不可兼得,总要付出一些代价。

1.为什么使用平衡二叉树

我们知道,一般情况下,搜索二叉树的查找时间是O(logN),这个时间复杂度已经很恐怖了。但是存在一种极端情况,就是如果我们插入的都是有序数据时,搜索二叉树就会退化为链表,只有一侧有数据,时间复杂度退化为O(N),这样使用搜索二叉树就没有什么效益了。

为了避免这种极端情况,我们三个大佬就研究出了一种平衡树,也就是之前说的左右子树高度差绝对值不超过1。

2.旋转

此时有{ 1, 2, 3 }这3个数据插入树(以下我们说的树都是平衡二叉搜索树)中,你会发现他们都在一边。

此时该怎么办?对没错,就是旋转。

2.1 左旋 

上述情况,我们可以将根节点向左旋转,将 “2” 作为新的根节点:

2.2 右旋

如果我们插入的是 { 3, 2, 1 } 呢?聪明的你肯定想到了就右旋这棵树:

那么好,恭喜你已经掌握了两个最基本的武器,我们就是利用这两个武器来完成平衡树的!

3.冲突节点

你肯定还有疑问?就这?没有其他情况了?有。我们先不考虑树很高的情况,这里先把冲突节点的问题解决。当我们左旋时,根节点的右孩子的左节点(有些拗口,不过大家可以看图)可能会存在节点,这是根节点向左旋就会产生冲突,所以这里有一个口诀:冲突的左孩变右孩。

也就是说,当左旋时有冲突节点,该节点变成根节点的右孩子(别急看图,原谅作者不会搞动画,必须抽空学习一下了!)。

(以上图中,subR表示parent右孩子,subRL表示subR的左孩子,下面例子中,我们都这样举例)

当然,右旋的话就是冲突的右孩变左孩:

4.平衡因子

既然我们已经知道了如何旋转,旋转的时候如何解决冲突。但是我们怎么知道什么时候进行旋转呢?这时候我们就需要用到平衡因子了。也就是说,每个节点中都有一个成员变量,就是平衡因子,它是判断我们是否需要旋转树的关键。

我们想:是不是当前树的左子树高度和右子树高度差绝对值大于2时,就应该旋转了?对,没错。但是我们要计算出平衡因子的具体值,这样才能知道该如何旋转它。

本篇使用的 平衡因子_bf = 右子树高度 - 左子树高度

所以,是不是当_bf(平衡因子)为2时,我们至少知道此时应该左旋这个节点;反之_bf为-2时,该右旋这个节点(当然有其他情况,不过我们后面补充)。

5.双旋

我们之前已经说明了两种情况:左旋 和 右旋。

它们是最基本的单旋,我们将所有节点标注上_bf再次观察:

当没有冲突节点时左旋

当有冲突节点时左旋

当有没冲突节点时右旋

当有冲突节点时右旋

但是难道只有这两种情况吗?当然还有其他情况,比如我们一开始就是这样插入的:

你可能会想,对情况一右旋,情况二左旋会不会解决问题?

你会发现无论如何使用左旋和右旋都解决不了,所以以上方法不行,但是怎么办呢? 

此时我们需要对其旋转两次,就情况一而言,首先是对subL左旋,之后对parent右旋。

5.1 左右双旋(LR)

对情况一而言,我们需要对subL先左旋,之后右旋:

通过上图我们发现,最终还是会转换为单旋。

5.2 右左双旋(RL)

通过上图我们可以发现(parent的孩子通通以cur统称) ,当cur和parent的平衡因子异号时,需要双旋,同号时,只需要单旋

所以其实一共有以下四种情况:

而且我们可以发现,LL型进行右单旋;RR型进行左单旋;LR型先左旋后右旋;RL型先右旋后左旋

6.平衡因子的更新

本篇主要讲解迭代法,那么_bf该如何更新呢?

我们可以发现,根据本篇的规则,当向右边插入,该节点_bf应当自增1,当向左边插入应当自减1。因为树要保持平衡,所以我们插入节点时,当当前节点不是非常平衡时(也就是1或者-1时)应当继续向上调整,比如:

或者另一种情况:

但是如果 _bf == 0 时,则说明不用继续向上更新,因为此时两边平衡,没有必要更新。

7.冲突节点问题补充

大家看到这里可以想一下,这里我们会循环调整,所以我们最开始说的单旋那种有冲突节点的情况会存在吗?对,根本不存在!因为有冲突节点的之前树就已经被调整到平衡状态了,那么是否说明我们可以不考虑冲突节点了呢?并不是,比如以下这棵树:

此时有冲突节点,我们左旋:

其实这里有一个结论:只要旋转,就不用继续往上调整了!

也就意味着我们只要进行了旋转操作,树就平衡了。

三:创建平衡二叉树

1.定义树节点

我们要明确思路,因为是迭代法,我们要去找父节点,所以这里我们定义树要有三个指针,左孩子,右孩子和父母,因为要调整树的平衡,所以要有平衡因子来作为判断旋转的依据,所以我们这样定义树节点(这里使用pair作为对象,是为了能够存储键值对,所以提供两个模板参数)。

成员变量名称前加上 "_" 方便和后面作区分。

template<class K, class V>
struct AVLTreeNode
{pair<K, V> _kv;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;int _bf; // balance factorAVLTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){}
};

这样树节点就定义好了。

2.定义树 

因为树节点名称太长,所以这里现对其重命名。树中成员有一个根节点即可。使用编译器提供的默认构造方法。

template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:AVLTree() = default;
private:Node* _root = nullptr;
};

3.插入节点

插入节点非常复杂,因为我们会去调整树,对其旋转,所以这里先按照二叉搜索树的逻辑写一个插入方法:

bool Insert(const pair<K, V>& 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;}else{parent->_left = cur;}cur->_parent = parent; //记得更新其父节点return true;
}

这里就是二叉搜索的逻辑,只不过是加上了_parent的修改,这里不在赘述。

3.1 平衡因子的迭代调整 

当我们插入节点时,上面讲到,如果平衡因子为-1或者1时,要迭代向上调整;为0时无需调整;为-2或2时旋转树且只旋转一次,所以我们先把大逻辑写好:

//因为可能更新到根
// 更新平衡因子
while (parent)
{if (cur == parent->_left)parent->_bf--;elseparent->_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);}
}

3.2 左单旋方法(RR类型)

parent->_bf == 2 && cur->_bf == 1 此时是RR类型,我们要进行左旋操作。

这里我们完善左旋函数(因为左旋使用者不会调用也不能调用,所以写成私有函数):

//这里我们使用的非递归 所以一种4种情况 我们每个都单独写一遍
//先写LL 左单旋
void RotateL(Node* parent)
{//这里需要记录传入节点的父节点Node* parentP = parent->_parent;//先把大逻辑写好 这里和递归的左旋逻辑一样 但是需要考虑的情况很多//比如 _bf平衡因子 _parent 等Node* subR = parent->_right;  //还是一样 先记录要作为根节点的值Node* subRL = subR->_left;    //之后记录冲突节点parent->_right = subRL;    //冲突的左孩变右孩subR->_left = parent;      //新根左孩是旧根//先考虑所有节点的 父节点 最先考虑subR, subRL//因为subRL可能为空 所以判断一下if (subRL){subRL->_parent = parent;}parent->_parent = subR; //这一步不需要判断 当不平衡时旋转 parent绝不为空//此时需要判断parentP是否为空 if (parentP){//不为空时 subR作为新的根节点 一定要改变其parentsubR->_parent = parentP;//此时不为空 需要判断 parent 在 parentP 的哪一边if (parent == parentP->_left){//在左边parentP->_left = subR;}else{//在右边parentP->_right = subR;}}else{//此时parentP为空 旋转好的节点作为根节点_root = subR;//还需要更新subR->_parentsubR->_parent = nullptr;}//之后就是_bf的更新了//parent的高度降低了 这里是直接降低了2层 因为我们是 == 2/-2 所以直接置0parent->_bf = 0;//新的根节点变平衡了 所以也置零subR->_bf = 0;   //之后后面会向上更新
}

因为冲突节点可能为空,所以我们要单独判断。而且我们发现,单旋只有两个节点高度改变,且调整完之后高度平衡,所以更新两个高度改变节点的_bf,都是0

还要记得更新_parent节点。且可能要改变根节点(大家可以看一下注释,很清楚)。

3.3 右单旋方法(LL类型) 

parent->_bf == -2 && cur->_bf == -1 此时是LL类型,我们要进行右旋操作。这里我们完善右旋函数:

//RR 右单旋
void RotateR(Node* parent)
{//还是要记录传入节点的父节点Node* parentP = parent->_parent;//还是先把大逻辑写好//这里是冲突的右孩变左孩Node* subL = parent->_left;Node* subLR = subL->_right;//之后开始旋转subL->_right = parent;parent->_left = subLR;//之后还是一样 先考虑他们_parentparent->_parent = subL;//先考虑 subLR 是否为空if (subLR){subLR->_parent = parent;}//考虑传入节点是否为根节点if (parent == _root){subL->_parent = nullptr;_root = subL;}else{//判断在传入节点的哪一侧if (parent == parentP->_left){parentP->_left = subL;}else{parentP->_right = subL;}subL->_parent = parentP; //记得更新新根节点的_parent}//之后就是 _bf 平衡因子 只有两个改变了 都变为了0parent->_bf = 0;subL->_bf = 0;
}

这里和左旋就是镜像了。

为了方便各位理解,这里使用抽象图来说明插入的一些情况。

3.4 RL双旋抽象图

①当 h = 0 时: 

也就意味着a,b,c,d没有节点,60就是当前插入的节点。此时RL双旋后,该树平衡。

所以我们如果写RL函数,里面可以复用左单旋和右单旋,但是我们要更新_parent_bf,但是我们之前写的单旋函数里面都会改变调整节点的_bf, 所以我们要在旋转前记录_bf。至于记录哪个,其实就是插入节点(cur)的_bf

//RL 右左双旋
void RotateRL(Node* parent)
{//记住 在外满足条件 parent->_bf == 2 && cur->_bf == -1 才会使用这个双旋//此时我们要判断cur->_left的平衡因子来更新新的平衡因子//这里我们还需要改变平衡因子 必须在旋转之前拿到平衡因子Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;  //关键就看要插入节点的parent的平衡因子//我们直接复用代码即可RotateR(subR); //先将右节点右旋RotateL(parent);         //再将传入节点左旋//...
}
②当 h = 1 时: 

⑴此时插入的位置在右边:

⑵此时插入的位置在左边:

所以此时我们就可以给出RL方法了:

	//RL 右左双旋void RotateRL(Node* parent){//记住 在外满足条件 parent->_bf == 2 && cur->_bf == -1 才会使用这个双旋//此时我们要判断cur->_left的平衡因子来更新新的平衡因子//这里我们还需要改变平衡因子 必须在旋转之前拿到平衡因子Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;  //关键就看要插入节点的parent的平衡因子//我们直接复用代码即可RotateR(subR); //先将右节点右旋RotateL(parent);         //再将传入节点左旋if (bf == 0){//这时的情况就是//     a      ----> parent//      \     //       b    ----> subR//      ///     c      ----> subRL   c就是新插入的节点 最后一定平衡subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;//之后就是其他的插入情况 就是可能会插入 c 的左边或者右边 //所以我们就要通过subRL(c)的平衡因子来判断情况}  else if (bf == 1){//这里是插入 c 的右边parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (bf == -1){//这里是插入 c 的左边parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else{assert(false);}}
③当 h = 2 时:  

我们先来观察抽象图,当 h = 2 时:

RL双旋总结: 

其实最终就像我们之前 h = 1 一样,最终subRL都会变为新的根节点,所以调整后一定两边平衡,也就是说 subRL->_bf = 0

subR->_bfparent->_bf 是根据插入节点在 subRL 左边还是右边决定的(以 h = 1 为例)。

  • 当插入 subRL 的右边时(subRL->_bf = 1),最终会使插入节点在 subR 的左边,从而 subR->_bf = 0 。而 parent->_bf = -1 。
  • 当插入 subRL 的左边时(subRL->_bf = -1),最终会使插入节点在 parent 的右边,从而 parent->_bf = 0 。而 subR->_bf = 1 。
  • 最终 subRL 都会变为新的根节点,左右两边平衡,所以 subRL->_bf = 0

3.5 LR双旋抽象图 

①当 h = 0 时: 

②当 h = 1 时: 

⑴此时插入的位置在右边:

⑵此时插入的位置在左边:

这里就不介绍 h = 2 了,因为和RL是一样的。

所以我们给出LR方法:

	//LR 左右双旋void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;//旋转之前需要记录 subLR 的平衡因子int bf = subLR->_bf;//先对parent的左边节点左旋RotateL(subL);RotateR(parent);   //再对parent右旋//当原来只有三个节点时 最后每个平衡因子都是0if (bf == 0){//      a//     ///    b//     \//      cparent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1){//此时插入节点在 c 的右边subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else if (bf == -1){//此时插入节点在 c 的左边subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else{assert(false);}}
LR双旋总结: 

但是我们最终都可以得出和RL类似的结果:

最终subLR都会变为新的根节点,所以调整后一定两边平衡,也就是说 subLR->_bf = 0

subL->_bfparent->_bf 是根据插入节点在 subLR 左边还是右边决定的(以 h = 1 为例)。

  • 当插入 subLR 的右边时(subLR->_bf = 1),最终会使插入节点在 parent 的左边,从而 parent->_bf = 0 。而 subL->_bf = -1 。
  • 当插入 subLR 的左边时(subLR->_bf = -1),最终会使插入节点在 subL 的右边,从而 subL->_bf = 0 。而 parent->_bf = 1 。
  • 最终 subLR 都会变为新的根节点,左右两边平衡,所以 subLR->_bf = 0

4.其他方法(拷贝构造)

之后就是其他方法了,比如拷贝构造了,Find查找函数了,等等。你可能会问,删除呢?哈哈,这个小编精力有限,等有空了再搞定。这里其他方法对各位而言应该轻而易举,这里就不在一一赘述。

这里还是要强调和其他树的拷贝构造不同的是,我们要考虑其 _bf 和 _parent ,这里我们需要多添加几条语句:

四:AVL树迭代方法全部代码

这里先给出头文件(AVLTree.h)代码:

#pragma once
#include<iostream>
#include<assert.h>using namespace std;template<class K, class V>
struct AVLTreeNode
{pair<K, V> _kv;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;int _bf; // balance factorAVLTreeNode(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:AVLTree() = default;AVLTree(const AVLTree<K, V>& t){_root = Copy(t._root);}AVLTree<K, V>& operator=(AVLTree<K, V> t){swap(_root, t._root);return *this;}~AVLTree(){Destroy(_root);_root = nullptr;}bool Insert(const pair<K, V>& 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;}else{parent->_left = cur;}cur->_parent = parent;//因为可能更新到根// 更新平衡因子while (parent){if (cur == parent->_left)parent->_bf--;elseparent->_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){//左旋RotateL(parent);}else if (parent->_bf == -2 && cur->_bf == -1){//右旋RotateR(parent);}else if (parent->_bf == 2 && cur->_bf == -1){//可以发现规律 同号单旋 异号双旋//这里我们先把步骤写全 RL 右左双旋RotateRL(parent);}else{//左右双旋RotateLR(parent);}//旋转完以后就不用处理了break;}else{//此时旋转也解决不了问题 直接报错assert(false);}}return true;}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;}//写一个中序遍历void Inorder(){//写一个子函数_Inorder(_root);cout << endl;}void Preorder(){_Preorder(_root);cout << endl;}//写一函数 检查自己是否是平衡树bool IsBalanceTree(){return _IsBalanceTree(_root);}//获取树高int GetHeight(){return _GetHeight(_root);}private://因为没有必要给用户提供旋转、销毁等函数 所以写成私有的int _GetHeight(Node* root){if (root == nullptr)return 0;//max (左子树高度, 右子树高度) + 1int leftH = _GetHeight(root->_left);int rightH = _GetHeight(root->_right);return leftH > rightH ? leftH + 1 : rightH + 1;}bool _IsBalanceTree(Node* root){if (root == nullptr)return true;//看左子树高度和右子树高度差int leftH = _GetHeight(root->_left);int rightH = _GetHeight(root->_right);//获取高度差int diff = rightH - leftH;//if (abs(diff) > 1 || root->_bf != diff)//	return false;  //这里顺便把平衡因子检查了//为了方便调试 我们这里异常就打印出来if (abs(diff) > 1){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);}void _Inorder(Node* root){if (root == nullptr){return;}_Inorder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << " ";_Inorder(root->_right);}void _Preorder(Node* root){if (root == nullptr)return;cout << root->_kv.first << ":" << root->_kv.second << " ";_Preorder(root->_left);_Preorder(root->_right);}//这里我们使用的非递归 所以一种4种情况 我们每个都单独写一遍//先写LL 左单旋void RotateL(Node* parent){//这里需要记录传入节点的父节点Node* parentP = parent->_parent;//先把大逻辑写好 这里和递归的左旋逻辑一样 但是需要考虑的情况很多//比如 _bf平衡因子 _parent 等Node* subR = parent->_right;  //还是一样 先记录要作为根节点的值Node* subRL = subR->_left;    //之后记录冲突节点parent->_right = subRL;    //冲突的左孩变右孩subR->_left = parent;      //新根左孩是旧根//先考虑所有节点的 父节点 最先考虑subR, subRL//因为subRL可能为空 所以判断一下if (subRL){subRL->_parent = parent;}parent->_parent = subR; //这一步不需要判断 当不平衡时旋转 parent绝不为空//此时需要判断parentP是否为空 if (parentP){//不为空时 subR作为新的根节点 一定要改变其parentsubR->_parent = parentP;//此时不为空 需要判断 parent 在 parentP 的哪一边if (parent == parentP->_left){//在左边parentP->_left = subR;}else{//在右边parentP->_right = subR;}}else{//此时parentP为空 旋转好的节点作为根节点_root = subR;//还需要更新subR->_parentsubR->_parent = nullptr;}//之后就是_bf的更新了//parent的高度降低了 这里是直接降低了2层 因为我们是 == 2/-2 所以直接置0parent->_bf = 0;//新的根节点变平衡了 所以也置零subR->_bf = 0;   //之后后面会向上更新}//因为要把所有的旋转情况都写一遍 所以这里写右单旋 RR//RR 右单旋void RotateR(Node* parent){//还是要记录传入节点的父节点Node* parentP = parent->_parent;//还是先把大逻辑写好//这里是冲突的右孩变左孩Node* subL = parent->_left;Node* subLR = subL->_right;//之后开始旋转subL->_right = parent;parent->_left = subLR;//之后还是一样 先考虑他们_parentparent->_parent = subL;//先考虑 subLR 是否为空if (subLR){subLR->_parent = parent;}//考虑传入节点是否为根节点if (parent == _root){subL->_parent = nullptr;_root = subL;}else{//判断在传入节点的哪一侧if (parent == parentP->_left){parentP->_left = subL;}else{parentP->_right = subL;}subL->_parent = parentP; //记得更新新根节点的_parent}//之后就是 _bf 平衡因子 只有两个改变了 都变为了0parent->_bf = 0;subL->_bf = 0;}//RL 右左双旋void RotateRL(Node* parent){//记住 在外满足条件 parent->_bf == 2 && cur->_bf == -1 才会使用这个双旋//此时我们要判断cur->_left的平衡因子来更新新的平衡因子//这里我们还需要改变平衡因子 必须在旋转之前拿到平衡因子Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;  //关键就看要插入节点的parent的平衡因子//我们直接复用代码即可RotateR(subR); //先将右节点右旋RotateL(parent);         //再将传入节点左旋if (bf == 0){//这时的情况就是//     a      ----> parent//      \     //       b    ----> subR//      ///     c      ----> subRL   c就是新插入的节点 最后一定平衡subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;//之后就是其他的插入情况 就是可能会插入 c 的左边或者右边 //所以我们就要通过subRL(c)的平衡因子来判断情况}  else if (bf == 1){//这里是插入 c 的右边parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (bf == -1){//这里是插入 c 的左边parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else{assert(false);}}//LR 左右双旋void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;//旋转之前需要记录 subLR 的平衡因子int bf = subLR->_bf;//先对parent的左边节点左旋RotateL(subL);RotateR(parent);   //再对parent右旋//当原来只有三个节点时 最后每个平衡因子都是0if (bf == 0){//      a//     ///    b//     \//      cparent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1){//此时插入节点在 c 的右边subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else if (bf == -1){//此时插入节点在 c 的左边subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else{assert(false);}}void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}Node* Copy(Node* root){if (root == nullptr)return nullptr;Node* newNode = new Node(root->_kv);newNode->_left = Copy(root->_left);newNode->_right = Copy(root->_right);//记得赋值平衡因子newNode->_bf = root->_bf;//更新_parentif (newNode->_left)newNode->_left->_parent = newNode;if (newNode->_right)newNode->_right->_parent = newNode;return newNode;}private:Node* _root = nullptr;
};

之后是测试文件:

#include"AVLTree.h"void testAVL()
{AVLTree<int, int> t;//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };int a[] = { 14, 9, 5, 17, 11, 12, 7, 19, 16, 27 };for (auto e : a){t.Insert({ e, e });//cout << e << t.IsBalanceTree() << endl; //方便调试报错}cout << "中序遍历为:";t.Inorder();cout << "前序遍历为:";t.Preorder();if (t.IsBalanceTree()){cout << "t是平衡树" << endl;}else{cout << "t不是平衡树" << endl;}if (t.Find(2)) //只是测试Find代码{cout << "2是树中节点" << endl;}else{cout << "2不是树中节点" << endl;}cout << "测试拷贝构造" << endl;AVLTree<int, int> t1(t);if (t1.IsBalanceTree()){cout << "t1是平衡树" << endl;}else{cout << "t1不是平衡树" << endl;}
}int main()
{testAVL();return 0;
}

以上数组构建的AVL树为:

其运行结果为:

总结:

本来小编想把使用C语言递归方式创建AVL树写出来,但是代码粘贴上去之后发现太多了,有机会在写一篇相关文章吧。

如果你能看到这,恭喜你,你几乎可以独立写出AVL树了,非递归方法很复杂,但是我们理解的就会很好,之后,我们就可以进阶到红黑树了,这是你成为数据结构大佬的重要一环,大家敬请期待!

麻烦您动动小手点个赞吧,写了快2天,真的不容易,谢谢老铁焖支持! 

相关文章:

【C++】平衡二叉树(AVL树)迭代版

目录 前言&#xff1a; 一&#xff1a;判断一棵树是否为平衡二叉树 二&#xff1a;明确思路 1.为什么使用平衡二叉树 2.旋转 2.1 左旋 2.2 右旋 3.冲突节点 4.平衡因子 5.双旋 5.1 左右双旋(LR) 5.2 右左双旋(RL) 6.平衡因子的更新 7.冲突节点问题补充 三&…...

双链表详解

一、双向链表介绍 二、实现双向链表 1.定义双向链表的结构 2.双向链表的初始化 3.双向链表的尾插 4.双向链表的头插 5.双向链表的打印 6.双向链表的尾删 7.双向链表的头删 8.查找指定位置的数据 9.在指定位置之后插入数据 10.删除指定位置的数据 11.链表的销毁 三、…...

6.9.单源最短路径问题-BFS算法

一.前言&#xff1a; 问题1&#xff1a; 以上述图片为例&#xff0c;比如从G港到Y城&#xff0c;可以是G港->R城->Y城&#xff0c;也可以是G港->P城->Y城等&#xff0c;有很多条路径都可以实现从G港到Y城&#xff0c;但要从中找出G港到Y城距离最短的那一条路径&am…...

react js 查看字体效果

起因&#xff0c; 目的: 想查看某个字体&#xff0c;对中英文的支持情况。 效果图: 完整项目见这里&#xff0c; 需要积分下载&#xff0c;不然的话&#xff0c;显得太水了。 过程: AI 对话&#xff0c; 生成代码。我检查运行&#xff0c; 来回修改。写个博客&#xff0c;…...

GZIPInputStream 类详解

GZIPInputStream 类详解 GZIPInputStream 是 Java 中用于解压缩 GZIP 格式数据的流类,属于 java.util.zip 包。它是 InflaterInputStream 的子类,专门处理 GZIP 压缩格式(.gz 文件)。 1. 核心功能 解压 GZIP 格式数据(RFC 1952 标准)自动处理 GZIP 头尾信息(校验和、时…...

数字智慧方案6206丨智慧园区大数据整体解决方案(45页PPT)(文末有下载方式)

资料解读&#xff1a;智慧园区大数据整体解决方案 详细资料请看本解读文章的最后内容。 在数字化快速发展的当下&#xff0c;智慧园区成为推动产业升级和城市发展的关键力量。这份智慧园区大数据整体解决方案&#xff0c;融合前沿技术与创新理念&#xff0c;为园区的高效管理、…...

Linux系统常用命令、标准C库函数和系统调用

目录 一、常用命令 env echo $name 键值 export name unset name gcc -c xxx.c ar 命令 ar -r libxxx.a xxx1.o xxx2.o gcc -c -fpic xxx.c gcc -shared -fpic xxx1.c xxx2.c -o libxxx.so kill [-信号] PID kill -l 软链接&#xff1a;ln -s xxx yyy 硬链接&…...

【Linux】基础指令(2)

man linux中有很多指令&#xff0c;我们不可能全部记住&#xff0c;man是linux/unix系统中的手册页指令&#xff0c;当我们遇到不熟悉的命令可以用man来查看命令&#xff0c;函数&#xff0c;配置文件的详细使用说明。 man手册分为多个章节&#xff0c;详情如下&#xff1a; …...

“会话技术”——Cookie_(2/2)原理与使用细节

经过Cookie的快速入门与代码使用。如果想深入理解Cookie的技术实现&#xff0c;就得去理解它的原理。 且有些时候使用Cookie&#xff0c;还要根据需求设置存活期限以及确定Cookie获取范围等其他细节。最后&#xff0c;我们会总结Cookie这门客户端会话技术的作用。 一、原理 注…...

Linux操作系统--进程间通信(中)(命名管道)

目录 1.命名管道&#xff1a; 1.1创建一个命名管道 1.2匿名管道与命名管道的区别 1.3命名管道的打开规则 1.4例子1-用命名管道实现文件拷贝 1.5例子2-用命名管道实现server&client通信 1.命名管道&#xff1a; 毫不相关的进程进行进程间通信管道应用的一个限制就是只能…...

数据结构6 · BinaryTree二叉树模板

代码函数功能顺序如下&#xff1a; 1&#xff1a;destroy&#xff1a;递归删除树 2&#xff1a;copy&#xff1a;复制二叉树 3&#xff1a;preOrder&#xff1a;递归前序遍历 4&#xff1a;inOrder&#xff1a;递归中序遍历 5&#xff1a;postOrder&#xff1a;递归后续遍…...

ubuntu的libc 库被我 sudo apt-get --reinstall install libc6搞没了

我系统的libc 没了 今天为了运行一个开源的yuv 播放器&#xff0c;在运行的时候提醒 Inconsistency detected by ld.so: dl-call-libc-early-init.c: 37: _dl_call_libc_early_init: Assertion sym ! NULL failed!然后听从AI 的建议 当我去执行ls 时&#xff0c;系统提示 就这…...

cat file.tar.gz | tar -xzf - -C /target/dir两个减号之间为什么有个空格?是写错了吗?(管道命令后续)

在 tar 命令的参数 -xzf - -C 中&#xff0c;两个减号&#xff08;-&#xff09;之间的空格是故意保留的语法&#xff0c;没有写错。具体原因如下&#xff1a; 1. -xzf - 的语法解析 -xzf 是 tar 命令的组合参数&#xff1a; x&#xff1a;表示解压&#xff08;extract&#x…...

手机的数据楚门世界是如何推送的

手机推送&#xff0c;也叫茧影算法&#xff0c;手机的数据“楚门世界”&#xff1a;信息推送机制的深度剖析与社会影响 在数字化时代&#xff0c;手机已然成为人们生活中不可或缺的伴侣。当我们沉醉于手机带来的便捷与娱乐时&#xff0c;或许未曾察觉&#xff0c;自己正置身于…...

体系结构论文(八十二):A Comprehensive Analysis of Transient Errors on Systolic Arrays

研究背景与动机 TPU架构&#xff08;Tensor Processing Unit&#xff09;广泛应用于DNN推理&#xff0c;其核心是脉动阵列&#xff0c;由大量的乘加单元&#xff08;MAC&#xff09;组成。 由于使用了纳米级CMOS技术&#xff0c;TPU对辐射引发的瞬态错误&#xff08;SET&#…...

综合案例:使用vuex对购物车的商品数量和价格等公共数据进行状态管理

文章目录 0.实现需求1.新建购物车模块cart2.使用json-server模拟向后端请求数据3.在vuex请求获取并存入数据,并映射到组件中,在组件中渲染【重点】3.1.安装axios3.2.准备actions和mutations,获取和存入数据到vuex中3.3.动态渲染:用mapState映射 其他1.为什么在axios在项目中要局…...

二叉搜索树的判断(双指针解决)

98. 验证二叉搜索树 - 力扣&#xff08;LeetCode&#xff09; class Solution { public:TreeNode*preNULL;bool isValidBST(TreeNode* root) {if(rootNULL){return true;}bool leftisValidBST(root->left);if(pre!NULL&&pre->val>root->val){return fals…...

关于CSDN创作的常用模板内容

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 好文评论新文推送 &#x1f4c3;文章前言 &…...

不小心误删了文件,找Windows数据恢复工具来帮忙

相信很多人都遇到过这样的情况&#xff1a;不小心在电脑上删除了一些重要的文件&#xff0c;等到想要找回来时&#xff0c;却感觉特别棘手。 今天我要给大家推荐一款超棒的Windows数据恢复工具&#xff0c;它能轻松帮你找回那些被误删的文件。 &#xff08;文末附下载链接&…...

[Verilog]跨时钟域数据传输解决方案

跨时钟域数据传输解决方案 摘要:跨时钟域数据传输 (Clock Domain Crossing, CDC) 是 SoC 设计中常见且关键的问题,因为现代 SoC 通常包含多个时钟域,不同模块可能运行在不同频率或相位的时钟下。跨时钟域传输数据时,如果处理不当,可能会导致亚稳态 (Metastability)…...

Linux——进程终止/等待/替换

前言 本章主要对进程终止&#xff0c;进程等待&#xff0c;进程替换的详细认识&#xff0c;根据实验去理解其中的原理&#xff0c;干货满满&#xff01; 1.进程终止 概念&#xff1a;进程终止就是释放进程申请的内核数据结构和对应的代码和数据 进程退出的三种状态 代码运行…...

数据结构与算法:图论——最短路径

最短路径 先给出一些leetcode算法题&#xff0c;以后遇见了相关题目再往上增加 最短路径的4个常用算法是Floyd、Bellman-Ford、SPFA、Dijkstra。不同应用场景下&#xff0c;应有选择地使用它们&#xff1a; 图的规模小&#xff0c;用Floyd。若边的权值有负数&#xff0c;需要…...

双指针(5)——有效三角形个数

题目&#xff1a; 这道题我们首先可能会想到暴力解法&#xff0c;三个for循环然后进行check&#xff08;&#xff09;。时间复杂度肯定是不允许的。 同时&#xff0c;验证可以组成三角形的条件是任意两边之和大于第三边&#xff0c;这就意味着我们每组要进行三次比较。但也有捷…...

Qt QGraphicsScene 的用法

背景&#xff0c;为什么要写这篇博客 今天学习 model - view 模式的时候还看到有 scene - view 模式。不知道还有这个模式&#xff0c;所以学习了下。 学习后总体的感觉是&#xff1a;其实没有也是可以的&#xff0c;但有了方便许多。 从两种画图的方法开始说 以前有个项目也…...

使用 Tesseract 实现藏文OCR

要识别藏文&#xff0c;最常用且有效的方法是使用Tesseract OCR&#xff08;谷歌开源的OCR工具&#xff09;&#xff0c;因为它拥有针对藏文的预训练模型支持。 &#x1f680; 一、安装 Tesseract OCR 软件&#xff1a; 下载链接&#xff1a;Tesseract OCR 下载页面 Windows用…...

数字智慧方案5873丨智慧交通设计方案(57页PPT)(文末有下载方式)

资料解读&#xff1a;智慧交通设计方案 详细资料请看本解读文章的最后内容。 智慧交通设计方案是一份详尽的交通规划文件&#xff0c;旨在通过科学的交通设计方法&#xff0c;优化交通系统&#xff0c;提升交通效率&#xff0c;确保交通安全&#xff0c;并促进可持续发展。该…...

【quantity】6 温度单位实现(temperature.rs)

一源码 以下代码实现了一个温度单位系统&#xff0c;支持开尔文(Kelvin)和摄氏度(Celsius)之间的转换和运算。 /// Temperature (kelvin) / 温度 (开尔文) use super::{Quantity, prefix::*}; use crate::unit::Kelvin; use derive_more::{Add, Sub, AddAssign, SubAssign};/…...

ARConv的复现流程

使用环境 Python 3.10.16 torch 2.1.1cu118 torchvision 0.16.1cu118 其它按照官方提供代码的requirements.txt安装 GitHub - WangXueyang-uestc/ARConv: Official repo for Adaptive Rectangular Convolution 数据准备 从官方主页下载pancollection数据集PanCollection…...

安卓游戏APK文件解密与编辑的完整攻略

在移动游戏开发中,保护游戏数据不被篡改是开发者的重要任务。然而,随着逆向工程技术的发展,破解游戏数据也变得可能。本文将详细介绍如何分析、解密和编辑APK安装包中的加密JSON文件,特别关注assets/task目录下的文件,并提供一种绕过checkfile.json中MD5校验的有效方法。通…...

JVM——JVM 是如何执行方法调用的?

JVM 是如何执行方法调用的&#xff1f; 在 Java 世界的底层运作中&#xff0c;方法调用机制是理解 Java 虚拟机&#xff08;JVM&#xff09;行为的关键之一。JVM 作为 Java 程序运行的核心&#xff0c;承担着执行字节码、管理内存、调度线程等多项职责。而方法调用作为程序逻辑…...

一天学完JDBC!!(万字总结)

文章目录 JDBC是什么 1、环境搭建 && 入门案例2、核心API理解①、注册驱动(Driver类)②、Connection③、statement(sql注入)④、PreparedStatement⑤、ResultSet 3、jdbc扩展(ORM、批量操作)①、实体类和ORM②、批量操作 4. 连接池①、常用连接池②、Durid连接池③、Hi…...

【愚公系列】《Manus极简入门》011-习惯养成教练:“习惯塑造师”

&#x1f31f;【技术大咖愚公搬代码&#xff1a;全栈专家的成长之路&#xff0c;你关注的宝藏博主在这里&#xff01;】&#x1f31f; &#x1f4e3;开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主&#xff01; &#x1f…...

精益数据分析(38/126):SaaS模式的流失率计算优化与定价策略案例

精益数据分析&#xff08;38/126&#xff09;&#xff1a;SaaS模式的流失率计算优化与定价策略案例 在创业和数据分析的领域中&#xff0c;我们不断探索如何更精准地把握业务发展的关键要素。今天&#xff0c;带着与大家共同进步的想法&#xff0c;深入研读《精益数据分析》&a…...

50.【必备】二分答案法与相关题目

本文的网课内容学习自B站左程云老师的算法详解课程&#xff0c;旨在对其中的知识进行整理和分享~ 网课链接&#xff1a;算法讲解051【必备】二分答案法与相关题目_哔哩哔哩_bilibili 一.爱吃香蕉的珂珂 题目&#xff1a;爱吃香蕉的珂珂 算法原理 整体思路 这是一个二分查找算法…...

C# 方法(局部变量和局部常量)

本章内容: 方法的结构 方法体内部的代码执行 局部变量 局部常量 控制流 方法调用 返回值 返回语句和void方法 局部函数 参数 值参数 引用参数 引用类型作为值参数和引用参数 输出参数 参数数组 参数类型总结 方法重载 命名参数 可选参数 栈帧 递归 局部变量 和第5章介绍的字段…...

MQTT 协议与 HTTP 协议的区别

在现代的网络通信中&#xff0c;MQTT 协议和 HTTP 协议都扮演着重要的角色&#xff0c;但它们有着不同的特点和适用场景。下面我们就从多个方面来详细探讨它们之间的区别。 一.协议设计理念 1. MQTT 协议 MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;即…...

博弈论思维——AI与思维模型【90】

一、定义 博弈论思维模型是一种研究在相互影响的决策情境中&#xff0c;参与者如何通过策略选择来实现自身利益最大化的理论框架。它分析参与者之间的相互作用、策略组合以及由此产生的结果&#xff0c;帮助人们理解在竞争或合作环境下的决策逻辑和行为模式。 二、由来 博弈…...

【Bootstrap V4系列】学习入门教程之 表格(Tables)和画像(Figure)

Bootstrap V4系列 学习入门教程之 表格&#xff08;Tables&#xff09;和画像&#xff08;Figure&#xff09; 表格&#xff08;Tables&#xff09;一、Examples二、Table head options 表格头选项三、Striped rows 条纹行四、Bordered table 带边框的表格五、Borderless table…...

第 3 篇:有序的世界:有序表 (TreeMap/TreeSet) 的概念与优势

上一篇我们探讨了哈希表如何以牺牲顺序为代价换取极致的平均速度。然而&#xff0c;在现实世界的许多应用中&#xff0c;数据的有序性不仅是锦上添花&#xff0c;甚至是核心需求。想象一下&#xff1a; 你需要显示一个按价格排序的商品列表。你需要找到某个时间点之前或之后的…...

VulnHub-DC-2靶机

主机发现 sudo arp-scan -l 以sudo管理员权限扫描本地活动ip地址 Interface: eth0, type: EN10MB, MAC: 08:00:27:22:46:4f, IPv4: 192.168.252.230 Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan) 192.168.252.6 4c:5f:70:74:3c:3b …...

论文笔记(八十三)STACKGEN: Generating Stable Structures from Silhouettes via Diffusion

STACKGEN: Generating Stable Structures from Silhouettes via Diffusion 文章概括摘要I. INTRODUCTIONII. 相关工作A. 从直觉物理学学习稳定性B. 用于姿态生成的扩散模型C. 自动化顺序装配 III. 方法A. 用于 S E ( 3 ) SE(3) SE(3)积木姿态生成的扩散模型B. 模型架构C. 数据生…...

论文阅读笔记——TesserAct: Learning 4D Embodied World Models

TesserAct 论文 采用RGB-DN&#xff08;RGB深度法线&#xff09; 作为 4D 场景中间表示&#xff0c;由此建模 4D 场景&#xff0c;比纯 2D 视频更准确地建模 3D 几何结构。相比现有的 4D 视频生成&#xff0c;优化速度快&#xff0c;收敛好&#xff0c;且首次从当前帧和文本描述…...

变转速振动信号分析处理与故障诊断算法模块

变转速振动信号分析处理与故障诊断算法模块&#xff0c;作为信号处理算法工具箱的主要功能模块&#xff0c;形成了以变转速振动信号分析处理与故障诊断算法模块的经典算法模型&#xff0c;可应用于各类关键机械部件&#xff08;轴承、齿轮、转子等&#xff09;的信号分析、故障…...

每日算法-250502

每日算法 - 2025.05.02 记录一下今天刷的几道 LeetCode 算法题。 3191. 使二进制数组全部等于 1 的最少操作次数 I 题目 思路 贪心 解题过程 遍历数组 nums。当我们遇到 nums[i] 时&#xff1a; 如果 nums[i] 是 1&#xff0c;我们不需要进行操作&#xff0c;因为目标是全 …...

如何在纯C中实现类、继承和多态(小白友好版)

基本实现原理 /* 通过结构体函数指针模拟类 */ typedef struct {// 成员变量int x; // 成员方法&#xff08;函数指针&#xff09; void (*print)(void* self); } MyClass;/* 成员函数实现 */ void my_print(void* self) {MyClass* obj (MyClass*)self;p…...

AE/PR插件 转场创建大师专业版 Transition Master Pro v2.0.2 Win+使用教程

Transition Master Pro v2.0.2是一款原生转场插件&#xff0c;专为Adobe Premiere Pro和After Effects设计。它提供了创建、导出和销售自己的转场效果&#xff0c;或从一个庞大的转场预设库中选择。使用Transition Master Pro v2.0.2&#xff0c;您可以快速轻松地创建令人惊叹的…...

[Linux]从零开始的STM32MP157 Buildroot根文件系统构建

一、前言 在前面的教程中&#xff0c;教了大家如何移植一个LInux的内核并且正确启动&#xff0c;我们发现Linux内核在启动后会出现一个错误&#xff0c;提示我们没有找到根文件系统。那么什么是根文件系统呢&#xff1f;之前我们使用Ubuntu编译了STM32MP157的TF-A,UBOOT,LINUX内…...

阿里云服务器 篇五(加更):短链服务网站:添加反垃圾邮件功能

文章目录 系列文章(可选)更新YOURLS版本安装 Compliance 插件安装 Phishtank-2.0 插件(可选)安装 httpBL 插件样例网站(不推荐)使用谷歌解决方案更多系列文章 阿里云服务器 篇一:申请和初始化 阿里云服务器 篇二:搭建静态网站 阿里云服务器 篇三:提交搜索引擎收录 阿…...

状压 DP 详解

文章目录 简介做法洛谷 P1171 简介 状压 DP 其实约等于一个 DP 的小技巧&#xff0c;一般应用在处理一个或多个集合的问题中&#xff08;因为状压 DP 的下标就是一个集合&#xff09;&#xff0c;而且在 n n n 太大的时候建议不要使用这种方法。&#xff08;如果你不懂&#…...

多模态大模型轻量化探索-视觉大模型SAM(Segment Anything Model)

往期&#xff0c;笔者基于LLava的数据对齐训练&#xff0c;搞了一个Reyes多模态大模型&#xff0c;并且看了些多模态大模型&#xff0c;相关开源的多模态大模型如&#xff1a;KimiVL、Internvl、QwenVL等&#xff0c;其视觉编码器的尺寸都比较大&#xff0c;如&#xff1a;Moon…...