後臺開發學習筆記(八、B樹、B+樹)

終於到最後一棵樹了,最後一顆樹是B樹,B樹是爲磁盤或其他直接存取的輔助設備而設計的一種平衡搜索樹,在降低磁盤I/O操作數方面要更好一些,許多數據庫系統使用B樹或者B樹的變種來存儲信息。

8.1 磁盤介紹

爲什麼介紹磁盤,因爲這個B樹就是爲了存儲在磁盤中使用的,介紹磁盤可以更好的理解B樹。

8.1.1 磁盤構造

計算機中有兩種存儲介質,一種是主存(main memory)通常由硅存儲芯片組成,就是內存;還有基於磁盤的輔存(secondary storage),就是硬盤之類的。內存容量一般都比較小,硬盤大小會比內存的大小多幾個數量級,如果我們數據量過大,還有需要斷電不丟失,就需要存儲在硬盤中。

在這裏插入圖片描述
在這裏插入圖片描述
這樣就很清楚了,驅動器由一個或多個盤片(platter)組成,它們以一個固定的速度繞着一個共同的主軸旋轉。每個盤的表面覆蓋着一層可磁化的物質。驅動器通過磁臂末尾的磁頭來讀/寫盤片。磁臂可以將磁頭向主軸移近或移遠。當一個給定的磁頭處於靜止時,它下面經過的磁盤表面稱爲一個磁道。多個盤片增加的僅僅是磁盤的容量,而不影響性能。 (出自《算法導論》)
在這裏插入圖片描述

  1. 磁道
    什麼是磁道呢?每個盤片都在邏輯上有很多的同心圓,最外面的同心圓就是 0 磁道。我們將每個同心圓稱作磁道(注意,磁道只是邏輯結構,在盤面上並沒有真正的同心圓)。硬盤的磁道密度非常高,通常一面上就有上千個磁道。但是相鄰的磁道之間並不是緊挨着的,這是因爲磁化單元相隔太近會相互產生影響。

  2. 扇區
    那扇區又是什麼呢?扇區其實是很形象的,大家都見過摺疊的紙扇吧,紙扇打開後是半圓形或扇形的,不過這個扇形是由每個扇骨組合形成的。在磁盤上每個同心圓是磁道,從圓心向外呈放射狀地產生分割線(扇骨),將每個磁道等分爲若干弧段,每個弧段就是一個扇區。每個扇區的大小是固定的,爲 4K。扇區也是磁盤的最小存儲單位。

  3. 柱面
    柱面又是什麼呢?如果硬盤是由多個盤片組成的,每個盤面都被劃分爲數目相等的磁道,那麼所有盤片都會從外向內進行磁道編號,最外側的就是 0 磁道。具有相同編號的磁道會形成一個圓柱,這個圓柱就被稱作磁盤的柱面,如圖 所示

當磁盤驅動器執行讀/寫功能時。盤片裝在一個主軸上,並繞主軸高速旋轉,當磁道在讀/寫頭(又叫磁頭) 下通過時,就可以進行數據的讀 / 寫了。一般磁盤分爲固定頭盤(磁頭固定)和活動頭盤。固定頭盤的每一個磁道上都有獨立的磁頭,它是固定不動的,專門負責這一磁道上數據的讀/寫。

活動頭盤 (如上圖)的磁頭是可移動的。每一個盤面上只有一個磁頭(磁頭是雙向的,因此正反盤面都能讀寫)。它可以從該面的一個磁道移動到另一個磁道。所有磁頭都裝在同一個動臂上,因此不同盤面上的所有磁頭都是同時移動的(行動整齊劃一)。當盤片繞主軸旋轉的時候,磁頭與旋轉的盤片形成一個圓柱體。各個盤面上半徑相同的磁道組成了一個圓柱面,我們稱爲柱面 。因此,柱面的個數也就是盤面上的磁道數
參考博客https://www.cnblogs.com/sunsky303/p/11497448.html
http://c.biancheng.net/view/879.html
https://blog.csdn.net/guozuofeng/article/details/90369471

8.1.2 讀寫效率

磁盤上數據必須用一個三維地址唯一標示:柱面號(磁道)、盤面號、塊號(磁道上的扇區)。

讀/寫磁盤上某一指定數據需要下面3個步驟:
(1) 首先移動臂根據柱面號使磁頭移動到所需要的柱面上,這一過程被稱爲定位或查找 。
(2) 所有磁頭都定位到所有盤面的指定磁道上(磁頭都是雙向的)。這時根據盤面號來確定指定盤面上的磁道。(不是很清楚)
(3) 盤面確定以後,盤片開始旋轉,將指定塊號(扇區)的磁道段移動至磁頭下。
經過上面三個步驟,指定數據的存儲位置就被找到。這時就可以開始讀/寫操作了。

