平衡二叉搜索树之 红黑 树的模拟实现【C++】

news2024/12/21 6:43:10

文章目录

  • 红黑树的简单介绍
    • 定义
    • 红黑树的特性
    • 红黑树的应用
  • 全部的实现代码放在了文章末尾
  • 准备工作
    • 包含头文件
    • 类的成员变量和红黑树节点的定义
  • 构造函数和拷贝构造
  • swap和赋值运算符重载
  • 析构函数
  • find
  • insert【重要】
    • 第一步:按照二叉搜索树的方式插入新节点
    • 第二步:调整颜色,维护红黑树的规则
      • 情况一:新插入的节点的父亲节点颜色为黑
      • 情况二:新插入的节点的父亲节点颜色为红,且叔叔节点不为空且为红
      • 情况三:新插入的节点的父亲节点颜色为红,且叔叔节点为空或者为黑
  • empty
  • size
  • 中序遍历
  • 红黑树和AVL树的比较
  • 全部代码

红黑树的简单介绍

定义

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,只能是Red或Black。
通过对任何一条从根到空节点的路径上各个结点着色方式的限制
红黑树确保没有一条路径会比其他路径长出俩倍,即最长路径的长度最多是最短路径长度的两倍,这样的话就能保证红黑树是接近平衡的树

在这里插入图片描述


红黑树的特性

红黑树通过以下特性来保持树的平衡,保证最长路径的长度最多是最短路径长度的两倍

  1. 节点颜色:每个节点只能是红色或黑色。

  2. 根节点:树的根节点是黑色。

  3. 红色规则:如果一个节点是红色,则它的两个子节点都是黑色(也就是说,一条路径不能有两个连续的红色节点)。

  4. 黑色高度:从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

  5. 空节点:所有的空节点(NIL节点,通常是叶子节点的子节点)都是黑色。

为什么满足上面的特性,红黑树就能保证:最长路径的长度最多是最短路径长度的两倍呢?

其实很好理解

因为
特性2:根节点是黑的

特性4:每条路径的黑色节点个数是相同的

特性5:所有的空节点(NIL节点,通常是叶子节点的子节点)都是黑色。

那么任何一个红黑树可能出现的最短路径就是只有黑色节点的路径,因为每条路径的黑色节点个数是相同的

而任何一个红黑树可能出现的最长路径就是一黑一红交替的路径

为什么?

因为
特性1:每个节点只能是红色或黑色

特性3:每一条路径都不可能出现连续的红色节点

特性4:每条路径的黑色节点个数是相同的
所以最短路径和最长路径的黑色节点个数一样

所以最长也只能是黑红节点交替出现

因此
可能的最短的路径,是全黑节点

可能的最长路径是一黑一红,交替出现的路径,红黑节点个数相同

所以
如果全黑节点的路径长度为h
那么一黑一红交替出现的路径长度最多只能是2*h


红黑树的应用

红黑树是一种高效的自平衡二叉搜索树,因其出色的性能和良好的平衡特性,在计算机科学中被广泛应用。以下是红黑树的一些主要应用场景:

  1. Java 和 C++ 中的集合和数据结构

    • 在 Java 中,红黑树被用于实现 TreeSetTreeMap,分别提供有序集合和有序映射的功能。
    • 在 C++ 中,红黑树是 std::setstd::multisetstd::mapstd::multimap 的底层数据结构。
  2. 数据库索引

    • 红黑树常用于数据库系统中,作为索引结构来提高数据查询和更新的效率。
      通过维持数据的有序性和快速搜索特性,红黑树能够有效地管理大量数据。
  3. 操作系统的内存管理

    • 在操作系统中,红黑树可用于管理内存分配。例如,在 Linux 的虚拟内存管理中,红黑树被用来维护页面分配的信息,确保内存的高效使用。
  4. 文件系统和文件管理

    • 在文件系统中,红黑树可以用来存储文件元数据,比如文件名、大小和修改时间等信息,以便快速检索和排序。
  5. 网络协议和算法

    • 在一些网络协议的实现中,红黑树可以用来管理连接状态或路由表,以实现高效的数据包转发和路由决策。
  6. 图形渲染和游戏开发

    • 在图形渲染和游戏开发中,红黑树可用于管理场景中的物体,通过快速搜索和排序来优化渲染列表。
  7. 科学计算和数据分析

    • 在科学计算和数据分析中,红黑树可以用于管理大量的有序数据,提供快速的搜索和更新操作,这对于时间序列数据的处理尤为重要。

