【数据结构高阶】B-树

news2025/1/17 23:50:07

目录

一、常见的搜索结构

二、B树

2.1 B树的概念

2.2 B树插入数据的分析

2.3 B树的性能分析

2.4 模拟实现B树

2.4.1 B树节点的定义

2.4.2 B树数据的查找

2.4.3 B树节点的数据插入

2.4.4 B树的遍历

2.4.5 模拟实现B树实现的完整代码

三、B+树

3.1 B+树的概念

3.2 B+树插入数据的分析

四、B*树

五、B树系列总结

六、B-树的应用

6.1 索引

6.2 MySQL索引简介


一、常见的搜索结构

在正式介绍B树之前的我们先来看一下常用到的搜索结构:

种类数据格式时间复杂度
顺序查找无要求O(N)
二分查找有序O(㏒⑵N)
二叉搜索树无要求O(N)
二叉平衡树(AVL树&红黑树)无要求O(㏒⑵N)
哈希无要求O(1)

以上结构适合用于数据量相对不是很大,能够一次性存放在内存中,进行数据查找的场景。如果 数据量很大,比如有100G数据,无法一次放进内存中,那就只能放在磁盘上了。

如果放在磁盘上,有需要搜索某些数据,那么如果处理呢?那么我们可以考虑将存放关键字及其映射的数据的地址放到一个内存中的搜索树的节点中,那么要访问数据时,先取这个地址去磁盘访问数据。

但是这样的存储方式一旦数据量较大,内存很想要存储全部的数据是很难的;如果我们为了节省空间只存外存地址不存关键字的话,需要我们不断的去访问外存,即便是O(㏒⑵N)的访问次数对于时间的消耗也是巨大的,外存的IO是很慢的:

那使用哈希表呢?哈希表的效率很高是O(1),但是一些极端场景下某个位置冲突很多,导致访问次数剧增,也是难以接受的。

为了解决这种方法我们来引入本期的主角:B树

二、B树

2.1 B树的概念

1970年,R.Bayer和E.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树,称为B树 (后面有一个B的改进版本B+树,然后有些地方的B树写的的是B-树,注意不要误读成"B减树")。

一棵m阶(m>2)的B树,是一棵平衡的m路平衡搜索树,可以是空树或者满足一下性质:

1. 根节点至少有两个孩子

2. 每个分支节点都包含k-1个关键字和k个孩子,其中 ceil(m/2) ≤ k ≤ m,ceil是向上取整函数

3. 每个叶子节点都包含k-1个关键字,其中 ceil(m/2) ≤ k ≤ m

4. 所有的叶子节点都在同一层

5. 每个节点中的关键字从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域划分

6. 每个结点的结构为:(n,A0,K1,A1,K2,A2,… ,Kn,An)其中,Ki(1≤i≤n)为关键 字,且Ki < Ki+1(1 ≤ i ≤ n-1),Ai(0≤i≤n)为指向子树根结点的指针。且Ai所指子树所有结点中的 关键字均小于Ki+1。n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。

上面这些乍一眼看让人不知所云,下面我们结合代码和图来细细分析这些规则:
 

2.2 B树插入数据的分析

为了简单起见,我们先来看看m=3(三叉树)情况下的B树的插入过程:

来分析一下,当m=3时,每个节点中存储最多两个数据(data1,data2),这两个数据可以将该节点的区间分为三部分(即小于data1、大于data1小于data2、大于data2这三个部分):

但是在具体实现时,为了方便数据的插入我们将每个节点存储数据的大小设为初始大小+1:

至于为什么在原初始大小上+1,下面在数据插入的时候来体会:

下面我们插入数据:{40, 126, 62, 36, 132, 23, 188}

我们插入62这个数据后发现节点中数据达到m个,但节点中最多存储m-1个数据,所以下一步我们要进行节点的分裂数据的迁移

