高阶数据结构——B树

news2024/12/27 2:09:01

1. 常见的搜索结构

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

使用平衡二叉树搜索树的缺陷:平衡二叉树搜索树的高度是logN,这个查找次数在内存中是很快的。但是当数据都在磁盘中时,访问磁盘速度很慢,在数据量很大时,logN次的磁盘访问,是一个难以接受的结果。

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

那如何加速对数据的访问呢

  • 1. 提高IO的速度(SSD相比传统机械硬盘快了不少,但是还是没有得到本质性的提升)
  • 2. 降低树的高度---多叉树平衡树

2. B树概念

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,如下图所示。

二叉树的示例图:

3. B-树的插入分析

为了简单起见,假设M = 3. 即三叉树,所以每个结点应该包含两个关键字和三个孩子指针。但是为了方便写代码,我们增加一个关键字和孩子指针。

注意:孩子永远比数据多一个

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

前两个直接插入即可。

插入第三个时,则不满足我们的条件,每个结点最多只能有k - 1个关键字,所以这里要进行分裂。

分裂步骤:

  • 分裂出一个兄弟结点,并拷贝一半的关键字和孩子指针给兄弟结点。
  • 提取中位数给父亲,没有父亲则创建。
  • 将根结点,兄弟结点和当前结点进行连接。

中位数是75,所以将75后边的所有值拷贝给兄弟,而当前结点没有父亲结点,所以还要创建一个父亲结点,并将75拷给父亲。

注意,B是是一颗平衡搜索树,所以插入时要注意插入位置,保持有序。

插入36时,我们会发现,左下角的那个结点满了,此时又需要分裂,将中位数49给他的父亲,49后面的数给他的兄弟结点。

右下角的结点又不满足B树的条件了,开始分裂,和前面一样,将139交给父亲,145给兄弟结点。

此时,我们发现,根节点又不满足B树性质了,我们要对根节点进行分裂,如下图所示。

4. B树的插入实现

4.1 插入key的过程

我们可以将插入的过程分为这几步。

  • 1. 判断树是否为空,为空则需要创建根结点。
  • 2. 找到要插入的结点(一定为叶子结点)。
  • 3. 将key插入到结点中。
  • 4. 判断当前结点是否满了,如果满了,则需要开始分裂,没满则结束。
  • 5. 如果当前结点是根节点(没有父亲),则重新创建一个结点当根,并连接好相应关系,如果不是根节点,则将当前结点的中位数当作key,要插入的结点变成了当前结点的父亲,回到第三步。

1 - 4步很好理解,可以对照前面的图来分析,第5步不好理解,我们举两个例子来看。

1. 当前结点是根节点

例如这里,我们插入75,已经完成了第三步了,第四步是分裂,将一半的值拷贝给他的兄弟。

还有最后的一步,我们就可以完成这个插入过程了,即将75给当前结点的父结点。但是当前结点没有父结点,所以我们直接创建一个新结点当作根结点即可。

当前结点不是根节点

如果插入的是36,还是一样要分裂一半给他兄弟。

现在我们需要将75插入父亲结点,我们会发现插入父结点和插入当前结点的逻辑其实都是一样的,所以我们,在分裂完之后,要将75当成新的key插入到父亲结点当中(一个很重要的逻辑)。所以我们现在又要到第三步去将75插入父亲结点。

第三步执行完,到第四步,发现父亲结点还没有满,插入过程结束。

4.2 B树结点的设计

//K是数据类型,M是树的路数,将来就是M叉树
template<class K, size_t M>
struct BTreeNode
{
	BTreeNode()
	{
		for (size_t i = 0; i < M; i++)
		{
			_keys[i] = K();
			_chlids[i] = nullptr;
		}
		_childs[M] = nullptr;
		_parent = nullptr;
		_size = 0;
	}