红黑树可以确保数据结构的稳定性和效率,对于处理大量动态变化的数据集合尤为重要。


全部的实现代码放在了文章末尾

准备工作

创建两个文件,一个头文件RBTree.hpp,一个源文件test.cpp

【因为模板的声明和定义不能分处于不同的文件中,所以把成员函数的声明和定义放在了同一个文件RBTree.hpp中】

  1. RBTee.hpp:存放包含的头文件,命名空间的定义,成员函数和命名空间中的函数的定义

  2. test.cpp:存放main函数,以及测试代码


包含头文件

  • iostream:用于输入输出
  • cmath:提供数学函数

类的成员变量和红黑树节点的定义

红黑树类的成员变量只有一个,就是指向红黑树的根节点的指针

在这里插入图片描述

红黑树的节点类:
在这里插入图片描述
为什么红黑树新插入的节点【或者新节点】一定是红色的?

其实就是维护成本的问题:

1.如果新插入的节点为黑色

因为插入之前,这棵树一定是红黑树

所以他的每一条路径上的黑色节点个数都相同

但是因为你新插入了一个黑色的,这一条路径就会比其他路径多一个黑色节点,为了保持红黑树的规则(每一条路径上的黑色节点个数都相同

就得想办法让每一条路径也都增加一个黑色节点,维护成本太高了

2.如果新插入的节点为红色

① 新插入的节点的父亲为黑色,就可以直接结束了,因为满足红黑树的所有规则

②就算新插入的节点的父亲为红色,调整的情况也只有两种,维护成本更低


构造函数和拷贝构造

构造函数没什么好说的,默认构造就行了

RBTree() 
	:_root(nullptr)
{}

拷贝构造:
因为节点都是从堆区new出来的,所以要深拷贝

使用递归实现深拷贝:
因为拷贝构造不能有多余的参数,但是递归函数又必须使用参数记录信息
在这里插入图片描述

然后在拷贝构造里面调用一下这个函数就行了

RBTree(const RBTree& obj)
{
	_root = Copy(obj._root, nullptr);
}


swap和赋值运算符重载

交换两颗红黑树的本质就是交换两颗数的资源(数据),而它们的资源都是从堆区申请来的,然后用指针指向这些资源
在这里插入图片描述

并不是把资源存储在了红黑树对象中

所以资源交换很简单,直接交换_root指针的指向即可

void Swap(RBTree& obj)
{
	std::swap(_root, obj._root);
}

赋值运算符重载

RBTree& operator=(RBTree obj)
{
	this->Swap(obj);
	return *this;
}

为什么上面的两句代码就可以完成深拷贝呢?
这是因为:

使用了传值传参,会在传参之前调用拷贝构造,再把拷贝构造出的临时对象作为参数传递进去

赋值运算符的左操作数,*this再与传入的临时对象obj交换,就直接完成了拷贝

在函数结束之后,存储在栈区的obj再函数结束之后,obj生命周期结束

obj调用析构函数,把指向的从*this那里交换来的不需要的空间销毁


析构函数

使用递归遍历,把所有从堆区申请的节点都释放掉:
因为析构函数不能有多余的参数,但是递归函数又必须使用参数记录信息
所以再封装一个成员函数,专门用来递归释放:

在这里插入图片描述

然后在拷贝构造里面调用一下这个函数就行了

~RBTree()
{
	Destroy(_root);
	_root = nullptr;
}

find

具体流程:
从根节点开始,将目标值(传入的key)与当前节点的key进行比较。
如果目标值小于当前节点值,则在左子树中继续查找;
如果目标值大于当前节点值,则在右子树中继续查找。

这个过程一直进行,直到找到与目标值或者到达空节点为止。

把上述过程转成代码:

在这里插入图片描述


insert【重要】

红黑树就是在二叉搜索树的基础上节点有了颜色,因此红黑树也可以看成是二叉搜索树。

那么AVL树的插入过程可以分为两步:

  1. 先按照二叉搜索树的方式插入新节点

  2. 再调整颜色,维护红黑树的规则


第一步:按照二叉搜索树的方式插入新节点

插入的具体过程如下:

  1. 树为空,则直接新增节点,赋值给二叉搜索树的成员变量_root指针

  2. 树不空,则按照查找(find)的逻辑找到新节点应该插入的位置

  3. 树不空,如果树中已经有了一个节点的key值与要插入的节点的key相同,就插入失败

这个过程一直进行,直到找到与传入的key相等的节点或者到达空节点为止。

把上述过程转成代码:

在这里插入图片描述

第二步:调整颜色,维护红黑树的规则

颜色调整分以下3种情况:

  1. 新插入的节点(cur)的父亲节点(parent)颜色为
  2. 新插入的节点(cur)的父亲节点(parent)颜色为,且叔叔(uncle)节点不为空且为红
  3. 新插入的节点(cur)的父亲节点(parent)颜色为,且叔叔(uncle)节点为空或者为黑

情况一:新插入的节点的父亲节点颜色为黑

此时插入节点,并没有违反任何红黑树规则,所以不需要调整颜色/树的结构

直接结束插入就行


情况二:新插入的节点的父亲节点颜色为红,且叔叔节点不为空且为红

请添加图片描述
(cur为新插入的节点,g是爷爷节点,u是父亲的兄弟节点;a,b,c,d,e是都是符合条件的红黑树)
cur的位置是不固定的
cur也可以是上图的c,d,e
cur的p,g,u相应的改变即可
最主要看的是cur和它的g,p,u的颜色,是否满足情况一

因为插入的节点的颜色为红色,而且它的父亲节点也是红色
那么它就违反了红黑树的第3条规则:同一路径不能出现连续的红色节点
所以必须进行调整。
因为叔叔节点不为空且为红,所以没有严重违反红黑树的规则,只需要对颜色进行调整即可

所以调整方案是:

把p,u都变黑,把g变红:这样就能让连续的红色节点去除,但是g(爷爷接节点)变成红色了,g的父亲节点也可能是红色
所以需要
再把g看作cur继续循环向上调整,再根据新的c,p,u,g节点的颜色,判断是情况一,情况二还是情况三

具体实现代码:
在这里插入图片描述


情况三:新插入的节点的父亲节点颜色为红,且叔叔节点为空或者为黑

请添加图片描述
此时cur的位置也是不固定的
cur也可以是上图的c,d,e
cur的p,g,u相应的改变即可
最主要看的是cur和它的g,p,u的颜色,是否满足情况二

因为插入的节点的颜色为红色,而且它的父亲节点也是红色
那么它就违反了红黑树的第3条规则:同一路径不能出现连续的红色节点
所以必须进行调整。
因为叔叔(uncle)节点为空或者为黑,所以严重违反红黑树的规则,需要调整红黑树的部分结构和调整颜色

所以此时的调整方案是:
先旋转[根据cur,p和g的相对位置,进行左旋,右旋,双旋],再变颜色
【如果不知道旋转是什么的话,可以看我这篇文章:平衡二叉搜索树之 AVL 树的模拟实现【C++】中的怎么旋转】

①单旋:p变黑,g变红
因为单旋之后, p就变成了c,p,g及其子树组成的这棵树的根节点
然后又因为g是黑色,所以他旋转下去的话,
那么c那条路径就会比其他路径少一个黑色节点
所以需要把作为新的根节点的p变黑,这样c那条路径的黑色节点数量才会与其他路径上的相同
但是这样又会让g那条路径比其他的路径多一个黑色节点
所以再把g变红
请添加图片描述
②双旋:c变黑,g变红
因为单旋之后, c就变成了c,p,g及其子树组成的这棵树的根节点
然后的推导就与单旋的类似了

具体代码实现:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


empty

bool Empty()
{
    如果_root为空,那么树就是空的
	return _root == nullptr;
}

size

使用递归实现二叉搜索树的节点个数统计:
因为我们经常使用的stl的容器的size都是没有参数的,但是递归函数又必须使用参数记录信息
所以再封装一个成员函数,专门用来递归:

在这里插入图片描述

然后再size里面调用一下就行了

size_t Size()
{
	return _Size(_root);
}

中序遍历

中序遍历的递归函数:
在这里插入图片描述

然后再调用递归函数

void InOrder()
{
	_InOrder(_root);
}

红黑树和AVL树的比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N)

