算法筆記:B樹

B樹普遍應用於各類文件系統,文件系統中,數據都是按照數據塊來進行讀取操做。結合二叉樹的優勢和文件系統的特色,因而就有了B樹:node

B樹當中每一個節點存儲着一組數據,數據的數量由B樹的來決定。c++

B樹中的節點包含如下內容:git

  • 大小(size):用來記錄當前節點中元素的個數;github

  • 關鍵字(key):B樹是有序集合,關鍵字是可比的,用於在集合中定位衛星數據;數據結構

  • 是否爲葉子節點(leaf):節點類型能夠分爲內部節點和葉子節點。ui

  • 孩子節點(children):若是當前節點是一個內部節點的話,那麼就有更加底層的孩子節點。若是是葉子節點。那就沒有孩子節點。孩子節點的數量老是比數據的數量多一個。spa

對於一個度數爲N的B樹,須要保持如下性質:指針

  • 一個節點中的數據量不小於N-1而且不大於2N-1,一個例外就是根節點能夠包含小於N-1數量的數據;code

  • 全部的葉子節點高度相等;cdn

  • 在節點中,全部的數據關鍵字保持遞增關係(key[0] < ... < key[size-1]);

  • 對於一個孩子節點children[i],children[i]子樹中全部關鍵大於key[i-1]而且小於key[i](key[-1]=-∞,key[size]=+∞)。

度數小於2的B樹是沒有意義的,所以度數最小的取值爲2。完整的B樹實現見Gist

節點定義

B樹中不存在循環引用,所以能夠大膽使用shared_ptr來代替原生指針,簡化析構操做。B樹的拷貝能夠經過遞歸進行,所以數據結構地拷貝也就簡單了不少。

struct Node {
	bool _leaf;
	int _size;
	vector<Key> _keys = vector<Key>(2*N-1);
	vector<Value> _values = vector<Value>(2*N-1);
	vector<shared_ptr<Node>> _children = vector<shared_ptr<Node>>(2*N);
	Node() = default;
	Node(const Node &node): _leaf(node._leaf), _size(node._size), _keys(node._keys), _values(node._values)
	{
		if (!_leaf)
			for (int i = 0; i <= _size; i++)
				_children[i] = std::make_shared<Node>(*node._children[i]);
	}
};
複製代碼

建立

建立一個空地葉子結點,做爲根節點。

BTree()
{
	root = std::make_shared<Node>();
	root->_leaf = true;
	root->_size = 0;
}
複製代碼

查找

若是在當前節點找到關鍵字,返回衛星數據指針。若是沒有找到關鍵字,那麼須要檢查當前節點是否爲葉節點,若是是葉節點,那麼說明關鍵字不存在,返回指針,若是是內部節點,那麼在子樹中查找關鍵字。

Value* find(shared_ptr<Node> node, Key key) {
	// search key in node
	int i = 0;
	while (i < node->_size && key > node->_keys[i])
		i++;
	if (i < node->_size && key == node->_keys[i])
		return &node->_values[i];
	else if (node->_leaf)
		return nullptr;
	else return find(node->_children[i], key);
}
複製代碼

分裂

當一個節點數據量到達2N-1時後,若是想要繼續插入,就須要對節點進行分裂。把孩子節點中間元素提高到父節點中,而後產生兩個新的孩子節點分別插入中間元素兩側。

// split a full node (child.size == 2*N-1)
void split(shared_ptr<Node> parent, int i, shared_ptr<Node> child) {
	shared_ptr<Node> nchild = std::make_shared<Node>();
	nchild->_leaf = child->_leaf;
	nchild->_size = child->_size = N-1;
	// move k-v
	for (int j = 0; j < N-1; j++) {
		nchild->_keys[j] = child->_keys[j + N];
		nchild->_values[j] = child->_values[j + N];
	}
	// move children
	if (!child->_leaf)
		for (int j = 0; j < N; j++)
			nchild->_children[j] = child->_children[j + N];
	// move child->key[N-1] up
	for (int j = parent->_size; j > i; j--) {
		parent->_keys[j] = parent->_keys[j-1];
		parent->_values[j] = parent->_values[j-1];
		parent->_children[j+1] = parent->_children[j];
	}
	parent->_keys[i] = child->_keys[N-1];
	parent->_values[i] = child->_values[N-1];
	parent->_children[i+1] = nchild;
	parent->_size++;
}
複製代碼

