【C++STL】红黑树(更新中)

news2025/1/10 23:30:53

前言

上篇博客学习了平衡二叉搜索树(AVLTree),了解到AVL树的性质,二叉搜索树因为其独特的结构,查找、插入和删除在平均和最坏情况下都是O(logn)。AVL树的效率就是高在这个地方。
但是在AVL树中插入或者删除结点,使得高度差的绝对值大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵平衡二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理,但是因为每个结点的高度差的绝对值都要小于1,这个条件较为的严格,所以导致多数情况的插入和删除都需要旋转调整,导致插入和删除的效率降低。
这时红黑树应运而生,并且因为其接近平衡的结构,使其查找效率也很高效,同时因为没有AVL树那样的严格要求,所以其插入和删除效率有时还高于AVL树,使其综合性能高于AVL树,所以红黑树的应用十分的广泛,比如在 Java 的集合框架 (HashMap、TreeMap、TreeSet),C++ 的 STL中都有以红黑树为底层结构实现的容器
那红黑树到底是什么呢?又是如何实现的呢?

在这里插入图片描述

文章目录

  • 前言
  • 一. 什么是红黑树
  • 二. 红黑树的效率
  • 三. 红黑树实现
    • (1). 结点结构体
    • (2). 结点的插入
      • 1. 情况一
      • 2. 情况二
      • 3. 情况三
      • 4. 情况四
      • 5. 小总结
      • 6. 代码实现
    • (3). 查找
    • (4). 红黑树的销毁
    • (5). 测试
  • 结束语

一. 什么是红黑树

首先,红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制红黑树确保没有一条路径会是其他路径的两倍,因而是接近平衡
在这里插入图片描述

AVL树是依靠每个结点的左右高度差小于1来完成整棵树的平衡,而不会出现歪脖子树的场景
但是这样的要求较为的严格,导致插入和删除效率降低,而红黑树保持接近平衡是依靠以下几个规则:

  1. 每个结点不是红色就是黑色
  2. 根结点是黑色的
  3. 如果一个结点是红色的,则它的两个孩子结点是黑色的
  4. 对于每个结点,从该节点到其后代叶子结点的简单路径上,均包含相同数目的黑色结点
  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点

从3规则我们可以推导出,红黑树中没有连续的红色结点
同时,红黑树中,最短路径最少是全是黑色结点的路径,而最长路径最多就是红黑相间的结点
而每个路径的黑色结点个数相同,最长路径最多是黑红相间,所以最长路径最多是 黑+红=2黑

二. 红黑树的效率

红黑树的最短路径:全黑
红黑树的最长路径:一黑一红,红黑相间

假设总共有N个结点
那么最短路径就是以2为底的logN
最长路径就是2logN
所以红黑树的效率最差就是2logN
而AVL树的查找效率是logN,二者的效率仅是2倍
因为以2为底的logN已经是不大的数了,所以2倍差距并不大。
但是红黑树的调整没有AVL树频繁,所以综合效率红黑树更胜一筹

在这里插入图片描述
像这样一棵树,如果是AVL树,则右边高度比左边高2,需要旋转,但是符合红黑树的条件,不用旋转

三. 红黑树实现

(1). 结点结构体

红黑树结点的结构体大致与AVL树相同,但不需要平衡因子,而需要一个标记颜色的存储位
我们可以使用枚举体定义这个颜色的存储位。
代码如下:

//颜色的枚举体
enum Colour
{
	RED,
	BLACK,
};

//三叉链
//结点的结构体
template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>*_left;//左指针
	RBTreeNode<K, V>*_right;//右指针
	RBTreeNode<K, V>*_parent;//双亲指针
	Colour _col;//颜色标记位
	pair<K, V>_kv;//KV值

	//构造函数
	RBTreeNode(const pair<K, V>&kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)
	{
	}
};

//红黑树的类
template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V>Node;
private:
	//根节点
	Node*_root = nullptr;
};

(2). 结点的插入