红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,减少了旋转的次数

所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单
所以实际运用中红黑树更多


全部代码

#define _CRT_SECURE_NO_WARNINGS

#include<iostream>
#include<cmath>
using namespace std;

//定义枚举,枚举节点的颜色
enum Color
{
	RED,//红色
	BLACK//黑色
};

template<class T>
struct RBTreeNode
{
	T _data;//节点中存储的数据

	RBTreeNode* _parent;//指向节点的父亲节点

	RBTreeNode* _left;//指向节点的左孩子
	RBTreeNode* _right;//指向节点的右孩子
	Color _color;//存储节点的颜色

	//默认构造
	RBTreeNode(const T& data = T())
		:_data(data),
		_parent(nullptr),
		_left(nullptr),
		_right(nullptr),
		_color(RED)//新节点的颜色默认是红色
	{}
};


//模板参数T,就是节点中存储的数据的类型
template<class T>
class RBTree
{
	//给节点类重命名
	typedef RBTreeNode<T> Node;
private:
	//指向红黑树的根节点的指针
	Node* _root = nullptr;


public:
	RBTree()
		:_root(nullptr)
	{}

	RBTree(const RBTree& obj)
	{
		_root = Copy(obj._root, nullptr);
	}
	RBTree& operator=(RBTree obj)
	{
		this->Swap(obj);
		return *this;
	}
	~RBTree()
	{
		Destroy(_root);
		_root = nullptr;
	}

