1. 程式人生 > >B樹操作詳解

B樹操作詳解

性質4. 所有的葉子結點都位於同一層。
下面給出實際程式設計時B樹以及B樹結點的資料結構定義。
typedef struct LinkKey
{
	int key;
	struct LinkKey *next;
} LinkKey;

typedef struct LinkNode
{
	void *node;
	struct LinkNode *next;
} LinkNode;

typedef struct BNode
{
	int n;	// 孩子個數,so key個數為n-1
	bool leaf;
	LinkKey *key;
	LinkNode *children;
	struct BNode *parent;
} BNode;

typedef struct BTree
{
	BNode *root;
	int t;	// B樹的最小度數
} Btree;
考慮到B樹結點中多關鍵字和多結點的遍歷,刪除以及插入操作以及其數目的不確定性,為了降低時間和空間複雜度,我們不使用陣列對這些資料進行組織,取而代之的是單鏈表。上述定義中的LinkKey和LinkNode就是儲存一個結點的關鍵字和孩子結點指標的連結串列資料結構。一個結點的關鍵字以非遞減的順序儲存,結點指標的儲存順序與關鍵字順序對應。BNode的leaf屬性記錄該結點是否為葉子結點(true/false)。

1. B樹的插入

B樹的插入操作需要先根據要插入的關鍵字k值找到正確的插入位置,該過程與二叉搜尋樹的插入類似。從樹根出發,遍歷當前結點的關鍵字連結串列,找到一個前面的關鍵字比k小,後面的關鍵字比k大的位置,然後再取出這個位置的孩子指標,繼續遍歷該孩子結點的關鍵字。如此往復,直到當前結點為葉子結點時,找到合適的位置將關鍵字k插入葉子結點的關鍵字連結串列中即可。

1.1 單鏈表操作

該過程主要涉及到的是對單鏈表的操作,這裡不做詳細討論,只給出後續程式中使用到的方法的定義。

/**
* 以下是連結串列的操作
*/
LinkKey *initLinkKey()
{
	LinkKey *head = (LinkKey *)malloc(sizeof(LinkKey));
	head->key = INT_MIN;
	head->next = NULL;
	return head;
}

void destroyLinkKey(LinkKey *link)
{
	while (link != NULL)
	{
		LinkKey *next = link->next;
		free(link);
		link = next;
	}
}

int getKey(LinkKey *link, int which)
{
	int i = -1;
	while (i < which && link->next != NULL)
	{
		link = link->next;
		i++;
	}
	if (i == -1)		return INT_MIN;
	return link->key;
}

void insertLinkKey(LinkKey *link, int key)
{
	while (link->next != NULL)
	{
		if (key <= link->next->key)
		{
			break;
		}
		link = link->next;
	}
	LinkKey *node = (LinkKey *)malloc(sizeof(LinkKey));
	node->key = key;
	node->next = link->next;
	link->next = node;
}

bool deleteLinkKey(LinkKey *link, int key)
{
	while (link->next != NULL && key > link->next->key)
	{
		link = link->next;
	}
	if (key == link->next->key)
	{
		link->next = link->next->next;
		return true;
	}
	return false;
}

LinkNode *initLinkNode()
{
	LinkNode *head = (LinkNode *)malloc(sizeof(LinkNode));
	head->next = NULL;
	head->node = NULL;
	return head;
}

void destroyLinkNode(LinkNode *link)
{
	while (link != NULL)
	{
		LinkNode *next = link->next;
		free(link);
		link = next;
	}
}

BNode *getLinkNode(LinkNode *link, int which)
{
	int i = -1;
	while (i < which && link->next != NULL)
	{
		link = link->next;
		i++;
	}
	if (i == -1)		return NULL;
	return (BNode*)(link->node);
}

void insertLinkNode(LinkNode *link, BNode *node, int which)
{
	int i = 0;
	while (i < which && link->next != NULL)
	{
		link = link->next;
		i++;
	}
	LinkNode *linkNode = (LinkNode *)malloc(sizeof(LinkNode));
	linkNode->node = node;
	linkNode->next = link->next;
	link->next = linkNode;
}

bool deleteLinkNode(LinkNode *link, int which)
{
	int i = 0;
	while (link->next != NULL && i < which)
	{
		link = link->next;
		i++;
	}
	if (i == which)
	{
		link->next = link->next->next;
		return true;
	}
	return false;
}
上述方法涵蓋了對LinkKey連結串列和LinkNode連結串列的初始化,插入,刪除和獲取指定值的操作。

1.2 分裂