訪問某一具體信息,由3部分時間組成:
● 查找時間(seek time) Ts: 完成上述步驟(1)所需要的時間。這部分時間代價最高,最大可達到0.1s左右。
● 等待時間(latency time) Tl: 完成上述步驟(3)所需要的時間。由於盤片繞主軸旋轉速度很快,一般爲7200轉/分(電腦硬盤的性能指標之一, 家用的普通硬盤的轉速一般有5400rpm(筆記本)、7200rpm幾種)。因此一般旋轉一圈大約0.0083s。
● 傳輸時間(transmission time) Tt: 數據通過系統總線傳送到內存的時間,一般傳輸一個字節(byte)大概0.02us=2*10^(-8)s

爲了提高效率,文件系統中有做了數據緩存,等到一定數據要修改的時候,就可以一次性寫入到磁盤中,所以我們儘量的減少磁盤存取的次數。

磁盤讀取數據是以盤塊(block)爲基本單位的。位於同一盤塊中的所有數據都能被一次性全部讀取出來。而磁盤IO代價主要花費在查找時間Ts上。因此我們應該儘量將相關信息存放在同一盤塊,同一磁道中。或者至少放在同一柱面或相鄰柱面上,以求在讀/寫信息時儘量減少磁頭來回移動的次數,避免過多的查找時間Ts。

所以,在大規模數據存儲方面,大量數據存儲在外存磁盤中,而在外存磁盤中讀取/寫入塊(block)中某數據時,首先需要定位到磁盤中的某塊,如何有效地查找磁盤中的數據,需要一種合理高效的外存數據結構,就是下面所要重點闡述的B-tree結構。

8.2 B樹的定義

8.2.1 B樹的定義

一顆B樹T具有以下性質的有根樹(根爲T.root)

  1. 每個結點x有下面屬性:
    a,x.n,當前存儲在結點x中的關鍵字個數。
    b,x,n個關鍵字本身x.key1,x.key2,…。x.keyx.n,以非降序存放,使得x.key1≤x.key2≤…≤x.keyx.n
    c,x.leaf,一個布爾值,如果x是葉結點,則爲TRUE;如果x爲內部結點,則爲FALSE。

  2. 每個內部結點x還包含x.n+1個指向其孩子的指針x.c1,x.c2,…,x.cx.n+1。葉結點沒有孩子,所以它們的ci屬性沒有定義。

  3. 關鍵字x.keyi對存儲在各子樹中的關鍵字範圍加以分割:如果ki爲任意一個存儲在以x.ci爲根的子樹中的關鍵字,那麼
    ki≤x.key1≤k2≤x.key2≤…≤x.keyx.n≤kx.n+1

  4. 每個葉子結點具有相同深度,即樹的高度h.

  5. 每個結點所包含的關鍵字個數有上限和下限。用一個被稱爲B樹的最小度數的固定整數t≥2來表示這些界。
    a,除了根結點以外的每個結點必須至少有t-1個關鍵字。因此,除了根結點以外的每個內部結點至少有t個孩子。如果樹非空,根結點至少有一個關鍵字。
    b,每個結點至多可包含2t-1個關鍵字。因此,一個內部結點至多可有2t個孩子。當一個結點恰好有2t-1個關鍵字時,稱該節點是滿的。 (來自《算法導論》)

8.2.2 自己總結

算法導論寫的真複雜,本來不想寫的,但是還是需要一個專業的定義,然後我自己再來一個定義,上面的定義可以簡單的理解爲,一個結點x有n個關鍵字,關鍵字是按非降序存放的,並且在x結點中,還有n+1的指向孩子的結點,各個孩子結點也是按照非降序排列的,x結點可以是內部結點也可以是葉子結點,每個葉子節點都具有相同的深度。
下面是b樹的度比較重要,除了根結點以外,每個結點至少有t-1個關鍵字,並且至多有2t-1個關鍵字,如果達到2t-1個關鍵字就需要分裂。

講了這麼多文字,來個B樹的圖更直觀:
在這裏插入圖片描述

8.2.3 B樹的結構

typedef int Elemtype;
	
			
#define BTREE_ENTRY(name, type)			\
	struct name 						\
	{									\
		struct type 	**child;		\
		Elemtype 		*key; 			\
		int 			leaf;			\
		int 			num;			\
	}	
			