	void Swap(RBTree& obj)
	{
		std::swap(_root, obj._root);
	}

	bool Insert(const T& data)
	{
		Node* newnode = new Node(data);

		if (_root == nullptr)
		{
			_root = newnode;
			_root->_color = BLACK;
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		if (parent->_data > data)
		{
			parent->_left = newnode;
		}
		else
		{
			parent->_right = newnode;
		}
		newnode->_parent = parent;

		cur = newnode;

		//如果新插入的节点的父节点的颜色是  黑色
		// 因为已经符合红黑树的规则了,直接结束插入
		if (parent->_color == BLACK)
			return true;//直接结束
		else//否则
		{
			Node* grandpa = parent->_parent;//爷爷节点

			Node* uncle = nullptr;//叔叔节点,即cur的父亲节点的  兄弟节点

			if (parent == grandpa->_left)//如果parent是左孩子
			{
				uncle = grandpa->_right;//那么它的兄弟节点就是右孩子
			}
			else//如果parent是右孩子
			{
				uncle = grandpa->_left;//那么它的兄弟节点就是左孩子
			}

			//情况二:
			// cur的父亲节点(parent)为红,且叔叔(uncle)节点不为空且为红
			//只需要继续  循环  向上调整
			while (parent->_color == RED && uncle && uncle->_color == RED)
			{
				//调整颜色
				grandpa->_color = RED;
				parent->_color = BLACK;
				uncle->_color = BLACK;

				//如果调整到根节点,就不需要再向上调整了
				if (grandpa == _root)
				{
					//因为根节点只能是黑色
					//所以把爷爷节点(grandpa)的颜色调成黑色
					grandpa->_color = BLACK;
					return true;//直接结束
				}

				//重新赋值
				//继续向上调整
				cur = grandpa;//让原来的爷爷节点作为cur

				parent = cur->_parent;//重新得出新的父亲节点

				//如果新的父亲节点为黑,就和刚插入节点时一样
				// 因为已经符合红黑树的规则了,直接结束插入
				if (parent->_color == BLACK)
					return true;//直接结束

				grandpa = parent->_parent;//重新得出新的爷爷节点

				//重新得出新的叔叔节点
				if (parent == grandpa->_left)
				{
					uncle = grandpa->_right;
				}
				else
				{
					uncle = grandpa->_left;
				}
			}

			//情况三:
			// cur的父亲节点(parent)为红,且叔叔(uncle)节点为空或者为黑
			//就只能根据情况,旋转处理一次
			if (parent->_color == RED&&uncle == nullptr || uncle->_color == BLACK)
			{
				if (parent == grandpa->_right && cur == parent->_right)//左单旋
				{
					RotateL(grandpa);
					//调整颜色
					parent->_color = BLACK;
					grandpa->_color = RED;
				}
				else if (parent == grandpa->_left && cur == parent->_left)//右单旋
				{
					RotateR(grandpa);
					//调整颜色
					parent->_color = BLACK;
					grandpa->_color = RED;
				}
				else if (parent == grandpa->_left && cur == parent->_right)//左右双旋
				{
					RotateL(parent);
					RotateR(grandpa);
					//调整颜色
					cur->_color = BLACK;
					grandpa->_color = RED;
				}
				else if (parent == grandpa->_right && cur == parent->_left)//右左双旋
				{
					RotateR(parent);
					RotateL(grandpa);
					//调整颜色
					cur->_color = BLACK;
					grandpa->_color = RED;
				}
			}
		}
		return true;
	}


	Node* Find(const T& data)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_data < data)
			{
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	bool Empty()
	{
		return _root == nullptr;
	}
	size_t Size()
	{
		return _Size(_root);
	}
	size_t Height()
	{
		return _Height(_root);
	}
	bool IsRBTree()
	{
		if (_root == nullptr)
			return true;

		if (_root->_color != BLACK)
			return false;

		int count = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_color == BLACK)
				count++;
			cur = cur->_left;
		}
		return _IsRBTree(_root, count, 0);
	}
