DS高阶:B树系列

news2024/11/27 0:49:40

一、常见的搜索结构

1、顺序查找     时间复杂度:O(N)

2、二分查找     时间复杂度:O(logN)

        要求:(1)有序 (2)支持下标的随机访问

3、二叉搜索树(BS树)       时间复杂度:O(logN)——>O(N)

        若接近有序的数据插入到BS中,会导致退化成单支树,时间复杂度退化为O(N)

4、平衡搜索树 (AVL树和RB树)   时间复杂度:O(logN)

       在BS的基础上,通过一些规则加以限制,通过旋转来限制高度,维持logN的时间复杂度

5、哈希    时间复杂度:O(1)

        底层是散列表,要注意解决哈希冲突。综合效率优于平衡搜索树

        以上结构适合用于数据量相对不是很大,能够一次性存放在内存中(内查找),进行数据查找的场景。如果数据量很大,比如有100G数据,无法一次放进内存中,那就只能放在磁盘上了,如果放在磁盘上,有需要搜索某些数据,那么如果处理呢?那么我们可以考虑将存放关键字及其映射的数据的地址放到一个内存中的搜索树的节点中,那么要访问数据时,先取这个地址去磁盘访问数据。(B树系列   解决外查找的问题)

         根据上面的分析,我们知道B树系列是为了解决外查找的问题而生的,但是你可能会有这样的疑惑:虽然高度下降了,但是由于我的一个节点存储这多个关键字信息,那么我即使找到这个节点,不也是要遍历关键字信息,效率真的能提高么??

        答:在磁盘中的搜索来说,定位的效率低,但是如果准确定位到了(节点),后面效率就会很高(顺序遍历节点中的关键字),这个跟磁盘的底层结构有关,具体可以参照下面的文献去理解。