typedef struct bTree_node
{
	Elemtype data;							//結點數據
	BTREE_ENTRY(, bTree_node) bst;			//B樹結點信息
}_bTree_node;
		
		
typedef struct bTree
{
	struct bTree_node *root;				//指向根結點
	int  degree;							//B樹的度數
}_bTree;

從B樹的結點也看出是按B樹的定義實現的,num爲這個結點的關鍵字個數,leaf標記這個結點是否葉子結點,ley就是這個結點的關鍵字,child二重指針就是指向孩子的指針。B樹的根結點,有一個指向B樹的根結點的指針,還有一個degree表示B的度,這個度決定着B樹的結點的關鍵字個數爲degree-1≤num≤2*degree-1。

8.3 B樹的其他函數

8.3.1 B樹搜索

B樹的搜索根紅黑樹也差不多的,不過差別是紅黑樹是二叉,B樹是多叉,這個多叉的選擇是根據關鍵字的大小去尋找各自的子樹,遞歸查詢;因爲B樹有多個結點,一個結點有n個子結點,所以返回值需要返回結點

typedef struct bTree_position
{
	struct bTree_node *x;				//b的結點
	int  	i;							//結點中的第幾個元素
}_bTree_position;

/**
    * @brief  B樹的搜索
    * @param   p 輸出參數
    * @retval 
    */ 
    int bTree_search(struct bTree_node *node, Elemtype k, struct bTree_position *p)
	{
		int i = 0;
		assert(p);
	
		//循環判斷k在結點上是哪個位置
		while(i < node->bst.num && k > node->bst.key[i]) {
			i++;
		}

		if(i < node->bst.num && k == node->bst.key[i]) {
			//返回結點的指針和結點的第幾個元素
			p->x = node;
			p->i = i;
		} else if(node->bst.leaf)  {		//如果是葉子結點
			p->x = NULL;
			p->i = 0;
		} else {
			bTree_search(node->bst.child[i], k, p);
		}

		return 0;
	}

① 感覺還是先寫插入的比較好,不過都寫搜索了,就寫把,因爲這是B樹,一個結點有幾個關鍵字,所以返回值組織成一個結構體,這個結果體裏面有x結點的指針和x的關鍵字的下標,暫且這樣寫吧。
② 搜索的時候,傳入結點node,然後遍歷這個結點的關鍵字,如果k大於關鍵字,關鍵字需要往後走,如果不大,就是我們感興趣部分。
③ 首先判斷是否等於關鍵字,如果等於關鍵字的話,就是找到的這個值,返回。
④ 如果沒找到,也分爲兩種情況,一種是當前結點是葉子結點,這種情況就是說明沒有找到關鍵字,所以返回空。
⑤ 另一種情況是內部結點,內部結點的意思就是還有孩子結點,所以需要遞歸調用,往孩子結點繼續尋找。

8.3.2 B樹遍歷

B樹的遍歷比較簡單,就是遞歸加循環,結點到結點之間利用遞歸,一個結點利用循環,因爲一個結點有幾個關鍵字,所以需要循環遍歷。
代碼:

/**
		* @brief  B樹遍歷,遞歸遍歷
		* @param	
		* @retval 
		*/ 
		static void btree_printf(struct bTree_node *node)
		{	
			int i = 0;
			if(node == NULL)
				return ;

			//遍歷當前結點
		printf("keynum = %d is_leaf = %d\n", node->bst.num, node->bst.leaf);

			for(i=0; i<node->bst.num; i++) {
				printf("%c ", node->bst.key[i]);
			}

			printf("\n");
			for(i=0; i<=node->bst.num; i++) {
				btree_printf(node->bst.child[i]);
			}
			
		}

8.4 B樹的插入

8.4.1 創建結點

按照慣例,插入的時候都會先創建結點,這樣才符合我們的步驟

/**
    * @brief  B樹創建結點
    * @param   p 輸出參數
    * @retval 
    */ 
    struct bTree_node *bTree_creat_node(struct bTree *T, Elemtype k, int leaf)
	{
		//申請一個結點
		struct bTree_node *node = (struct bTree_node *)malloc(sizeof(struct bTree_node));
		assert(node);

		//填充數據
		node->data = 0;				//不知道data是怎麼用,以後分析一些具體使用B樹的實例應該就清楚了
		node->bst.num = 0;			//node結點個數
		node->bst.leaf = leaf;		//是否是葉子結點,1爲葉子節點,0爲非葉子節點
		node->bst.key = (Elemtype *)calloc(1, (2*T->degree-1)*sizeof(Elemtype));   //2t-1個關鍵字
		node->bst.child = (struct bTree_node **)calloc(1, (2*T->degree)*sizeof(struct bTree_node *));

		return node;
    }