结点的插入,首先我们面临的一个问题是,默认插入的结点为什么是红色的呢?
新插入的结点是红色还是黑色,本质是要违反规则3,还是规则4
而我们为什么不能让新插入的结点为黑色的话,一定改变路径上的黑色结点的个数,因为一次只能插入一个结点;但是如果插入红色结点,如果父亲节点是黑色,那插入就没有问题,如果为红色,那也只是影响最多两条路径,所以新增结点默认为红色。

而红黑树也是二叉搜索树,所以最开始时,结点的插入和二叉搜索树一样,都是通过kv值找到应插入的位置
在这里插入图片描述


那么接下来,我们就来分析结点插入的几种可能

1. 情况一

在这里插入图片描述
cur是新增结点,因为parent是黑色,所以插入结束


2. 情况二

在这里插入图片描述

结点15是新增结点,但是parent是红色结点,违反了规则3,出现了连续的红色结点,所以我们需要调整,又因为规则4,每个路径的黑色结点的个数相同,所以我们可能会修改隔壁路径。所以我们将这几个结点命名一下
调整的方式是这样的:
为了同时满足3,4规则,我们要改变结点颜色的同时,路径上的黑色结点个数还要相同,那么其实就是一直改变颜色,并且往上更新,最后到达两个路径的公共结点,然后在公共结点更改颜色,则不会影响这两个路径。
具体方法:如果存在uncle结点,且uncle结点为红色,则将parent结点和unclude结点都变成黑色,然后让grandfather结点变成红色
在这里插入图片描述

然后因为grandfather当前为根结点,所以再将grandfather结点变成黑色

在这里插入图片描述

我们发现,改变后的红黑树依然满足5条规则

如果结点数更多一点呢?
比如这样一棵红黑树
在这里插入图片描述
28是新增的结点
我们同样使用上述方法调节颜色
在这里插入图片描述
可以看到以25为根结点的子树完成了调整
但是这只是完成了一颗子树,所以还需要继续向上调整
所以我们让 cur=grandfather ,parent=cur->parent
在这里插入图片描述
这时parent有三种情况:

  1. 为整棵树的根结点。调整结束
  2. parent结点为黑色。调整结束
  3. parent结点为红色,继续调整

此处parent结点是红色,所以我们还需继续调整
在这里插入图片描述
最后因为根节点需要是黑色的,我们再将grandfather变成黑色

在这里插入图片描述
这样就完成了调整

这就是情况二:

插入结点的父亲结点是红色的,uncle结点存在且为红
则将parent和uncle都变黑grandfather变红,同时还需要继续向上更新


3. 情况三

在这里插入图片描述

当我们在6的右边新增结点
此时没有uncle结点,不满足情况二。

我们将grandfather这棵子树拎出来
在这里插入图片描述
我们会发现,这棵树的高度很不均衡,我们可以采用旋转的方式,降低这棵树的高度
图中这棵树的右边高度较高,所以我们要采用左单旋的方式

先将grandfather -> right = parent -> left,将parent的左结点给grandfather的右
再 parent -> left = grandfather,将grandfather给parent的左

在这里插入图片描述
这样子,这棵子树的高度就被我们降低了,并且没有破坏二叉搜索树的结构
但是还不满足红黑树,所以我们还需要调色
将parent变黑,grandfather变红
在这里插入图片描述

然后整棵树也就符合规则
在这里插入图片描述


单旋即后续的双旋,更详细的讲解可以阅读【C++STL】AVL树
简单来说
当出现这样的情况时,线性使用单旋
在这里插入图片描述
前者左边较高,使用右单旋平衡高度
后着右边较高,使用左单旋平衡高度

出现以下情况,折线型使用双旋
在这里插入图片描述

前者,左右双旋:先以拐点(红点)为轴点,先进行左单旋,变成左边高,再以黑点为轴点进行右单旋
后者,右左双旋:先以拐点(红点)为轴点,先进行右单旋,变成右边高,再以黑点为轴点进行左单旋
在这里插入图片描述
这里只作大致讲解,详细讲解可以阅读上面提到的博客


红黑树右单旋的情况和左单旋类似,读者可自己先尝试一下
在这里插入图片描述

旋转过程如下:
在这里插入图片描述