从这个过程可以看出,通过节点的分裂和数据的迁移,即便节点的存储数据的大小为m但实际只能存储m-1个有效数据

下面我们接着插入数据:

后面的数据插入不再赘述,我们看看到关键的节点分裂和数据迁移的过程:

最后我们综合之前的规律,插入最后一个数据

一直到现在我们都发现这棵数一直都是平衡的,这是为什么呢?B树怎么是一棵自平衡树呢?

这是因为B树是向右和向上增长的,这种分裂方式让B树始终是一棵平衡树(其他的大部分树都是纵向向下增长的)

2.3 B树的性能分析

实际的B树不会这么平凡的分裂,一般将M设为1024,那么想象一下,当M = 1024是,插入数据时,这个树的高度会如何变化?

第一层:1023个关键字;

第二层:1024个子结点 * 1023个关键字,大约是100W的级别;

第三层:1024 * 1024 * 1023,大约是10亿的级别;

第四层:1024 * 1024 * 1024 * 1023,大约是万亿级别;

但是上面的情况是理想化的满数据和节点的情况,那我们来算一下在最坏情况下:

第一层:1个关键字;

第二层:2个子结点 * 512个关键字,大约是1K的级别;

第三层:2 * 512 * 512,大约是10W的级别;

第四层:2 * 512 * 512 * 512,大约是2.5亿级别;

可以看到这个数据规模也高的惊人

对于一棵节点为N度为M的B-树,查找和插入需要㏒(M-1)N ~ ㏒(M/2)N次比较(每次的比较都使用二分查找),这个很好证明:对于度为M的B-树,每一个节点的子节点个数为M/2 ~(M-1)之间,因此树的高度应该在要㏒(M-1)N ~ ㏒(M/2)N之间,在定位到该节点后,再采用二分查找的方式可以很快的定位到该元素。

那么它的时间复杂度在㏒(M-1)N ~ ㏒(M/2)N之间,也就是说M越大,效率越高,但是M也不是越大越好,因为会有空间的浪费,有因为结点满了要拷走一半,浪费一个结点一半的空间;

最后我们来算一下对于N = 62*1000000000个节点,如果度M为1024,则㏒(M/2)N <= 4,即在620亿个元素中,如果这棵树的度为1024,则需要小于4次即可定位到该节点,然后利用二分查找可以快速定位到该元素,大大减少了读取磁盘的次数。

2.4 模拟实现B树

下面我们来手搓一棵B树,但这只是最基本的B树,想要它作为数据库的引擎还是需要很多优化和改进的:

2.4.1 B树节点的定义

template<class K, size_t M>//K我们要存储的关键字,M控制B树的叉数
struct BTreeNode
{
	//将节点所能存储的数据数和孩子数+1,方便我们后续的插入
	K _keys[M];//最多有M-1个数据
	BTreeNode<K, M>* _subs[M+1];//最多有M个孩子
	BTreeNode<K, M>* _pather;//记录节点的父亲节点,方便后续插入操作
	size_t _n;//记录节点中有效数据个数

	BTreeNode()
	{
		for (size_t i = 0; i < M; ++i)
		{
			_keys[i] = K();
			_subs[i] = nullptr;
		}
		_subs[M] = nullptr;
		_n = 0;
		_pather = nullptr;
	}
};

template<class K,size_t M>
class BTree
{
	typedef BTreeNode<K, M> Node;

private:
	Node* _root = nullptr;
};

2.4.2 B树数据的查找

template<class K,size_t M>
class BTree
{
	typedef BTreeNode<K, M> Node;

public:
	pair<Node*, int> Find(const K& key)//查找key值所对应的节点
	{
		Node* parent = nullptr, * cur = _root;
		size_t i = 0;
		while (cur)
		{
			while (i < cur->_n)//在当前节点查找适合的位置
			{
				if (key < cur->_keys[i])
				{
					break;
				}
				else if (key > cur->_keys[i])
				{
					++i;
				}
				else
				{
					return make_pair(cur, i);
				}
			}
			parent = cur;
			cur = cur->_subs[i];//当前节点没找到,向其孩子节点再找
			i = 0;
		}
		return make_pair(parent, -1);//B树中没有该值,返回parent节点方便后续数据的插入
	}

private:
	Node* _root = nullptr;
};