	//为了方便写代码,所以关键字和孩子指针我们都多开了一个空间。
	K _keys[M];  //关键字
	BTreeNode<K, M>* _childs[M + 1]; //孩子指针
	BTreeNode<K, M>* _parent;        //父亲指针
	size_t _size; //实际存储的个数
};

我们使用一个类模板来适配各种类型,M表示树的路树,一个B树的结点中包含以下几个内容:

  • 1. _keys用来保存关键字的数组。
  • 2. _childs是孩子的指针,指向他的孩子结点。
  • 3. _parent是父亲指针,将来可以很容易找到当前结点的父亲。
  • 4. _size是保存的关键字个数,也可以得到孩子的个数(B树结点中,孩子的数量永远比关键字多一个)。

4.3 B树的插入实现

B树的插入过程:

  • 1. 判断树是否为空,为空则需要创建根结点。
  • 2. 找到要插入的结点(一定为叶子结点)。
  • 3. 将key插入到结点中。
  • 4. 判断当前结点是否满了,如果满了,则需要开始分裂,没满则结束。
  • 5. 如果当前结点是根节点(没有父亲),则重新创建一个结点当根,并连接好相应关系,如果不是根节点,则将当前结点的中位数当作key,要插入的结点变成了当前结点的父亲,回到第三步。
bool Insert(const K& key)
{
	//1.如果是一颗空树,创建一个新结点即可。
	if (_root == nullptr)
	{
		_root = new Node();
		_root->_keys[0] = key;
		_root->_size++;

		return true;
	}

	//2.每次插入都是往叶子结点插入,所以先找到要插入的叶子结点。
	pair<Node*, int> ret = Find(key);
	if (ret.second >= 0)
	{
		//找到了一个值为key的结点,不需要再插入了(当前B树不支持插入重复值)
		return false;
	}

	//3.将key插入结点中
	//ret顺便将key应该插入的结点带回来了,我们直接将key插入该结点即可
	Node* cur = ret.first;
	K insertKey = key;
	Node* insertChild = nullptr;
	InsertKey(cur, insertKey, insertChild);

	return true;
}

上面的代码已经完成了前三步的动作了,而第二步和第三步中的Find和InsertKey的函数我们还需要手动实现一下。

//返回一个pair,第一个参数是要查找的那个结点,第二个参数是要查找的位置下标,为-1表示查找失败。
pair<Node*, int> Find(const K& key)
{
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur != nullptr)
	{
		size_t i = 0;
		parent = cur;
		while (i < cur->_size)
		{
			if (cur->_keys[i] > key)       //向当前结点的孩子去查找
			{
				break;
			}
			else if (cur->_keys[i] < key)  //比当前值大,继续向后查找
			{
				i++;
			}
			else
			{
				return make_pair(cur, i);  //查找成功,返回结果
			}
		}
		cur = cur->_childs[i]; //向当前结点的孩子去查找。
	}

	//查找失败,不存在值为key的结点,我们将parent返回,为了方便以后将key插入parent
	return make_pair(parent, -1);
}

首先是Find函数,以下图为例:

现在我们需要找到36要插入的结点,cur指向的就是最上面那个结点,从左向右判断,发现36比75小,那么就进入75对应下标的childs,也就是childs[0],cur指向了右下角那个结点,再次从左往右判断,发现比49小,于是进入49对应下标的childs,也就是childs[0],此时cur是nullptr,循环结束,我们此时需要返回cur的parent,也就是左下角的结点,这个结点也就是将来36要插入的结点。

void InsertKey(Node* cur, const K& key, Node* child)
{
	auto& childs  = cur->_childs;
	auto& keys = cur->_keys;
	size_t size = cur->_size - 1;

	while (size >= 0)
	{
		if (keys[size] > key)
		{
			//将keys[size+1]和childs[size+2]向后移动,为了给key和child提供位置。
			keys[size + 1] = keys[size];
			childs[size + 2] = childs[size + 1];
		}
		else
		{
			break;
		}

		size--;

	}

	//将key和child插入进cur->_keys和cur->_childs中去。
	keys[size + 1] = key;
	childs[size + 2] = child;

	//不要忘记修改孩子的父亲
	if (child)
		child->_parent = cur;

	//新插入了一个key,所以size要加1
	cur->_size++;
}

