高阶数据结构(2):浅谈B树系列

"一条路踏过太多日落,一封信写下太多如果。"


一、初识B树

(1)B树系列为何而来? 

常见的搜索结构:
 

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

        很多时候,数据量比较小时,我们可以直接 把数据量一次性放入 内存中,并进行的数据的查找。然而,如果是数据量很大(100G)的情况下呢? 很显然此刻,再也无法一次性将数据放入 读取到内存中,以供查找,而是只能放到磁盘上了。

        如何解决呢?

我们可以通过 建立存放的关键字 与 磁盘地址的映射关系。从而通过关键字拿到 存放数据的磁盘地址,访问在磁盘中的数据。 

         如何理解磁盘的IO流?

然而事实上,磁盘(外存)具有比主存(内存)更大的容量,它的代价就在于IO流效率的损失。

因此,对于“海量数据”的查找,最根本上的在于 “减少磁盘的IO交互”。

        为什么别的搜索结构不行? 

从根本上,B树系列是继承了 平衡搜索二叉树部分 高查找效率的精华。

可以形象地理解为,将整颗树压扁,而不是像二叉树那样变高。 B树系列更多的感觉就是一种“矮(高度)、胖(多叉)” 。

①压缩高度、二叉变多叉

②一个节点里,存放多个关键字的映射关系。

-----------------前言


二、认识B树 

R.Bayer和E.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树,称为B树.

它的优化版本有 B+树 \ B*树B+树各位肯定不会陌生,因为这与我们数据库的索引联系密切。 

 因此从根本上,B树系列是适合 外查找(磁盘、光盘、U盘)的平衡多叉树。

(1)B树性质

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)B树的插入分析

是否有那么一点点的启发?好像是懂那么一点点了。下面来看看 B树是如何实现插入的。 

①巧妙设计 

②分裂与提取 

分裂+提取:

1.创建一个兄弟节点,并以mid为界限 拷贝一半关键字给兄弟。

2.提取mid关键字  给父亲。 如果没有父亲 就 需要去 创建父亲节点。

 

(3)B树插入的实现

单从B树设计者的图 画出来,就足以展现其巧妙之处。 恐怕也对 B树插入有一点点的理解了。 

①基本结构;

template<class K,size_t M>
struct BTreeNode
{
	K _keys[M];
	BTreeNode<K, M>* _subs[M +1];
	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;
		_parenet = nullptr;
		_n = 0;
	}
};

template<class K,size_t M>
class BTree
{
	typedef BTreeNode<K, M> BTree;
private:
	BTree* _root = nullptr;
};

②查找 

查找是一棵搜索树核心的步骤。

	pair<Node*,int> FindRoot(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;

		while (cur)
		{
			size_t i = 0;
			while (i < cur->_n)
			{
				if (key > cur->_keys[i])
				{
					i++;
				}
				else if (key < cur->_keys[i])
				{
					break;
				}
				else
				{
					return make_pair(cur, i);
				}
			}
			parent = cur;
			cur = cur->_subs[i];
		}

		return make_pair(parent, -1);
	}

③插入 

 有了以上的铺垫我们 进行B树实现的核心。

第一步判定是否root 没有节点:

 

第二步 判定关键字是否存在:

第三步:插入函数

	void _insert(Node* parent, const K& key, Node* child)
	{
		int end = parent->_n - 1;
		while (end >= 0)
		{
			if (key < parent[end])
			{
				parent->_keys[end + 1] = parent->_keys[end];
				parent->_subs[end + 2] = parent->_subs[end + 1];
				--end;
			}
			else
			{
				break;
			}
		}
		parent->_keys[end + 1] = key;
		parent->_subs[end + 2] = child;

		if (child)
		{
			child->_parent = parent;
		}
		parent->_n++;
	}

 

第四步:插入 满时 如何进行 分裂?

        Node* parent = ret.first;
		K newkey = key;
		Node* child = nullptr;
		while (1)
		{
			_insert(parent, newkey, child);
			if (parent->_n < M)
			{
				return true;
			}
			else
			{
				size_t mid = M / 2;
				Node* brother = new Node;

				size_t p_i = mid + 1;
				size_t b_j = 0;
				for (;p_i <= M - 1;++p_i)
				{
					brother->_keys[b_j] = parent->_keys[p_i];
					brother->_subs[b_j] = parent->_subs[p_i];

					if (parent->_subs[p_i])
					{
						parent->_subs[p_i]->parent = brother;
					}
					b_j++;

					parent->_keys[p_i] = K();
					parent->_subs[p_i] = nullptr;
				}

				brother->_subs[b_j] = parent->_subs[p_i];
				if (parent->_keys[p_i])
				{
					parent->_keys[p_i]->parent = brother;
				}
				parent->_subs[p_i] = nullptr;

				brother->_n = b_j;
				parent->_n -= b_j + 1;

				K midkey = parent->_keys[mid];
				parent->_keys[mid] = K();

				if (parent->_parent == nullptr)
				{
					_root = new Node;
					_root->_keys[0] = midkey;
					_root->_subs[1] = parent;
					_root->_subs[2] = brother;
					_root->_n = 1;

					parent->_parent = _root;
					brother->_parent = _root;
					break;
				}
				else
				{
					newkey = midkey;

					child = brother;
					parent = parent->_parent;
				}
			}
		}

 