2.4.3 B树节点的数据插入

template<class K,size_t M>
class BTree
{
	typedef BTreeNode<K, M> Node;

	bool InsertKey(Node* node, Node* child, const K& key)//在node节点中插入新值及其孩子节点
	{
		int end = node->_n - 1;
		while (end >= 0)//直接插入排序
		{
			if (node->_keys[end] > key)
			{
				node->_keys[end + 1] = node->_keys[end];
				node->_subs[end + 2] = node->_subs[end + 1];//移动的数据节点所对应的右孩子节点也需要向后移动
				--end;
			}
			else
			{
				break;
			}
		}
		node->_keys[end + 1] = key;
		node->_subs[end + 2] = child;
		node->_n++;

		if (child)//插入的孩子节点不为空要连接上其父亲节点
		{
			child->_parent = node;
		}

		return true;
	}

public:

	pair<Node*, int> Find(const K& key)//查找key值所对应的节点
	{
        .....
	}

	bool Insert(const K& key)
	{
		if (_root == nullptr)//第一次插入
		{
			_root = new Node;
			_root->_keys[0] = key;
			_root->_n++;
			return true;
		}
		pair<Node*, int>ret = Find(key);//查找要插入的值是否在B树中存在
		if (ret.second >= 0)
		{
			return false;//存在就直接返回
		}

		//不存在在该节点中进行插入
		Node* parent = ret.first;
		K newKey = key;
		Node* child = nullptr;
		while (1)
		{
			InsertKey(parent, child, newKey);//将数据插入到节点中
			//判断该节点是否需要进行分裂
			if (parent->_n < M)
			{
				return true;
			}
			else//进行节点的分裂
			{
				Node* brother = new Node;//创建兄弟节点
				//将一半的数据转移到兄弟节点
				size_t mid = M / 2;
				size_t j = 0;
				for (size_t i = mid + 1; i < M; ++i)
				{
					//转移节点数据
					brother->_keys[j] = parent->_keys[i];
					parent->_keys[i] = K();//转移掉的数据恢复初始值
					//转移左节点孩子
					brother->_subs[j++]= parent->_subs[i];
					if (parent->_subs[i])//转移走的孩子节点不为空要转换其父亲节点
					{
						parent->_subs[i]->_parent = brother;
					}
					parent->_subs[i] = nullptr;//转移掉的孩子置空
				}
				//转移最后的右节点孩子
				brother->_subs[j] = parent->_subs[M];
				if (parent->_subs[M])//转移走的孩子节点不为空要转换其父亲节点
				{
					parent->_subs[M]->_parent = brother;
				}
				parent->_subs[M] = nullptr;//转移掉的孩子置空

				brother->_n = j;

				//将该节点的中间值拿出来作为newKey,继续插入到该节点的父亲节点中
				parent->_n -= (brother->_n + 1);
				newKey = parent->_keys[mid];
				parent->_keys[mid] = K();//转移掉的数据恢复初始值

				if (parent->_parent == nullptr)//分裂的节点是根节点
				{
					_root = new Node;
					_root->_keys[0] = newKey;
					_root->_subs[0] = parent;
					_root->_subs[1] = brother;
					_root->_n = 1;
					parent->_parent = _root;
					brother->_parent = _root;
					break;//分裂完毕后直接返回
				}
				else
				{
					//向上跳一层接着进行插入
					parent = parent->_parent;
					child = brother;
				}
			}
		}
		return true;
	}

private:
	Node* _root = nullptr;
};

2.4.4 B树的遍历