将一个值和孩子插入一个结点,我们使用的是直接插入排序,如下图所示。

需要注意的是,不仅仅需要插入key,而且还需要插入child,因为B树的性质就是key永远比child少一个,也不要忘记插入结点的父亲指针需要改变。

现在我们可以开始实现第4和第5步了:

  • 4. 判断当前结点是否满了,如果满了,则需要开始分裂,没满则结束。
  • 5. 如果当前结点是根节点(没有父亲),则重新创建一个结点当根,并连接好相应关系,如果不是根节点,则将当前结点的中位数当作key,要插入的结点变成了当前结点的父亲,回到第三步。
bool Insert(const K& key)
{
	//1.如果是一颗空树,创建一个新结点即可。
	if (_root == nullptr)
	{
		_root = new Node();
		_root->_keys[0] = key;
		_root->_size++;

		return true;
	}

	//2.每次插入都是往叶子结点插入,所以先找到要插入的叶子结点。
	pair<Node*, int> ret = Find(key);
	if (ret.second >= 0)
	{
		//找到了一个值为key的结点,不需要再插入了(当前B树不支持插入重复值)
		return false;
	}

	//3.将key插入结点中
	//ret顺便将key应该插入的结点带回来了,我们直接将key插入该结点即可
	Node* cur = ret.first;
	K insertKey = key;
	Node* insertChild = nullptr;

	InsertKey(cur, insertKey, insertChild);

	4.如果没满就退出,满了就需要分裂
	if (cur->_size < M)
	{
		break;
	}
	else 
	{
		//创建兄弟结点,拷贝一半的值给兄弟。
		Node* brother = new Node;
		size_t mid = cur->_size / 2;
		size_t k = 0;
		for (size_t i = mid + 1; i < cur->_size; i++)
		{
			brother->_keys[k] = cur->_keys[i];       //将关键字拷贝给兄弟
			brother->_childs[k] = cur->_childs[i];   //将孩子拷给兄弟
			if (cur->_childs[i])
				cur->_childs[i]->_parent = brother;  //将孩子的父亲改成兄弟
			k++;

			cur->_keys[i] = K();       //设置为默认值,方便观察
			cur->_childs[i] = nullptr;
		}
		brother->_childs[k] = cur->_childs[cur->_size];  //将最后一个孩子拷给兄弟
		if (cur->_childs[cur->_size]) 
			cur->_childs[cur->_size]->_parent = brother; //将孩子的父亲改成兄弟。
		cur->_childs[cur->_size] = nullptr;
		brother->_size += k;  //更改兄弟结点的关键字个数
		cur->_size -= k;   //当前结点的关键字数要减去拷贝给兄弟的。

		size_t midKey = cur->_keys[mid];  //将中位数保存起来
		cur->_keys[mid] = K();
		cur->_size--;      //当前结点的关键字数还要减去拷贝给父亲的。
	}

	return true;
}

先来看第四步,第四步是要将一半的结点拷贝给兄弟,代码如上图所示,为了方便观察,我们将拷贝过的值设置为默认值。注意的是,B树的指针错综复杂,需要非常小心,不然很容易出错。

如何实现第五步,第五步的思想是将父结点当作一个新节点,也就是有了循环的思想,所以我们可以将3-5步放到一个循环中去。