B樹的結點比較複雜,因爲要申請2degree-1的關鍵字和2degree個指向孩子的指針

8.4.2 插入預熱

插入結點,還是按原來的步驟一個一個添加,這次我們添加26個英文字母,這個B樹我選度爲3,關鍵字個數爲2 * 3 - 1=5,孩子結點爲 2 * 3 = 6.

  1. 添加A,B,C,D, E
    在這裏插入圖片描述
    這個關鍵字個數最大是5,所以前面5個都是直接插入的

  2. 插入F
    插入F的時候,判斷到根結點爲5的時候,開始分裂
    在這裏插入圖片描述
    分裂的步驟:先申請一個結點作爲頭結點,這個結點爲s,原來的根結點作爲y,從第一個結點開始分裂,就形成上圖所示的分裂。
    然後繼續插入F,
    在這裏插入圖片描述
    這樣就符合要求,記住是先分裂再添加

  3. 插入G,H
    在這裏插入圖片描述

  4. 插入I
    遍歷到右邊結點的時候,發現是滿結點,分裂,這個分裂跟結點分裂不一樣,這個直接調用分裂函數即可
    在這裏插入圖片描述
    分裂的結點是右邊結點的第3個,調用分裂函數,就可以得出如圖所示的結果,然後插入I
    在這裏插入圖片描述

  5. 插入J K
    在這裏插入圖片描述

  6. 插入L
    這次又到了滿結點,又進行分裂
    在這裏插入圖片描述
    繼續插入L
    在這裏插入圖片描述

  7. 插入M N
    在這裏插入圖片描述

  8. 插入O
    分裂
    在這裏插入圖片描述
    插入
    在這裏插入圖片描述

  9. 插入P Q
    在這裏插入圖片描述

  10. 插入R

分裂
在這裏插入圖片描述
插入
在這裏插入圖片描述

  1. 插入 S
    插入S結點,根結點是滿的,所以要分裂,這個很容易不小心出錯,我也是出錯了,之後再回來改正的
    在這裏插入圖片描述
    插入S
    在這裏插入圖片描述

12.插入T
在這裏插入圖片描述

  1. 插入U V W
    分裂
    在這裏插入圖片描述
    插入U V W
    在這裏插入圖片描述

  2. 插入X Y Z
    分裂
    在這裏插入圖片描述
    插入
    在這裏插入圖片描述

至此26個字母的B樹就插入完成。

8.4.3 插入結點

通過上的插入順序,應該瞭解到B樹插入的過程了,插入過程是先判斷是否爲滿結點,如果是滿的結點,先需要分裂,這個分裂我們下節再細講,這裏就先調用一個空函數,表示已經分裂了,如果不是滿結點,就進行插入,但是這個分裂也分爲兩種情況,一種是根結點,一種是其他結點,寫代碼的時候需要注意

代碼:

/**
	* @brief  B樹插入結點
	* @param   輸出參數
	* @retval 
	*/ 
	static int btree_insert_nonfull(struct bTree *T, struct bTree_node *node, Elemtype k) 
	{
		int i = node->bst.num - 1;
		//這個纔是真正的插入函數

		if(node->bst.leaf)  {		
			//如果是葉子結點,就可以插入了

			//循環比較關鍵字,看插入哪個位置
			while(i>=0 && k<node->bst.key[i]) {
				node->bst.key[i+1] = node->bst.key[i];  	
				//能到這一步插入了,就都不是滿結點,不需要考慮溢出問題
				i--;
			}

			//以i爲分界,往後移動,留下i作爲新結點的位置
			node->bst.key[i+1] = k;
			node->bst.num++;
		}else { 					//不是葉子結點
			//循環比較關鍵字,看插入哪個位置
			while(i>=0 && k<node->bst.key[i]) i--;
		
			//判斷child[i]指向的子結點是否是滿的
			if(node->bst.child[i+1]->bst.num == T->degree*2-1) {
				//分裂
				btree_split_child(T, node, i+1);
				//分裂完成之後,再判斷一下新添加到x結點的i+1的值和k比較
				if(k > node->bst.key[i+1]) i++;
			}

			btree_insert_nonfull(T, node->bst.child[i+1], k);
		}

		return 0;
	}