private:

	bool _IsRBTree(const Node* root, const int& total, int count)
	{
		if (root == nullptr)
		{
			if (count != total)
				return false;
			else
				return true;
		}

		if (root->_color == RED && root->_parent->_color == RED)
			return false;


		if (root->_color == BLACK)
			count++;


		if (_IsRBTree(root->_left, total, count) == false)
			return false;
		if (_IsRBTree(root->_right, total, count) == false)
			return false;
		return true;
	}
	size_t _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int left = _Height(root->_left);
		int right = _Height(root->_right);

		return left > right ? left + 1 : right + 1;
	}


	// 左单旋
	void RotateL(Node* parent)
	{
		//记录一下PR和PRL
		Node* PR = parent->_right;
		Node* PRL = PR->_left;

		//把PRL链接在parent的右边
		parent->_right = PRL;

		//因为PRL可能为空,为了防止空指针访问,必须判断一下
		if (PRL != nullptr)
		{
			//PRL不为空,就把它的父亲节点变成parent
			PRL->_parent = parent;
		}

		//把parent链接在PR的左边
		PR->_left = parent;

		//更改PR的父亲节点
		PR->_parent = parent->_parent;

		//只有AVL树的根节点的父亲节点为空
		//所以parent是根节点
		if (parent->_parent == nullptr)
		{
			//PR变成了这颗子树的根,替代了原来parent的位置
			//所以得把AVL树的根节点更新一下
			_root = PR;
		}
		else//如果parent不是根节点
		{
			//PR还是要带替parent的位置
			//所以parent在父亲的左,PR就在左
			//parent在父亲的右,PR就在右
			if (parent == parent->_parent->_left)
				parent->_parent->_left = PR;
			else
				parent->_parent->_right = PR;
		}
		//更新一下parent的父亲节点
		parent->_parent = PR;
	}



	// 右单旋
	void RotateR(Node* parent)
	{
		//记录一下PL和PLR
		Node* PL = parent->_left;
		Node* PLR = PL->_right;

		//把PLR链接在parent的右边
		parent->_left = PLR;

		//因为PLR可能为空,为了防止空指针访问,必须判断一下
		if (PLR != nullptr)
		{
			//不为空,就把它的父亲节点变成parent
			PLR->_parent = parent;
		}

		//把parent链接在PL的左边
		PL->_right = parent;

		//更新PL的父亲节点
		PL->_parent = parent->_parent;

		//只有AVL树的根节点的父亲节点为空
		//所以parent是根节点
		if (parent->_parent == nullptr)
		{
			//PL变成了这颗子树的根,替代了原来parent的位置
			//所以得把AVL树的根节点更新一下
			_root = PL;
		}
		else//如果parent不是根节点
		{
			//PL还是要带替parent的位置
			//所以parent在父亲的左,PL就在左
			//parent在父亲的右,PL就在右
			if (parent == parent->_parent->_left)
				parent->_parent->_left = PL;
			else
				parent->_parent->_right = PL;
		}
		//更新一下parent的父亲节点
		parent->_parent = PL;
	}





	//因为无法直接获取到父亲节点的地址
	//所以需要传参传进来
	Node* Copy(Node* root, Node* parent)
	{
		//空节点不需要拷贝,直接返回nullptr
		if (root == nullptr)
			return nullptr;

		//从堆区申请空间,并初始化节点
		Node* newnode = new Node(root->_data);

		//连接上传入函数的父亲节点
		newnode->_parent = parent;

		//拷贝颜色
		newnode->_color = root->_color;

		//递归拷贝左右子树
		newnode->_left = Copy(root->_left, newnode);
		newnode->_right = Copy(root->_right, newnode);

		return newnode;//返回新节点
	}


	//使用后序遍历进行释放
	void Destroy(Node* root)
	{
		//空节点
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);

		delete root;
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);

		cout << root->_data << "  ";

		_InOrder(root->_right);
	}
	size_t _Size(Node* root)
	{
		if (root == nullptr)
			return 0;
		int left = _Size(root->_left);
		int right = _Size(root->_right);

		return left + right + 1;
	}
};

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

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