bool Insert(const K& key)
{
	//1.如果是一颗空树,创建一个新结点即可。
	if (_root == nullptr)
	{
		_root = new Node();
		_root->_keys[0] = key;
		_root->_size++;

		return true;
	}

	//2.每次插入都是往叶子结点插入,所以先找到要插入的叶子结点。
	pair<Node*, int> ret = Find(key);
	if (ret.second >= 0)
	{
		//找到了一个值为key的结点,不需要再插入了(当前B树不支持插入重复值)
		return false;
	}

	//3.将key插入结点中
	//ret顺便将key应该插入的结点带回来了,我们直接将key插入该结点即可
	Node* cur = ret.first;
	K insertKey = key;
	Node* insertChild = nullptr;

	while (1)
	{
		InsertKey(cur, insertKey, insertChild);

		4.如果没满就退出,满了就需要分裂
		if (cur->_size < M)
		{
			break;
		}
		else 
		{
			//创建兄弟结点,拷贝一半的值给兄弟。
			Node* brother = new Node;
			size_t mid = cur->_size / 2;
			size_t k = 0;
			for (size_t i = mid + 1; i < cur->_size; i++)
			{
				brother->_keys[k] = cur->_keys[i];       //将关键字拷贝给兄弟
				brother->_childs[k] = cur->_childs[i];   //将孩子拷给兄弟
				if (cur->_childs[i])
					cur->_childs[i]->_parent = brother;  //将孩子的父亲改成兄弟
				k++;

				cur->_keys[i] = K();       //设置为默认值,方便观察
				cur->_childs[i] = nullptr;
			}
			brother->_childs[k] = cur->_childs[cur->_size];  //将最后一个孩子拷给兄弟
			if (cur->_childs[cur->_size]) 
				cur->_childs[cur->_size]->_parent = brother; //将孩子的父亲改成兄弟。
			cur->_childs[cur->_size] = nullptr;
			brother->_size += k;  //更改兄弟结点的关键字个数
			cur->_size -= k;   //当前结点的关键字数要减去拷贝给兄弟的。

			size_t midKey = cur->_keys[mid];  //将中位数保存起来
			cur->_keys[mid] = K();
			cur->_size--;      //当前结点的关键字数还要减去拷贝给父亲的。


			//5.如果当前结点是根节点,则需要重新创建一个根。
			//  如果当前结点不是根结点,则直接将当前结点当作新结点即可。
			if (cur->_parent == nullptr)
			{
				_root = new Node;
				_root->_keys[0] = midKey;
				_root->_childs[0] = cur;
				_root->_childs[1] = brother;
				cur->_parent = _root;
				brother->_parent = _root;
				_root->_size = 1;

				break;
			}
			else
			{
				insertKey = midKey;
				insertChild = brother;
				cur = cur->_parent;
			}
		}
	}

	return true;
}

4.4 B树的简单验证

到这里B树的插入过程的代码就写完了,我们可以做一个简单的验证,和前面的例子一样{ 53, 139, 75, 49, 145, 36, 101 },将这组数插入到树中去,最终判断结果是否和我们分析的一样。

经过调试,可以看到结果是正确的:

这样可能不太方便观察,我在这里转换。

可以看到最终的结果是正确的,我们也可以使用前序遍历,如果结果是有序的说明应该没什么大问题。

void InOrder()
{
	_InOrder(_root);
}

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

	size_t i = 0;
	for (i = 0; i < root->_size; i++)
	{
		_InOrder(root->_childs[i]);
		cout << root->_keys[i] << " ";
	}
	_InOrder(root->_childs[i]);
}

遍历结果:

4.5 B树的性能分析

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$之间,在定位到该节点后,再采用二分查找的方式可以很快的定位到该元素。

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

4.6 整体代码

#pragma once

#include <iostream>
#include <vector>

using namespace std;

//K是数据类型,M是树的路数,将来就是M叉树
template<class K, size_t M>
struct BTreeNode
{
	BTreeNode()
	{
		for (size_t i = 0; i < M; i++)
		{
			_keys[i] = K();
			_childs[i] = nullptr;
		}
		_childs[M] = nullptr;
		_parent = nullptr;
		_size = 0;
	}