下面具體討論在插入操作中會遇到的問題:定義孩子關鍵字個數為2t-1的結點為滿結點,在沿樹下降尋找插入位置時,經過滿結點時會有兩種情況:

情況1. 關鍵字k有可能會插入到其中,就會導致該結點度數過大,破壞了B樹的性質。

情況2. 關鍵字k有可能插入到滿結點的滿結點孩子中,然後導致孩子結點度數過大,通過分裂操作(後面介紹)使孩子節點度數降低,但又會導致當前結點度數過大,破壞了B樹的性質。

因此,在沿路下降搜尋插入位置時,對經過的滿結點,均進行分裂操作以消除滿結點。

以下圖為例介紹分裂操作,該樹按關鍵字S分裂結點得到右圖,關鍵字S從下面的結點移動到了上面的結點中,下面的結點以S為界分裂成了兩個結點分別位於S的兩側。

分裂操作使的原來的關鍵字個數為2t-1的滿結點分裂成了兩個關鍵字個數為t-1的結點,其父結點的關鍵字個數也增加1,這也是上面情況2所敘述的分裂結點會導致滿父結點失衡的原因。下面給出分裂操作的程式。

void splitBNode(BNode *parent, int which)
{
	int i;
	BNode *node = getLinkNode(parent->children, which);
	BNode *right = createBNode();

	// 配置分裂出來的右結點
	right->leaf = node->leaf;
	right->n = node->n / 2;
	for (i = 0; i < right->n; i++)
	{
		if (!right->leaf)		insertLinkNode(right->children, getLinkNode(node->children, right->n + i), i);
		if (i != right->n - 1)	insertLinkKey(right->key, getKey(node->key, right->n + i));
	}
	if (!right->leaf)		insertLinkNode(right->children, getLinkNode(node->children, right->n + i), i);
	right->parent = parent;

	// 原來的node作為左結點
	node->n -= right->n;
	i = 0;
	LinkKey *keys = node->key;
	while (i < node->n - 1)
	{
		keys = keys->next;
		i++;
	}
	int key = keys->next->key;
	keys->next = NULL;	// 斷開與被分裂出去的關鍵字的聯絡

	// 將key和分裂後的結點插入parent結點
	LinkKey *pKeys = parent->key;
	LinkNode *pNodes = parent->children;
	insertLinkKey(pKeys, key);
	insertLinkNode(pNodes, right, which + 1);
	parent->n++;
}
splitNode方法分裂指定的parent結點位於which位置的孩子結點。該方法首先訪問parent結點的LinkNode連結串列得到第which個孩子結點node,然後將其分裂成兩個結點,並將原來node的中間關鍵字移到parent結點的關鍵字連結串列LinkKey中,再修改node到分裂後的兩個結點的指向。

分裂有一個特殊情況:當要分裂的結點是根結點時,需要先用一個空(沒有關鍵字,不是NULL)的結點newRoot指向根結點,然後再對根結點執行分裂操作,最後再將B樹的根結點指標指向newRoot。

1.3 沿樹單程下行插入關鍵字

向B樹插入關鍵字的演算法流程是,從樹根開始,沿著樹往下查抄新的關鍵字所屬位置時,分裂沿途遇到的每一個滿結點(包括葉子結點),直到找到正確的葉子結點並插入關鍵字。插入的程式如下。

void insertBTree(BTree *tree, int key)
{
	BNode *root = tree->root;

	// 新根
	if (root == NULL)
	{
		BNode *newRoot = createBNode();
		newRoot->leaf = true;
		insertLinkKey(newRoot->key, key);
		newRoot->n++;
		tree->root = newRoot;
		return;
	}

	// 分裂根
	if (root->n == tree->t * 2)
	{
		BNode *newRoot = createBNode();
		newRoot->leaf = false;
		newRoot->n = 1;
		insertLinkNode(newRoot->children, root, 0);
		root->parent = newRoot;
		splitBNode(newRoot, 0);
		tree->root = newRoot;
	}

	// 尋找插入路徑
	BNode *node = tree->root;
	while (node->leaf == false)
	{
		LinkKey *keys = node->key;
		int i = 0;
		while (keys->next != NULL && key > keys->next->key)
		{
			keys = keys->next;
			i++;
		}
		BNode *next = getLinkNode(node->children, i);
		if (next->n == tree->t * 2)
		{
			splitBNode(node, i);	// 分裂滿結點
			if (key < getKey(node->key, i))
			{
				node = next;
			}
			else
			{
				node = getLinkNode(node->children, i + 1);
			}
		}
		else
		{
			node = next;
		}
	}

	// 將key插入葉子結點
	LinkKey *pKeys = node->key;
	insertLinkKey(pKeys, key);
	node->n++;
}
上述insertBTree方法可以用於從零開始構造B樹,因為它涵蓋了插入空樹的情況。