相关文章

线性数据结构

数组 数组&#xff08;Array&#xff09; 是一种很常见的数据结构。它由相同类型的元素&#xff08;element&#xff09;组成&#xff0c;并且是使用一块连续的内存来存储。 我们直接可以利用元素的索引&#xff08;index&#xff09;可以计算出该元素对应的存储地址。 数组…

GoFly框架使用vue flow流程图组件说明

Vue Flow组件库是个高度可定制化的流程图组件&#xff0c;可用于工作流设计、流程图及图表编辑器、系统架构展示。可以根据自己的需求&#xff0c;设计独特的节点和边&#xff0c;实现个性化的流程图展示。这不仅增强了应用的视觉效果&#xff0c;也使得用户交互更为直观和流畅…

MySQL数据库:SQL语言入门 【2】(学习笔记)

目录 2&#xff0c;DML —— 数据操作语言&#xff08;Data Manipulation Language&#xff09; &#xff08;1&#xff09;insert 增加 数据 &#xff08;2&#xff09;delete 删除 数据 truncate 删除表和数据&#xff0c;再创建一个新表 &#xff08;3&#xf…

“南海明珠”-黄岩岛(民主礁)领海基线WebGIS绘制实战

目录 前言 一、关于岛屿的基点位置 1、领海基点 二、基点坐标的转换 1、最底层的左边转换 2、单个经纬度坐标点转换 3、完整的转换 三、基于天地图进行WebGIS展示 1、领海基点的可视化 2、重要城市距离计算 四、总结 前言 南海明珠黄岩岛&#xff0c;这座位于南海的…

19.UE5道具掉落

2-21 道具掉落&#xff0c;回血、回蓝、升级提升伤害_哔哩哔哩_bilibili 目录 1.道具的创建&#xff0c;道具功能的实现 2.随机掉落 1.道具的创建&#xff0c;道具功能的实现 新建Actor蓝图&#xff0c;并命名为道具总类&#xff0c;添加一个Niagara粒子组件和一个碰撞箱bo…

Cartographer激光雷达slam -20241116

Cartographer Cartographer代码结构 cartographer&#xff1a;负责处理来自雷达、IMU和里程计的数据并基于这些数据进行地图的构建&#xff0c;是cartographer理论的底层实现cartographer_ros&#xff1a;基于ros的通信机制获取传感器的数据并将它们转换成cartographer中定义…

node.js学习笔记-Window下MongoDB数据库安装(二)

一、介绍 MongoDB 是一个基于分布式文件存储的开源数据库系统&#xff0c;在当前的软件开发和数据存储领域中应用广泛&#xff0c;以下是对 MongoDB 的详细介绍&#xff1a; 文档型数据库&#xff1a;MongoDB 以 BSON&#xff08;Binary JSON&#xff09;格式存储数据&#x…

STM32G4的数模转换器(DAC)的应用

目录 概述 1 DAC模块介绍 2 STM32Cube配置参数 2.1 参数配置 2.2 项目架构 3 代码实现 3.1 接口函数 3.2 功能函数 3.3 波形源代码 4 DAC功能测试 4.1 测试方法介绍 4.2 波形测试 概述 本文主要介绍如何使用STM32G4的DAC模块功能&#xff0c;笔者使用STM32Cube工具…

【论文复现】轻松利用自适应特征融合实现去雾

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀ 智慧医疗 介绍创新点网络结构特征提取阶段自适应融合阶段图像重建阶段上下文增强模块CEM特征融合模块AFM 结果分析 提示 论文题目&#xff1…

常用在汽车PKE无钥匙进入系统的高度集成SOC芯片:CSM2433