	//为了方便写代码,所以关键字和孩子指针我们都多开了一个空间。
	K _keys[M];  //关键字
	BTreeNode<K, M>* _childs[M + 1]; //孩子指针
	BTreeNode<K, M>* _parent;        //父亲指针
	size_t _size; //实际存储的个数
};

//K是数据类型,M是树的路数,将来就是M叉树
template<class K, size_t M>
class BTree
{
	typedef BTreeNode<K, M> Node;
public:
	BTree()
		: _root(nullptr)
	{}

	//返回一个pair,第一个参数是要查找的那个结点,第二个参数是要查找的位置下标,为-1表示查找失败。
	pair<Node*, int> Find(const K& key)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			size_t i = 0;
			parent = cur;
			while (i < cur->_size)
			{
				if (cur->_keys[i] > key)       //向当前结点的孩子去查找
				{
					break;
				}
				else if (cur->_keys[i] < key)  //比当前值大,继续向后查找
				{
					i++;
				}
				else
				{
					return make_pair(cur, i);  //查找成功,返回结果
				}
			}
			cur = cur->_childs[i]; //向当前结点的孩子去查找。
		}

		//查找失败,不存在值为key的结点,我们将parent返回,为了方便以后将key插入parent
		return make_pair(parent, -1);
	}

	void InsertKey(Node* cur, const K& key, Node* child)
	{
		auto& childs  = cur->_childs;
		auto& keys = cur->_keys;
		size_t size = cur->_size - 1;

		while (size >= 0)
		{
			if (keys[size] > key)
			{
				//将keys[size+1]和childs[size+2]向后移动,为了给key和child提供位置。
				keys[size + 1] = keys[size];
				childs[size + 2] = childs[size + 1];
			}
			else
			{
				break;
			}

			size--;

		}

		//将key和child插入进cur->_keys和cur->_childs中去。
		keys[size + 1] = key;
		childs[size + 2] = child;

		//不要忘记修改孩子的父亲
		if (child)
			child->_parent = cur;

		//新插入了一个key,所以size要加1
		cur->_size++;
	}

	bool Insert(const K& key)
	{
		//1.如果是一颗空树,创建一个新结点即可。
		if (_root == nullptr)
		{
			_root = new Node();
			_root->_keys[0] = key;
			_root->_size++;

			return true;
		}

		//2.每次插入都是往叶子结点插入,所以先找到要插入的叶子结点。
		pair<Node*, int> ret = Find(key);
		if (ret.second >= 0)
		{
			//找到了一个值为key的结点,不需要再插入了(当前B树不支持插入重复值)
			return false;
		}

		//3.将key插入结点中
		//ret顺便将key应该插入的结点带回来了,我们直接将key插入该结点即可
		Node* cur = ret.first;
		K insertKey = key;
		Node* insertChild = nullptr;

		while (1)
		{
			InsertKey(cur, insertKey, insertChild);

			4.如果没满就退出,满了就需要分裂
			if (cur->_size < M)
			{
				break;
			}
			else 
			{
				//创建兄弟结点,拷贝一半的值给兄弟。
				Node* brother = new Node;
				size_t mid = cur->_size / 2;
				size_t k = 0;
				for (size_t i = mid + 1; i < cur->_size; i++)
				{
					brother->_keys[k] = cur->_keys[i];       //将关键字拷贝给兄弟
					brother->_childs[k] = cur->_childs[i];   //将孩子拷给兄弟
					if (cur->_childs[i])
						cur->_childs[i]->_parent = brother;  //将孩子的父亲改成兄弟
					k++;

					cur->_keys[i] = K();       //设置为默认值,方便观察
					cur->_childs[i] = nullptr;
				}
				brother->_childs[k] = cur->_childs[cur->_size];  //将最后一个孩子拷给兄弟
				if (cur->_childs[cur->_size]) 
					cur->_childs[cur->_size]->_parent = brother; //将孩子的父亲改成兄弟。
				cur->_childs[cur->_size] = nullptr;
				brother->_size += k;  //更改兄弟结点的关键字个数
				cur->_size -= k;   //当前结点的关键字数要减去拷贝给兄弟的。

				size_t midKey = cur->_keys[mid];  //将中位数保存起来
				cur->_keys[mid] = K();
				cur->_size--;      //当前结点的关键字数还要减去拷贝给父亲的。


				//5.如果当前结点是根节点,则需要重新创建一个根。
				//  如果当前结点不是根结点,则直接将当前结点当作新结点即可。
				if (cur->_parent == nullptr)
				{
					_root = new Node;
					_root->_keys[0] = midKey;
					_root->_childs[0] = cur;
					_root->_childs[1] = brother;
					cur->_parent = _root;
					brother->_parent = _root;
					_root->_size = 1;

					break;
				}
				else
				{
					insertKey = midKey;
					insertChild = brother;
					cur = cur->_parent;
				}
			}
		}

		return true;
	}

	void InOrder()
	{
		_InOrder(_root);
	}

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

		size_t i = 0;
		for (i = 0; i < root->_size; i++)
		{
			_InOrder(root->_childs[i]);
			cout << root->_keys[i] << " ";
		}
		_InOrder(root->_childs[i]);
	}