二、B树的概念

      1970年,R.Bayer和E.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树,称为B树(后面有一个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。

 (7)实际中M通常会设计得比较大(比如1024)

以上规则可能还有点抽象,我们通过分析B树的插入来剖析具体的过程

三、B树的插入过程分析

       为了简单起见,假设M = 3. 即三叉树,每个节点中存储两个数据,两个数据可以将区间分割成三个部分,因此节点应该有三个孩子,为了后续实现简单期间,节点的结构如下:

注意孩子比关键字多一个,并且为了防止越界的问题,我们多开一个空间

 用序列{53, 139, 75, 49, 145, 36, 50,47,101}构建B树的过程如下

(1)插入53

(2)插入139

(3)插入75

(4)进行分裂

 这里可以解释为什么ceil(m/2) ≤ k ≤ M  

假设M是偶数  比如是10  那么后面5个给兄弟,中位数给父亲,自己还剩下4个,兄弟会多一个

假设M是奇数  比如是11  那么后面5个给兄弟,中位数给父亲,自己还剩下5个,正好一样多

(5)插入49 

 分析以下,当B树有多个叶子节点的时候,如何去选取我们要插入的叶子节点。

答:B树的逻辑是 左-根-左-根-左-根……-右,我们先忽略掉后面这个右子树,把它抽象成一个节点对应一个左子树,在拓展兄弟的时候是向右拓展的,所以我们找的是要左(小)的找。  举个例子,49比75小,那么必然在75左孩子那边,此时可以直接往下走,但是如果139比75大,那么此时不能直接到下一层,而是先往后找,直到找到一个比自己大的节点(如果后面没有,就是当前节点)的左孩子去找。

(6)插入145

(7)插入36

(8)进行分裂 

(9)插入50

(10)插入47

 (11)插入101

(12)继续分裂

 (13)二次分裂

 四、B树简单模拟实现

4.1 B树的节点设计

template<class K,size_t M> 
struct BTreeNode
{
	BTreeNode()
	{
		for (size_t i = 0; i < M; ++i)
		{
			_keys[i] = K();  //K的默认构造
			_subs[i] = nullptr;
		}
		_subs[M] = nullptr;
		_parent = nullptr;
		_n = 0;
	}
	//关键字永远比孩子少一个  为了方便插入,我们多开一个空间 预防边界情况
	K _keys[M]; //M-1个关键字
	BTreeNode<K, M>* _subs[M + 1];// 孩子的集合 M个孩子
	BTreeNode<K, M>* _parent;//父亲节点  方便插入的时候向上回溯
	size_t _n; //实际存了多少节点
};
template<class K, size_t M>
class BTree
{
	typedef BTreeNode<K, M> Node;
public:

private:
	Node* _root=nullptr;
};

1、K代表K类型,一般是表示地址,当然也可以是KV模型

2、M表示这是M路多叉树

3、_subs表示孩子节点的集合,_keys表示关键字的集合,为了防止边界情况的判断,统一多开一个空间。

4、_n表示一共个有效的关键字

5、_parent是父亲节点,维护父亲的原因是我们需要向上传中位数,如果不维护一个父亲节点,会比较难实现,但是增加了一个指针,同时也要十分注意去维护这个指针(容易忽略)。

4.2 B树的查找

       在B树不允许键值冗余的情况下,如果我们想插入一个节点,那么我们要保证B树没有该节点,因此我们在实现插入之前,先实现一个查找的函数

pair<Node*, int> Find(const K& key)  //查找这个节点以及对应关键字的下标
{
	Node* cur = _root;
	Node* parent = nullptr;//如果没找到, 把父亲节点带回来
	while (cur) //因为i每次都要重头开始算
	{
		size_t i = 0; 
		while (i < cur->_n)
		{
			if (key < cur->_keys[i]) break; //keys[i]的左孩子根他的下标是相等的
			else if (key > cur->_keys[i]) ++i;     //左才会往下跳  比右小i++
			else return make_pair(cur, i);
		}
		//但是有可能走到空都不会结束  找不到就往自己的孩子去跳
		parent = cur;
		cur = cur->_subs[i];
	}
	return make_pair(parent, -1);
}

1、返回值pair<Node*,int> 前一个返回对应的节点,后一个表示对应节点中的下标。

2、parent指针的意义:因为我们在插入之前必须要调用这个查找函数,并且必须插入到相应的叶子节点中去。那么我们可以顺便通过这个返回值返回我们要插入的叶子节点。这样在insert函数中接受find函数的返回值的时就可以直接拿到待插入的叶子节点。

3、因为拓展都是往右拓展的,所以我们必须要确保比key当前元素小,我们才能跳到下一层去找他的左孩子,并且每次都要从第一个位置开始找,如果比当前元素大的话,那么先往后找,而不是直接往该节点的右孩子找!!

4.3 插入key的过程

         我们多开一块空间的目的先进行无脑插入,然后再去检查该节点是否满了,如果满了再进行分裂调整,但是我们有些时候可能不光要插入key,还要插入新增的节点。

//每次循环往cur插入newkey和child
void InsertKey(Node* node, const K& key, Node* child)
{
	int end = node->_n - 1;
	while (end >= 0)  //如果我比你小,你就往后挪   类似插入排序逻辑
	{
		if (key < 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;
	if (child)  child->_parent = node; // 一定要记得反向链接维护parent指针
	++node->_n;
}

1、只有多次分裂的时候才会出现需要链接新增的节点,如果只有一次分裂的话,child就是nullptr,所以在反向链接的时候要注意!!!

2、在插入关键字的时候,我们按照插入排序的逻辑从后开始往前找,不断将比自己大的元素往后挪,挪动的时候要别忘了把他的右子树也跟着往后挪动

3、end必须设置成int而不能是size_t,因为是从后往前找的,所以end是有可能会出现负数的。

4.4 B树的插入整体实现

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);
	if (ret.second>=0) return false;
	//如果没有找到,find顺便带回了要插入的叶子节点
	Node* cur = ret.first;
	//每次循环往cur插入newkey和child
	
	K newKey = key;
	Node* child = nullptr;
	while (1)
	{
		InsertKey(cur, newKey, child);
		//情况1 没满, 直接结束
		if (cur->_n < M) return true;

		Node* brother = new Node;
		//分裂一半[mid+1,M-1]给兄弟  找到中间那个值
		size_t mid = M / 2;
		size_t i = mid + 1;
		size_t j = 0;
		for (; i < M; ++i, ++j)
		{
			//拷贝key和key的左孩子
			brother->_keys[j] = cur->_keys[i]; 
			brother->_subs[j] = cur->_subs[i]; //节点也拷过去
			//与父亲建立连接
			if (cur->_subs[i])  cur->_subs[i]->_parent = brother;
		
			//清理一下方便观察
			cur->_keys[i] = K();
			cur->_subs[i] = nullptr;
		}
		// 还有最后一个右孩子拷过去
		brother->_subs[j] = cur->_subs[i];
		if (cur->_subs[i])  cur->_subs[i]->_parent = brother; //孩子如果不是空  那么父亲就得更新一下
		cur->_subs[i] = nullptr;

		brother->_n = j;
		cur->_n -= (brother->_n + 1);//因为还要把中位数往上放

		K midKey = cur->_keys[mid];
		cur->_keys[mid] = K();//方便观察
		//转化成往cur的parent去插入 cur->[mid]和 brother
		// 说明刚刚分裂是根节点
		if (cur->_parent == nullptr)
		{
			_root = new Node; //最坏情况 我的父亲是空,那就造一个新的根出来
			_root->_keys[0] = midKey;
			_root->_subs[0] = cur;
			_root->_subs[1] = brother;
			_root->_n = 1;
			//链接起来
			cur->_parent = _root;
			brother->_parent = _root;
			break;
		}
		else //如果父亲不是空,还可以向上调整
		{
			// 转换成往parent->parent 去插入parent->[mid] 和 brother
			newKey = midKey;
			child = brother;
			cur = cur->_parent;
		}
	}
	return true;
}

1、如果什么也没有,那么自己就成为新的树。

2、通过find函数去找B树中是否存在这个关键字,如果存在就结束,不存在,那就把返回的pair中的first(待插入的叶子节点)提取出来。

3、因为有可能会涉及到多次分裂,所以我们要将插入的函数写在循环里面(通过cur、newkey、child来帮助我们迭代 ),然后每次插入之后就去判断是否还要进行分裂。如果没满就结束,如果满了就分裂。

4、分裂一半的key和节点(要注意节点的反向链接)给自己的兄弟,然后清理一下数据方便我们调试观察,最后有一个右孩子还得再拷贝一次。

5、传中位数的时候,如果cur没有父亲,那么就直接造一个父亲出来。如果cur有父亲,就更新一下cur、newkey、child,继续往上迭代去走。将问题转化成往父亲节点插入中位数和一个brother节点。

4.5 B树的中序遍历验证

       他的整体逻辑是左、根、左、根、左、根……右  所以我们可以将前两个过程抽出来,然后最后再单独处理右。走一个中序遍历的逻辑实现有序。

	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]); // 最后的那个右子树
	}

	void InOrder()
	{
		_InOrder(_root);
	}

 附上测试用例:

void testBtree()
{
	BTree<int, 3> t;
	int a[] = { 53, 139, 75, 49, 145, 36, 101 };
	for (auto e : a)  t.Insert(e);
	t.InOrder();
}

 4.6 整体的代码

#pragma once
#include<iostream>
using namespace std;

//K表示存的地址  M表示最多有几个分支
template<class K,size_t M> 
struct BTreeNode
{
	BTreeNode()
	{
		for (size_t i = 0; i < M; ++i)
		{
			_keys[i] = K();  //K的默认构造
			_subs[i] = nullptr;
		}
		_subs[M] = nullptr;
		_parent = nullptr;
		_n = 0;
	}
	//关键字永远比孩子少一个  为了方便插入,我们多开一个空间 预防边界情况
	K _keys[M]; //M-1个关键字
	BTreeNode<K, M>* _subs[M + 1];// 孩子的集合 M个孩子
	BTreeNode<K, M>* _parent;//父亲节点  方便插入的时候向上回溯
	size_t _n; //实际存了多少节点
};


template<class K, size_t M>
class BTree
{
	typedef BTreeNode<K, M> Node;
public:
	pair<Node*, int> Find(const K& key)  //查找这个节点以及对应关键字的下标
	{
		Node* cur = _root;
		Node* parent = nullptr;//如果没找到, 把父亲节点带回来
		while (cur) //因为i每次都要重头开始算
		{
			size_t i = 0; 
			while (i < cur->_n)
			{
				if (key < cur->_keys[i]) break; //keys[i]的左孩子根他的下标是相等的
				else if (key > cur->_keys[i]) ++i;     //左才会往下跳  比右小i++
				else return make_pair(cur, i);
			}
			//但是有可能走到空都不会结束  找不到就往自己的孩子去跳
			parent = cur;
			cur = cur->_subs[i];
		}
		return make_pair(parent, -1);
	}

