【算法和數據結構】平衡查找樹之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;


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章