private:
	Node* _root;
};

5. B+树和B*树

5.1 B+树

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

  • 1. 分支节点的子树指针与关键字个数相同
  • 2. 分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间
  • 3. 所有叶子节点增加一个链接指针链接在一起
  • 4. 所有关键字及其映射数据都在叶子节点出现
  • 5.分支结点存放的是叶子结点的索引
  • 6.父亲用孩子的最小关键字来做索引的

B+树的特性:

  • 1. 所有关键字都出现在叶子节点的链表中,且链表中的节点都是有序的
  • 2. 不可能在分支节点中命中。
  • 3. 分支节点相当于是叶子节点的索引,叶子节点才是存储数据的数据层。

B+树的分裂:

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

5.2 B*树

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

B*树的分裂:

当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。

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

5.3 总结

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

单论树的高度,查找效率来看B树系列确实不错,但是也存在一些缺点。

  • 1.空间利用率低,占用的空间大。
  • 2.插入和删除数据时,需要进行分裂,涉及到数据的挪动,效率低。
  • 3.虽然比起红黑树等,高度更低,但是在内存中都是一个量级的,搜索效率并不比红黑树快多少(在内存中没什么优势)

6. B树的应用

6.1 索引

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

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

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

6.2 MySQL索引

mysql是目前非常流行的开源关系型数据库,不仅是免费的,可靠性高,速度也比较快,而且拥有灵活的插件式存储引擎,如下:

MySQL中索引属于存储引擎级别的概念,不同存储引擎对索引的实现方式是不同的。
 

6.2.1 MyISAM

MyISAM引擎是MySQL5.5.8版本之前默认的存储引擎,不支持事物,支持全文检索,使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址,其结构如下:

上图是以以Col1为主键,MyISAM的示意图,可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。如果想在Col2上建立一个辅助索引,则此索引的结构如下图所示:

同样也是一棵B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。MyISAM的索引方式也叫做“非聚集索引”的。

6.2.2 InnoDB

InnoDB存储引擎支持事务,其设计目标主要面向在线事务处理的应用,从MySQL数据库5.5.8版本开始,InnoDB存储引擎是默认的存储引擎。InnoDB支持B+树索引、全文索引、哈希索引。但InnoDB使用B+Tree作为索引结构时,具体实现方式却与MyISAM截然不同。

第一个区别是InnoDB的数据文件本身就是索引文件。MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而InnoDB索引,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。

图是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录,这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。

第二个区别是InnoDB的辅助索引data域存储相应记录主键的值而不是地址,所有辅助索引都引用主键作为data域。

聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
 

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

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

