【C++】AVL树,平衡二叉树详细解析

news2024/10/7 14:31:00

文章目录

  • 前言
  • 1.AVL树的概念
  • 2.AVL树节点的定义
  • 3.AVL树的插入
  • 4.AVL树的旋转
    • 左单旋
    • 右单旋
    • 左右双旋
    • 右左双旋
  • AVL树的验证
  • AVL树的删除
  • AVL树的性能


前言

前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡二叉树树来实现。

1.AVL树的概念

AVL树,又称平衡二叉搜索树。二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),左右子树的高度差被称为平衡因子(平衡因子=右子树高度-左子树高度),即可降低树的高度,从而减少平均搜索长度。

AVL树可以是一棵空树,也可能是具有一下性质的一棵平衡二叉树:

  • 树的左右子树都是一棵AVL树
  • 树的左右子树高度之差(平衡因子)的绝对值不超过1(可以是-1,0,1)

在这里插入图片描述
如果一棵二叉树是高度平衡的,它就是AVL树,如果它有n个结点,其高度可保持在O(logN),搜索时间复杂度为O(NlogN)。

2.AVL树节点的定义

我们需要实现一个KV模型的AVL树,在这里最好定义成三叉链的结构,多引入一个_parent(父节点),方便后序插入等操作。除此之外还要在每个结点中引入平衡因子,由于新构造的结点的左右子树均为空树,所以初始化的时候将平衡因子设置为0就好

为什么要设置平衡因子?为什么要设置成三叉链结构?

由于我们插入结点后需要倒着往上进行平衡因子的更新,所以我们将AVL树结点的结构设置为了三叉链结构,这样我们就可以通过父指针找到其父结点,进而对其平衡因子进行更新。当然,我们也可以不用三叉链结构,可以在插入结点时将路径上的结点存储到一个栈当中,当我们更新平衡因子时也可以通过这个栈来更新祖先结点的平衡因子,但是相对较麻烦。

注意平衡因子不是必须的,它只是AVL树其中一种实现方式,采用其它方法也可以判断左右子树高度差绝对值是否小于等于1,但是我认为引入平衡因子可以给我们更加直观的呈现平衡二叉树的整体特征

//定义AVL树的结点
//三叉链+平衡因子
template<class K,class V>
struct AVLTreeNode
{
	//存储<key,value>数据的结点pair
	pair<K,V> _kv;

	//三叉链
	AVLTreeNode<K, V>* _parent;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;


	int _bf;//平衡因子=右子树高度-左子树高度(-1,0,1)

	//结点的构造函数
	AVLTreeNode(const pair<K,V>& kv)
		:_kv(kv)
		, _parent(nullptr)
		, _left(nullptr)
		, _right(nullptr)
		,_bf(0)
	{}
};

3.AVL树的插入

思路:

  1. 插入数据到平衡二叉树中,我们首先需要一个根
  2. 按照二叉搜索树的插入方法,找到待插入位置。
    待插入结点的key值比当前结点小就插入到该结点的左子树。
    待插入结点的key值比当前结点大就插入到该结点的右子树。
    待插入结点的key值与当前结点的key值相等就插入失败。
  3. 找到待插入位置后,将待插入结点插入到树中。(注意保持三叉链结构)
  4. 更新平衡因子: 一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能也需要更新。
    新增结点在parent的右边,parent的平衡因子+ + 。
    新增结点在parent的左边,parent的平衡因子− − 。

每更新完一个结点的平衡因子后,还需要再进行以下判断:

如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

注意: parent的平衡因子在更新前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。

跳出循环的条件

在最坏情况下,平衡因子时一路更新到根结点。直到找到与待插入结点的key值相同的结点判定为插入失败或者最终走到空树位置进行结点插入。所以循环的条件是parent不为空,这也是使用三叉链结构的原因,我们可以迭代着往上走。

插入代码如下:

template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	//插入函数:我们需要将给的pair数据插入到AVL树中。
	bool insert(const pair<K, V>& kv)
	{
		//1.插入数据到平衡二叉树中,我们首先需要一个根
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		//2.根据二叉搜索树的特性(小的往左走,大的往右走),找到适合插入的位置
		Node* parent = nullptr;
		Node* cur = _root;//从根开始找合适的位置
		while (cur)
		{
			//注意:在平衡二叉搜索树中,键值对pair是按key值进行比较的
			if (cur->_kv.first < kv.first)
			{
				cur = cur->_right;
				parent = cur;
			}
			else if (cur->_kv.first > kv.first)
			{
				cur = cur->_left;
				parent = cur;
			}
			else
			{
				return false;//相等直接返回false
			}
		}

		//3.插入结点,注意保持三叉链的链接
		//此时parent指向待插入结点位置的父节点
		cur = new Node(kv);
		if (parent->_kv.first>kv.first)
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		// 1、更新平衡因子
		while (parent) // parent为空,也就更新到根
		{
			// 新增在右,parent->bf++;
			// 新增在左,parent->bf--;
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			// 是否继续更新依据:子树的高度是否变化
			// 1、parent->_bf == 0说明之前parent->_bf是 1 或者 -1
			// 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新
			// 2、parent->_bf == 1 或 -1 说明之前是parent->_bf == 0,两边一样高,现在插入一边更高了,
			// parent所在子树高度变了,继续往上更新
			// 3、parent->_bf == 2 或 -2,说明之前parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则
			// 就地处理--旋转

			// 旋转:
			// 1、让这颗子树左右高度不超过1
			// 2、旋转过程中继续保持他是搜索树
			// 3、更新调整孩子节点的平衡因子
			// 4、让这颗子树的高度跟插入前保持一致
			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)
				{
					RotateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				else
				{
					assert(false);
				}

				break;
			}
			else
			{
				assert(false);
			}
		}
		return true;
	}
private:
	Node* _root = nullptr;
};

4.AVL树的旋转

在AVL树的插入中,我们提到若是在更新平衡因子的过程当中,出现了平衡因子为-2/2的结点,这时我们需要对以该结点为根结点的树进行旋转处理。平衡二叉搜索树的旋转有多种,分别是左单旋,右单旋,左右双旋,右左双旋,我们分别介绍。

旋转的目的:

  • 让这棵树的左右子树高度差的绝对值不超过1
  • 旋转过程中依然要保存AVL树的特性
  • 更新被调整的结点的平衡因子
  • 让这棵子树的高度跟插入当前结点之前的高度一致

左单旋

1.什么情况下进行的是左单旋?

单纯的右边高,当前的parent的平衡因子==2,其右孩子的平衡因子为1,即cur的平衡因子为1的时候进行做单旋。 我们就需要把左边压下来。

if (parent->_bf == 2 && cur->_bf == 1)
{
	RotateL(parent);
}

2.如何进行左单旋?

在这里插入图片描述

为了提高代码的可阅读性,我们进行一下命名:当前parent指向30,subR(右子树)指向60,subRL(右子树的左子树)指向b。

步骤:

  1. SubR的左子树作为parent的右子树(这里要注意完善三叉链的连接,勿忘_parent,所以这里有一个注意事项,就是进行subRL->_parent的时候,需要判断subRL是否为空);
  2. 让parent作为subR的左子树
  3. 让subR作为整个子树的根(注意这里要判断subR是子树的根,还是整棵树的root)
  4. 更新平衡因子。

代码实现:

//左单旋
		void RotateL(Node* parent)
		{
			//命名结点
			Node* subR = parent->_right;
			Node* subRL = subR->_left;
			//若parent是某棵子树,则要保留当前结点的父节点,这样旋转之后subR成为根,才能和这棵树连成整体
			Node* ppNode = parent->_parent;

			//1.将subR的左子树给到parent的右子树
			parent->_right = subRL;
			if (subRL)//注意subRL不为空,才能进行这步,否则会造成对空指针解引用的错误
			{
				//完善三叉链的连接
				subRL->_parent = parent;
			}
			//2.将parent给到subR的左子树,subR变成当前子树的根
			subR->_left = parent;
			parent->_parent = subR;
			//3.将旋转完的子树;连接回去
			if (ppNode == nullptr)
			{
				//如果ppNode为空,则subR就是当前这棵树的根了
				_root = subR;
				subR->_parent = nullptr;
			}
			else
			{
				//如果刚开始parent是左子树
				if (ppNode->_left == parent)
				{
					ppNode->_left = subR;
				}
				else
				{
					ppNode->_right = subR;
				}
				subR->_parent = ppNode;
			}
			//更新平衡因子
			parent->_bf = subR->_bf = 0;
		}

右单旋

1,什么情况下进行右单旋?