/**
    * @brief  B樹插入結點
    * @param   輸出參數
    * @retval 
    */ 
    int bTree_insert(struct bTree *T, Elemtype k)
	{
		//插入結點的時候,要先判斷是否是滿結點,判斷滿結點也是分兩種情況,一種是根結點,一種是其他結點
		
		//根結點爲滿結點的時候
		if(T->root->bst.num == T->degree*2-1) {
			//分裂根結點

			//創建結點x
			struct bTree_node *x = bTree_creat_node(T, 0, 0);	
			x->bst.child[0] = T->root;
			T->root = x;
			 
			btree_split_child(T, x, 0);

			int i = 0;
			if(k > x->bst.key[0]) i++;
			btree_insert_nonfull(T, x->bst.child[i], k);
		} else {
			btree_insert_nonfull(T, T->root, k); 
		}

		return 0;
	}

這個插入結點不是完整的代碼,還有分裂需要補上,下節補。

8.4.4 分裂結點

B樹中插入一個關鍵字要比二叉搜索樹中插入一個關鍵字複雜的多,不能想二叉搜索樹那樣,尋找到要插入的位置的時候,直接創建一個新的加點,然後插入;B樹的插入,是將一個新的關鍵字插入到一個已經存在的葉子結點上。由於不能插入到一個滿的葉子結點,所以引入了一個操作,叫分裂;分裂是指將一個滿的結點y(2degree-1個關鍵字)按其中間關鍵字 y.keyi分裂爲兩個各包含degree-1個關鍵字的結點,中間關鍵字被提升到y的父節點,以標識兩顆新樹的劃分點。但是如果y的父結點也是滿的,就必須在插入新的關鍵字之前就進行分裂。

  1. 分裂其他結點
    其他結點的分裂簡單一點,沒有那麼難,需要知道分裂結點的父節點x,和要分裂結點指向的x的下標i,通過x的下標i獲取到要分裂的結點y, 然後申請一個新的結點z,然後把y中的一部分數據拷貝到z中,把y中的中間關鍵字提升到父節點x中,
/**
	* @brief  B樹分裂結點
	* @param   要分裂的結點的父節點x,和指向要分裂結點的x的下標i
	* @retval 
	*/ 
	static int btree_split_child(struct bTree *T, struct bTree_node *x, int i)
    {
		int j = 0;
		//獲取到要分裂的結點的指針
		struct bTree_node *y = x->bst.child[i];

		//創建一個新的結點
		struct bTree_node *z = bTree_creat_node(T, 0, y->bst.leaf);		

		//拷貝y的一半關鍵字給z
		for(j = 0; j<T->degree-1; j++)
		{
			z->bst.key[j] = y->bst.key[T->degree+j];   
		}

		//判斷是否是是葉子結點,如果不是,拷貝指針
		if(!y->bst.leaf)
		{
			for(j = 0; j<=T->degree-1; j++)
			{
				z->bst.child[j] = y->bst.child[T->degree+j];   
			}
		}

		//更新y,z的num
		y->bst.num = T->degree-1;
		z->bst.num = T->degree-1;

		//移動x的結點,留下i的空位,然後插入
		for(j=x->bst.num; j>=i; j--)
		{
			x->bst.key[j+1] = x->bst.key[j];
		}

		x->bst.key[i] = y->bst.key[T->degree-1];

		//移動x的孩子結點,添加指針z的指針
		for(j=x->bst.num-1; j>=i; j--)
		{
			x->bst.child[j+1] = x->bst.child[j];
		}

		x->bst.child[i+1] = z;
		x->bst.num += 1;

		return 0;
	}

其他結點分裂,只要按照參數傳參即可,這個函數內部已經實現了

  1. 分裂根結點
    分裂根結點不一樣的地方是要重新申請一個根結點做爲x,原來的根結點作爲y,x分裂的位置爲1,一些細節還是需要注意

代碼如下:

//分裂根結點

//創建結點x
struct bTree_node *x = bTree_creat_node(T, 0, 0);	
x->bst.child[0] = T->root;
T->root = x;
 
btree_split_child(T, x, 0);

int i = 0;
if(k > x->bst.key[0]) i++;
btree_insert_nonfull(T, x->bst.child[i], k);

分裂根結點上面也有了,這裏在補補。

8.5 B樹的刪除

樹的刪除都是比較麻煩的,因爲刪除樹的結點的時候,需要考慮到全面,不像插入的時候,只考慮當前,但是有插入就有刪除,這是必須的,所以刪除的操作也必須要熟悉。

8.5.1 釋放結點