由于B树的有序性,我们选择中序对其遍历(与二叉搜索树大同小异),在我们遍历其节点时要先走完其数据的所有的左子树,最后再走右子树:

template<class K,size_t M>
class BTree
{
	typedef BTreeNode<K, M> Node;

	void _InOrder(Node* cur)
	{
		if (cur == nullptr)
		{
			return;
		}
		size_t i = 0;
		for (; i < cur->_n; ++i)
		{
			_InOrder(cur->_subs[i]);//遍历左子树
			cout << cur->_keys[i] << " ";//遍历根
		}
		_InOrder(cur->_subs[i]);//遍历右子树
	}

public:

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

private:
	Node* _root = nullptr;
};

2.4.5 模拟实现B树实现的完整代码

#include<utility>
using namespace std;

template<class K, size_t M>//K我们要存储的关键字,M控制B树的叉数
struct BTreeNode
{
	//将节点所能存储的数据数和孩子数+1,方便我们后续的插入
	K _keys[M];//最多有M-1个数据
	BTreeNode<K, M>* _subs[M+1];//最多有M个孩子
	BTreeNode<K, M>* _parent;//记录节点的父亲节点,方便后续插入
	size_t _n;//记录节点中有效数据个数

	BTreeNode()
	{
		for (size_t i = 0; i < M; ++i)
		{
			_keys[i] = K();
			_subs[i] = nullptr;
		}
		_subs[M] = nullptr;
		_n = 0;
		_parent = nullptr;
	}
};

template<class K,size_t M>
class BTree
{
	typedef BTreeNode<K, M> Node;

	bool InsertKey(Node* node, Node* child, const K& key)//在node节点中插入新值及其孩子节点
	{
		int end = node->_n - 1;
		while (end >= 0)//直接插入排序
		{
			if (node->_keys[end] > key)
			{
				node->_keys[end + 1] = node->_keys[end];
				node->_subs[end + 2] = node->_subs[end + 1];//移动的数据节点所对应的右孩子节点也需要向后移动
				--end;
			}
			else
			{
				break;
			}
		}
		node->_keys[end + 1] = key;
		node->_subs[end + 2] = child;
		node->_n++;

		if (child)//插入的孩子节点不为空要连接上其父亲节点
		{
			child->_parent = node;
		}

		return true;
	}

	void _InOrder(Node* cur)
	{
		if (cur == nullptr)
		{
			return;
		}
		size_t i = 0;
		for (; i < cur->_n; ++i)
		{
			_InOrder(cur->_subs[i]);//遍历左子树
			cout << cur->_keys[i] << " ";//遍历根
		}
		_InOrder(cur->_subs[i]);//遍历右子树
	}

public:
	pair<Node*, int> Find(const K& key)//查找key值所对应的节点
	{
		Node* parent = nullptr, * cur = _root;
		size_t i = 0;
		while (cur)
		{
			while (i < cur->_n)//在当前节点查找适合的位置
			{
				if (key < cur->_keys[i])
				{
					break;
				}
				else if (key > cur->_keys[i])
				{
					++i;
				}
				else
				{
					return make_pair(cur, i);
				}
			}
			parent = cur;
			cur = cur->_subs[i];//当前节点没找到,向其孩子节点再找
			i = 0;
		}
		return make_pair(parent, -1);//B树中没有该值,返回parent节点方便后续数据的插入
	}