相关文章

STM32——PWM波形输出

一、IC和OC 可以看到&#xff1a;定时器除了基本的定时中断功能&#xff0c;输入捕获、输出比较均是STM32定时器的功能 输入捕获IC&#xff08;Input Capture&#xff09; 输入捕获是一种用于测量外部信号脉冲宽度或频率的技术。它通过定时器模块捕获外部信号的特定事件&…

创客匠人对话标杆(上)|央视嘉宾揭秘心理抑郁赛道爆款的六大逻辑

今天是我们对话标杆栏目第61期内容&#xff0c;本期我们邀请到【钧岚心理平台】创始人杨钧岚老师&#xff0c;为我们分享了心理学领域如何精准定位垂直赛道&#xff0c;并详细阐述了她如何打造爆品&#xff0c;以高质量课程交付&#xff0c;高效实现高客单转化&#xff0c;实现…

centos8以上系统安装docker环境

由于docker官方更新了相关镜像路由&#xff0c;导致国内用户无法正常手段安装使用docker&#xff0c;本人推荐使用下面操作进行安装。 1.docker-ce安装 # 添加docker-ce仓库&#xff0c;本次使用的是阿里云的仓库 dnf config-manager --add-repo https://mirrors.aliyun.com/do…

c#实现数据导出为PDF的方式

PdfSharp vs iTextSharp: C#中PDF导出功能比较 PdfSharp 优点 轻量级&#xff1a;适合简单的PDF生成任务易于学习&#xff1a;API相对简单&#xff0c;学习曲线较缓开源&#xff1a;提供开源版本&#xff0c;可自由使用和修改纯C#实现&#xff1a;不依赖外部库或COM组件支持…

江协科技STM32学习笔记(第11章 RTC实时时钟)

第11章 RTC实时时钟 实时时钟本质上是一个定时器&#xff0c;但是这个定时器是专门用来产生年月日时分秒&#xff0c;这种日期和时间信息的。学会了RTC实时时钟&#xff0c;就可以在STM32内部拥有一个独立运行的钟表。想要记录或读取日期和时间&#xff0c;就可以通过操作RTC来…

【机械原理学习】——《机械原理》(第二版)机构部分

机械原理 绪论&#xff1a; 机械机器机构 第一章&#xff1a;平面机构的结构分析 构件与零件 每个独立运动的单元体称为构件机构总是由一些零件组成的‌过盈配合是指两个配合零件之间存在一定的过盈量&#xff0c;即一个零件的孔径比另一个零件的轴径小&#xff0c;装配时…

算法:排序(下)

六、快速排序 快速排序用到了分治思想&#xff0c;同样的还有归并排序。乍看起来快速排序和归并排序非常相似&#xff0c;都是将问题变小&#xff0c;先排序子串&#xff0c;最后合并。不同的是快速排序在划分子问题的时候经过多一步处理&#xff0c;将划分的两组数据划分为一…

【IPD流程】产品开发V模型阶段介绍

目录 阶段简介 配图 阶段详解 作者简介 阶段简介 V模型大体可以划分为以下几个不同的阶段步骤: 需求分析、软件需求分析、概要设计、详细设计、软件编码、单元测试、集成测试、系统测试、验收测试。配图 refer:https://t.zsxq.com/NS41O 阶段详解 客户需求定义: 此阶段…

C/C++圣诞树代码

目录 系列文章 写在前面 圣诞节 C语言 圣诞树 写在后面 系列文章 序号目录直达链接1爱心代码https://want595.blog.csdn.net/article/details/1363606842李峋同款跳动的爱心https://want595.blog.csdn.net/article/details/1397222493满屏飘字代码https://want595.blog.…

247.2k star! 超强大的私有化ChatGPT,支持图像识别/文生图/语音输入/文本朗读,个人电脑即可运行!试试吧