2. 刪除關鍵字

從B樹種刪除關鍵字操作前面的過程與二叉搜尋樹類似,根據要刪除的關鍵字沿樹下行找到該關鍵字所屬的結點,然後從該結點的關鍵字連結串列中刪除之。和插入操作一樣,這裡也涉及到了結點度的問題。刪除關鍵字會使結點的度降低,如果結點本來只有t-1個關鍵字,刪除關鍵字後就會破壞其平衡。和插入操作一樣,刪除操作也是沿樹下降搜尋的時候,對遇到的每一個關鍵字個數只有t-1的結點做出處理,直到找到關鍵字所屬的結點為止。該處理是合併結點或者移動關鍵字,其中,合併結點是分裂結點的逆過程。

下面給出刪除操作搜尋關鍵字所屬結點的過程中可能會遇到的情況:

情況1. 如果關鍵字k在葉子結點x中,則直接刪除之;

情況2. 如果關鍵字在內部結點x中:

2.a 考察關鍵字k前面的孩子結點y是否至少包含t個關鍵字,如果是,則找出關鍵字k在以y為根的子樹中的前驅k‘,並用k’代替k,再遞迴地刪除k‘;

2.b 在考察關鍵字k後面的孩子結點z是否至少包含t個關鍵字,如果是,則找出關鍵字k在以z為根的子樹中的後驅k‘,並用k’代替k,再遞迴地刪除k‘;

2.c 如果關鍵字k前後的結點y和z都只有t-1或以下個數的關鍵字,則合併y和z,再從合併後的結點所在的子樹中刪除k;

情況3. 如果關鍵字k不在當前結點中,則考慮該結點中包含k的孩子結點c,如果孩子結點c的關鍵字個數只有t-1,那麼則執行3.a或者3.b操作,然後再繼續搜尋包含k的結點:

3.a 如果結點c的前一個結點或者後一個結點(均稱為結點z)的關鍵字個數都大於t-1個,則從結點x中移動一個關鍵字到c中,再從結點z中移動一個關鍵字到x中,這樣x的關鍵字個數不變,c的關鍵字個數變為t,z的關鍵字個數也知道有t-1個,B樹的平衡不被破壞;

3.b 如果結點c的前後結點都只有t-1個關鍵字,那麼就選擇一個結點與結點c合併。

上述操作其實都是在保證刪除操作在B樹下降搜尋關鍵字k所屬結點的過程中,所經過的結點均能滿足在刪除關鍵字或者子結點合併操作後仍然能夠保持B樹性質的要求。執行上述操作,在最後,關鍵字看都會落在葉子結點中,然後就可以直接刪除之。

下面給出合併結點的程式。

void unionBNodes(BNode *parent, int a, int b)
{
	int middleKey = getKey(parent->key, a);
	BNode *aNode = getLinkNode(parent->children, a);
	BNode *bNode = getLinkNode(parent->children, b);
	LinkKey *aKey = aNode->key;
	LinkKey *bKey = bNode->key;
	LinkNode *aNodes = aNode->children;
	LinkNode *bNodes = bNode->children;

	while (aKey->next != NULL)	aKey = aKey->next;
	LinkKey *middle = (LinkKey *)malloc(sizeof(LinkKey));
	middle->key = middleKey;
	aKey->next = middle;
	middle->next = bKey->next;
	free(bKey);

	while (aNodes->next != NULL)		aNodes = aNodes->next;
	aNodes->next = bNodes->next;
	free(bNodes);

	aNode->n += bNode->n;

	deleteLinkKey(parent->key, middleKey);
	deleteLinkNode(parent->children, b);
	parent->n--;
}
unionNodes方法將parent結點中第a個和第b個結點合併成一個新的結點,a和b必須是相鄰的。

下面給出針對情況3.a的移動關鍵字的操作的方法moveKey。