	bool Insert(const K& key)
	{
		if (_root == nullptr)//第一次插入
		{
			_root = new Node;
			_root->_keys[0] = key;
			_root->_n++;
			return true;
		}
		pair<Node*, int>ret = Find(key);//查找要插入的值是否在B树中存在
		if (ret.second >= 0)
		{
			return false;//存在就直接返回
		}

		//不存在在该节点中进行插入
		Node* parent = ret.first;
		K newKey = key;
		Node* child = nullptr;
		while (1)
		{
			InsertKey(parent, child, newKey);//将数据插入到节点中
			//判断该节点是否需要进行分裂
			if (parent->_n < M)
			{
				return true;
			}
			else//进行节点的分裂
			{
				Node* brother = new Node;//创建兄弟节点
				//将一半的数据转移到兄弟节点
				size_t mid = M / 2;
				size_t j = 0;
				for (size_t i = mid + 1; i < M; ++i)
				{
					//转移节点数据
					brother->_keys[j] = parent->_keys[i];
					parent->_keys[i] = K();//转移掉的数据恢复初始值
					//转移左节点孩子
					brother->_subs[j++]= parent->_subs[i];
					if (parent->_subs[i])//转移走的孩子节点不为空要转换其父亲节点
					{
						parent->_subs[i]->_parent = brother;
					}
					parent->_subs[i] = nullptr;//转移掉的孩子置空
				}
				//转移最后的右节点孩子
				brother->_subs[j] = parent->_subs[M];
				if (parent->_subs[M])//转移走的孩子节点不为空要转换其父亲节点
				{
					parent->_subs[M]->_parent = brother;
				}
				parent->_subs[M] = nullptr;//转移掉的孩子置空

				brother->_n = j;

				//将该节点的中间值拿出来作为newKey,继续插入到该节点的父亲节点中
				parent->_n -= (brother->_n + 1);
				newKey = parent->_keys[mid];
				parent->_keys[mid] = K();//转移掉的数据恢复初始值

				if (parent->_parent == nullptr)//分裂的节点是根节点
				{
					_root = new Node;
					_root->_keys[0] = newKey;
					_root->_subs[0] = parent;
					_root->_subs[1] = brother;
					_root->_n = 1;
					parent->_parent = _root;
					brother->_parent = _root;
					break;//分裂完毕后直接返回
				}
				else
				{
					//向上跳一层接着进行插入
					parent = parent->_parent;
					child = brother;
				}
			}
		}
		return true;
	}



	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

private:
	Node* _root = nullptr;
};

void TestBTree()
{
	int a[] = { 40, 126, 62, 36, 132, 23, 188 };
	BTree<int, 3> tree;
	for (auto& x : a)
	{
		tree.Insert(x);
	}
	tree.InOrder();
}

 

三、B+树

3.1 B+树的概念

B+树是B树的变形,是在B树基础上优化的多路平衡搜索树,B+树的规则跟B树基本类似,但是又在B树的基础上做了以下几点改进优化:

1. 分支节点的子树指针与关键字个数相同

2. 分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间

3. 所有叶子节点增加一个链接指针链接在一起

4. 所有关键字及其映射数据都在叶子节点出现

我们来分析一下B+树和B树的区别:

由第一点规则我们可以发现B+树是将B树每个节点的最左边的子树去除了,使得数据个数与孩子树相等;

第二点规则可以看到B+树的分支节点都是叶子节点的值的最小值,以此来让父节点存储最小值来做索引方便查找;

第三点规则极大程度上方便了B+树的遍历,我们只需要遍历叶子节点就可以得到全部的数据,为范围查找提供了基础

第四点规则就可以让k-v结构的B+树的分支节点只存储key值,最后在叶子节点中也可以找到对应的value值,减少了空间上的花费

 

3.2 B+树插入数据的分析

为了简单起见,我们先来看看m=3(三叉树)情况下的B+树的插入过程:

来分析一下,当m=3时,每个节点中存储最多两个数据(data1,data2,data3),这三个数据可以将该节点的区间分为三部分(即大于等于data1小于data2、大于等于data2小于data3、大于等于data3这三个部分):

和B树一样,在具体实现时,为了方便数据的插入我们将每个节点存储数据的大小设为初始大小+1:

下面我们插入数据:{40, 126, 62, 36, 132, 23, 188}

