【算法和数据结构】平衡查找树之B树

B    

     B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B-树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库文件系统

B树是二叉搜索树:

       1.所有非叶子结点至多拥有两个儿子(LeftRight);

       2.所有结点存储一个关键字

  3.非叶子结点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树;

   B树的搜索,从根结点开始,如果查询的关键字与结点的关键字相等,那么就命中;如果查询关键字比结点关键字小,就进入左儿子;如果比结点关键字大,就进入右儿子;如果左儿子或右儿子的指针为空,则报告找不到相应的关键字;如果B树的所有非叶子结点的左右子树的结点数目均保持差不多(平衡),那么B树的搜索性能逼近二分查找;但它比连续内存空间的二分查找的优点是,改变B树结构(插入与删除结点)不需要移动大段的内存数据,甚至通常是常数开销;但B树在经过多次插入与删除后,有可能导致不同的结构,也就是“平衡问题”。

B-树(是“B树”不是“B减树”)

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

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

      2. 每个非根节点有[「M/2」,M]个孩子(孩子个数的区间)

      3. 每个非根节点有[ 「M/2」,M-1]个关键字,并且以升序排列(关键字个数的区间)

      4. key[i]和key[i+1]之间的孩子节点的值介于key[i]、key[i+1]之间

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

      ps:「M/2」是向上取整

       B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。

如:(M=3


  以B-树的性能总是等价于二分查找(与M值无关),也就没有B树平衡的问题;由于M/2的限制,在插入结点时,如果结点已满,需要将结点分裂为两个各占M/2的结点;删除结点时,需将两个不足M/2的兄弟结点合并。

下面简单说明分裂:

  

下面对B-树进行实现

#pragma once

//3阶B树
template<class K, int M = 3>
struct BTreeNode
{
	K _keys[M];//存储有M-1个key,多一个是为了方便分裂
	BTreeNode<K, M>* _subs[M + 1];//存储有M个subs,多一个是为了方便分裂
	BTreeNode<K, M>* _parent;
	size_t _size;//数组中存在的有效关键字的个数
	BTreeNode()
		:_parent(NULL)
		, _size(0)
	{
		int i = 0;
		for (; i <= M; ++i)
		{
			_keys[i] = 0;
			_subs[i] = NULL;
		}
		_keys[i] = 0;
	}
};
template<class K,class V>//结构体实现K,V形式
struct Pair
{
	K _first;
	V _second;
	Pair(const  K& key = k(), const V& value = V())
		:_first(key)
		, _second(value)
	{}
};
template<class K, int M = 3>
class BTree
{
	typedef BTreeNode<K, M> Node;
public:
	BTree()
		:_root(NULL)
	{}
	bool Insert(const K& key)//插入节点
	{
		if (_root == NULL)
		{
			_root = new Node;
			_root->_size++;
			_root->_keys[0] = key;
			return true;
		}

		Pair<Node*, int> ret= Find(key);
		if (ret._second != -1)//判断key是否已经存在
			return false;
		//在节点cur中插入key和sub
		Node* cur = ret._first;
		K insertkey = key;
		Node* sub = NULL;
		while (1)
		{
			_InsertKey(cur, insertkey, sub);
			if (cur->_size < M)
				return true;
			//插入数据后,节点关键字个数大于M-1,需分裂节点,拷贝右半部分
			int mid = (cur->_size - 1) / 2;//找到中间值,进行上移
			int index = 0;
			Node* tmp = new Node;//tmp分裂出来的右半部分,左半部分在cur中

			//拷贝key和subs
			for (size_t i = mid + 1; i < cur->_size; ++i)
			{
				//拷贝key
				tmp->_keys[index++] = tmp->_keys[i];
				tmp->_size++;
				//拷贝subs
				tmp->_subs[index++] = cur->_subs[i];
				if (cur->_subs[i])
					cur->_subs[i]->_parent = tmp;
			}

			cur->_size = (cur->_size - 1) / 2;//更新cur(分裂后的左半部分)大小
			if (cur->_parent == NULL)//插入分裂后上移的元素
			{
				_root = new Node;
				_root->_keys[0] = cur->_keys[mid];
				_root->_size = 1;
				_root->_subs[0] = cur;
				_root->_subs[1] = tmp;

				cur->_parent = _root;
				tmp->_parent = _root;
				break;
			}
			else
			{
				insertkey = cur->_keys[mid];
				sub = tmp;
				cur = cur->_parent;//上移
			}
		}
		return true;
	}
	Pair<Node*, int> Find(const K& key) //查找key,返回节点及对应节点中数组下标
	{
		Node* parent = NULL;
		Node* cur = _root;
		while (cur)
		{
			size_t index = 0;
			while (index < cur->_size)//遍历整个节点关键字
			{
				if (key == cur->_keys[index])
					return Pair<Node*, int>(cur, index);
				else if (key > cur->_keys[index])
					index++;
				else//小于_key[index] --> 结束循环,在_key[index]所在节点查找
					break;
			}
			parent = cur;
			cur = cur->_subs[index];
		}
		return Pair<Node*, int>(parent, -1);//没有找到,注意返回cur的父结点和-1
	}
	void InOrder()//中序遍历输出
	{
		_InOrder(_root);
	}
private:
	void _InsertKey(Node* cur,const K& key, Node* sub)//插入key值
	{
		int index = cur->_size - 1;//从后向前比较移位
		while (index >= 0 && key < cur->_keys[index])//后面的数据(包括_sub[])向后移
		{
			cur->_keys[index + 1] = cur->_keys[index];
			cur->_subs[index + 2] = cur->_subs[index + 1];//画图分析,_subs[]移动两位
			--index;
		}
		cur->_keys[index + 1] = key;
		cur->_subs[index + 2] = sub;
		if (sub)
			sub->_parent = cur;
		++cur->_size;
	}
	void _InOrder(Node* root)
	{
		if (root == NULL)
		{
			return;
		}
		for (int i = 0; i < _root->_size; ++i)
		{
			_InOrder(root->_subs[i]);
			cout << root->_keys[i] << " ";
		}
	}
protected:
	Node* _root;
};

void BTreeTest()
{
	int a[] = { 53, 75, 139, 49, 145, 36, 101 };
	BTree<int, 3> bt;
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
	{
		bt.Insert(a[i]);
	}
	bt.InOrder();
}

B+

       B+树是B-树的变体,也是一种多路搜索树:

       1.其定义基本与B-树同,除了以下几点不同。

       2.非叶子结点的子树指针与关键字个数相同;

       3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树B-树是开区间);

       5.为所有叶子结点增加一个链指针;

       6.所有关键字都在叶子结点出现;

  B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找。