	//每次循环往cur插入newkey和child
	void InsertKey(Node* node, const K& key, Node* child)
	{
		int end = node->_n - 1;
		while (end >= 0)  //如果我比你小,你就往后挪   类似插入排序
		{
			if (key < 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;
		if (child)  child->_parent = node; // 要记得向上连接
		++node->_n;
	}

	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);
		if (ret.second>=0) return false;
		//如果没有找到,find顺便带回了要插入的叶子节点
		Node* cur = ret.first;
		//每次循环往cur插入newkey和child
		
		K newKey = key;
		Node* child = nullptr;
		while (1)
		{
			InsertKey(cur, newKey, child);
			//情况1 没满, 直接结束
			if (cur->_n < M) return true;

			Node* brother = new Node;
			//分裂一半[mid+1,M-1]给兄弟  找到中间那个值
			size_t mid = M / 2;
			size_t i = mid + 1;
			size_t j = 0;
			for (; i < M; ++i, ++j)
			{
				//拷贝key和key的左孩子
				brother->_keys[j] = cur->_keys[i]; 
				brother->_subs[j] = cur->_subs[i]; //节点也拷过去
				//与父亲建立连接
				if (cur->_subs[i])  cur->_subs[i]->_parent = brother;
			
				//清理一下方便观察
				cur->_keys[i] = K();
				cur->_subs[i] = nullptr;
			}
			// 还有最后一个右孩子拷过去
			brother->_subs[j] = cur->_subs[i];
			if (cur->_subs[i])  cur->_subs[i]->_parent = brother; //孩子如果不是空  那么父亲就得更新一下
			cur->_subs[i] = nullptr;

			brother->_n = j;
			cur->_n -= (brother->_n + 1);//因为还要把中位数往上放

			K midKey = cur->_keys[mid];
			cur->_keys[mid] = K();//方便观察
			//转化成往cur的parent去插入 cur->[mid]和 brother
			// 说明刚刚分裂是根节点
			if (cur->_parent == nullptr)
			{
				_root = new Node; //最坏情况 我的父亲是空,那就造一个新的根出来
				_root->_keys[0] = midKey;
				_root->_subs[0] = cur;
				_root->_subs[1] = brother;
				_root->_n = 1;
				//链接起来
				cur->_parent = _root;
				brother->_parent = _root;
				break;
			}
			else //如果父亲不是空,还可以向上调整
			{
				// 转换成往parent->parent 去插入parent->[mid] 和 brother
				newKey = midKey;
				child = brother;
				cur = cur->_parent;
			}
		}
		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]); // 最后的那个右子树
	}

	void InOrder()
	{
		_InOrder(_root);
	}


private:
	Node* _root=nullptr;
};


void testBtree()
{
	BTree<int, 3> t;
	int a[] = { 53, 139, 75, 49, 145, 36, 101 };
	for (auto e : a)  t.Insert(e);
	t.InOrder();
}

 4.7 B树的性能分析

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

         B-树的效率是很高的,对于N = 62*1000000000个节点,如果度M为1024,则$log_{M/2}N$ <=4,即在620亿个元素中,如果这棵树的度为1024,则需要小于4次即可定位到该节点,然后利用二分查找可以快速定位到该元素,大大减少了读取磁盘的次数。

4.8 B树的删除过程分析 

 1、删除36

 2、删除40

3、删49 

 4、删150

5、删160 

 五、B树系列

5.1 B+树

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

(1)分支节点的子树指针与关键字个数相同(相当于去掉了左边的子树,相比B树取消了孩子和关键字的包含关系,而是一一对应)