我们可以发现B+树一开始就是有两层的结构的,第一层作为索引,第二层才真正的存储数据,每当新插入的数据值小于索引值时,索引是要进行更新的(例如36的插入)

插入36这个数据后发现节点中数据达到m个,但节点中最多存储m-1个数据,所以下一步我们要进行节点的分裂数据的迁移

下面我们接着插入数据:

最后我们再插入两个数据,来看看没有父亲节点的节点的分裂:

这时这课B+树的根节点满了,我们来看看其分裂:

 

四、B*树

B*树是B+树的变形,在B+树的非根和非叶子节点再增加指向兄弟节点的指针:

但是B*树的节点数据满了并不进行分裂,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);但如果兄弟节点也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点(这样子原节点,兄弟节点和新节点的空间都使用了2/3),最后在父结点增加新结点的指针。

所以,B*树分配新结点的概率比B+树要低,空间使用率更高

但是由于B树系列常常在外存中使用,对于存储空间丰富的磁盘来说,B*树对空间利用率的提升效果并不明显,所以在很多使用场景中常常使用结构更为简单的B+树

五、B树系列总结

通过上述的介绍分析,大致将B树,B+树,B*树总结如下:

B树:有序数组+平衡多叉树

B+树:有序数组链表+平衡多叉树

B*树:一棵更丰满的,空间利用率更高的B+树

接下来我们来谈谈,B树系列在内存和外存中使用与哈希和二叉平衡搜索树的对比:

在内存中:

单论树高度,搜索效率而言,B树确实不错

但是B树系列有一些隐形的坏处:

1、空间利用率低,消耗高。
2、插入删除数据,分裂和合并节点时,必然会挪动数据,效率低。
3、虽然高度更低,但是在内存中而言,跟哈希和平衡搜索树还是一个量级的(因为内存的空间并不大,在N数量级较小时log以2为底的和以M(M≈1024)为底的结果差距并不大;而且这些微小的差距在极快的内存处理效率面前体现极小)

结论:实质上B树系列在内存中体现不出优势。

在外存中:

在外存容量是内存好几个数量级的场景里,B树系列多消耗的空间几乎微乎其微

接下来我们来算一下1亿以2为底的对数约为30,以1024为底的对数为3;假设外存完成一次搜索需要1s,那这两者之间就相差了29s,这在时间上的体现是极大的

结论:B树系列在外存中的优势极大。

六、B-树的应用

6.1 索引

B-树最常见的应用就是用来做索引。索引通俗的说就是为了方便用户快速找到所寻之物,比如: 书籍目录可以让读者快速找到相关信息,hao123网页导航网站,为了让用户能够快速的找到有价值的分类网站,本质上就是互联网页面中的索引结构。

MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构,简单来说:索引就是数据结构。

当数据量很大时,为了能够方便管理数据,提高数据查询的效率,一般都会选择将数据保存到数据库,因此数据库不仅仅是帮助用户管理数据,而且数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用数据,这样就可以在这些数据结构上实现高级查找算法, 该树据结构就是索引。

6.2 MySQL索引简介

对于此有兴趣的同学们可以看到这里:MySQL索引详解:概念、类型与优化


本期博客到这里就结束啦,让大家久等了,后期会保持连续更新~

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

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

相关文章

Linux 服务器挖矿木马防护实战:快速切断、清理与加固20250114

Linux 服务器挖矿木马防护实战&#xff1a;快速切断、清理与加固 引言 挖矿木马作为一种常见的恶意软件&#xff0c;对服务器资源和安全构成严重威胁。据安全机构统计&#xff0c;2023 年全球约 45%的 Linux 服务器遭受过挖矿木马攻击&#xff0c;平均每台被感染服务器每月造…

015: 深度学习之正向传播和反向传播