测试: 

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

 


三、浅谈B树系列

在后来,为了让B树的理解变得简单,对B树的规则 进行了一些的修改。从而衍生出了,其他B树的优化版本。 

(1)B树其他系列

①B+树 

 B+树是B树的变形,是在B树基础上优化的多路平衡搜索树,B+树的规则跟B树基本类似:

B树规则的以下变动:

1.分支节点的子树指针与关键字个数相同.
2.分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间.
3.所有叶子节点增加一个链接指针链接在一起.

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

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

 ②B*树

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

 ③分裂

B树系列的精华就在于 分裂。

B+树的分裂:
当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;

B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。

单次分裂: 

多次分裂:

B*树的分裂:
当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);

如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。

不管是B+ 还是B树,对待满节点的状态下,都是拷贝1/2  给兄弟节点,一定程度上,第一次分裂的情况下,空间的利用率是较低的。

反观B*树,B*树虽然分配新结点的概率比B+树要低,但空间使用率更高。
 

④B树系列的优缺点

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

(2)B树系列的应用

索引:

B树系列常见的用法就是 用来做索引。

比如:书籍目录可以让读者快速找到相关信息,hao123网页导航网站,为了让用户能够快速的找到有价值的分类网站,本质上就是互联网页面中的索引结构。
 

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

mysql引擎 与 索引

mysql有两个常用的 存储引擎:MyiSam \ innodb;

它们的性质上的区别在于: 聚簇索引 、 非聚簇索引。

一个是数据、索引存在一起(聚簇索引)

一个是数据、索引 分开存储(非聚簇索引) 


总结: 

①B树系列适用的检索 场景。

②B树系列的 性质 决定 为什么是平衡树。

③B树系列的应用 innodb 、 myisam

本小篇也就到此结束了;感谢您的阅读

祝你好运~

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
TypeScript是一种静态类型的编程语言,它提供了丰富的类型系统和面向对象的特性,使得开发者可以更好地组织和管理代码。在TypeScript中,高阶数据结构和算法可以通过类和泛型等特性来实现。 高阶数据结构是指那些在基本数据结构的基础上进行扩展或组合得到的数据结构。例如,堆、图、树等都可以被视为高阶数据结构。在TypeScript中,我们可以使用类来定义这些高阶数据结构,并通过泛型来指定其内部存储的数据类型。 下面是一个使用TypeScript实现的堆(Heap)数据结构的示例: ```typescript class Heap<T> { private data: T[] = []; public size(): number { return this.data.length; } public isEmpty(): boolean { return this.data.length === 0; } public insert(value: T): void { this.data.push(value); this.siftUp(this.data.length - 1); } public extractMin(): T | null { if (this.isEmpty()) { return null; } const min = this.data[0]; const last = this.data.pop()!; if (!this.isEmpty()) { this.data[0] = last; this.siftDown(0); } return min; } private siftUp(index: number): void { while (index > 0) { const parentIndex = Math.floor((index - 1) / 2); if (this.data[index] >= this.data[parentIndex]) { break; } [this.data[index], this.data[parentIndex]] = [this.data[parentIndex], this.data[index]]; index = parentIndex; } } private siftDown(index: number): void { const size = this.size(); while (index * 2 + 1 < size) { let childIndex = index * 2 + 1; if (childIndex + 1 < size && this.data[childIndex + 1] < this.data[childIndex]) { childIndex++; } if (this.data[index] <= this.data[childIndex]) { break; } [this.data[index], this.data[childIndex]] = [this.data[childIndex], this.data[index]]; index = childIndex; } } } ``` 以上是一个最小堆的实现,使用了数组来存储数据,并提供了插入和提取最小值的操作。堆是一种常见的高阶数据结构,用于解决许多问题,如优先队列和排序等。 通过使用TypeScript,我们可以更加清晰地定义和使用高阶数据结构和算法,并通过类型检查来减少错误和提高代码的可维护性。当然,这只是其中的一个例子,还有许多其他高阶数据结构和算法可以在TypeScript中实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值