单纯的左边高,当前parent的平衡因子== -2,parent左节点的平衡因子等于-1,即cur的平衡因子为-1,我们就需要把右边压下来。

if (parent->_bf == -2 && cur->_bf == -1)
{
	RotateR(parent);
}

2,如何进行右单旋?

在这里插入图片描述

步骤:

  1. 让subL的右子树作为parent的左子树;
  2. 让parent作为subL的右子树;
  3. 让subL作为整个子树的根;
  4. 更新平衡因子。
void RotateR(Node* parent)
		{
			//命名结点
			Node* subL = parent->_left;
			Node* subLR = subL->_right;
			//若parent是某棵子树,则要保留当前结点的父节点,这样旋转之后subL成为根,才能和这棵树连成整体
			Node* ppNode = parent->_parent;

			//1.将subL的右子树给到parent的左子树
			parent->_left = subLR;
			if (subLR)//注意subRL不为空,才能进行这步,否则会造成对空指针解引用的错误
			{
				//完善三叉链的连接
				subLR->_parent = parent;
			}
			//2.将parent给到subL的右子树,subL变成当前子树的根
			subL->_right = parent;
			parent->_parent = subL;
			//3.将旋转完的子树;连接回去
			if (ppNode == nullptr)
			{
				//如果ppNode为空,则subR就是当前这棵树的根了
				_root = subL;
				subL->_parent = nullptr;
			}
			else
			{
				//如果刚开始parent是左子树
				if (ppNode->_left == parent)
				{
					ppNode->_left = subL;
				}
				else
				{
					ppNode->_right = subL;
				}
				subL->_parent = ppNode;
			}
			//更新平衡因子
			parent->_bf = subL->_bf = 0;
		}

左右双旋

1.什么情况下进行左右双旋?

不难发现,前面讲的左单旋和右单旋,它们触发旋转的图形是一条直线,所以当图中出现折线型的路径时,就会触发双旋。
在这里插入图片描述

2.如何进行左右双旋?

步骤:

  1. 以subL为轴点进行左单旋(经过左单旋后,由图可以发现,折线型的路径变成了直线型);
  2. 以parent为轴点进行右单旋(当出现直线型的路径时,再进行一次单旋即可);
  3. 更新平衡因子:这是最复杂的一步,我们画的图是在b处插入新的结点,旋转成功后,subL和subLR的平衡因子更新为0,parent的平衡因子变成了1。如果我们是在c处插入新的结点,那么结果又是不同的(这里大家可以自己尝试着换一下旋转过程图)

左右双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:
在插入结点后,先记录一下subLR的平衡因子,若平衡因子为-1,则是在b处进行插入,最后subL和subR的平衡因子更新为0,parent的平衡因子变成了1;

若平衡因子为1,则是在c处进行插入,最后subLR和parent的平衡因子更新为0,subL的平衡因子变成了-1;

当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。

代码实现:

//左右双旋
		void RotateLR(Node* parent)
		{
			Node* subL = parent->_left;
			Node* subLR = subL->_right;
			int bf = subLR->_bf;

			//以subL为轴点进行左单旋,左边往下压
			RotateL(parent->_left);
			//以parent为轴点进行右单旋,右边往下压
			RotateR(parent);

			//更新平衡因子
			if (bf == 1)//subLR右子树新增结点
			{
				parent->_bf= 0;
				subL->_bf = -1;
				subLR->_bf = 0;
			}
			else if (bf == -1)
			{
				parent->_bf = 1;
				subL->_bf = 0;
				subLR->_bf = 0;
			}
			else if (bf == 0)
			{
				parent->_bf = 0;
				subL->_bf = 0;
				subLR->_bf = 0;
			}
			else
			{
				assert(false);
			}
		}

右左双旋

在这里插入图片描述

步骤:

  1. 以subR为旋转点进行右单旋。
  2. 以parent为旋转点进行左单旋。
  3. 更新平衡因子。
//右左双旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	//1、以subR为轴进行右单旋
	RotateR(subR);

	//2、以parent为轴进行左单旋
	RotateL(parent);

	//3、更新平衡因子
	if (bf == 1)
	{
		subRL->_bf = 0;
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else if (bf == -1)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 0)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert(false); //在旋转前树的平衡因子就有问题
	}
}

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 lh = Height(root->_left);
		int rh = Height(root->_right);

		return lh > rh ? lh + 1 : rh + 1;
	}
	bool IsBalance()
	{
		return IsBalance(_root);
	}

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

		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树的删除

