AVL树的介绍与实现

前言

我们上一期介绍了二叉搜索树并做了实现,本期我们来继续学习另一个更优的树即AVL树!

本期内容介绍

什么是AVL树?

AVL树的实现

AVL树的性能分析

在正式的介绍AVL树之前,我们先来回忆一下二叉搜索树的特点左子树的值一定小于根节点的值,右子树的值一定大于根节点的值;基于他的这个特点,可以缩短查找的区间即可以提升查找的效率!但是他在有些情况下效率并不是很好。例如:当数据是有序或接近有序时,查找得需要O(N)的时间复杂度即退化成单链就和和链表一样了,效率不太好!为了解决这个问题,有人就提出了AVL树!

什么是AVL树?

为了解决二叉搜索树的弊端,在1962年来自俄罗斯的两位数学家G.M.Adelson-Velskii和E.M.Landis研究出了一种解决上述问题的方法:当向二叉树种插入新节点时,如果保每个节点的左右子树的高度的绝对值之差不超过1(如果超过了1需要内部调整)既可以降低树的高度,从而减少平均查找长度!符合这样的二叉搜索树就叫做平衡二叉搜索树即AVL树。也就是:

一颗AVL树要么是空树要么是符合下面性质的二叉搜索树:

该二叉搜索树的左右子树都是AVL树;

它的左右子树高度差(简称平衡因子)的绝对值不超过1(-1/0/1);

平衡因子可以是左减右,也可以右减左;这里采用后者!

所以,下面这棵二叉搜索树就是一个AVL树:

AVL树的实现

还是和以前一样先搭个架子出来,多次用到节点的开辟等操作,所以我们创建一个专门的AVL节点类!在创建一个AVL树的类用于插入查找等操作!

template<class K, class V>
struct AVLTreeNode
{AVLTreeNode<K, V>* _left;//左子节点AVLTreeNode<K, V>* _right;//右子节点AVLTreeNode<K, V>* _parent;//父节点pair<K, V> _kv;int _bf;//平衡因子AVLTreeNode(const pair<K,V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_bf(0)//默认一个新节点是叶子节点,左右子树都为空所以高度差(平衡因子)为0{}
};template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:private:Node* _root = nullptr;//这里只有一个成员可以给一个缺省值就不用写构造函数了
};

AVL树的插入

AVL树的本质还是二叉搜索树,所以插入的时候还是遵循二叉搜索树的特点的!但是多了一个平衡因子,所以他的插入是分为两步的:

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

2、调节平衡因子

前者很好理解,这里主要解释一下后者:我们采用的平衡因子的方式是右子树的高度 - 左子树高度所以当新插入节点后,要更新其父先节点的平衡因子,如果插入的节点是在parent的左,平衡因子--; 如果是右平衡parent的因子++;

1、如果更新后父节点的平衡因子是0,即原先的parent左或右是有一个孩子的,现在是插入到原先没有孩子的那边了即平衡了,此时直接结束调节平衡因子;

2、如果更新parent的平衡因子后不是0,而是-1/1则需要继续向上更新!(直到cur更新到根节点,停止)

3、如果更新后父节点的平衡因子的绝对值超过了1就要旋转。(旋转后面单独介绍)

OK,举个例子:

这里咱们先暂时不管旋转是怎么旋转的,我们先把上一般的给搞出来:

bool Insert(const pair<K, V>& kv)
{Node* parent = nullptr;//记录插入位置的父节点Node* cur = _root;//当前插入节点//第一次插入if (_root == nullptr){_root = new Node(kv);return true;}//不是第一次插入,寻找插入位置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);//找到插入位置//判断是parent的左子树还是右子树if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;//将当前节点的父节点设置是为parent//调节平衡因子 ---> bf = right - leftwhile (parent){//更新当前节点的父节点的平衡因子if (cur == parent->_left)//在parent的左{parent->_bf--;//bf--}else{parent->_bf++;//在parent的右, bf++}if (parent->_bf == 0)//如果更新完,cur的parent的bf,发现是0。说明是 -1/1 --> 0 即parent原先左或右是有一个孩子的{break;//更新结束}else if (parent->_bf == 1 || parent->_bf == -1)//如果是parent的bf是1/-1{cur = parent;parent = parent->_parent;//需要继续更新其祖先节点}else{//此时parent的bf是-2/2需要旋转break;//旋转结束跳出平衡因子的调节}}return true;//插入成功
}

OK,先来验证一下,目前的逻辑对不对?AVL树的本质还是搜索树,所以他的中序是有序的,所以可以通过走中序验证目前的对不对:

中序遍历

实现思路:左->中->右

注意:由于AVL树的根节点是私有的,类的外面是访问呢不到的,所有以下三种思路:

1、将中序设置为友元(强烈推荐)

2、提供get和set函数

3、通过子函数(推荐)

void _InOrder(const Node* root)
{if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);
}

这是子函数,因为只有AVL类里面专用所以可以将它设置为私有的!

OK,就以上面介绍过的为例:

我们乱序插入到当前的AVL树中,如果中序是有序的说明我们的当前逻辑是对的!

OK,现在parent的bf==1/-1以及等于0的情况解决了,剩下的就是parent的bf是-2/2的情况了,此时就需要旋转了!我们下面来专门谈一谈旋转!

AVL树的旋转

旋转是由于parent的bf到了-2/2,此时的结构不符合AVL的平衡了;而parent为-2/2是由其孩子造成的,而孩子有两种情况即左边和右边即cur是-1/1;所以此时就有四种情况:
1、parent == -2 && cur == -1即父亲的左边高、孩子(左子树)的左边高 --> 右单旋

2、parent == 2  && cur == 1即父亲的右边高、孩子(右子树)的右边高 --> 左单旋

3、parent == -2 && cur == 1即父亲的左边高、孩子(左子树)的右边高 --> 左右双旋

4、parent == 2  && cur == -1即父亲的右边高、孩子(右子树)的左边高 --> 右左双旋

右单旋

parent的左边高,并且他的左孩子也高!

void RotateR(const Node* parent)
{Node* subL = parent->_left;//parent的左子树parent->_left = subL->_right;//将subL的又给parent的左if (subL->_right)//如果subL的右不为空subL->_right = parent;//此时subL右的父节点就是parentNode* ppNode = parent->_parent;//记录parent的父节点方便后面subL不是root时候的链接subL->_right = parent;//将parent连接到subL的右边parent->_parent = subL;//将parent的父节点设置为subLif (parent == _root)//当前的parent是根节点{_root = subL;//新的根节点就是subLppNode = nullptr;//根节点的父亲为空}else//当前的parent不是根节点{if (ppNode->_left == parent)//如果parent的是ppNode的左{ppNode->_left = subL;//将subL连接到ppNode的左边}else//parent的是ppNode的右{ppNode->_right = subL;//将subL连接到ppNode的右边}subL->_parent = ppNode;//subL的父节点指向ppNode}subL->_bf = parent->_bf = 0;//右单旋后subL和parent的bf都是0
}

左单旋

parent的右边高,并且他的右孩子也高!

void RotateL(const Node* parent)
{Node* subR = parent->_right;//parent的右子树parent->_right = subR->_left;//将subR的左子树给parent的右if (subR->_left)//如果subR的左不为空subR->_left->_parent = parent;//subR的左的父亲就是parentNode* ppNode = parent->_parent;//记录parent的父节点方便后面subL不是root时候的链接subR->_left = parent;//将parent连接到subR的左parent->_parent = subR;//parent的父节点就是subRif (parent == _root)//parent是根节点{_root = subR;//此时subR就是新的根_root->_parent = nullptr;}else//parent不是根节点{if (ppNode->_left == parent)//parent是ppNode的左{ppNode->_left = subR;//将subR连接到ppNode的左}else//parent是ppNode的右{ppNode->_right = subR;//将subR连接到ppNode的右}subR->_parent = ppNode;//subR的父节点指向ppNode}subR->_bf = parent->_bf = 0;//左单旋后subR和parent的bf都是0
}

左右双旋

parent的左孩子高,并且他左孩子的右边高!

void RotateLR(const Node* parent)
{Node* subL = parent->_left;//左子树Node* subLR = subL->_right;//左子树的右子树int bf = subLR->_bf;RotateL(parent->_left);//先对左子树左旋RotateR(parent);//在对整个树进行右旋if (bf == 0){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 0;}else if (bf == 1){subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else if (bf == -1){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else{assert(false);//正常不可能到这,这里是防止一开始就不是AVL}
}

右左双旋

parent的右孩子高,他的右孩子的左边高!

void RotateRL(const Node* parent)
{Node* subR = parent->_right;//右子树Node* subRL = subR->_left;//右子树的左子树int bf = subRL->_bf;RotateR(parent->_right);//先对右子树右旋RotateL(parent);//在对整个树进行左旋if (bf == 0){subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else if (bf == -1){subR->_bf = 1;subRL->_bf = 0;parent->_bf = 1;}else{assert(false);//正常不可能到这,这里是防止一开始就不是AVL}
}

ok,这就是所有的情况,我们现在加上旋转来看看,为了验证是否是平衡的,我们可以写一个判断是否平衡的函数:

判断平衡

实现思路:某一个节点的左右子树的差的绝对值不可以超多1

由于AVLTree类外面访问不到根,所以我们还是写成子函数的形式:
 

bool _IsBalance(const Node* root)
{if (root == nullptr)//空树也平衡return true;int left = _Hight(root->_left);//求左子树的高度int right = _Hight(root->_right);//求右子树的高度if (abs(left - right) >= 2)//如果左右子树的高度差的绝对值差超过1则就是不平衡return false;if (right - left != root->_bf)//不平衡,打印出他的节点的key值{cout << root->_kv.first << endl;return false;}return _IsBalance(root->_left) && _IsBalance(root->_right);//左右子树都得平衡
}int _Hight(Node* root)
{if (root == nullptr)//空树的个数是0return 0;return max(_Hight(root->_left), _Hight(root->_right)) + 1;//不是空返回左右子树的较大值 + 本身
}

ok ,验证一下:

OK,没有问题!我们再来把其他的完善一下:

AVL树的查找

实现思路:和二叉搜索树的一样,比根的去右边找,比根小去左边找!