本文为合集收录&#xff0c;欢迎查看合集/专栏链接进行全部合集的系统学习。 合集完整版请参考这里。 上一节介绍了训练和推理的概念&#xff0c;这一节接着训练和推理的概念讲一下&#xff0c;神经网络的正向传播和反向传播。 正反向传播 其实单看正向传播和反向传播这两个…

梁山派入门指南2——滴答定时器位带操作按键输入(包括GPIO中断)

梁山派入门指南2——滴答定时器&位带操作&按键输入 1. 滴答定时器1.1 滴答定时器简介1.2 相关寄存器1.3 固件库函数 2. 位带操作2.1 位带操作介绍2.2 位带操作的优势2.3 支持位带操作的内存地址2.4 位带别名区地址的计算方式2.5 位带操作使用示例 3 按键输入3.1 独立按…

安全类脚本:拒绝ssh暴力破解

要求如下&#xff1a; 一个小时内&#xff0c;连续密码错误4次。 Linux lastb 命令用于列出登入系统失败的用户相关信息。 实验过程如下&#xff1a; 1. 创建两个IP地址不同的干净环境&#xff0c;分别是&#xff1a;192.168.46.101 Rocky 2 和 192.168.46.120 openEuler 2. 2.…

HugeGraph集群部署

部署HugeGraph集群 最近工作中&#xff0c;需要部署一个HugeGraph的多副本集群&#xff0c;要求一个主节点&#xff0c;两个从节点。由于HugeGraph官网并没有完整的搭建集群实例&#xff0c;所以这里写一篇文章记录自己搭建集群的过程&#xff0c;供自己和大家学习和参考。 注…

SqlSugar连接达梦数据库集群超时或异常缓慢

《SqlSugar配置连接达梦数据库集群》文章中介绍SqlSugar连接达梦数据库集群&#xff0c;只需按下图所示位置添加dm_svc.conf文件&#xff0c;并在SqlSugar的连接字符串中指定服务名即可。   但在使用过程中发现&#xff0c;基于.net 6开发的WebApi&#xff0c;编译为ANYCPU&…

Qt/C++进程间通信:QSharedMemory 使用详解(附演示Demo)

在开发跨进程应用程序时&#xff0c;进程间通信&#xff08;IPC&#xff09;是一个关键问题。Qt 框架提供了多种 IPC 技术&#xff0c;其中 QSharedMemory 是一种高效的共享内存方式&#xff0c;可以实现多个进程之间快速交换数据。本文将详细讲解 QSharedMemory 的概念、用法及…

STM32的集成开发环境STM32CubeIDE安装

STM32CubeIDE - STM32的集成开发环境 - 意法半导体STMicroelectronics

NanoKVM简单开箱测评和拆解,让普通电脑实现BMC/IPMI远程管理功能

Sipeed推出了NanoKVM&#xff0c;简直是没有BMC的台式机和工作站的福音。有了这个就可以轻松实现以往服务器才有的远程管理功能。 NanoKVM 简介 Lichee NanoKVM 是基于 LicheeRV Nano 的 IP-KVM 产品&#xff0c;继承了 LicheeRV Nano 的极致体积 和 强大功能。 NanoKVM 包含…

唐刘:TiDB 的 2024 - Cloud、SaaS 与 AI

2024 年已经过去&#xff0c;在去年我也写过两篇类似的文章&#xff0c;TiDB in 2023 - 一次简单的回顾和 TiDB Cloud in 2023 - 一次简单的回顾&#xff0c;这一次&#xff0c;我准备将 TiDB 和 cloud 一起写&#xff0c;一方面原因是我懒了&#xff0c;另外一个更重要的原因在…

Web端实时播放RTSP视频流(监控)

一、安装ffmpeg: 1、官网下载FFmpeg: Download FFmpeg 2、点击Windows图标,选第一个:Windows builds from gyan.dev 3、跳转到下载页面: 4、下载后放到合适的位置,不用安装,解压即可: 5、配置path 复制解压后的\bin路径,配置环境变量如图: <

