C++ -->STL 搜索平衡二叉树 AVL树
前言--- 平衡二叉的由来 &&平衡的策略
1. 由来
在本文开始之前,我想同各位探讨为什么要有平衡二叉。 首先搜索二叉树由其插入节点有一定的规则(大的插根右,反之根左)这样查找的效率如下图:往往能达到 O(log) 这是基于比较查找的最优时间复杂度了。
图1
但是,天不如人愿,如下图2,当插入的节点一直在父节点的右侧,二叉树退化为单边链,这是二叉树最差的情况,几乎就一条分支了,所以会导致它长得跟链表没什么区别,效率也是,查找的效率直接退化为O(n)
图2
2. 平衡策略
为了解决单边链问题,本质上要理解为什么会照成单边链问题,它的核心问题就是因为根的左右子树高度差太大了,如上图,同样的数据,倘若让8来做根再修改插入的顺序高度差肯定就比原来至少减少了一半,这样查找的效率就又达到了O(logn)
所以总之平衡就是为了解决根的左右子树高度差的问题,我们的思路就是平衡根的左右子树高度进而实现我们的目的,为此,最直接的策略就是: 严格控制每一个父节点的左右子树的高度差 ----这就是AVL树的思路
还有一种策略就是: 红黑树: 因为红黑树认为AVL树严格控制左右子树的高度,每次插入需要调整的策略太高了,所以红黑树对高度的要求并没有那么严格它的核心就是: 保证最长的路径不会超过最短路径的2倍。 大量实验证明它的搜索效率确实也接近O(log n)同时调整的次数比AVL树少。
一. AVL树的设计
1.1 设计思路
AVL树,通过严格控制节点子树的左右高度差来达到平衡的目的,倘若让左右子树的高度差相等,未免太严格了做不到。 但是可以取弱条件,让左右子树的高度差 小于2 即不超过1 我们的平衡二叉树 节点一般都是设计为三叉链 如图:
对于每一个节点的左右子树高度差,我们这里采取的方法是给节点一个平衡因子: _bf
以及平衡因子的计算:右子树-左子树高度 (左-右也行 读者可以自行尝试区别不大)
设计好树节点: 就是AVL树类的设计,我们一般在树类中放一个成员就是: _root 根节点。本文打算只挑AVL树的核心也就是插入节点后根据平衡因子调整树的4种旋转情况。
所以本文接下来将讲解AVL的插入:
1.2 树节点和树类初步代码实现
namespace mystl {// 树节点类 template<class K,class V>struct AVLTreeNode {AVLTreeNode(const std::pair<K, V>& kv):_parent(nullptr),_left(nullptr),_right(nullptr),_bf(0),_kv(kv){ }AVLTreeNode* _parent;AVLTreeNode* _left;AVLTreeNode* _right;std::pair<K, V>_kv;int _bf;};//AVL树类template<class K, class V>class AVLTree {typedef AVLTreeNode<K, V> Node;public:AVLTree():_root(nullptr) {}private:Node* _root;}
二. AVL树插入详解
2.1 插入总体思路理解
如上图,加入要插入6,对于AVL树而言跟普通的BST(搜索二叉树)同样,首先要找到插入的位置,然后链接起来,区别就是我们的AVL树的节点是三叉链了,要链接起来更为复杂一些。
链接起来之后AVL树的精华到了,那就是从自底向上的调节平衡因子,更新的策略就是: cur节点和parent节点 ,cur初始化指向待插入节点: 如果cur是parent的左parent->_bf--
反之++ 如果当前parent->bf处理之后变为0 说明新插入的节点对高度没有影响就不需要继续向上调整了。
如果parent->_bf变为2/-2 也就意味着高度差大于1了,那么就需要对树进行调整了,只需要从当前的parent节点开始调整即可,策略就是旋转 +平衡因子的调节
2.2 找到插入位置+链接
插入之前要找到插入位置这个跟搜索二叉的思路没什么区别,直接跳过了,以及让新节点链接它的父节点 毕竟是三叉链节点: 也是很简单的 。---如下代码:
template<class K, class V>
class AVLTree {typedef AVLTreeNode<K, V> Node;public:AVLTree():_root(nullptr) {}private:void destroy(Node*& root) {if (root == nullptr)return;destroy(root->_left);destroy(root->_right);delete root;root = nullptr;}
public:bool insert(const std::pair<K, V>&kv) {// 根为空 if (_root == nullptr) {_root = new Node(kv);return true;}// 若根不为空 就让cur去找即可Node* cur = _root;Node* parent = _root;while (cur) {if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first) {parent = cur;cur = cur->_right;}else {return false;}}// 链接起来 cur 肯定是空了 让cur指向新节点 然后判断为父节点的左右子树cur = new Node(kv);cur->_parent = parent;if (kv.first < parent->_kv.first) {parent->_left = cur;}else {parent->_right = cur;}
2.3 更新平衡因子
平衡因子的更新就是从cur节点(初始化指向新插入节点)开始自底向上的调整,之前总设计思路提过:
接下来:AVL树的精华到了,那就是从自底向上的调节平衡因子,更新的策略就是: cur节点和parent节点 ,cur初始化指向待插入节点: 如果cur是parent的左parent->_bf--
反之++ 如果当前parent->bf处理之后变为0 说明新插入的节点对高度没有影响就不需要继续向上调整了。
如果parent->_bf变为2/-2 也就意味着高度差大于1了,那么就需要对树进行调整了,只需要从当前的parent节点开始调整即可。
这个平衡因子的更新我建议读者提起笔尝试画一遍心中会清晰许多。
如下代码:
// ..上面的暂时省略
while(parent)
{// 一路自底向上调整 if (cur == parent->_left){parent->_bf--;// --想退出只能是因为-2 或者0if (parent->_bf == -2||parent->_bf==0)break;cur = parent;parent = parent->_parent;}else {parent->_bf++;if (parent->_bf == 2 || parent->_bf == 0)break;cur = parent;parent = parent->_parent;}}
2.4 树的调整----旋转
当你的代码自底向上调整后,若当前parent节点的高度差达到2了那么也就是需要调整了,对于树的调整其实总体上分为两种: 新插入后是在parent子树的外侧(左外侧 和右外侧) 以及子树的内侧(左内侧 和右内侧)
2.4.1 左外侧
其实左外侧也就是新插入的位置在parent节点(自底向上更新第一个高度差为2的节点)的左侧插入,判断起来也很方便就是parent->_bf ==-1 && parent->left->_bf == -1
我在这采取了一种抽象的方式,对左外侧抽象为如下图这样的形式。
对于这样的h 可以是0 ,1 甚至更多,推荐读者可以尝试自行画一下 然后自底向上更新平衡因子,这样会更清晰
右单旋思路设计
首先你要理解事情就是,我们搜索二叉树种每一个元素的位置都必须这样吗? 比如同样是一堆数: 如下: 10,9,8, 7
同样是一堆数放一块不同的插入顺序,导致的跟的左右子树高度差都会不同,但是本质上依然符合二叉树搜索树的规则。
所以这也就证明我们可以通过某种特定的方式让我们通过旋转解决某父节点左右子树高度差的问题。---- 右单旋:
如上图,当我们判断出了AVL树种新插入位置是左左外侧的插入情况的时候,我们可以把来一次右单旋,也就是如上图:
让 subL=parent->_left; parent ->_left = subL->right;
subL->right = parent; //以及 让subL替代原来的位置:
到这就是旋转的过程 ,但是旋转仅仅能保证它符合二叉树的规则, 我们的节点是三叉链 : 所以我们还要更新旋转后各自的父节点,以及判断当前parent节点是不是根节点保证要更新根节点。
细节就是: 要提前判断节点:parent->_parent ==nullptr ? 如果不是空 我们还要判断parent是它父节点的左还是右 让subL与parent节点的父节点链接起来这棵树才不会散。
以及就是更新subL,parent 和现在parent->_left (也就是4)节点的父节点
右单旋代码设计
为了进一步理解左左外侧的情况,以及进行右单旋的过程,我们这里画抽象图,首先我们到这能明白一个事情对于一个左左外侧新插入情况我们只需要进行一次就可以调整左左外侧的情况了。
右单旋的函数声明 void RotateR(Node*parent)
注意我们这里设计的旋转仅仅只是调整了树的结果,不更新平衡因子。
代码实现:
void RotateR(Node* parent) {// 旋转 首先就是链接起来 然后就是 判断是不是根节点 以及链接父节点 再更新各自的parent节点即可// 第一步旋转Node* subL = parent->_left;parent->_left = subL->_right;subL->_right = parent; // 链接 if (parent->_parent == nullptr){_root = subL;}else {if (parent == parent->_parent->_left){parent->_parent->_left = subL;}else {parent->_parent->_right = subL;}}// 更新父节点subL->_parent = parent->_parent;parent->_parent = subL;if (parent->_left)parent->_left->_parent = parent;}
2.4.2 右外侧
如果你能深刻理解左外侧,那么你也可以匀速理解右外侧的解决思路,这里便不过多阐述细节了。 首先这个时候的节点: parent->_bf ==2 subR->_bf==1
我们采取的是左单旋来调整树结构。
左单旋的代码设计和实现
左单旋和右单旋思路上没什么区别: 总之判断出来当前是右右外侧之后我们就调用左单旋函数。 我们通过一次右单旋就可以解决右右外侧新插入到这的高度差问题。
思路就是提前存一个subR: subR= parent->_right;
旋转:
parent->_right = subR->_left; subR->_left = parent;
2. 链接树以及判断是否要更新_root
if(parent==nullptr){ _root = subR;}
else{ .../ 这里要判断节点parent是parent->_parent的左还是右在链接起来如: parent->_parent->_left = subR; }
3.更新subR 和parent以及 parent->_right(原来subR->_left) 的parent
subR->_parent = parent->_parent; parent->_parent = subR;// 对于parent->_right 要判空
if(parent->_right ){parent->_right->_parent = parent;}
代码如下:
// 左旋
void RotateL(Node* parent) {// 旋转 首先就是链接起来 然后就是 判断是不是根节点 以及链接父节点 再更新各自的parent节点即可// 第一步旋转Node* subR = parent->_right;parent->_right = subR->_left;subR->_left = parent;// 链接 if (parent->_parent == nullptr){_root = subR;}else {if (parent == parent->_parent->_left){parent->_parent->_left = subR;}else {parent->_parent->_right = subR;}}// 更新父节点subR->_parent = parent->_parent;parent->_parent = subR;if (parent->_right)parent->_right->_parent = parent;
}
2.4.3 左右内侧
调整AVL树的问题我们要深刻理解它的策略是: 通过平衡因子来判断树高度差表示是否要调整以及调整哪种情况, 也就是通过左单旋或者右单旋来调整,或者组合。 之后在调节平衡因子。
所以接下来第一件事要做的就是,遇到左右外侧的情况我们要尝试把它调整:
如图我们很舒服的看见了,哇塞好爽!我直接来一个右单旋,但是呢? 显然现实如此残酷。 事实上我们这种情况属于内侧插入的情况,我们需要需要对他具体化一层也就是对3的右子树再具体画一层: 这里建议读者尝试画一下不同的h对应的抽象图即可:
不同层数如下:
左右内侧----双旋策略 RotateLR
这图画的我真的是头疼,所以强烈建议读者也试试!
首先呢 其实遇到内侧插入导致高度差失衡的情况,我们采用的策略就是双旋,其实没什么高级的就是旋转两次,如果你已经理解了左单旋和右单旋那么这个就是小case: 如下图解:
对应插入到sub_subR的左子树或者右子树其实对于调整来说没什么区别但是对于后面的旋转却是有区别的。 总之思路就是: 提前存好subL 和sub_subR 这样参与旋转的节点便于后面修改平衡因子因为只有他们的平衡因子会变。
首先对subL来一次左单旋 再对parent来一次右单旋 --- 到这就是双旋了 。
之后再调节平衡因子左右内侧就被你解决了 :
其实调节平衡因子 你只需要旋转之后的最终结构即可 上面的所有情况都是一样的,你只需要调用旋转函数,之后调节平衡因子即可
调节平衡因子和左右内侧图解
2.4.4 右左内侧
能看到这,那么你肯定已经超过99.9%的读者了,毕竟AVL树画图画的真的是手软。 但是其实你也已经摸透AVL树的尿性了,无非就是外侧和内侧的插入情况。
右左内侧的插入其实也很简单: 首先认识认识:
像这样的内侧插入显然我们肯定还要继续向下画一层(上一小节左右内侧详见),它的旋转策略也简单:
调整策略如下也就是精力两次旋转:
、
右左内侧的平衡因子的调节图解
2.5 AVL插入代码
bool insert(const std::pair<K, V>&kv) {// 根为空 if (_root == nullptr) {_root = new Node(kv);return true;}// 若根不为空 就让cur去找即可Node* cur = _root;Node* parent = _root;while (cur) {if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first) {parent = cur;cur = cur->_right;}else {return false;}}// 链接起来 cur 肯定是空了 让cur指向新节点 然后判断为父节点的左右子树cur = new Node(kv);cur->_parent = parent;if (kv.first < parent->_kv.first) {parent->_left = cur;}else {parent->_right = cur;}// 链接起来之后 更新平衡因子// 新插入节点的平衡因子 为0 一路向上更新// 父节点的平衡因子 可能为 0 1/-1 或者-2/+2// 如果是0 那么就不用向上更新了说明新插入的节点并没有影响高度// 如果是 +/-1 说明需要向上更新 直到更新到0 或者根节点的的父节点 为空 就直接退出 // 如果遇到2/-2 说明需要调整 // 代码到这 parent肯定不为空 同时parent的平衡因子还没更新呢 所以基于上面的判断 我们可以做一个do whilewhile(parent){// 一路自底向上调整 if (cur == parent->_left){parent->_bf--;// --想退出只能是因为-2 或者0if (parent->_bf == -2||parent->_bf==0)break;cur = parent;parent = parent->_parent;}else {parent->_bf++;if (parent->_bf == 2 || parent->_bf == 0)break;cur = parent;parent = parent->_parent;}} // 代码到这 parent 可能是空 也可能它的bf是0 // 但是需要调整的情况只有 bf是2和-2 肯定不可能是1if (parent && parent->_bf != 0) {// 到这说明 parent 左右子树高度差已经为2了 需要调整 // 调整大体上分为外侧和内侧的情况 if (parent->_bf == -2) {// -2 肯定是因为左侧的情况// 左左 ---外侧if (parent->_left->_bf == -1) {// 这里需要一个右单旋Node* subL = parent->_left;RotateR(parent);parent->_bf = 0;subL->_bf = 0;}else if (parent->_left->_bf == 1) {// 左右 -- 内侧// parent 和subL 以及 subL_subR Node* subL = parent->_left;Node* subL_subR = subL->_right;// 对 subL来一次左单旋 再对parent来一次右旋就调整好高度差 // 之后在调整平衡因子RotateL(subL);RotateR(parent);// 调节平衡因子 我最不想面对的就是它了。。。if (subL_subR->_bf == 0) {parent->_bf = subL->_bf = 0;}else if (subL_subR->_bf == -1) {subL_subR->_bf = subL->_bf = 0;parent->_bf = 1;}else {subL_subR->_bf = parent->_bf = 0;subL->_bf = -1;}}}else {// bf一定是2 了 判断是 右右--外侧 还是 右左--内侧//右右--外侧if (parent->_right->_bf == 1) {//这里需要一个左单旋Node* subR = parent->_right;RotateL(parent);subR->_bf = parent->_bf = 0;}else if (parent->_right->_bf == -1) {// 右左 -- 内侧// parent 和subR 以及 subR_subLNode* subR = parent->_right;Node* subR_subL = subR->_left;// 对 subR来一次右单旋 再对parent来一次左单旋就调整好高度差 // 之后在调整平衡因子RotateR(subR);RotateL(parent);// 调节平衡因子 我最不想面对的就是它了。。。if (subR_subL->_bf == 0) {parent->_bf = subR->_bf = 0;}else if (subR_subL->_bf == 1) {subR_subL->_bf = subR->_bf = 0;parent->_bf = -1;}else {subR_subL->_bf = parent->_bf = 0;subR->_bf = 1;}}}}return true;}
三. AVL树总结 以及全代码和简单的测试
总结和代码
呜呼,终于把全部图画完了! 接下来总结一下AVL树种最核心的插入逻辑,以及我们是如何实现的: 总之我们的实现思路就是,借助平衡因子来判断是否要调整树,调整树我们通过判断四种情况是哪种旋转,之后再是画图判断调节平衡因子,对于平衡因子的调整是非常有趣的因为我无需知道你调整的过程是什么样的,我只需要知道最终结构是什么样的,因为旋转只是把一个搜索二叉树换一种形态而已。 到这你可以思考一下所以AVL树通过严格控制高度来达到平衡的效果,可是当插入的节点每次的都是新的一层(单边链),几乎每次都要旋转,如果是内侧插入那岂不是要双旋,插入效率未免不如人意,其实旋转都是常树级别的,插入的时间复杂度主要还是在找插入的位置上O(logn) 但是这个常数由旋转次数决定,红黑树就是觉得AVL树的旋转太多了所以选择了一个更弱的平衡条件来控制树高度--》 也就是红黑节点的情况而不是根据高度差,通过红黑节点的情况间接控制高度。
好了到这,基本总结完了,对于AVL树的删除笔者不打算说咯,毕竟也挺复杂的,但是根插入类似。 但是树基本的函数我还是写了的如下吧:
#pragma once
#include<iostream>namespace mystl {// 树节点类 template<class K,class V>struct AVLTreeNode {AVLTreeNode(const std::pair<K, V>& kv):_parent(nullptr),_left(nullptr),_right(nullptr),_bf(0),_kv(kv){ }AVLTreeNode* _parent;AVLTreeNode* _left;AVLTreeNode* _right;std::pair<K, V>_kv;int _bf;};template<class K, class V>class AVLTree {typedef AVLTreeNode<K, V> Node;public:AVLTree():_root(nullptr) {}~AVLTree() {destroy(_root);std::cout << "all node has been done" << std::endl;}private:void destroy(Node*& root) {if (root == nullptr)return;destroy(root->_left);destroy(root->_right);delete root;root = nullptr;}public:bool insert(const std::pair<K, V>&kv) {// 根为空 if (_root == nullptr) {_root = new Node(kv);return true;}// 若根不为空 就让cur去找即可Node* cur = _root;Node* parent = _root;while (cur) {if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first) {parent = cur;cur = cur->_right;}else {return false;}}// 链接起来 cur 肯定是空了 让cur指向新节点 然后判断为父节点的左右子树cur = new Node(kv);cur->_parent = parent;if (kv.first < parent->_kv.first) {parent->_left = cur;}else {parent->_right = cur;}// 链接起来之后 更新平衡因子// 新插入节点的平衡因子 为0 一路向上更新// 父节点的平衡因子 可能为 0 1/-1 或者-2/+2// 如果是0 那么就不用向上更新了说明新插入的节点并没有影响高度// 如果是 +/-1 说明需要向上更新 直到更新到0 或者根节点的的父节点 为空 就直接退出 // 如果遇到2/-2 说明需要调整 // 代码到这 parent肯定不为空 同时parent的平衡因子还没更新呢 所以基于上面的判断 我们可以做一个do whilewhile(parent){// 一路自底向上调整 if (cur == parent->_left){parent->_bf--;// --想退出只能是因为-2 或者0if (parent->_bf == -2||parent->_bf==0)break;cur = parent;parent = parent->_parent;}else {parent->_bf++;if (parent->_bf == 2 || parent->_bf == 0)break;cur = parent;parent = parent->_parent;}} // 代码到这 parent 可能是空 也可能它的bf是0 // 但是需要调整的情况只有 bf是2和-2 肯定不可能是1if (parent && parent->_bf != 0) {// 到这说明 parent 左右子树高度差已经为2了 需要调整 // 调整大体上分为外侧和内侧的情况 if (parent->_bf == -2) {// -2 肯定是因为左侧的情况// 左左 ---外侧if (parent->_left->_bf == -1) {// 这里需要一个右单旋Node* subL = parent->_left;RotateR(parent);parent->_bf = 0;subL->_bf = 0;}else if (parent->_left->_bf == 1) {// 左右 -- 内侧// parent 和subL 以及 subL_subR Node* subL = parent->_left;Node* subL_subR = subL->_right;// 对 subL来一次左单旋 再对parent来一次右旋就调整好高度差 // 之后在调整平衡因子RotateL(subL);RotateR(parent);// 调节平衡因子 我最不想面对的就是它了。。。if (subL_subR->_bf == 0) {parent->_bf = subL->_bf = 0;}else if (subL_subR->_bf == -1) {subL_subR->_bf = subL->_bf = 0;parent->_bf = 1;}else {subL_subR->_bf = parent->_bf = 0;subL->_bf = -1;}}}else {// bf一定是2 了 判断是 右右--外侧 还是 右左--内侧//右右--外侧if (parent->_right->_bf == 1) {//这里需要一个左单旋Node* subR = parent->_right;RotateL(parent);subR->_bf = parent->_bf = 0;}else if (parent->_right->_bf == -1) {// 右左 -- 内侧// parent 和subR 以及 subR_subLNode* subR = parent->_right;Node* subR_subL = subR->_left;// 对 subR来一次右单旋 再对parent来一次左单旋就调整好高度差 // 之后在调整平衡因子RotateR(subR);RotateL(parent);// 调节平衡因子 我最不想面对的就是它了。。。if (subR_subL->_bf == 0) {parent->_bf = subR->_bf = 0;}else if (subR_subL->_bf == 1) {subR_subL->_bf = subR->_bf = 0;parent->_bf = -1;}else {subR_subL->_bf = parent->_bf = 0;subR->_bf = 1;}}}}return true;}private:void RotateR(Node* parent) {// 旋转 首先就是链接起来 然后就是 判断是不是根节点 以及链接父节点 再更新各自的parent节点即可// 第一步旋转Node* subL = parent->_left;parent->_left = subL->_right;subL->_right = parent; // 链接 if (parent->_parent == nullptr){_root = subL;}else {if (parent == parent->_parent->_left){parent->_parent->_left = subL;}else {parent->_parent->_right = subL;}}// 更新父节点subL->_parent = parent->_parent;parent->_parent = subL;if (parent->_left)parent->_left->_parent = parent;}// 左旋void RotateL(Node* parent) {// 旋转 首先就是链接起来 然后就是 判断是不是根节点 以及链接父节点 再更新各自的parent节点即可// 第一步旋转Node* subR = parent->_right;parent->_right = subR->_left;subR->_left = parent;// 链接 if (parent->_parent == nullptr){_root = subR;}else {if (parent == parent->_parent->_left){parent->_parent->_left = subR;}else {parent->_parent->_right = subR;}}// 更新父节点subR->_parent = parent->_parent;parent->_parent = subR;if (parent->_right)parent->_right->_parent = parent;}public:// 中序遍历(公开接口)void inOrder() const {_inOrder(_root);std::cout << std::endl;}// 获取树的高度(公开接口)int getHeight() const {return _Height(_root);}bool IsBalanceTreeR() {return _IsBalanceTreeR(_root);}private:// 递归中序遍历(私有实现)void _inOrder(Node* root) const {if (root == nullptr) return;_inOrder(root->_left);std::cout << root->_kv.first << " "; // 输出键,验证有序_inOrder(root->_right);}// 递归计算树高(私有实现)int _Height(Node* root) const {if (root == nullptr) return 0;int left_height = _Height(root->_left);int right_height = _Height(root->_right);return left_height > right_height ? left_height + 1 : right_height+1;}bool _IsBalanceTreeR (Node* root){// 空树也是AVL树if (nullptr == root) return true;// 计算pRoot节点的平衡因子:即pRoot左右子树的高度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者// pRoot平衡因子的绝对值超过1,则一定不是AVL树if (diff != root->_bf || (diff > 1 || diff < -1))return false;// pRoot的左和右如果都是AVL树,则该树一定是AVL树return _IsBalanceTreeR(root->_left) && _IsBalanceTreeR(root->_right);
}private:Node* _root;};}
测试代码
#include<iostream>
#include<vector>
#include<utility>
#include "myAVL.hpp"
using namespace mystl;int main() {AVLTree<int, int> at;int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };for (int c : arr) {at.insert(std::make_pair(c,c));}at.inOrder();if (at.IsBalanceTreeR()) {std::cout << "good"<<std::endl;}else {std::cout << "no" << std::endl;}return 0;
}
输出情况: