C++:AVL树

news2024/9/28 21:24:18

AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下,时间复杂度为O(N);

两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。AVL树,即是高度平衡的二叉搜索树。

一棵AVL树是一棵平衡二叉搜索树,也能是一棵空树。

AVL树的性质:

①它的左右子树都是AVL树

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

③如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在log_2N,搜索的时间复杂度是log_2N。

 AVL树的定义:

AVL树的定义中:①拥有键值对。②多加一个双亲节点,用于调整平衡二叉树。③增加平衡因子,用于判断插入或删除后,是否还是一棵AVL树。

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 factor

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
	{}
};

AVL树的插入

AVL树的插入分成两步:第一步是按照二叉搜索树的方式来新增节点。第二步是是调整节点,使其成为一棵平衡的二叉搜索树。

先展示代码,我们分析以下思路:

1.首先按照二叉搜索树的方式来新增节点,但这需要新增一个双亲指针,方便后续在调节节点。因此,在新增节点的最后部分的代码中,我们需要让cur->_parent指向双亲节点parent。

2.完成二叉搜索树的创建后,开始去判断各个节点的平衡因子。

①当平衡因子_bf等于0的时候,说明parent节点一边高一边矮,新增的这个节点填上了矮的地方,这种情况就不需要更新了,直接beak掉。

②当平衡因子_bf等于1或者-1的时候,说明parent原本的平衡因子是0,parent两边一样高,新增了节点之后,有一边变高了。这种情况需要继续往上走。

③当平衡因子_bf等于2或-2的时候,说明之前parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则,此时需要原地旋转,即以当前节点为轴旋转。

随后将重点分析:当平衡因子是2或-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)
		{
			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;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		//更新平衡因子,判断插入后的树,是否是一棵AVL树
		while (parent)
		{
			//插入的是在父节点的左边,即是左孩子
			if (cur == parent->_left)
			{
				parent->_bf--;//一般是右-左,因此,如果是插入在左边,那就是减一
			}
			else  //是右孩子
			{
				parent->_bf++;
			}

			//更新完平衡因子后,判断是否是一棵AVL树
			//如果平衡因子是0,任何节点的平衡因子都没被改变
			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				//如果平衡因子是1或-1,那么就说明,父节点往上的节点的平衡因子有可能被改变了
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -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  //不排除在创建一棵AVL树的时候,代码写错了
			{
				assert(false);
			}
		}
		return true;
	}

旋转节点

旋转的要求:

⭐让这颗子树左右高度之差不超过1

⭐旋转过程中继续保持他是搜索树

⭐更新调整孩子节点的平衡因子

⭐让这颗子树的高度跟插入前保持一致。因为这样就不会对上层的平衡因子造成影响,此时就可以结束对这棵树的更新旋转。

旋转的情况有四种:

①新节点插入较高左子树的左侧---左左:右单旋

这种情况是新增的节点位于比较高的左子树的左侧的某个位置上,此时在往上检查平衡因子发现值为60的节点是平衡因子为-2,说明左子树的高度是比右子树高的(这里选择右减左),所以当我们的判断条件if(parent->_bf==-2 && cur->_bf == -1)成立时,就表示着符合当前情况。所以,我们需要将60的节点的平衡因子减小,那就是将它按下去!以60的节点为轴旋转:

⭐旋转的动作:因为b是30节点的右孩子,根据二叉搜索树的性质,b子树所有的值肯定是大于30,小于60的,而且60节点需要下来,说明60节点是要成为30节点的右孩子的,因此b子树就需要成为60节点的左孩子了。

⭐当然,我们需先判断一下,30节点的右孩子是否为空,即30节点没有右孩子。如果没有右孩子,那么就不能让它指向60节点的左孩子。

⭐然而,我们旋转的这颗树,可能是一颗子树,因此需要判断一下60节点的双亲节点是否为空,如果为空,说明它不是子树,此时就可以让_root指向subL,成为新的根,然后subL的双亲节点置为nullptr,因为subL->parent原本是指向60节点的。

⭐如果不为空,那就说明它是一棵子树,那么就让60节点的双亲节点的左或右孩子点指向30节点,30节点的双亲指向60原先的双亲节点。

右单旋的代码如下:

	void RotateR(Node* parent)
	{
		//一开始,例子中的60节点便是parent
		//先创建指向30节点的指针和指向b节点的指针
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//这一步是让60节点的左孩子指向b节点
		parent->_left = subLR;
		//这一步判断b节点是否为空,如果不为空,那么就让它的双亲节点指向60节点(本来是指向subL的)
		if (subLR)
		{
			subLR->_parent = parent;
		}
		//上面两步成功将b节点改链接到60节点上去
		//先保存60节点的双亲节点
		Node* ppNode = parent->_parent;
		//让30节点subL的右孩子指向60节点,即60节点链接到了30节点subL的右孩子上
		subL->_right = parent;
		//60节点的双亲节点指向30节点subL
		parent->_parent = subL;
		//判断60节点原本的双亲节点是否为空
		//为空
		if (ppNode == nullptr)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else  //不为空,说明是一棵子树
		{
			if (ppNode->_left == parent) //如果parent是原先的双亲节点的左孩子
			{
				ppNode->_left = subL; 

			}
			else                         //如果parent是原先的双亲节点的右孩子
			{
				ppNode->_right = subL;
			}
			
			//链接后,再让成为新根后的30节点subL的双亲节点指向ppNode
			subL->_parent = ppNode;
		}

		//最后将30节点subL和60节点parent的平衡因子修改
		//
		subL->_bf = parent->_bf = 0;
		//此时,右单旋完成
		//因为旋转的要求是让这颗子树的高度跟插入前保持一致
		//那就说明,此时完成旋转的这树,不会对上层的平衡因子造成影响,此时就可以结束对这棵树的更新旋转
	}

②新节点插入较高右子树的右侧---右右:左单旋

这种情况是新增的节点位于比较高的右子树的右侧的某个位置上,此时在往上检查平衡因子发现值为30的节点是平衡因子为2,说明右子树的高度是比左子树高的(这里选择右减左),所以当我们的判断条件if(parent->_bf==2 && cur->_bf == 1)成立时,就表示着符合当前情况。所以,我们需要将30的节点的平衡因子减小,那就是将它按下去!以30的节点为轴旋转:

⭐旋转的动作:因为b是60节点的左孩子,根据二叉搜索树的性质,b子树所有的值肯定是大于30,小于60的,而且30节点需要下来,说明30节点是要成为60节点的左孩子孩子的,因此b子树就需要成为30节点的右孩子了。

⭐同样的,我们需先判断一下,60节点的左孩子是否为空,即60节点没有左孩子。如果没有左孩子,那么就不能让它指向30节点的右孩子。

⭐同样的,需要盘点一下是否是一棵子树。因此需要判断一下30节点的双亲节点是否为空,如果为空,说明它不是子树,此时就可以让_root指向subR,成为新的根,然后subR的双亲节点置为nullptr,因为subR->parent原本是指向30节点的。

⭐如果不为空,那就说明它是一棵子树,那么就让30节点的双亲节点的左或右孩子点指向60节点,60节点的双亲指向30原先的双亲节点。

左单旋代码如下:

	void RotateL(Node* parent)
	{
		//创建60节点subR,右右是左旋嘛
		Node* subR = parent->_right;
		//创建指向b节点的指针subRL
		Node* subRL = subR->_left;
		//先让parent的右孩子节点指向subRL。
		parent->_right = subRL;
		//判断subRL是否为空,不为空,那就让subRL的父节点指向parent
		if (subRL)
		{
			subRL->_parent = parent;
		}
		//上面步骤成功将b节点链接到了parent上
		
		//先把parent的父节点保存起来,不管存在不存在
		Node* ppNode = parent->_parent;
		//接下来就是把parent按下了,成为subR的左孩子,让subR成为新根
		subR->_left = parent;
		parent->_parent = subR;
		//让subR成为新根
		if (ppNode == nullptr)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left = parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}
		//修改平衡因子
		parent->_bf = subR->_bf = 0;
	}

③新节点插入较高左子树的右侧---左右:先左单旋再右单旋

这种情况是新增的节点位于比较高的左子树的右侧的某个位置上,此时在往上检查平衡因子发现值为parent节点的平衡因子为-2,说明左子树的高度是比右子树高的(这里选择右减左),所以当我们的判断条件if(parent->_bf==-2 && cur->_bf == 1)成立时,就表示着符合当前情况。这种情况采取的旋转方式是先左旋后右旋。左旋的轴是subL节点,右旋的轴就是parent节点。

此时,我们复用左单旋和右单旋的情况即可。但是需要注意的是,尽管在右单旋和左单旋中,已经对平衡因子进行了修改,但我们通过画图可以看出来,修改过的平衡因子并不符合实际上的值,因此我们需要重新修改一遍。

代码如下:

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;//记录调整节点之前,subLR的平衡因子,因为subLR最后是新根
		//开始调整
		RotateL(parent->_left);
		RotateR(parent);

		//修改平衡因子
		if (bf == -1)//说明是在左子树上新增节点,即图中的b子树
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1)//说明是在右子树c上新增节点
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else if(bf==0)//说明subLR自己就是新增的节点
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

④新节点插入较高右子树的左侧---右左:先右单旋再左单旋

这种情况是新增的节点位于比较高的右子树的左侧的某个位置上,此时在往上检查平衡因子发现值为parent节点的平衡因子为2,说明右子树的高度是比左子树高的(这里选择右减左),所以当我们的判断条件if(parent->_bf==2 && cur->_bf == -1)成立时,就表示着符合当前情况。这种情况采取的旋转方式是先右旋后左旋。右旋的轴是subR节点,左旋的轴就是parent节点。

此时,我们复用左单旋和右单旋的情况即可。但是需要注意的是,尽管在右单旋和左单旋中,已经对平衡因子进行了修改,但我们通过画图可以看出来,修改过的平衡因子并不符合实际上的值,因此我们需要重新修改一遍。

代码如下:

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;//记录调整节点之前,subRL的平衡因子,因为subRL最后是新根

		RotateR(parent->_right);
		RotateL(parent);

		if (bf == 1) //在右子树上新增节点
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1) //在左子树上新增节点
		{
			parent->_bf = 0;
			subR->_bf = -1;
			subRL->_bf = 0;
		}
		else if (bf == 0) //说明subRL本身就是那个新增的节点
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

总结:

假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑:

①pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR。
当pSubR的平衡因子为1时,执行左单旋
当pSubR的平衡因子为-1时,执行右左双旋

②pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
当pSubL的平衡因子为-1是,执行右单旋
当pSubL的平衡因子为1时,执行左右双旋

旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。

验证AVL树

由于AVL树是在二叉搜索树的基础上加了平衡性后得到的树,因此需要确认一棵树是AVL树,那么就需要以下两步:

1.先确定是否是一棵二叉搜索树:如果中序遍历可得到一个有序的序列,就说明为二叉搜索树。

2.验证其是否平衡:①每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)。②节点的平衡因子是否计算正确。

代码如下:

①中序遍历:

	void Inorder()
	{
		_Inorder(_root);
	}

	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		cout << root->_kv.first << ": " << root->_kv.second << endl;
		_Inorder(root->_right);
	}

②计算高度:

	int Height(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		//先计算左子树的高度
		int ln = Height(root->_left);
		//然后计算右子树的高度
		int rn = Height(root->_right);

		return ln > rn ? ln + 1 : rn + 1;
	}

③验证平衡:

	bool IsBalance()
	{
		return _IsBalance(_root);
	}

	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;

		//计算当前节点root的平衡因子
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		//如果不同,那就将当前节点的值打印出来,并提升异常
		if (rightHeight - leftHeight != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}
		//通过递归,验证每一个节点的平衡因子是否符合
		return abs(rightHeight - leftHeight) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

AVL树性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2 (N)。

但是如果要对AVL树做一些结构修改的操作,性能非常低下,因为做修改就很大可能需要进行旋转,每一次旋转都是比较消耗性能的!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机试_4_数学问题

在机试中&#xff0c;我们经常会面对这样一类问题&#xff0c;它们并不涉及很深奥的算法和数据结构&#xff0c;而只与数理逻辑相关&#xff0c;将这类题目称为数学问题。 这类问题通常不需要用到特别高深的数学知识&#xff0c;而只需要掌握简单的数理逻辑知识。本文重点记录…

解决访问GitHub时出现的“您的连接不是私密连接”的问题!

Content问题描述解决办法问题描述 访问github出现您的连接不是私密连接问题&#xff0c;无法正常访问&#xff0c;如下图所示&#xff1a; 解决办法 修改hosts文件。hosts文件位于&#xff1a;C:\Windows\System32\drivers\etc\hosts 首先在https://www.ipaddress.com/查找两…

Linux之case语句和循环语句

一、case语句1.case语句的结构case语句主要适用于以下情况&#xff1a;某个变量存在多种取值&#xff0c;需要对其中的每一种取值分别执行不同的命令序列。这种情况与多分支的if语句非常相似&#xff0c;只不过if语句需要判断多个不同的条件&#xff0c;而case语句只是判断一个…

Linux高级命令之查找文件命令

查找文件命令学习目标能够说出查找文件使用的命令1. find命令及选项的使用命令说明find在指定目录下查找文件(包括目录)find命令选项:选项说明-name根据文件名(包括目录名)字查找find命令及选项的效果图:2. find命令结合通配符的使用通配符:是一种特殊语句&#xff0c;主要有星…

Linux中几个在终端中有趣的命令

uhh…最近我不知道该更新些什么&#xff0c;所以就更新Linux几个很有趣的命令 文章目录前言1.命令&#xff1a;sl安装 sl输出2. 命令&#xff1a;telnet命令&#xff1a;fortune安装fortune4.命令&#xff1a;rev&#xff08;反转&#xff09;安装rev5. 命令&#xff1a;factor…

第二章 Opencv图像处理基本操作

目录1.读取图像1-1.imread()方法2.显示图像2-1.imshow()方法2-2.waitKey()方法2-3.destroyAllWindows()方法2-4.小总结3.保存图像3-1.imwrite()方法4.查看图像属性4-1.常见的三个图像属性1.读取图像 要对一幅图像进行处理&#xff0c;第一件事就是要读取这幅图像。 1-1.imread(…

Vue驼峰与短横线分割命名中有哪些坑

目录 0.前言 驼峰和短横线分割命名注意事项 组件注册命名 父子组件数据传递时命名 父子组件函数传递 0.前言 Vue驼峰命名法指的是将变量以驼峰形式命名&#xff0c;例如 userName、userId 等&#xff0c;而短横线分隔符法则指的是用短横线分隔变量名&#xff0c;例如 user…

Python 高级编程之生成器与协程进阶(五)

文章目录一、概述二、生成器1&#xff09;生成器和迭代器的区别2&#xff09;生成器创建方式1、通过生成器函数创建2、通过生成器表达式创建3&#xff09;生成器表达式4&#xff09;yield关键字5&#xff09;生成器函数6&#xff09;return 和 yield 异同7&#xff09;yield的使…

RocketMQ底层源码解——事务消息的实现

1. 简介 RocketMQ自身实现了事务消息&#xff0c;可以通过这个机制来实现一些对数据一致性有强需求的场景&#xff0c;保证上下游数据的一致性。 以电商交易场景为例&#xff0c;用户支付订单这一核心操作的同时会涉及到下游物流发货、积分变更、购物车状态清空等多个子系统…

Linux高级命令之压缩和解压缩命令

压缩和解压缩命令学习目标能够使用tar命令完成文件的压缩和解压缩1. 压缩格式的介绍Linux默认支持的压缩格式:.gz.bz2.zip说明:.gz和.bz2的压缩包需要使用tar命令来压缩和解压缩.zip的压缩包需要使用zip命令来压缩&#xff0c;使用unzip命令来解压缩压缩目的:节省磁盘空间2. ta…

如何在VMware虚拟机上安装运行Mac OS系统(详细图文教程)

一、安装前准备 虚拟机运行软件&#xff1a;VMware Workstation Pro&#xff0c;版本&#xff1a;16.0.0 。VMware Mac OS支持套件&#xff1a;Unlocker。Mac OS系统镜像。 如果VMware 在没有安装Unlocker的情况下启动&#xff0c;在选择客户机操作系统时没有支持Mac OS的选项…

Mock.js初步使用(浏览器端)

Mock.js&#xff1a;生成随机数据&#xff0c;拦截 Ajax 请求。官方地址&#xff1a;http://mockjs.com/第一个demodemo.html<!DOCTYPE html> <html> <head><meta charset"utf-8"><title>mockjs demo</title> </head> <…

STM32单片机OLED显示

OLED接口电路STM32单片机OLED显示程序源代码#include "sys.h"#define OLED_RST_Clr() PCout(13)0 //RST#define OLED_RST_Set() PCout(13)1 //RST#define OLED_RS_Clr() PBout(4)0 //DC#define OLED_RS_Set() PBout(4)1 //DC#define OLED_SCLK_Clr()PCout(15)0 //SCL…

详解Python文件pyinstaller打包

本文python文件打包用到的是pyinstaller库并且以如下格式的文件为例 其中bird.py用到了images文件夹当中的png pyinstaller有两种打包方式: 方法1:文件夹模式 onedir 在终端用命令 pyinstaller -D flappybird.py执行完后文件格式如下 可以看到多了.idea,pycache,build,dis…

Linux系列 备份与分享文档

作者简介&#xff1a;一名在校云计算网络运维学生、每天分享网络运维的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.备份与分享文档 1.使用压缩和解压缩工具 &#xff08;1&…

Java零基础教程——数据类型

目录数据类型数据类型的分类运算符算术运算符符号做连接符的识别自增、自减运算符赋值运算符关系运算符逻辑运算符短路逻辑运算符三元运算符运算符优先级数据类型 数据类型的分类 引用数据类型&#xff08;除基本数据类型之外的&#xff0c;如String &#xff09; 基本数据类…

【STM32】【HAL库】遥控关灯2 分机

相关连接 【STM32】【HAL库】遥控关灯0 概述 【STM32】【HAL库】遥控关灯1主机 【STM32】【HAL库】遥控关灯2 分机 【STM32】【HAL库】遥控关灯3 遥控器 需求 接收RF433和红外信号,根据信号内容控制舵机 硬件设计 主控采用stm32F103c6 STM32 433接收 其他接口 软件设计 接…

[SSD固态硬盘技术 14] GC垃圾回收太重要了

今天介绍臭名昭著的垃圾收集 过程(或“GC”),maybe 这是对JAVA 工程师而言。当遇到GC导致速度降低时候, 他们真的想跳脚。 我想到我的小孩打疫苗,哭的哇哇叫, 在他的眼里疫苗应该也是讨厌的吧, 但事实真的如此吗? 但首先,让我们考虑一下如果根本没有 GC,闪存系统会发…

【Shell1】shell语法,ssh/build/scp/upgrade,环境变量,自动升级bmc,bmc_wtd,

文章目录1.shell语法&#xff1a;Shell是用C语言编写的程序&#xff0c;它是用户使用Linux的桥梁&#xff0c;硬件>内核(os)>shell>文件系统1.1 变量&#xff1a;readonly定义只读变量&#xff0c;unset删除变量1.2 函数&#xff1a;shell脚本传递的参数中包含空格&am…

微信小程序 学生选课系统--nodejs+vue

系统分为学生和管理员&#xff0c;教师三个角色 学生小程序端的主要功能有&#xff1a; 1.用户注册和登陆系统 2.查看选课介绍信息 3.查看查看课程分类 4.查看课程详情&#xff0c;在线选课&#xff0c;提交选课信息 5.在线搜索课程信息 6.用户个人中心修改个人资料 7.用户查看…