Excel 技巧10 - 如何检查输入重复数据(★★)

本文讲了如何在Excel中通过COUNTIF来检查输入重复数据。 当输入重复数据时&#xff0c;显示错误提示。 1&#xff0c;通过COUNTIF来检查输入重复数据 比如下面是想检查不要输入重复的学号。 选中C列&#xff0c;点 Menu > 数据 > 数据验证 在数据验证页面&#xff0c…

IEC103 转 ModbusTCP 网关

一、产品概述 IEC103 转 ModbusTCP 网关型号 SG-TCP-IEC103 &#xff0c;是三格电子推出的工业级网关&#xff08;以下简 称网关&#xff09;&#xff0c;主要用于 IEC103 数据采集、 DLT645-1997/2007 数据采集&#xff0c; IEC103 支持遥测和遥 信&#xff0c;可接…

Unity3d 实时天气系统基于UniStorm插件和xx天气API实现(含源码)

前言 实时天气在Unity3d三维数字沙盘中的作用非常重要&#xff0c;它能够增强虚拟环境的真实感和互动性&#xff0c;实时天气数据的应用可以提供更为精准和直观的天气信息支持&#xff0c;如果真实的数据加上特效、声音和模型反馈会提高产品档次&#xff0c;提高真实感。 目前…

Linux命令行工具-使用方法

参考资料 Linux网络命令&#xff1a;网络工具socat详解-CSDN博客 arm-linux-gnueabihf、aarch64-linux-gnu等ARM交叉编译GCC的区别_aarch64-elf-gcc aarch64-linux-gnu-CSDN博客 解决Linux内核问题实用技巧之-dev/mem的新玩法-腾讯云开发者社区-腾讯云 热爱学习地派大星-CS…

【Flink系列】9. Flink容错机制

9. 容错机制 在Flink中&#xff0c;有一套完整的容错机制来保证故障后的恢复&#xff0c;其中最重要的就是检查点。 9.1 检查点&#xff08;Checkpoint&#xff09; 9.1.1 检查点的保存 1&#xff09;周期性的触发保存 “随时存档”确实恢复起来方便&#xff0c;可是需要我…

【深度学习】关键技术-激活函数(Activation Functions)

激活函数&#xff08;Activation Functions&#xff09; 激活函数是神经网络的重要组成部分&#xff0c;它的作用是将神经元的输入信号映射到输出信号&#xff0c;同时引入非线性特性&#xff0c;使神经网络能够处理复杂问题。以下是常见激活函数的种类、公式、图形特点及其应…

20.<Spring图书管理系统①(登录+添加图书)>

PS&#xff1a;关于接口定义 接口定义&#xff0c;通常由服务器提供方来定义。 1.路径&#xff1a;自己定义 2.参数&#xff1a;根据需求考虑&#xff0c;我们这个接口功能完成需要哪些信息。 3.返回结果&#xff1a;考虑我们能为对方提供什么。站在对方角度考虑。 我们使用到的…

【Sql递归查询】Mysql、Oracle、SQL Server、PostgreSQL 实现递归查询的区别与案例(详解)

文章目录 Mysql 5.7 递归查询Mysql 8 实现递归查询Oracle递归示例SQL Server 递归查询示例PostgreSQL 递归查询示例 更多相关内容可查看 Mysql 5.7 递归查询 MySQL 5.7 本身不直接支持标准 SQL 中的递归查询语法&#xff08;如 WITH RECURSIVE 这种常见的递归查询方式&#xf…

vue2修改表单只提交被修改的数据的字段传给后端接口

效果&#xff1a; 步骤一、 vue2修改表单提交的时候&#xff0c;只将修改的数据的字段传给后端接口&#xff0c;没有修改得数据不传参给接口。 在 data 对象中添加一个新的属性&#xff0c;用于存储初始表单数据的副本&#xff0c;与当前表单数据进行比较&#xff0c;找出哪些…