然后再调色
在这里插入图片描述


4. 情况四

情况二是uncle结点是红色,情况三是不存在uncle结点
情况四则是uncle结点为黑色
首先,什么时候会出现uncle结点为黑色
比如这样一棵红黑树
在这里插入图片描述
我们在4的左结点新增结点
那么首先看到parent和uncle都是红色,这是情况二
我们将parent和uncle变成黑色,grandfather变成红色
在这里插入图片描述

grandfather这棵子树完成调色,但是还需要继续向上调色
在这里插入图片描述

这时发现parent是黑色,插入成功,本次插入就结束了
我们在再在11的右子树新增结点
在这里插入图片描述

同样调整颜色,然后继续向上调整
在这里插入图片描述

而此时我们发现parent是红色,但是uncle却是黑色。
因为在之前的插入,uncle位置从红色变成了黑色,而parent在之后的一次插入中是grandfather,变成了红色
导致两个兄弟节点的颜色不同。
并且此时处于折现型
在这里插入图片描述
则我们需要使用双旋

这就是我们的情况四:parent为红,uncle存在且为黑色


双旋的过程如下:
在这里插入图片描述
我们先以parent为轴点,进行左单旋
在这里插入图片描述
先将parent->right=cur->left; 将cur的左给parent的右
在这里插入图片描述

再将cur->left=parent; 将parent变成cur的左

第一步旋转变成这样
在这里插入图片描述

第二步旋转,再以grandfather为轴点,进行一次右单旋
在这里插入图片描述
先将grandfather->left=cur->right;将cur的右给grandfather的左

在这里插入图片描述
再将cur->right=grandfather;将grandfather变成cur的右

最后将cur变成黑色,grandfather变成红色
在这里插入图片描述
这样,红黑树的左右双旋就完成了

5. 小总结

上述的四种情况,对应的是代码中的分类条件及旋转后的调色方法
而单旋或双旋的使用情况,是依照树的结构:线性,则使用单旋折线型,则使用双旋

6. 代码实现

//插入
	bool Insert(const pair<K, V>&kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			//根节点是黑色的
			_root->_col = BLACK;
			return true;
		}

		Node*cur = _root;
		Node*parent = nullptr;
		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&&parent->_col == RED)
		{
			//保存爷爷节点
			Node*grandfather = parent->_parent;
			if (grandfather->_left == parent)
			{
				Node*uncle = grandfather->_right;
				//uncle结点存在且为红
				//调色,并继续向上调整
				if (uncle&&uncle->_col == RED)
				{
					//调色
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;
					//继续向上调色
					cur = grandfather;
					parent = cur->_parent;
				}
				else//unlce不存在,或者uncle存在且为黑
				{

					//     g
					//   p   u
					// c
					//右单旋
					if (cur == parent->_left)
					{
						//以parent为轴点,进行右单旋
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
						break;
					}
					else
					{
						//     g
						//   p   u
						//     c
						//左右双旋
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col=RED;
					}

					break;
				}
			}
			else  //parent是grandfather的右边
			{
				Node*uncle = grandfather->_left;
				//uncle结点存在且为红
				//调色,并继续向上调整
				if (uncle&&uncle->_col == RED)
				{
					//调色
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;
					//继续向上调色
					cur = grandfather;
					parent = cur->_parent;
				}
				else//unlce不存在,或者uncle存在且为黑
				{

					//     g
					//   u   p
					//         c
					//左单旋
					if (cur == parent->_right)
					{
						//以parent为轴点,进行左单旋
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
						break;
					}
					else
					{
						//     g
						//   u   p
						//     c
						//右左双旋
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

				}
			}
		}

		//将根的颜色变成黑色
		_root->_col = BLACK;
	}