還是先從簡答的說起,有沒有發現,在其他樹刪除的時候,都沒有這個釋放結點,那是因爲其他樹的時候結點都這麼複雜,只要free一次就足夠了,但是B樹是多子樹,每一個結點中升申請了一塊內存存放關鍵字,還有申請了一部分空間存放孩子結點的指針,這樣都釋放結點的時候都應該得到釋放。

代碼:

/**
	* @brief  B樹釋放結點
	* @param   p 輸出參數
	* @retval 
	*/ 
	int bTree_destroy_node(struct bTree_node *node)
	{
		assert(node);

		//釋放關鍵字內存
		free(node->bst.key);

		//釋放指向孩子結點指針內存
		free(node->bst.child);

		//釋放結點
		free(node);

		return 0;
	}

很簡單,依次釋放內存

8.5.2 刪除預熱

刪除的時候,還是按照老辦法,一個一個刪除,感受一下刪除的情況。

  1. 刪除z
    在這裏插入圖片描述

刪除z是最簡單的,也符合刪除的第一種情況:要刪除的元素在葉子節點,所以只需要直接刪除。這時候就有人眼尖看到了如果刪除A的話呢,A也是葉子節點,是不是直接刪除,這個刪除A的等下就講,先簡單然複雜。
在這裏插入圖片描述
2. 刪除U
刪除U是符合第2種情況,但是第二種情況也分爲了3種小情況,刪除U是符合2.2的情況,因爲後於U的孩子結點的關鍵字大於T->degree-1,所以按2.2處理。
在這裏插入圖片描述
3. 刪除O
刪除O符合2.3這種情況,左右兩邊的孩子的關鍵字都等於T->degee-1,所以需要用2.3的歸併再刪除的方式刪除。
歸併結果:
在這裏插入圖片描述
然後刪除O:
在這裏插入圖片描述

  1. 刪除A
    這第3種情況算法導論說的我也不是很明白,不過通過程序倒推回來,還是可以理解的,如果理解不對的地方可以在評論中指出,我好改正。
    我的理解是這樣的,刪除z的情況符合第1個條件,是葉子結點,並且關鍵字大於T->degree-1,但是如果要刪除A呢,這個也是葉子結點,明顯跟算法導論說的不太一樣,所以我在條件1加了一個條件,以便區別。
    就以刪除A舉例,A的結點和右結點都只有T->degree-1個關鍵字,所以不能直接刪除,這時候就要利用在遞歸查找過程中,先判斷孩子結點的情況,先判斷孩子結點的關鍵字是否等於T->degree-1,如果不是,就獲得這個孩子結點的指針,然後遞歸調用。
    如果是的話,就需要分類討論:

  2. 如果這個孩子結點的兄弟結點的關鍵字大於T->degree個關鍵字,則將x中的某一個關鍵字將至孩子結點中,然後再從相鄰的結點中提取一個關鍵字,升到父節點中,這樣孩子結點的關鍵字就等於T->degree個了,可以繼續遞歸調用

  3. 如果這個孩子結點以及所有相鄰的兄弟結點都只包含T->degree-1個關鍵字,則將這個孩子結點和兄弟結點進行合併,然後在把父節點中的一個關鍵字移到新合併的結點中,這個新的關鍵字就是新結點的中間關鍵字。

寫了這麼多,不知道有沒有了解,不瞭解的研究下代碼,就會理解了,代碼的邏輯很清晰,不像描述成文字這麼難。

刪除A步驟:
剛開始遞歸,判斷I的左孩子是等於T->degree-1個關鍵字,符合3.1情況,開始移位,移位後結果:
在這裏插入圖片描述
進入CFI結點,繼續處理,這次左邊孩子結點不存在,右邊的孩子結點(DE)滿足等於T->degree-1個關鍵字,所以符合3.2情況
首先合併:
在這裏插入圖片描述
然後刪除:
在這裏插入圖片描述
刪除A完成。

8.5.3 刪除遇到的情況

  1. 如果關鍵字k在結點x中,並且x是葉結點,x的結點的關鍵字大於T->degree則從x中刪除k。

  2. 如果關鍵字k在結點x中,並且x是內部結點,則要判斷如下
    2.1 x結點中前於k的子結點y,至少包含T->degree個關鍵字,則找出k在以y爲根的子樹中的前驅k’,遞歸的刪除k’,並在x中用k’代替k。
    2.2 對稱地,如果y有小於T->degree個關鍵字,則檢查結點x中後於k的子結點z。如果z至少有T->degree個關鍵字,則找出k在以z爲根的子樹中後繼k’,遞歸的刪除k’,並在x中用k’替換k。
    2.3 否則,如果y和z都只含有T->degree個關鍵字,則將k和z全部合併進y中,這樣x就失去了k和指向z的指針,並且y現在包含2T->degree-1個關鍵字,然後釋放z,並遞歸的地從y中刪除k。