合併

合併操做是分裂操做的逆向過程。

// combine children[i] and children[i+1]
void combine(shared_ptr<Node> parent, int i) {
	shared_ptr<Node> prev = parent->_children[i];
	shared_ptr<Node> next = parent->_children[i+1];
	// move parent->key[i] down
	prev->_keys[prev->_size] = parent->_keys[i];
	prev->_values[prev->_size] = parent->_values[i];
	prev->_size++;
	// move k-v from next to prev
	for (int j = 0; j < next->_size; j++) {
		prev->_keys[j + prev->_size] = next->_keys[j];
		prev->_values[j + prev->_size] = next->_values[j];
	}
	if (!prev->_leaf)
		for (int j = 0; j <= next->_size; j++)
			prev->_children[j + prev->_size] = next->_children[j];
	prev->_size += next->_size;
	// remove parent->key[i]
	parent->_size--;
	for (int j = i; j < parent->_size; j++) {
		parent->_keys[j] = parent->_keys[j+1];
		parent->_values[j] = parent->_values[j+1];
		parent->_children[j+1] = parent->_children[j+2];
	}
}
複製代碼

插入

當根節點滿了,須要建立一個新的根節點,而後將舊的根節點分裂,成爲新的根節點的子節點。

// insert k-v in root node
void insert(Key key, Value value) {
	shared_ptr<Node> ptr = root;
	if (ptr->_size == 2*N-1) {	// split root node
		root = std::make_shared<Node>();
		root->_leaf = false;
		root->_size = 0;
		root->_children[0] = ptr;
		split(root, 0, ptr);
		insert(root, key, value);
	} else insert(root, key, value);
}
複製代碼
  • 插入葉節點:直接插入便可。

  • 插入內部節點:首先,找到新關鍵字所在的子節點,若是子節點滿,進行分裂。而後插入到合適的子節點中。

void insert(shared_ptr<Node> node, Key key, Value value) {
	// find insert position
	int i = 0;
	while (i < node->_size && key > node->_keys[i])
		i++;
	if (node->_leaf) {	// insert k-v in a leaf
		for (int j = node->_size; j > i; j--) {
			node->_keys[j] = node->_keys[j-1];
			node->_values[j] = node->_values[j-1];
		}
		node->_keys[i] = key;
		node->_values[i] = value;
		node->_size++;
	} else {			// insert k-v in subnode
		shared_ptr<Node> ptr = node->_children[i];
		if (ptr->_size == 2*N-1) {
			split(node, i, ptr);
			if (key > node->_keys[i])
				i++;
		}
		insert(node->_children[i], key, value);
	}
}
複製代碼

刪除

和插入操做同樣,刪除操做也是自頂向下對B樹進行調整,必須保證要刪除的關鍵字位於B樹中,不然會產生意想不到的後果,對於刪除操做,處理方式以下:

  • 狀況1,當前節點是葉子節點,找到關鍵字:直接刪除關鍵字便可。

  • 狀況2,當前節點是內部節點,找到關鍵字key[i]:

    • 狀況2a,若是關鍵字的左孩節點children[i]->size>=N:用children[i]中的最大元素(關鍵字的前驅)代替關鍵字,而後在children[i]中刪除最大元素。

    • 狀況2b,若是關鍵字的右孩節點children[i+1]->size>=N:用children[i+1]中的最小元素(關鍵字的後繼)代替關鍵字,而後在children[i+1]中刪除最小元素

    • 狀況2c,若是關鍵字的左右兩個子節點都小於N-1:這時能夠合併兩個子節點,因而關鍵字key[i]落入新合併成的節點中,接着在新的節點中刪除關鍵字。

  • 狀況3,當前節點是內部節點,找到關鍵字所在的子樹children[i]:若是children[i]->size<N,那麼還須要進行一些調整以後再刪除。

    • 狀況3a,若是children[i]的某個相鄰兄弟節點children[x]->size>=N:經過移動操做,使得children[x]元素數量減一,children[i]元素數量加一。

    • 狀況3b,若是children[i]的全部相鄰兄弟節點children[x]->size < N:將children[i]和任意一個children[x]合併。(到底和左邊的合併好仍是右邊的好,好糾結~)

// remove key from node, key must be in node
void remove(shared_ptr<Node> node, Key key) {
	// find delete position
	int i = 0;
	while (i < node->_size && key > node->_keys[i])
		i++;
	if (node->_leaf) {										// case 1: remove k-v from leaf
		node->_size--;
		for (int j = i; j < node->_size; j++) {
			node->_keys[j] = node->_keys[j+1];
			node->_values[j] = node->_values[j+1];
		}
	} else if (i < node->_size && key == node->_keys[i]) {	// case 2: find key in internal node
		shared_ptr<Node> prevChild = node->_children[i];
		shared_ptr<Node> nextChild = node->_children[i+1];
		if (prevChild->_size >= N) {						// case 2a: move precursor to the position of key
			shared_ptr<Node> maxNode = max(prevChild);
			node->_keys[i] = maxNode->_keys[maxNode->_size-1];
			node->_values[i] = maxNode->_values[maxNode->_size-1];
			remove(prevChild, maxNode->_keys[maxNode->_size-1]);
		} else if (nextChild->_size >= N) {					// case 2b: move successor to the position of key
			shared_ptr<Node> minNode = min(nextChild);
			node->_keys[i] = minNode->_keys[0];
			node->_values[i] = minNode->_values[0];
			remove(nextChild, minNode->_keys[0]);
		} else {											// case 2c: combine previous child and next child
			combine(node, i);
			remove(node->_children[i], key);
		}
	} else {														// case 3
		shared_ptr<Node> subNode = node->_children[i];
		if (subNode->_size < N) {
			shared_ptr<Node> prevBrother, nextBrother;
			if (i > 0)				prevBrother = node->_children[i-1];
			if (i < node->_size)	nextBrother = node->_children[i+1];
			if (prevBrother && prevBrother->_size >= N) {			// case 3a
				// remove node->key[i] into subNode
				for (int j = subNode->_size; j > 0; j--) {
					subNode->_keys[j] = subNode->_keys[j-1];
					subNode->_values[j] = subNode->_values[j-1];
				}
				if (!subNode->_leaf)
					for (int j = subNode->_size; j >= 0; j--)
						subNode->_children[j+1] = subNode->_children[j];
				subNode->_keys[0] = node->_keys[i-1];
				subNode->_values[0] = node->_values[i-1];
				subNode->_children[0] = prevBrother->_children[prevBrother->_size];
				subNode->_size++;
				// remove prevBrother->key[prevBrother->size-1] into node
				node->_keys[i-1] = prevBrother->_keys[prevBrother->_size-1];
				node->_values[i-1] = prevBrother->_values[prevBrother->_size-1];
				prevBrother->_size--;
			} else if (nextBrother && nextBrother->_size >= N) {	// case 3a 
				// remove node->key[i] into subNode
				subNode->_keys[subNode->_size] = node->_keys[i];
				subNode->_values[subNode->_size] = node->_values[i];
				subNode->_children[subNode->_size+1] = nextBrother->_children[0];
				subNode->_size++;
				// remove nextBrother->key[0] into node
				node->_keys[i] = nextBrother->_keys[0];
				node->_values[i] = nextBrother->_values[0];
				nextBrother->_size--;
				for (int j = 0; j < nextBrother->_size; j++) {
					nextBrother->_keys[j] = nextBrother->_keys[j+1];
					nextBrother->_values[j] = nextBrother->_values[j+1];
				}
				if (!nextBrother->_leaf)
					for (int j = 0; j <= nextBrother->_size; j++)
						nextBrother->_children[j] = nextBrother->_children[j+1];
			} else if (nextBrother) {	// case 3b: combine child[i] and child[i+1]
				combine(node, i);
			} else {					// case 3b: combine child[i-1] and child[i]
				i--;
				combine(node, i);
			}
		}
		remove(node->_children[i], key);
	}
}
複製代碼

隨着刪除不斷進行,會出現根節點變爲空的狀況,這時候須要把空的根節點刪除。若是B樹中的元素被所有刪除,那麼須要從新建立一個根節點。因爲刪除過程會改變B樹結構,須要再刪除前檢查關鍵字是否存在。

void remove(const Key &key) {
	if (find(root, key))
		remove(root, key);
	if (root->_size == 0)
		root = root->_children[0];
	if (root == nullptr) {
		root = std::make_shared<Node>();
		root->_leaf = true;
		root->_size = 0;
	}
}
複製代碼
相關文章
相關標籤/搜索