//左单旋
	void RotateL(Node*parent)
	{
		Node*subR = parent->_right;
		Node*subRL = subR->_left;

		//1. 将subRL变成parent的右节点
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;


		//记录当前子树的父节点
		Node*ppnode = parent->_parent;

		//2. 将parent变成subR的左节点
		subR->_left = parent;
		parent->_parent = subR;

		//3. 链接ppnode
		if (ppnode == nullptr)
		{
			//如果是ppnode是空,代表parent是根节点

			//更新根
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subR;
			}
			else
			{
				ppnode->_right = subR;
			}
			subR->_parent = ppnode;
		}

	}


	//右单旋
	void RotateR(Node*parent)
	{
		Node*subL = parent->_left;
		Node*subLR = subL->_right;
		//1.将subLR变成parent的左节点
		parent->_left = subLR;
		//subLR可能是NULL,不是NULL才链接
		if (subLR)
			subLR->_parent = parent;

		//2.再将parent变成subL的右节点
		Node*ppnode = parent->_parent;//因为parent不一定是根节点,所以需要记录爷爷节点
		subL->_right = parent;
		parent->_parent = subL;

		//3.链接parent指针
		if (ppnode == nullptr)
		{
			//如果是根节点
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			//反之不是
			if (ppnode->_left == parent)
			{
				ppnode->_left = subL;
			}
			else
			{
				ppnode->_right = subL;
			}
			subL->_parent = ppnode;
		}

	}

(3). 查找

红黑树的查找本质就是二叉搜索树的查找,根据二叉搜索树的性质:右子树的所有结点都比当前结点的值小,左子树的所有结点都比当前结点的值大
这样我们使用循环,比较大小,就可以决定接下来要去往左子树遍历还是右子树遍历
代码如下:

//查找
	Node* Find(const K&key)
	{
		assert(_root);

		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;
	}

(4). 红黑树的销毁

销毁我们可以使用析构函数,在当前函数结束时,就自动销毁红黑树,而析构函数内部其实调用一个后续遍历依次释放每个节点
代码如下:

//析构函数
	~RBTree()
	{
		_Destroy(_root);
		_root = nullptr;
	}

//销毁
	void _Destroy(Node*root)
	{
		//后续递归销毁
		if (root == nullptr)
			return;

		_Destroy(root->_left);
		_Destroy(root->_right);
		free(root);
	}

(5). 测试

红黑树的测试可以分两步
第一步检查是否满足二叉搜索树的性质:我们使用中序遍历验证
第二步根据红黑树的性质:1. 根结点是黑色的 2. 每个路径的黑色结点个数相同 3. 不存在连续的红色结点 验证

中序遍历的代码如下:

//中序遍历接口
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

//中序遍历
	void _InOrder(Node*root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}

中序遍历较为简单,这里不作解释


检查红黑树性质代码如下:

	//检查是否满足红黑树
	bool IsBalance()
	{
		//检查黑色结点的个数
		//最左路径的黑色结点个数
		int benchMark = 0;

		Node*cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				benchMark++;

			cur = cur->_left;
		}

		return _Check(_root,0,benchMark);
	}


	//递归检查
	bool _Check(Node*root,int blackNum,int benchMark)
	{
		//检查三个条件
		//1.根结点是否是黑色
		//2.是否有连续的红色结点
		//3.是否每条路径的黑色结点个数相同

		if (root == nullptr)
		{
			if (blackNum == benchMark)
				return true;
			else
			{
				cout << "黑色结点个数异常" << endl;
				return false;
			}
		}

		//1.根结点如果是红色的,代表编写异常
		if (root == _root && root->_col == RED)
		{
			cout << "根结点颜色异常" << endl;
			return false;
		}

		//2.如果当前结点是红色的
		//则需要检查其父亲结点是否是红色
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "出现连续的红色结点" << endl;
			return false;
		}

		//如果是黑色结点,则要记录
		if (root->_col == BLACK)
		{
			blackNum++;
		}

		return _Check(root->_left, blackNum, benchMark) && 
		_Check(root->_right, blackNum, benchMark);
	}
  1. 如果根结点是红色的,异常
  2. 如果当前结点是红色的,并且父亲结点也是红色,即出现连续的红色结点,异常
    这里检查父亲呢:因为一个结点,一定有父亲结点,但是不一定有孩子结点
  3. blackNum记录黑色结点的个数,我们在检查前,先遍历最左路径,计算出黑色结点个数,然后拿着这个值去每一条路径比较,如果有不一样的,要么最左路径出问题,要么该路径出问题