Node* Find(const K& k)
{Node* cur = _root;while (cur){if (cur->_kv.first < k)//插入节点的键值比当前比较的节点的键值大{cur = cur->_right;//去当前节点的右子树查找}else if (cur->_kv.first > k)//插入节点的键值比当前比较的节点的键值小{cur = cur->_left;//去当前节点的左子树查找}else{return cur;//找到了}}return nullptr;//没找到
}

获取高度

实现思路:左子树 + 右子树 + 1(本身)

int _Hight(Node* root)
{if (root == nullptr)//空树的个数是0return 0;return max(_Hight(root->_left), _Hight(root->_right)) + 1;//不是空返回左右子树的较大值 + 本身
}

获取节点个数

实现思路:左子树的节点 + 右子树的节点 + 根

int _Size(const Node* root)
{if (root == nullptr)//空树return 0;return _Size(root->_left) + _Size(root->_right) + 1;//左子树+右子树+本身
}

AVL树的性能分析

AVL树是一颗绝对平衡的二叉搜索树,其要求每个节点的左右子树差都不超过1,这样可以保证查询高效的同时复杂度不会达到和二叉搜索树的极端情况的O(N)而是在logN;但是如果对AVL树做一些结构的修改,例如:插入太多次旋转也就多了,更差的是在删除时有可能旋转到根;因此如果需要一种查询且有序的数据结构,再者数据为静态的(不会改变)AVL树是一种不错的选择,但是如果经常修改其行能就不太好了,那要是既要改变还要效率高该如何弄呢?那就是下期介绍的红黑树了!!!

全部源码

#pragma once
#include <assert.h>template<class K, class V>
struct AVLTreeNode
{AVLTreeNode<K, V>* _left;//左子节点AVLTreeNode<K, V>* _right;//右子节点AVLTreeNode<K, V>* _parent;//父节点pair<K, V> _kv;int _bf;//平衡因子AVLTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0)//默认一个新节点是叶子节点,左右子树都为空所以高度差(平衡因子)为0{}
};template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:bool Insert(const pair<K, V>& kv){Node* parent = nullptr;//记录插入位置的父节点Node* cur = _root;//当前插入节点//第一次插入if (_root == nullptr){_root = new Node(kv);return true;}//不是第一次插入,寻找插入位置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);//找到插入位置//判断是parent的左子树还是右子树if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;//将当前节点的父节点设置是为parent//调节平衡因子 ---> bf = right - leftwhile (parent){//更新当前节点的父节点的平衡因子if (cur == parent->_left)//在parent的左{parent->_bf--;//bf--}else{parent->_bf++;//在parent的右, bf++}if (parent->_bf == 0)//如果更新完,cur的parent的bf,发现是0。说明是 -1/1 --> 0 即parent原先左或右是有一个孩子的{break;//更新结束}else if (parent->_bf == 1 || parent->_bf == -1)//如果是parent的bf是1/-1   0 ---> -1/1{cur = parent;parent = parent->_parent;//需要继续更新其祖先节点}else//此时parent的bf是-2/2需要旋转 -1/1 --> 2/-2{if (parent->_bf == -2 && cur->_bf == -1)//父亲和孩子的左边都高,右单旋{RotateR(parent);}else if (parent->_bf == 2 && cur->_bf == 1)//父亲和孩子的右边都高,左单旋{RotateL(parent);}else if (parent->_bf == -2 && cur->_bf == 1)//父亲的左边高,孩子的右边高,先左单旋,在右单旋{RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1)//父亲的右边高,孩子的左边高,先右单旋,在左单旋{RotateRL(parent);}else{assert(false);//正常情况不可能走到这里,这里是防止一开始就不是AVL树的情况}break;//旋转结束跳出平衡因子的调节}}return true;//插入成功}Node* Find(const K& k){Node* cur = _root;while (cur){if (cur->_kv.first < k)//插入节点的键值比当前比较的节点的键值大{cur = cur->_right;//去当前节点的右子树查找}else if (cur->_kv.first > k)//插入节点的键值比当前比较的节点的键值小{cur = cur->_left;//去当前节点的左子树查找}else{return cur;//找到了}}return nullptr;//没找到}void InOrder(){return _InOrder(_root);}bool IsBalance(){return _IsBalance(_root);}int Hight(){return _Hight(_root);}int Size(){return _Size(_root);}private:void _InOrder(const Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}void RotateR(Node* parent){Node* subL = parent->_left;//parent的左子树parent->_left = subL->_right;//将subL的又给parent的左if (subL->_right)//如果subL的右不为空subL->_right->_parent = parent;//此时subL右的父节点就是parentNode* ppNode = parent->_parent;//记录parent的父节点方便后面subL不是root时候的链接subL->_right = parent;//将parent连接到subL的右边parent->_parent = subL;//将parent的父节点设置为subLif (parent == _root)//当前的parent是根节点{_root = subL;//新的根节点就是subL_root->_parent = nullptr;//根节点的父亲为空}else//当前的parent不是根节点{if (ppNode->_left == parent)//如果parent的是ppNode的左{ppNode->_left = subL;//将subL连接到ppNode的左边}else//parent的是ppNode的右{ppNode->_right = subL;//将subL连接到ppNode的右边}subL->_parent = ppNode;//subL的父节点指向ppNode}subL->_bf = parent->_bf = 0;//右单旋后subL和parent的bf都是0}void RotateL(Node* parent){Node* subR = parent->_right;//parent的右子树parent->_right = subR->_left;//将subR的左子树给parent的右if (subR->_left)//如果subR的左不为空subR->_left->_parent = parent;//subR的左的父亲就是parentNode* ppNode = parent->_parent;//记录parent的父节点方便后面subL不是root时候的链接subR->_left = parent;//将parent连接到subR的左parent->_parent = subR;//parent的父节点就是subRif (parent == _root)//parent是根节点{_root = subR;//此时subR就是新的根_root->_parent = nullptr;}else//parent不是根节点{if (ppNode->_left == parent)//parent是ppNode的左{ppNode->_left = subR;//将subR连接到ppNode的左}else//parent是ppNode的右{ppNode->_right = subR;//将subR连接到ppNode的右}subR->_parent = ppNode;//subR的父节点指向ppNode}subR->_bf = parent->_bf = 0;//左单旋后subR和parent的bf都是0}void RotateLR(Node* parent){Node* subL = parent->_left;//左子树Node* subLR = subL->_right;//左子树的右子树int bf = subLR->_bf;RotateL(parent->_left);//先对左子树左旋RotateR(parent);//在对整个树进行右旋//重新更新平衡因子if (bf == 0){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 0;}else if (bf == 1){subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else if (bf == -1){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else{assert(false);//正常不可能到这,这里是防止一开始就不是AVL}}void RotateRL(Node* parent){Node* subR = parent->_right;//右子树Node* subRL = subR->_left;//右子树的左子树int bf = subRL->_bf;RotateR(parent->_right);//先对右子树右旋RotateL(parent);//在对整个树进行左旋//重新更新平衡因子if (bf == 0){subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else if (bf == -1){subR->_bf = 1;subRL->_bf = 0;parent->_bf = 0;}else{assert(false);//正常不可能到这,这里是防止一开始就不是AVL}}bool _IsBalance(const Node* root){if (root == nullptr)//空树也平衡return true;int left = _Hight(root->_left);//求左子树的高度int right = _Hight(root->_right);//求右子树的高度if (abs(left - right) >= 2)//如果左右子树的高度差的绝对值差超过1则就是不平衡return false;if (right - left != root->_bf)//不平衡,打印出他的节点的key值{cout << root->_kv.first << endl;return false;}return _IsBalance(root->_left) && _IsBalance(root->_right);//左右子树都得平衡}int _Hight(Node* root){if (root == nullptr)//空树的个数是0return 0;return max(_Hight(root->_left), _Hight(root->_right)) + 1;//不是空返回左右子树的较大值 + 本身}int _Size(const Node* root){if (root == nullptr)//空树return 0;return _Size(root->_left) + _Size(root->_right) + 1;//左子树+右子树+本身}private:Node* _root = nullptr;//这里只有一个成员可以给一个缺省值就不用写构造函数了
};

ok,本期分享就到这里,好兄弟。我们下期再见!

结束语:不要因为被人的三言两语就打破你原本的深思熟虑!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/340663.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OceanBase 4.3.0 列存引擎解读:OLAP场景的入门券

近期&#xff0c;OceanBase 发布了4.3.0版本&#xff0c;该版本成功实现了行存与列存存储的一体化&#xff0c;并同时推出了基于列存的全新向量化引擎和代价评估模型。通过强化这些能力&#xff0c;OceanBase V4.3.0 显著提高了处理宽表的效率&#xff0c;增强了在AP&#xff0…

PS插件一键轻松搞定电商产品摄影图!

在电商行业中&#xff0c;一张高质量的产品摄影图往往能够吸引更多潜在消费者的目光&#xff0c;从而增加产品的销量。然而&#xff0c;对于许多电商卖家和摄影师来说&#xff0c;后期处理产品图片却是一个既耗时又费力的工作。 最近我发现一款PS插件可以一键生成电商产品摄影…

Flutter基础 -- Dart 语言 -- 基础类型

目录 0. 配置 1. 变量 1.1 弱类型 var Object dynamic 1.2 强类型 1.3 使用场景 var 简化定义变量 查询参数定义 返回的实例对象 2. 常量 final 和 const 2.1 相同点 类型声明可以省略 初始后不能再赋值 不能和 var 同时使用 2.2 不同点 const 需要确定的值 …

【Python绘画】画笑脸简笔画

本文收录于 《一起学Python趣味编程》专栏&#xff0c;从零基础开始&#xff0c;分享一些Python编程知识&#xff0c;欢迎关注&#xff0c;谢谢&#xff01; 文章目录 一、前言二、代码示例三、知识点梳理四、总结 一、前言 本文介绍如何使用Python的海龟画图工具turtle&#…

fastjson 泛型转换问题(详解)

系列文章目录 附属文章一&#xff1a;fastjson TypeReference 泛型类型&#xff08;详解&#xff09; 文章目录 系列文章目录前言一、代码演示1. 不存在泛型转换2. 存在泛型转换3. 存在泛型集合转换 二、原因分析三、解决方案1. 方案1&#xff1a;重新执行泛型的 json 转换2. …

23种模式之一— — — —适配器模式的详细介绍与讲解

适配器介绍与讲解 一、概念二、适配器模式结构适配器分类核心思想核心角色模式的UML类图应用场景模式优点模式缺点 实例演示图示代码演示运行结果 一、概念 适配器模式&#xff08;别名&#xff1a;包装器&#xff09; 是一种结构型设计模式 将一个类的接口转换成客户希望的另…

每日一练:利用多态思想和ArrayList集合,编写一个模拟KTV点歌系统的程序。【多态思想和ArrayList集合的综合应用】

目录 一、设计程序使用ArrayList集合&#xff0c;编写一个模拟KTV点歌系统的程序。参考代码歌曲类歌单类KTV类测试类运行效果 总结 最后 一、设计程序 使用ArrayList集合&#xff0c;编写一个模拟KTV点歌系统的程序。 要求&#xff1a; 输入0代表添加歌曲输入1代表将所选歌曲…

STM32高级控制定时器之输入捕获模式

目录 概述 1 输入捕获模式 1.1 原理介绍 1.2 实现步骤 1.3 发生输入捕获流程 2 使用STM32Cube配置工程 2.1 软件环境 2.2 配置参数 2.3 生成项目文件 3 功能实现 3.1 PWM调制占空比函数 3.2 应用函数库 4 测试 4.1 功能框图 4.2 运行结果 源代码下载地址&#xf…

MySQL 存储过程(一)

本篇主要介绍MySQL存储过程的相关内容 目录 一、什么是存储过程&#xff1f; 二、基本语法 创建存储过程 调用存储过程 查看存储过程 删除存储过程 三、变量 系统变量 用户自定义变量 局部变量 四、存储过程的参数 in out inout 一、什么是存储过程&#xff1f…

9 个步骤内快速完成 SEO 审核

SEO审计对于提高网站在搜索引擎结果中的性能和可见性至关重要。这种系统评估涉及仔细检查各种元素&#xff0c;从关键字和页面优化到网站结构和页面速度等技术方面。在本指南中&#xff0c;我们将概述执行全面 SEO 检查器的 12 个基本步骤&#xff0c;帮助您确定优势、劣势和改…

基于小波区间相关的信号降噪方法(MATLAB 2021B)

在我们处理信号过程中最重要的任务就是找到信号隐藏的规律和信号的特征。常用的傅里叶分析法只能用于在时间域或者频率域上分析信号&#xff0c;而通常的数据会在时间域和频率域均有特征。而小波分析是继傅里叶分析之后的一大突破性创新&#xff0c;也是近年来在学术界非常热门…

小熊家务帮day5-day7 客户管理模块1 (小程序认证,手机验证码认证,账号密码认证,修改密码,找回密码等)

客户管理模块 1.认证模块1.1 认证方式介绍1.1.1 小程序认证1.1.2 手机验证码登录1.1.3 账号密码认证 1.2 小程序认证1.2.1 小程序申请1.2.2 创建客户后端工程jzo2o-customer1.2.3 开发部署前端1.2.4 小程序认证流程1.2.4.1 customer小程序认证接口设计Controller层Service层调用…

使用import语句导入模块

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 创建模块后&#xff0c;就可以在其他程序中使用该模块了。要使用模块需要先以模块的形式加载模块中的代码&#xff0c;这可以使用import语句实现。im…

react、vue动态form表单

需求在日常开发中反复写form 是一种低效的开发效率&#xff0c;布局而且还不同这就需要我们对其封装 为了简单明了看懂代码&#xff0c;我这里没有组件&#xff0c;都放在一起&#xff0c;简单抽离相信作为大佬的你&#xff0c;可以自己完成&#xff0c; 一、首先我们做动态f…

外包小菜鸡花了几个w报的课立志进大厂

不知不觉已经毕业了好几年&#xff0c;但是感觉还是自己的年龄增长了而已&#xff0c;对应的技术却没学到&#xff0c;最后一咬牙报了图灵的架构VIP班&#xff0c;不得不说&#xff0c;诸葛老师讲的是真的好呀&#xff0c;大家可以看看他的公开课&#xff0c;希望学完下面这些视…

JVMの内存泄漏内存溢出案例分析

1、内存溢出 内存溢出指的是程序在申请内存时&#xff0c;没有足够的内存可供分配&#xff0c;导致无法满足程序的内存需求&#xff0c;常见的内存溢出情况包括堆内存溢出&#xff08;Heap Overflow&#xff09;和栈溢出&#xff08;Stack Overflow&#xff09;&#xff1a; …

《数字图像处理-OpenCV/Python》第15章:图像分割

《数字图像处理-OpenCV/Python》第15章&#xff1a;图像分割 本书京东 优惠购书链接 https://item.jd.com/14098452.html 本书CSDN 独家连载专栏 https://blog.csdn.net/youcans/category_12418787.html 第15章&#xff1a;图像分割 图像分割是由图像处理到图像分析的关键步骤…

spark的简单学习二

一 spark sql基础 1.1 Dataframe 1.介绍&#xff1a; DataFrame也是一个分布式数据容器。然而DataFrame更像传统数据库的二维表 格&#xff0c;除了数据以外&#xff0c;还掌握数据的结构信息&#xff0c;即schema。同时&#xff0c;与Hive类似&#xff0c;DataFrame也支 持…

STM32_HAL_I2C_串行接口

电气特性 I2C&#xff08;Inter-Integrated Circuit&#xff09;是一种由飞利浦公司&#xff08;现恩智浦半导体&#xff09;开发的串行通信协议&#xff0c;用于连接低速外围设备。I2C总线只需要两根线&#xff08;SDA&#xff1a;串行数据线&#xff0c;SCL&#xff1a;串行…