3.孩子結點關鍵字等於T->degree-1的時候
3.1 如果這個孩子結點的兄弟結點的關鍵字大於T->degree個關鍵字,則將x中的某一個關鍵字將至孩子結點中,然後再從相鄰的結點中提取一個關鍵字,升到父節點中,這樣孩子結點的關鍵字就等於T->degree個了,可以繼續遞歸調用。
3.2 如果這個孩子結點以及所有相鄰的兄弟結點都只包含T->degree-1個關鍵字,則將這個孩子結點和兄弟結點進行合併,然後在把父節點中的一個關鍵字移到新合併的結點中,這個新的關鍵字就是新結點的中間關鍵字。

8.5.4 合併結點

B樹的刪除,有點借位的思想,如果不夠就往其他孩子結點借,如果左右孩子都不夠,就把左右孩子合併成一起,再刪除,這次說的就是合併。
代碼:

/**
	* @brief  歸併結點
	* @param   
	* @retval 
	*/ 
	static int btree_merge(struct bTree *T, struct bTree_node *node, int idx)
    {
		//歸併的只要思路,就是把node->bst.child[idx] 和 node->bst.child[idx+1]歸併  ,然後插入node->key[idx]作爲中間關鍵字

		int i=0;
		struct bTree_node *left = node->bst.child[idx];
		struct bTree_node *right = node->bst.child[idx+1];

		//data merge
		left->bst.key[T->degree-1] = node->bst.key[idx];			//node->key[idx]作爲中間關鍵字
		for(i=0; i<T->degree-1; i++)
		{
			left->bst.key[T->degree+i] = right->bst.key[i];
		}
		if(!left->bst.leaf)
		{
			for(i=0; i<T->degree; i++)
			{
				left->bst.child[T->degree+i] = right->bst.child[i];
			}
		}
	
		left->bst.num += T->degree;    //還有一個key

		bTree_destroy_node(right);

		//node 刪除node[idx],從後往前移 
		for(i=idx+1; i<node->bst.num; i++)
		{
			//這個拷貝的時候需要很注意
			node->bst.key[i-1] = node->bst.key[i];
			node->bst.child[i] = node->bst.child[i+1];
		}

		node->bst.child[node->bst.num] = NULL;
		node->bst.num -= 1;

		if(node->bst.num == 0) {
			T->root = left;
			bTree_destroy_node(node);
		}
	
		return 0;
	}

8.5.5 結點刪除

結點刪除就是按照8.5.3中那幾個情況進行處理的,代碼判斷邏輯比較清晰明瞭,好好看看就明白了。
代碼:

/**
	* @brief  B樹刪除結點,遞歸調用
	* @param   輸出參數
	* @retval 
	*/ 
	static void btree_delete_key(struct bTree *T, struct bTree_node *node, Elemtype k) 
	{
		int idx = 0, i;

		if(node == NULL) return ;
		
		//遍歷查找k是否在當前結點node中,這個是從0下標找起
		while(idx<node->bst.num && k > node->bst.key[idx]) {
			idx++;
		}
		printf("btree_delete_key %d %d %c %c\n", node->bst.num, node->bst.leaf, k, node->bst.key[idx]);
		if(idx < node->bst.num && k == node->bst.key[idx]) {
			//如果在當前結點上
			
			if(node->bst.leaf) {
				//判斷是否是葉子結點,如果是葉子節點就直接刪除
				//符合第1種情況
				printf("葉子點\n");
				//刪除結點,把後面的結點往前移,這個是葉子結點,不用移動指針
				for(i=idx; i<node->bst.num-1; i++) {
					node->bst.key[i] = node->bst.key[i+1];
				}

				node->bst.key[node->bst.num-1] = 0;
				node->bst.num -= 1;
				
				if(node->bst.num == 0) {	//如果num=0,說明只剩根結點了
					bTree_destroy_node(node);				//釋放根結點
					T->root = NULL;
				}
				
				return ;
			} 
			//後面的都不是葉子結點的
			else if(node->bst.child[idx]->bst.num >= T->degree)   {     //前於k的子結點y,至少包含T->degree各個關鍵字 
				//找出k在以**y爲根的子樹中的前驅k',遞歸的刪除k'**,並在x中用k'代替k。
				printf("前驅\n");
				struct bTree_node *left = node->bst.child[idx];

				//用k'替換k
				node->bst.key[idx] = left->bst.key[node->bst.num-1];

				//遞歸替換
				btree_delete_key(T, left, left->bst.key[node->bst.num-1]); 
			}	
			else if(node->bst.child[idx+1]->bst.num >= T->degree)   {     //後於k的子結點z,至少包含T->degree各個關鍵字 
				//找出k在以**z爲根的子樹中的後驅k',遞歸的刪除k'**,並在x中用k'代替k。
				printf("後驅\n");
				struct bTree_node *right = node->bst.child[idx+1];

				//用k'替換k
				node->bst.key[idx] = right->bst.key[0];

				//遞歸替換
				btree_delete_key(T, right, right->bst.key[0]); 
			}	
			else  { 	//y和z都包含T->degree-1個關鍵字,需要合併,然後在從y中遞歸刪除k
				printf("其他\n");
				btree_merge(T, node, idx);						//合併
				btree_delete_key(T, node->bst.child[idx], k);	//遞歸刪除
			}
		} else {
			//如果不在當前結點,就往孩子結點找
			struct bTree_node *child = node->bst.child[idx];
			
			if(child == NULL)  return ;

			if(child->bst.num == T->degree - 1) {    //判斷孩子結點的個數是不是小於最小值,如果是就要特殊處理
				struct bTree_node *left = NULL;
				struct bTree_node *right = NULL;
				
				if (idx - 1 >= 0)
					left = node->bst.child[idx-1];
				if (idx + 1 <= node->bst.num) 
					right = node->bst.child[idx+1];

				if((left && left->bst.num >= T->degree) ||
					(right && right->bst.num >= T->degree))  {
						
						int richR = 0;
						if(right) richR = 1;
						if (left && right) richR = (right->bst.num > left->bst.num) ? 1 : 0;
						printf("右孩子\n");
						if(right && right->bst.num >= T->degree && richR) {   //找到右邊兄弟替補
							//先把node結點的數據往child裏放,放到child最後一個key中
							child->bst.key[child->bst.num] = node->bst.key[idx];
							//把right的第一個孩子指針掛接到child的最後一個指針上
							child->bst.child[child->bst.num+1] = right->bst.child[0];
							child->bst.num++;

							//把右孩子結點往父節點提
							node->bst.key[idx] = right->bst.key[0];

							//右孩子結點往前移
							for(i=0; i<right->bst.num-1; i++) {
								right->bst.key[i] = right->bst.key[i+1];
								right->bst.child[i] = right->bst.child[i+1];
							}

							right->bst.child[right->bst.num-1] = right->bst.child[right->bst.num];
							right->bst.child[right->bst.num] = NULL;
							right->bst.num--;
						} else{   //找到左邊兄弟替補
							//左邊跟右邊不是對稱關係,操作有點不一樣,不過大體都一樣的
							printf("左孩子\n");
							//先移動child的結點,空出第一個結點,等到父節點node插入元素
							for(i=child->bst.num; i>0; i--) {
								child->bst.key[i] = child->bst.key[i-1];
								child->bst.child[i+1] = child->bst.child[i];
							}

							child->bst.child[1] = child->bst.child[0];	
							//把左邊孩子最後一個孩子指針掛接到child的0
							child->bst.child[0] = left->bst.child[left->bst.num];
							//把父結點的關鍵字賦值給left的0下標
							child->bst.key[0] = node->bst.key[idx-1];
							child->bst.num++;

							//把左孩子的最後一個元素賦值給父節點的元素
							node->bst.key[idx-1] = left->bst.key[left->bst.num];
							left->bst.child[left->bst.num] = NULL;
							left->bst.num--;
						}
				} else if((!left || left->bst.num == T->degree-1) && //左右兄弟都小於T->degree-1的時候
					(!right || right->bst.num == T->degree-1)) {

					if(left && left->bst.num == T->degree-1) {   //左邊兄弟存在,並且滿足T->degree-1
						btree_merge(T, node, idx-1);	
						child = left;
					} else if(right && right->bst.num == T->degree-1) {  //右邊兄弟存在,並且滿足T->degree-1
						btree_merge(T, node, idx);
						btree_printf(T->root);
						printf("\n");
					}
				}
			}

			//如果不是,直接調用刪除函數
			btree_delete_key(T, child, k);
		}		
	}

這次B樹寫的跟實際運用,還是很大差別的,以後有時間研究一下實際工程中的應用,再寫一篇真正實用的,這次就相當預習了。

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