可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,最差情况下要一直调整到根节点。

具体实现可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。

AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个结点的左右子树高度差的绝对值小于等于1,这样可以保证访问时的时间复杂度,O(logN),但是如果要对AVL树作一些结构修改的操作,性能就会降低。比如:插入时,我们需要维护其平衡,旋转的次数就比较多,在最差的情况下,还有可能要一直旋转到根。因此,如果需要一直查询高效且有序的数据结构,而且数据的个数是不变的,可以考虑AVL树,但如果一个结构经常需要修改,就不适合用AVL树。


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

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

相关文章

Linux基础命令-setfacl设置文件ACL策略规则

Setfacl 命令介绍 先查看文档中如何描述这个命令的NAME setfacl - set file access control lists setfacl&#xff08;Set file access control lists&#xff09;直译过来是设置文件访问控制列表 &#xff0c;其主要功能是用于设置文件ACL策略规则。FACL即文件访问控制列表…

GEC6818开发板JPG图像显示,科大讯飞离线语音识别包Linux_aitalk_exp1227_1398d7c6运行demo程序,开发板实现录音

GEC6818开发板JPG图像显示 | 开发板实现录音一.GEC6818开发板JPG图像显示1.jpg图片特性2.如何解压缩jpg图片1.对jpegsrc.v8c.tar.gz进行arm移植2.进入~/jpeg-8c对jpeg库进行配置3.编译4.安装&#xff0c;将动态库存放到 /home/gec/armJPegLib5.清空编译记录6.自己查看下 /home/…

C语言-基础了解-06-C存储类

C存储类 一、存储类 存储类定义 C 程序中变量/函数的的存储位置、生命周期和作用域。 这些说明符放置在它们所修饰的类型之前。 下面列出 C 程序中可用的存储类&#xff1a; auto register static extern auto 存储类 auto 存储类是所有局部变量默认的存储类。 定义在函数…

【IDEA】如何在Tomcat上创建部署第一个Web项目?

看了网上很多教程&#xff0c;发现或多或都缺失了一些关键步骤信息&#xff0c;对于新手小白很不友好&#xff0c;那么今天就教大家如何在Tomcat服务器&#xff08;本地&#xff09;上部署我们的第一个Web项目&#xff1a; 共分为三个部分&#xff1a; 1. IDEA创建Web项目&am…

【编程实践】简单是好软件的关键:Simplicity is key to good software

Simplicity is key to good software 简单是好软件的关键 目录 Simplicity is key to good software简单是好软件的关键 Complexity is tempting. 复杂性很诱人。 The smallest way to create value创造价值的最小方法 Simple 简单的 Complexity is tempting. 复杂性很诱人…

【Vue】Vue的简单介绍与基本使用

一、什么是VueVue是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面。无论是简单还是复杂的界面&#xff0c;Vue 都可以胜任。1.构建用户界面传统方…

每日的时间安排规划

14:23 2023年3月4日星期六 开始 现在我要做一套试卷。模拟6级考试。 现在是&#xff1a; 16:22 2023年3月4日星期六。 做完了线上的试卷&#xff01; 发现我真的是不太聪明的样子&#xff01; 明明买的有历年真题&#xff0c;做真题就行了&#xff0c;还要做它们出的模拟的…

现代卷积神经网络(AlexNet)

专栏&#xff1a;神经网络复现目录 本章介绍的是现代神经网络的结构和复现&#xff0c;包括深度卷积神经网络&#xff08;AlexNet&#xff09;&#xff0c;VGG&#xff0c;NiN&#xff0c;GoogleNet&#xff0c;残差网络&#xff08;ResNet&#xff09;&#xff0c;稠密连接网络…

操作系统---存储管理

存储管理 操作系统将外存的文件调入到内存中&#xff0c;以便CPU调用&#xff0c;如果调用的内容不在内存中&#xff0c;则会产生缺页中断&#xff1b;产生缺页中断后&#xff0c;这事需要从外存调数据到内存中&#xff0c;然后CPU接着从断点继续调用内存中的数据&#xff1b;在…

Webshell管理工具

Webshell管理工具Webshell简介Webshell作用Webshell管理工具菜刀蚁剑Webshell简介 Webshell是以ASP、PHP、JSP或者CGl等网页文件形式存在的一种代码执行环境&#xff0c;主要用于网站管理、服务器管理、权限管理等操作。 Webshell使用方法简单&#xff0c;只需上传一个代码文件…