结束语

本篇博客内容暂时到此,红黑树结点的删除,较为复杂,后续再继续补充

本篇内容到此就结束了,感谢你的阅读!

如果有补充或者纠正的地方,欢迎评论区补充,纠错。如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

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

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

相关文章

【GO 编程语言】数组

数组 文章目录 数组一、数组是什么二、初始化数组三、数组的遍历四、数组类型五、数组排序 一、数组是什么 Go 语言提供了数组类型的 数据结构。 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型。例如整型、字符串或者自定义类型。 …

HTTP第八讲——请求方法

目前 HTTP/1.1 规定了八种方法&#xff0c;单词都必须是大写的形式 GET&#xff1a;获取资源&#xff0c;可以理解为读取或者下载数据&#xff1b;HEAD&#xff1a;获取资源的元信息&#xff1b;POST&#xff1a;向资源提交数据&#xff0c;相当于写入或上传数据&#xff1b;P…

echarts 如何保存为图片时,如何同时保存滚动条隐藏的数据

echarts 如何保存为图片&#xff0c;如何保存滚动条隐藏的数据 效果展示上代码内容讲解面临的问题解决思路&#xff08;当前代码的思路&#xff09; 效果展示 这是直接将保存的图片显示在网页下方了 上代码 <!DOCTYPE html> <html><head><meta charse…

【2023/05/11】Edsger Dijkstra

Hello&#xff01;大家好&#xff0c;我是霜淮子&#xff0c;2023倒计时第6天。 Share Sorrow is hushed into peace in my heart like the evening among thr silent trees. 译文&#xff1a; 忧思在我的心里平静下去&#xff0c;正如暮色降临在寂静的山林中。 Some unsee…

【报告回顾】精、稳、敏、融,步入人民金融时代

易观分析&#xff1a;2022年&#xff0c;在深化金融供给侧结构性改革和高质量增长要求的指引下&#xff0c;赋能实体、公平普惠、审慎经营成为银行业转型发展的关键词。一方面面临内外部复杂的经济环境和不确定性风险&#xff0c;银行主打稳健策略&#xff0c;数字化转型仍在持…

Redis的伪集群搭建与配置

文章目录 Redis主从集群模式搭建过程分级管理容灾冷处理 Redis主从集群模式 Redis 的主从集群是一个“一主多从”的读写分离集群。集群中的 Master 节点负责处理客户端的读写请求&#xff0c;而 Slave 节点仅能处理客户端的读请求。只所以要将集群搭建为读写分离模式&#xff…

5.最长回文子串——Manacher(马拉车)算法

给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串。 示例 1&#xff1a; 输入&#xff1a;s "babad" 输出&#xff1a;"bab" 解释&#xff1a;"aba" 同样是符合…

【计算机组成原理】第三章 多层次的存储器

系列文章目录 第一章 计算系统概论 第二章 运算方法和运算器 第三章 多层次的存储器 第四章 指令系统 第五章 中央处理器 第六章 总线系统 第七章 外围设备 第八章 输入输出系统 文章目录 系列文章目录前言第三章 多层次的存储器3.1 存储器概述3.1.1 存储器的分类3.1.2 存储器…

软件性能测试有哪些测试指标?性能测试报告怎么编写?​

软件性能测试是软件测试过程中非常重要的一部分&#xff0c;它可以保证软件在正式上线之前的稳定性和可靠性。那么&#xff0c;在进行软件性能测试时&#xff0c;我们需要关注哪些测试指标呢?性能测试报告又应该如何编写呢? 一、软件性能测试的测试指标 1.响应时间 响应时…

为github项目提交补充(pr)教程

记录第一次提交PR 前言为github提交补充什么是PR&#xff1a;Fork&#xff1a;git clone自己仓库&#xff1a;git remote add upstream和他人仓库建立关系&#xff1a;git checkout branch名切换分支&#xff1a;开始DIY项目文件&#xff1a;推送修改到自己仓库&#xff1a;空H…

c高级day4