今天作者带大家实现一个普通配置电脑即可运行的私有化ChatGPT&#xff0c;支持以下功能&#xff1a; 1.界面体验与ChatGPT官方几乎一样。 2.支持多种开源模型&#xff0c;可以聊天、写代码、识别图片内容等。 3.支持文生图。 4.支持麦克风语音输入聊天。 5.支持自动朗读回…

如何在wordpress当中使用插件WP Coder(将html、css、javascript应用到wordpress上)

了解认识阶段 安装并运行好WP Coder之后如下图&#xff1a; 设置全局PHP 禁用gutenberg 输入代码 add_filter(gutenberg_can_edit_post, __return_false, 10); add_filter(use_block_editor_for_post, __return_false, 10); 记得点击save并勾选enable PHP code 禁用之后打…

从0开始Vue3数据交互之promise详解

目录 前言 1. 预先须知-普通函数和回调函数 1.1 普通函数: 1.2 回调函数: 2. Promise 简介 2.1 简介 2.2 特点 3. Promise 基本用法 3.1 Promise then 1. 没有传参 3.1.1 没有调用resolve 函数和reject 函数时 3.1.2 调用resolve()函数 3.1.3 调用 reject()函数 2…

【Linux详解】进度条实现 Linux下git 的远程上传

&#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人专栏&#xff1a;Linux—登神长阶 ⛺️ 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f442;&#x1f3fd;留言 &#x1f60d;收藏 &#x1f49e; &#x1f49e; &#x1f49e; &#x1f680;前言 &#x…

张飞硬件1~9电阻篇笔记

电阻有标定值和实际值&#xff0c;关于误差的问题&#xff1a; 精密的电流、电压采样可能会用到1%的精度。如果只是做限流用途的话&#xff0c;用5%就足够。 电阻功率&#xff1a;标定值、额定值、瞬态值&#xff1a; 标定值由封装所决定&#xff0c;例如5W额定值由电路中平…

结构开发笔记(三):solidworks软件(二):小试牛刀,绘制一个立方体

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/141122350 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

如果忘记了 Apple ID 密码,如何重设

“我忘记了我的 Apple ID 密码&#xff0c;如何恢复我的帐户&#xff1f;”为了方便用户&#xff0c;Apple 允许每个人使用唯一的 Apple ID 和密码激活设备并访问所有 Apple 服务。然而&#xff0c;实际上&#xff0c;手动选择某项并忘记它似乎很容易。例如&#xff0c;许多 Ap…

AI大模型零基础入门学习路线(非常详细)从入门到精通,看这篇就 够了

学习AI大模型从零基础入门到精通是一个循序渐进的过程&#xff0c;涉及到理论知识、编程技能和实践经验。下面是一份详细的指南&#xff0c;帮助你从头开始学习并逐步掌握AI大模型的构建与应用。 第一阶段&#xff08;10天&#xff09;&#xff1a;初阶应用 该阶段让大家对大…

北斗导航系统:助力保护生态环境的利器

近年来&#xff0c;随着科技的迅猛发展和生态危机的加剧&#xff0c;环保问题成了全球热点话题。而北斗导航系统&#xff0c;作为中国自主研发的全球卫星导航系统&#xff0c;不仅在军事和民用领域显示出了巨大潜力&#xff0c;也在应对生态保护挑战中发挥了重要作用。本篇文章…

ue5正确导入资源 content(内容),content只能有一个

把资源content下的东西&#xff0c;全部拷贝&#xff0c;放在项目的content下 content只能有一个

除毛除臭不够彻底?宠物空气净化器帮你解决

之前养猫的时候就想买一个空气净化器吸一吸空气的浮毛&#xff0c;尤其是夏天&#xff0c;因为夏天天气热流汗也会多&#xff0c;每次外出回家之后全身都是汗的时候想坐下来吹一下空调&#xff0c;但是一坐下去就会发现&#xff0c;沙发上全都是猫咪浮毛&#xff0c;而且还没开…