void moveKey(BNode *parent, int from, int to)
{
	BNode *f = getLinkNode(parent->children, from);
	BNode *t = getLinkNode(parent->children, to);
	if (from < to)
	{
		LinkKey *pLink = parent->key;
		int i = -1;
		while (i < from && pLink->next != NULL)
		{
			pLink = pLink->next;
			i++;
		}
		insertLinkKey(t->key, pLink->key);

		LinkKey *fLink = f->key;
		while (fLink->next->next != NULL)	fLink = fLink->next;
		pLink->key = fLink->next->key;
		free(fLink->next);
		fLink->next = NULL;
		f->n--;

		LinkNode *fNodes = f->children;
		while (fNodes->next->next != NULL)	fNodes = fNodes->next;
		insertLinkNode(t->children, (BNode*)(fNodes->next->node), 0);
		free(fNodes->next);
		fNodes->next = NULL;
	}
	else if (from > to)
	{
		LinkKey *pLink = parent->key;
		int i = -1;
		while (i < to && pLink->next != NULL)
		{
			pLink = pLink->next;
			i++;
		}
		insertLinkKey(t->key, pLink->key);
		t->n++;

		LinkKey *fLink = f->key;
		pLink->key = fLink->next->key;
		LinkKey *nextKey = fLink->next;
		fLink->next = fLink->next->next;
		free(nextKey);
		f->n--;

		LinkNode *fNodes = f->children;
		insertLinkNode(t->children, (BNode*)(fNodes->next->node), t->n);
		LinkNode *nextNode = fNodes->next;
		fNodes->next = fNodes->next->next;
		free(nextNode);
	}
}
moveKey方法將parent結點的第from個結點的某一個關鍵字移動到parent結點上,將parent結點的某一個關鍵字移動到第同個結點上。這些操作都是基於單鏈表實現的。

下面給出輔助獲取一個關鍵字的前驅和後驅的方法。

int getMinKey(BNode *tree)
{
	while (!tree->leaf)
	{
		LinkNode *link = tree->children;
		tree = (BNode *)(link->next->node);
	}
	return tree->key->next->key;
}

int getMaxKey(BNode *tree)
{
	while (!tree->leaf)
	{
		LinkNode *link = tree->children;
		while (link->next != NULL)	link = link->next;
		tree = (BNode *)(link->next->node);
	}
	LinkKey *key = tree->key;
	while (key->next != NULL)	key = key->next;
	return key->key;
}
getMinKey和getMaxKey方法分別獲取指定B樹的最小關鍵之和最大關鍵值,這兩個方法的編寫依賴與B樹遍歷的知識,以及二叉搜尋樹中前驅後驅的概念,不瞭解的可以先去檢視一下我之前的部落格 二叉搜尋樹
基於以上方法,編寫一個實現B樹刪除關鍵字操作的程式 如下。
void deleteFromBTree(BTree *tree, int key)
{
	BNode *node = tree->root;
	if (node == NULL)	return;

	while (!node->leaf)
	{
		LinkKey *keys = node->key;
		// 尋找匹配key的node
		int which = 0;
		while (keys->next != NULL && key > keys->next->key)
		{
			which++;
			keys = keys->next;
		}

		BNode *next = getLinkNode(node->children, which);
		if (key == keys->next->key)
		{
			// 當key匹配到當前結點
			if (next->n > tree->t)
			{
				// 情況2.a
				keys->next->key = getMaxKey(next);
				key = keys->next->key;	// 從next結點開始刪除新key
			}
			else
			{
				next = getLinkNode(node->children, which + 1);
				if (next->n > tree->t)
				{
					// 情況2.b
					keys->next->key = getMinKey(next);
					key = keys->next->key;	// 從next結點開始刪除新key
				}
				else
				{
					// 情況2.c
					unionBNodes(node, which, which + 1);
					next = getLinkNode(node->children, which);
				}
			}
		}
		else
		{
			// 當key在子樹中,且子樹的度數不合要求
			if (next->n <= tree->t)
			{
				BNode *left = getLinkNode(node->children, which - 1);
				BNode *right = getLinkNode(node->children, which + 1);
				if (left != NULL && left->n > tree->t)
				{
					// 情況3.a
					moveKey(node, which - 1, which);		// 從左子樹移動key
				}
				else if (right != NULL && right->n > tree->t)
				{
					// 情況3.a
					moveKey(node, which + 1, which);	// 從右子樹移動key
				}
				else
				{
					// 情況3.b
					unionBNodes(node, which, which + 1);	// 與右子樹合併
				}
			}
		}

		node = next;
	}

	// 情況1
	LinkKey *keys = node->key;
	if (deleteLinkKey(keys, key))
	{
		node->n--;
	}
}
deleteFromBTree方法在當前結點不是葉子結點的時候迴圈工作,依次判斷情況1~3並執行相應操作。該方法針對情況2.a和2.b沒有使用遞迴實現,而是使用了迴圈。

BTree實現的完整程式碼可以參考我的github專案 資料結構與演算法
該專案中包含了我的部落格中已經介紹過的以及即將要介紹的資料結構與演算法的C語言實現,由於我的演算法之路還很漫長,所以該專案將會持續更新哦~