一、编写一个名为myfirstshell.sh的脚本&#xff0c;它包括以下内容。 1、包含一段注释&#xff0c;列出您的姓名、脚本的名称和编写这个脚本的目的 2、和当前用户说“hello 用户名” 3、显示您的机器名 hostname 4、显示上一级目录中的所有文件的列表 5、显示变量PATH和H…

5.11总结:train/eval/BN、CNN与特征图、极大似然与EM、方差n与n-1(有偏估计/无偏估计)

目录 1.关于模型的train/eval与batchnorm1-1.理论1-2.实际运用&#xff08;包含loss反向传播&#xff09; 2.CNN详解&#xff0c;特征图是什么CNN处理过程特征图&#xff08;也叫通道&#xff09;(num_features)总结&#xff08;包含CNN图片的规律分析&#xff09;&#xff1a;…

SpringBoot基础篇3(SpringBoot+Mybatis-plus案例)

环境搭建&#xff1a;配置起步依赖pom.xml和配置文件application.yml 1.创建模块时&#xff0c;勾选的依赖有springMVC和MySQL驱动 2.手动添加的依赖有&#xff1a;MyBatis-plus、Druid、lombok <dependencies><dependency><groupId>org.springframework.…

Java笔记_20(多线程JUC)

一、多线程 1.1、多线程概述 进程 进程是程序的基本执行实体 线程 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中&#xff0c;是进程中的实际运作单位。简单理解:应用软件中互相独立&#xff0c;可以同时运行的功能 什么是多线程? 有了多线程&#xf…

SpringBoot整合JPA

JPA、Hibernate、Spring Data JPA的关系 JPA是Java Persistence API的简称&#xff0c;中文名Java持久层API&#xff0c;是JDK 5.0注解或XML描述对象&#xff0d;关系表的映射关系&#xff0c;并将运行期的实体对象持久化到数据库中。Sun引入新的JPA ORM规范出于两个原因&…

从零入门激光SLAM(九)——三维点云基础

大家好呀&#xff0c;我是一个SLAM方向的在读博士&#xff0c;深知SLAM学习过程一路走来的坎坷&#xff0c;也十分感谢各位大佬的优质文章和源码。随着知识的越来越多&#xff0c;越来越细&#xff0c;我准备整理一个自己的激光SLAM学习笔记专栏&#xff0c;从0带大家快速上手激…

新品发布全线添员,九号全力奔向“红海”深处?

5月10日&#xff0c;九号公司2023新品发布会声势达到顶峰。此次发布会的看点为九号电动2023产品线的更新&#xff0c;电动家族再添多员大将。 随着人们出行选择的多样化&#xff0c;国内短途出行工具发展迎来井喷期。在传统的电动两轮车市场上&#xff0c;雅迪、爱玛等品牌仍然…

基于java(springboot)和go-cqhttp实现QQ机器人

目录 yh-qqrobot机器人简介go-cqhttp搭建1.下载应用2.生成bat文件3. 初始化项目4. 配置5. 运行项目 yh-qqrobot搭建搭建后端1. 导入sql文件2. 配置文件3. 导入到idea 搭建前端 yh-qqrobot机器人简介 yh-qqrobot是一个基于若依框和go-cqhttp集成的系统&#xff0c;一开始我只是揣…

生成一个简版导游地图

目录 1 简版导游地图功能简介 2 注册并登录 3 设置景区&#xff08;商圈&#xff09;地图 3.1 新增景区 3.2 增加一个景点介绍 3.3 地图中增加一个景点 3.4 增加几个其他类型的点&#xff0c;如“美食”、“购物”、“停车” 4 申请审核 5 欣赏一下 1 简版导游地图功能…

【python 异常处理】零基础也能轻松掌握的学习路线与参考资料

Python 异常处理是编写高质量、功能稳定程序的关键之一&#xff0c;它可以帮助开发者优化程序的稳定性和可读性&#xff0c;更好地管理代码的错误和异常情况。 本文将介绍 Python 异常处理的学习路线、参考资料和优秀实践&#xff0c;以帮助 Python 开发者提高应对程序中错误和…