C语言详解双向链表的基本操作

目录 双链表的定义与接口函数 定义双链表 接口函数 详解接口函数的实现 创建新节点&#xff08;BuyLTNode&#xff09; 初始化双链表&#xff08;ListInit&#xff09; 双向链表打印&#xff08;ListPrint&#xff09; 双链表查找&#xff08;ListFind&#xff09; 双链…

SiteWhere 宣布推出 SiteWhere 企业版 (EE) 测试版

开源物联网应用程序支持平台 (AEP) 供应商 SiteWhere 刚刚宣布推出SiteWhere 企业版 (EE) Beta。SiteWhere EE基于SiteWhere 开源开发项目&#xff0c;是为企业客户打造的平台。SiteWhere EE 已经引起了全球各行业公司的兴趣。 SiteWhere EE 有许多功能&#xff0c;但 MachNati…

Vue3之组件间传值

何为组件间传值 在Vue3之组件文章中&#xff0c;我们学会了定义使用组件&#xff0c;但是我们似乎还缺少什么将组件之间联系起来&#xff0c;说到组件之间的联系就不得不提组件间的传值&#xff0c;而组件间的传值其实也不难理解&#xff0c;就是如何在子组件中接收到父组件传…

C语言再学习第三章

例题3-1 编写一个函数&#xff0c;实现华氏度和摄氏度的转化。 已知公式&#xff1a;c &#xff08;5/9)*(f-32) #include <stdio.h>double f_value 0; double c_value 0; int main(void) {printf("请输入华氏温度\n");scanf("%lf",&f_valu…

两阶段提交(2 Phase Commit) 在 PostgreSQL 和 RocksDB 中的实现

文章目录前言用法PostgreSQLRocksDB实现PostgreSQL 2PCRocksDB 2PCWRITE_COMMITTEDWRITE_PREPARED解决 snapshot-read 问题解决 rollback 问题WRITE_UNPREPARED总结前言 本节中提到的代码实现是基于 PG&#xff1a;REL_15_STABLE 和 Rocksdb: master-fcd816d534 代码介绍的 2PC…

shell:#!/usr/bin/env python作用是什么

我们经常会在别人的脚本文件里看到第一行是下面这样 #!/usr/bin/python或者 #!/usr/bin/env python 那么他们有什么用呢&#xff1f; 要理解它&#xff0c;得把这一行语句拆成两部分。 第一部分是 #! 第二部分是 /usr/bin/python 或者 /usr/bin/env python 关于 #! 这个…

Java8 新特性 之 lambda 表达 和 函数式接口

—— lambda 表达式 概念 lambda 表达式是一个匿名函数&#xff0c;可以把 lambda 表达式理解为是一段可以传递的代码。更简洁、更灵活&#xff0c;使 Java 的语言表达能力得到了提升lambda 表达式是作为接口的实现类的对象&#xff08;万事万物皆对象&#xff09; 使用语法…

世界那么大,你哪都别去了,来我带你了解CSS3(三)

文章目录‍❤️‍&#x1f525;CSS动画‍❤️‍&#x1f525;CSS雪碧图‍❤️‍&#x1f525;CSS字体图标‍❤️‍&#x1f525;CSS盒子模型&#xff08;Box Model&#xff09;‍❤️‍&#x1f525;CSS新特性‍❤️‍&#x1f525;CSS动画 动画是使元素从一种样式逐渐变化为另…

【Rides】使用Xshell 链接云服务器安装Rides及其三种启动方法详解

文章目录一.NoSQL和SQl的概念1.1 总结二.Rides2.1 Rides特点2.2 Rides安装2.2.1 上传安装包并解压2.3 Redis启动&#xff08;前台启动不推荐&#xff09;2.4.指定配置启动2.5 开机自启三.Redis客户端2.1.Redis命令行客户端2.2.图形化桌面客户端2.2.1.安装2.2.2.建立连接一.NoSQ…

Vue.js 实现带拖动功能的时间轴

带拖动功能的时间轴timeline-slider-vueDemoGithub环境node V12.20.0npm 6.14.8&#x1f4e6; Installnpm install --save timeline-slider-vue全局引用main.jsimport TimelineSliderVue from timeline-slider-vueimport timeline-slider-vue/lib/timeline-slider-vue.cssVue.u…