CSM2433是一款集成2.4GHz频段发射器、125KHz接收器和8位RISC&#xff08;精简指令集&#xff09;MCU的SOC芯片&#xff0c;用在汽车PKE无钥匙进入系统里。 什么是汽车PKE无钥匙进入系统&#xff1f; 无钥匙进入系统具有无钥匙进入并且启动的功能&#xff0c;英文名称是PKE&…

《TCP/IP网络编程》学习笔记 | Chapter 11:进程间通信

《TCP/IP网络编程》学习笔记 | Chapter 11&#xff1a;进程间通信 《TCP/IP网络编程》学习笔记 | Chapter 11&#xff1a;进程间通信进程间通信的基本概念通过管道实现进程间通信通过管道进行进程间双向通信 运用进程间通信习题&#xff08;1&#xff09;什么是进程间通信&…

计算机网络各层设备总结归纳(更新ing)

计算机网络按照OSI&#xff08;开放式系统互联&#xff09;模型分为七层&#xff0c;每一层都有其特定的功能和对应的网络设备。以下是各层对应的设备&#xff1a; 1. 物理层&#xff08;Physical Layer) 设备&#xff1a;中继器&#xff08;Repeater&#xff09;、集线器…

在kile 5中一个新工程的创建

这两天博主学习到了在kile5中创建一个工程&#xff0c;当然博主不会忘了小伙伴们的&#xff0c;这就和你们分享。 本次创建以STM32F103C8为例 创建过程&#xff1a; 1首先创建文件 名字随意&#xff0c;但也不要太随意&#xff0c;因为是外国软件&#xff0c;所以多少对中文…

AI写作(十)发展趋势与展望(10/10)

一、AI 写作的崛起之势 在当今科技飞速发展的时代&#xff0c;AI 写作如同一颗耀眼的新星&#xff0c;迅速崛起并在多个领域展现出强大的力量。 随着人工智能技术的不断进步&#xff0c;AI 写作在内容创作领域发挥着越来越重要的作用。据统计&#xff0c;目前已有众多企业开始…

Javascript垃圾回收机制-运行机制(大厂内部培训版本)

前言 计算机基本组成&#xff1a; 我们编写的软件首先读取到内存&#xff0c;用于提供给 CPU 进行运算处理。 内存的读取和释放&#xff0c;决定了程序性能。 冯诺依曼结构 解释和编译 这两个概念怎么理解呢。 编译相当于事先已经完成了可以直接用。好比去饭店吃饭点完上…

大数据技术之Hive:还是SQL好用

虽说 MapReduce 简化了大数据编程的难度&#xff0c;但是如果每来一个需求都要写一个 MapReduce 代码&#xff0c;那岂不是太麻烦了。尤其是在全民“CRM”的2000年代&#xff0c;对于像数据分析师已经习惯使用SQL进行分析和统计的工程师&#xff0c;让他们去 MapReduce 编程还是…

使用 Grafana api 查询 Datasource 数据

一、使用grafana 的api 接口 官方API 二、生成Api key 点击 Administration -》Users and accss -》Service accounts 进入页面 点击Add service account 创建 service account 点击Add service account token 点击 Generate token , 就可以生成 api key 了 三、进入grafana…

OceanBase 闪回查询

前言 在OB中&#xff0c;drop表可以通过 回收站 或者 以往的备份恢复来还原单表。当delete数据时&#xff0c;由于delete操作的对象不会进入回收站&#xff0c;此时需要通过闪回查询功能查看delete的数据&#xff0c;以便后续恢复 本次实验版本为 OceanBase 4.2.1.8&#xff0…

vue2 动态路由的实现

概述 一般情况下&#xff0c;路由都是前端约定好的&#xff0c;但是每当项目发布上线&#xff0c;或者客户需求新的页面的时候&#xff0c;都需要做出路由改变。这样运维就可以现场支持&#xff0c;方便做出可操作的中户中台&#xff0c;来管理我们的中心项目登录及权限&#x…

华为云前台展示公网访问需要购买EIP,EIP流量走向

华为云前台网络&#xff08;VPC,安全组&#xff0c;EIP&#xff09; 1.EIP网段是从哪里划分的&#xff1f; 管理员在后台Service_OM已设置 Service_OM-网络资源-外部网络-创建外部网络基本信息&#xff1a;配置参数&#xff1a;*名称 public*网络类型 LOCAL 不带标签 类似开…