B*

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

小结

B树:二叉树,每个结点只存储一个关键字,等于则命中,小于走左结点,大于走右结点;

B-树:多路搜索树,每个结点存储M/2M个关键字,非叶子结点存储指向关键字范围的子结点;所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;

B+树:在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;

B*树:在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率从1/2提高到2/3;


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据结构算法 排序算法 内排序 八大基础排序 选择排序 简单选择排序 思想 每次选择最大的数插入到末尾中 做法 外层for循环控制次数 内层for循环找出最大的值的角标 找出最大角标后,进行交换 优化思路 同时获取最大值和最小值,然后分别插入数组的首部和尾部 堆排序 思想 使用大顶堆的思想来排序,每次建堆后交换 做法 总体:建堆-替换 建堆 只要左子或右子大于当前根节点,则替换 替换后会导致下面的子发生了变化,因此同样需要进行比较,直至各个节点实现父>子这么一个条件(递归) 交换排序 冒泡排序 思想 每次俩俩交换,将最大值交换到末尾 做法 外层for控制循环次数 内层for控制比较次数 每次循环之后,比较次数都会减少一次 优化思路 如果一趟排序后没有发生位置变化,那么此时就是有序了 快速排序 思想 每次将比支点小的放在支点左边,比支点大的放在支点右边 做法 外循环while只要i和j不碰撞查找 内层两个while循环分别查找出比支点小的和比支点大的角标 如果i<=j满足条件,就交换 一趟排序后,支点的左边都比支点小,支点右边都比支点大 只要满足L<j,i0的条件查找出要插入的何时位置 退出内层while循环后就找到了合适的位置插入 优化思路 二分查找插入,找合适位置的时候使用二分查找算法 希尔排序 思想 用增量来将数组进行分隔,直到增量为1。底层干的还是插入排序干的活 做法 最外层for外循环控制增量的数量,每次/2 第二层for循环控制每次增量那组开始进行插入排序,直至完毕 第三层while循环找到要插入到哪个位置 归并排序 思想 将两个已排好序的数组合并成一个有序的数组 做法 递归拆分出两个有序的数组,从mid的位置开始拆分,递归出口:只有一个值的时候就不用拆分了 合并两个有序的数据 分别往两个数组填充已有序的数据 比较两个数组的值谁小,谁小就放到我们的数组中 如果比较完之后还有剩余的数据,那么用while直接添加到我们的总数组中 优化思路 当递归到规模足够小时,利用插入排序 归并前判断一下是否还有必要归并 只在排序前开辟一次空间 基数(桶)排序 思想 分配,回收(分配到不同的位置上,然后回收)..不断分配..回收来进行排序,直到有序 做法 分配一个[array.length][10列]的二维数组来装我们的元素 最外层for循环控制要分配和回收的次数(根据最大值) 将元素的个、十、百位依次放到桶子上(第一次就是放个位,第二次放十位) 依据每列回收桶子,两个for循环 外排序 查找算法 二分查找 分块查找 哈希查找 贪心算法 求最小生成的Prim算法和Kruskal算法 爬山问题 回溯算法 n皇后问题 动态规划Dynamic Planning 应用 求最长公共子序列LCS 矩阵连乘问题 爬楼梯问题 找零问题 0-1背包问题 分治算法Divide and Conquer 应用:归并排序 其它 Rabin fingerprints 文件指纹算法 BitMap 位图算法 BloomFilter 布隆过

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值