(2)分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间(和B树一致)
(3) 所有叶子节点增加一个链接指针链接在一起(这样就可以直接找到叶子节点,不一定需要从根去找了!!)
(4)所有关键字及其映射数据都在叶子节点出现(1、分支节点和叶子节点有重复的值,分支节点存的是叶子节点的索引->key.2、父亲中存的是孩子节点中的最小值做索引

和B树规则区别总结:

1、简化B树孩子比关键字多一个的规则,变成了相等(一一对应)。

2、而key value都存在叶子节点上,一方面是节省空间,一方面是方便遍历查找所有值

B+树的特性:
1. 所有关键字都出现在叶子节点的链表中,且链表中的节点都是有序的
2. 不可能在分支节点中命中(因为只存k而没有存kv)。
3. 分支节点相当于是叶子节点的索引,叶子节点才是存储数据的数据层

5.2 B+树的插入过程分析

 用序列{53, 139, 75, 49, 145, 36, 50,47,101}构建B+树的过程如下:

1、插入53

2、插入139

 3、插入75

4、插入49

 5、分裂

6、插入145

 7、插入36

8、插入50 

9、分裂

 10、插入47

11、插入101

 12、分裂

13、二次分裂

和B树插入的区别:

1、一开始创建的是两层,一层做根,一层做分支

2、父亲节点存的是孩子节点中的最小值做索引,如果最小值更新了,那么往上的索引值都要全部更新

3、孩子不再是比key多一个(包含关系),而是和key相等(一一对应关系)

4、分裂的时候,不再是把中位数往上拿,而是把分裂出来的兄弟节点的最小值往上拿 

5.3 B*树

B*树是B+树的变形,在B+树的非根和非叶子节点再增加指向兄弟节点的指针
为什么B*树的非叶子节点需要指向兄弟节点的指针呢?而B+树不需要呢? 究竟想达到什么目的?

B+树的分裂:
        当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。

B*树的分裂:
        当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。(所以B*树的关键字和孩子数量->[2/3M——M]
      所以,B*树分配新结点的概率比B+树要低,空间使用率更高;

5.3 B树系列总结

B树:有序数组+平衡多叉树;
B+树:有序数组链表+平衡多叉树;
B*树:一棵更丰满的,空间利用率更高的B+树。


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

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

相关文章

通俗易通解读Restaking,潜力如何?(bitget钱包玩转)

关于 Restaking 再质押&#xff1a; 1. Restaking 在功能上&#xff0c;将以太坊安全性标准化&#xff0c;并将其“货币化”&#xff0c;Restaking 出售的就是以太坊的安全性&#xff0c;同时&#xff0c;将安全性用质押后的通证数量直观表现出来 2. Restaking 在经济机制上的逐…

NSSCTF | [第五空间 2021]WebFTP

注意看这里的题目标签&#xff0c;目录扫描&#xff0c;.git泄露。那么这道题虽然打开是一个登录的界面&#xff0c;但是并不是我们熟悉的爆破和SQL注入。 但是可以在题目标签上看到目录扫描&#xff0c;我们就用dirsearch扫一扫看看 python dirsearch.py -u http://node4.ann…

下载wsl 网络出现问题,解决办法

查看能下载的wsl系统时&#xff0c;显示网络出现问题 解决办法&#xff1a;更换网络节点 最终效果&#xff1a;

学习java

在实验室看见这本书&#xff0c;无聊看了下&#xff0c;写出了第一个java代码 成功下载了eclipse并且汉化。 写了自己的第一个java程序&#xff1a; package ttttt;public class ttttt {public static void main(String[] args) {System.out.println("hello world")…

模型 洋葱模型(组织管理方向)

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。层层深入&#xff0c;探索核心。 1 洋葱模型的应用 1.1 洋葱模型用于职业规划 有一个名叫李明的大学生&#xff0c;他最近感到迷茫和压力&#xff0c;因为他即将毕业并面临职业选择。李明决定寻求心…

从零开始详解OpenCV车道线检测

前言 车道线检测是智能驾驶和智能交通系统中的重要组成部分&#xff0c;对于提高道路安全、交通效率和驾驶舒适性具有重要意义。在本篇文章中将介绍使用OpenCV进行车道线的检测 详解 导入包 import cv2 import matplotlib.pyplot as plt import numpy as np读入图像并灰度化…

用matplotlib制作代码和色块

代码如下&#xff1a; # 声明 # -*- coding: utf-8 -*- """ Created on Mon May 13 11:18:59 2024author: sankang """ # 这里调用包 import matplotlib as mpl import matplotlib.pyplot as plt import numpy as npplt.rcParams[axes.unicode_…

ASP.NET仪器设备管理系统设计与实现

摘 要 文中以某中小型企业的设备管理为例&#xff0c;对设备管理系统的设计与应用进行研究&#xff0c;旨在通过设备管理系统提高内部设备的利用率及实现其最大的经济效益。文中首先对设备管理的现状及其重要性进行了分析&#xff0c;分析实现设备管理信息系统的必要性与可行…

1.基本概念,半导体基础

1.电压降&#xff1a; 指电流通过阻抗负载时的电位降的大小。&#xff08;线段或部件两端的电压&#xff09;。 2.数量较多的载流子称为多子 3.二极管和稳压管 4.习题

银发经济背后百万亿市场,解析冷门暴利的中老年AI赚钱项目!

最近“银发经济”这个词频繁出现&#xff0c;如果你注意到了抖音被封号的“秀才”以及仍在活跃的“一笑倾城”这两个账号&#xff0c;你就会明白中老年赛道的前景是多么广阔。 《银发经济蓝皮书》数据显示&#xff0c;到目前为止&#xff0c;我国60岁及以上的老年人口已超过2.8…

2024最佳项目管理软件整理

在项目管理领域&#xff0c;选择合适的任务管理软件对于提高团队效率和确保项目顺利进行至关重要。 以下是一些优秀的项目任务管理软件&#xff0c;以及它们的官网地址和特点介绍 序号软件名称官网地址特点介绍1zz-planhttps://zz-plan.com/一款在线甘特图协作软件&#xff0c…

炫酷渐变官网源码

炫酷渐变官网源码 效果图部分代码领取源码下期更新预报 效果图 部分代码 <!DOCTYPE html> <html lang"en"><head><meta charset"utf-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><title…

下载驱动包提示 通常不会下载 未验证的下载

打开设置 添加允许站点 如果还是下载不了&#xff0c;那只能换资源了

基于网络的无人海洋船舶控制

书籍&#xff1a;Network-Based Control of Unmanned Marine Vehicles 作者&#xff1a;Yu-Long Wang&#xff0c;Qing-Long Han&#xff0c;Chen Peng&#xff0c;Lang Ma 出版&#xff1a;Springer 书籍下载-《基于网络的无人海洋船舶控制》控制系统中的通信网络可能引起延…

9.多数元素

文章目录 题目简介题目解答解法一&#xff1a;排序代码&#xff1a;复杂度分析&#xff1a; 解法二&#xff1a;摩尔投票法代码&#xff1a;复杂度分析&#xff1a; 解法三&#xff1a;哈希表代码复杂度分析&#xff1a; 题目链接 大家好&#xff0c;我是晓星航。今天为大家带来…

Meilisearch vs Elasticsearch

2个搜索引擎的比较&#xff08;官方说法&#xff09;&#xff1a;Meilisearch vs Elasticsearch Elasticsearch 做为老牌搜索引擎&#xff0c;功能基本满足&#xff0c;但复杂&#xff0c;重量级&#xff0c;适合大数据量。 MeiliSearch 设计目标针对数据在 500GB 左右的搜索需…

《intel开发手册卷3》读书笔记1

1、CPU工作模式 1&#xff09;实模式&#xff1a;8086的寄存器只有16位&#xff0c;我们也习惯于称8086的工作模式为16位模式。后续的CPU为了保持兼容性&#xff0c;在芯片上了电以后&#xff0c;还必须运行于16位模式之下。这种模式还有个正式的名字叫做实模式。在实模式下&am…

转移插槽笔记

4.3.4.转移插槽 我们要将num存储到7004节点&#xff0c;因此需要先看看num的插槽是多少&#xff1a; 如上图所示&#xff0c;num的插槽为2765. 我们可以将0~3000的插槽从7001转移到7004&#xff0c;命令格式如下&#xff1a; 具体命令如下&#xff1a; 建立连接&#xff1a;…

零一万物发布千亿参数模型Yi-Large,李开复呼吁关注TC-PMF,拒绝Ofo式烧钱打法

5月13日&#xff0c;在零一万物成立一周年之际&#xff0c;零一万物 CEO 李开复博士携带千亿参数 Yi-Large 闭源模型正式亮相&#xff0c;正式进军全球 SOTA 顶级大模型之首&#xff0c;在斯坦福最新的 AlpacaEval 2.0 达到全球大模型 Win Rate 第一。除此之外&#xff0c;零一…

你知道在 TS 中判断两个类型相等有多难吗?

公众号&#xff1a;程序员白特&#xff0c;欢迎一起交流学习~ TypeScript 中的类型相等 如果我们想判断两个变量是否相等&#xff0c;可以简单的通过 或 来进行比较&#xff0c;但是对比两个类型则不行。 在 TypeScript 中&#xff0c;类型是静态的&#xff0c;只会在编译时…