1. 程式人生 > >資料結構與演算法:B樹(B-Tree)定義及搜尋、插入、刪除基本操作

資料結構與演算法:B樹(B-Tree)定義及搜尋、插入、刪除基本操作

B樹(B-Tree)

在介紹什麼是B樹(B-Tree)之前,先看看為什麼存在B樹結構?
B樹B-Tree)是為磁碟或者其他輔助儲存裝置而設計的一種平衡搜尋樹,如有的資料庫系統使用B樹或者B樹的變種來儲存資訊。B樹的節點可以有很多孩子,從數個到數千個,不同於一般的二叉樹(每個節點最多隻有兩個孩子)。

下面以磁碟為例說明B樹的設計目的。下圖是一個典型的磁碟驅動器:
在這裡插入圖片描述
磁碟通過碟片的旋轉以及磁臂的移動定位然後讀/寫資料。目前,商用磁碟的旋轉速度是5400~15000轉/分鐘(RPM),以7200RPM為例,碟片旋轉一圈需要8.33ms,比記憶體的常見存取時間50ns要高出5個數量級,同時磁臂移動也需要時間,所以即使按平均只需要等待半圈計算,磁碟存取時間與記憶體存取時間的差距仍是巨大的。因此,為提高應用效率,提高資料處理速度,需要儘可能降低磁碟儲存次數

。在一棵樹中檢查任意一個節點都需要一次磁碟訪問,因此B樹的設計避免了大量的磁碟訪問。

B樹的定義

一棵B樹(B-Tree)是有如下性質的樹:

  1. 每個節點 x 有下面屬相:
    a. x.n,節點 x 中的關鍵字個數;
    b. n個關鍵字 x.key1, x.key2, x.key3, … , x.keyn 以非降序排序,即 x.key1 <= x.key2 <= x.key3 <= … <= x.keyn ;
    c. x.leaf,一個布林值,表示 x 是否為葉結點,是則為True,否則為False。
  2. 每個內部節點 x 最多包含 x.n + 1 個孩子(類似一條直線上n個點將直線分成 n+1 段),x.c(i) 為指向其第i個孩子的指標,葉結點沒有孩子,所以葉結點的x.c(i)沒有定義。
  3. 關鍵字x.key(i) 對儲存在各子樹中的關鍵字範圍加以分割(同樣的,類似直線上的點將直線分段):如果 k(i) 為 x.c(i) 對應的的子樹中的關鍵字,則:
    k1 <= x.key1 <= k2 <= x.key2 <= … <= x.keyn <= k(n+1)
    即,對於節點關鍵字x.key,左邊子樹的關鍵字不大於key,右邊子樹的關鍵字不小於key。
  4. 每個葉結點具有相同的深度,即樹的高度h。
  5. 每個葉結點包含的關鍵字個數有上界和下界。用一個被稱為B樹的最小度數minmum degree)的固定整數 t >= 2 來表示這個界:
    a
    . 除根節點以外的每個內部節點至少有 t 個孩子,除根節點以外的每個結點至少有 t-1 個關鍵字。如果樹非空,根結點至少有一個關鍵字。
    b. 每個內部結點最多有 2t 個孩子, 最多有 2t-1 個關鍵字。如果一個節點恰好有 2t-1 個關鍵字,則稱該結點是滿的full)。

下面就是一棵B樹:
在這裡插入圖片描述

B樹的高度

B樹上大部分操作所需磁碟存取次數與B樹的高度成正比。對於B樹的高度,有如下定理:
如果 n >= 1,那麼對任意一棵包含n個關鍵字、高度為h、最小度數t >= 2 的B樹T,有 在這裡插入圖片描述
所以,每個結點包含的關鍵字個數越多,B樹的高度越小,從而磁碟存取次數越少。

B樹上的基本操作

搜尋B樹

搜尋一棵B樹與搜尋二叉查詢樹類似,只是在每個節點所做的不是二叉兩路分支選擇,而是根據結點的孩子數做多路分支選擇。B樹搜尋虛擬碼如下:

B-Tree_Search(x, k)
	i = 1
	//找出最小下標 i ,使得 x.key[i] >= k
	while i <= x.n && x.key[i] < k                  
		i = i + 1
	//檢查是否找到該關鍵字,找到則返回,否則後面結束此次查詢
	if i <= x.n && k == x.key[i]
		return (x, i)
	else if x.leaf
		return null
	else DISK-READ(x, c[i])
		return B-Tree_Search(x.c[i], k)

B-Tree_Search(x, k) 的輸入是一個指向某(子)樹根結點x的指標,以及待搜尋關鍵字k,返回結點 y 以及使得 y.key[i] == k 的下標 i 組成的有序對 (y, i) ,否則返回null。

插入關鍵字

向一棵與二叉查詢樹插入新結點一樣,需要查詢插入新關鍵字的葉結點的位置。如果待插入的關鍵字已經存在,則返回該關鍵字位置 (x, i),不用再插入。與二叉查詢樹不同的是,B樹的插入不能隨便新建葉結點,否則會導致違反B樹性質,所以在已有葉結點中插入。但是如果插入葉結點 y 是滿的(full),則需要按其中間關鍵字y.keyty.key_t將 y 分裂split)兩個各加粗樣式含 t-1 個關鍵字的非滿結點(滿結點的關鍵字個數為 2t-1 ),中間關鍵字y.keyty.key_t被提升到 y 的父結點,以標識兩棵新樹的劃分點。但是如果 y 的父結點也是滿的,則其父結點也需要分裂,以此類推,最終滿結點的分裂會沿著樹向上傳播。

上面過程可能需要一下一上兩個操作過程:1.自上而下查詢插入葉結點位置;2.自下而上分裂滿結點。可以對該過程稍作修改,從樹根到葉結點這個單程向下過程中將關鍵字插入B樹中。為此,不是等到找出插入過程中實際要分裂的滿結點時才做分裂,而是自上而下查詢插入位置時,就分裂沿途遇到的每個滿結點(包括葉結點),這樣,當分裂一個滿結點 y 時,可以保證它的父結點不是滿的。

分裂一個 t = 4 的結點 x 示意圖如下:
在這裡插入圖片描述
分裂結點虛擬碼如下:

//分裂x結點的第i個孩子
B-Tree-Split-Child(x, i)
	y = x.ci
	//分配新節點z
	z = ALLOCATE-NODE()
	z.leaf = y.leaf
	z.n = t - 1
	//使用y後半部分的關鍵字初始化z的關鍵字
	for j=1 to (t-1)
		z.key[j] = y.key[j+t]
	y.n = t - 1
	//將x中i後面的所有指向孩子的指標向後移一位
	for j=(x.n + 1) downto (i+1)
		x.c[j+1] = x.c[j]
	//x的第(i+1)個孩子為新結點z
	x.c[i+1] = z
	//將x中i後面的所有關鍵字向後移一位
	for j=x.n downto i
		x.key[j+1] = x.key[j]
	//將y的中間關鍵字y.key[t]向上提為父結點x的第i個關鍵字
	x.key[i] = y.key[t]
	x.n = x.n + 1
	//寫磁碟
	DISK-WRITE(x)
	DISK-WRITE(y)
	DISK-WRITE(z)

插入虛擬碼如下:

//在B樹T中插入關鍵字k
B-Tree-Insert(T, k)
	r = T.root
	//如果根結點r是滿的,需要向上新提一個根結點
	if r.n == 2t - 1
		s = ALLOCATE-NODE()
		T.root = s
		s.leaf = False
		s.n = 0
		s.c[1] = r
		B-Tree-Split-Child(s, 1)
		//向以非滿結點s為根的樹中插入關鍵字k
		B-Tree-Insert-NonFull(s, k)
	else
		B-Tree-Insert-NonFull(r, k)

向以非滿結點x為根的樹中插入關鍵字k的虛擬碼如下:

//向以非滿結點x為根的樹中插入關鍵字k
B-Tree-Insert-NonFull(x, k)
	i = x.n
	//葉結點,直接在該結點插入
	if x.leaf
		while i >= 1 && k < x.key[i]
			x.key[i+1] = x.key[i]
			i = i - 1
		x.key[i+1] = k
		x.n = x.n + 1
		DISK-WRITE(x)
	//內部結點,需要找到插入的葉結點位置
	else
		while i >= 1 && k < x.key[i]
			i = i  - 1
		i = i + 1
		DISK-READ(x.c[i])
		if x.c[i].n == (2t-1)
			B-Tree-Split-Child(x, i)
			if k > x.key[i]
				i = i + 1
		B-Tree-Insert-NonFull(x.c[i], k)

刪除關鍵字

B樹上的刪除操作與插入操作類似,但是略微複雜一點,因為可以從任意一個結點刪除一個關鍵字,而不僅僅是葉結點,而且當從一個內部結點刪除一個關鍵字時,還需要重新安排這個結點的孩子。與插入操作一樣,必須防止因刪除操作而導致樹的結構違反B樹性質。就像插入操作必須保證結點關鍵字不會因為插入新關鍵字而太多一樣,刪除操作也必須保證結點關鍵字不會因為刪除關鍵字而太少(根結點除外,因為它允許關鍵字個數比最少關鍵字數 t-1 還少)。因此與插入需要分裂結點類似,當從一個只有最少關鍵字個數的非根結點中刪除關鍵字後,需要從其父結點把一個關鍵字移到該子結點中,與分裂需要向上回溯一樣,下降關鍵字到子結點的操作可能也需要回溯。

與插入類似,刪除操作也可以在第一趟下降過程中,處理沿途遇到的所有關鍵字個數為最少關鍵字數(t-1)的結點(根節點除外),將其父結點的一個關鍵字下降到該子結點,而不是刪除關鍵字後需要下降關鍵字時才從其父結點下降,這樣可以保證下降關鍵字到子結點後,父結點的關鍵字數不會少於最少關鍵字數(t-1),因此不用向上回溯。

下面介紹刪除操作如何工作,從B樹中刪除關鍵字分為以下幾種情況:

  1. 如果關鍵字 k 在結點 x 中,且 x 是葉結點,則直接從x中刪除k即可;
  2. 如果關鍵字 k 在結點 x 中,且 x 是內部結點,則做以下操作:
    a. 如果結點 x 中前於 k 的子節點 y 至少包含 t 個關鍵字,則找出 k 在以 y 為根的子樹中的前驅 k’(子樹中最“大”的關鍵字)。遞迴的刪除 k’,並在 x 中用 k’ 代替 k 。
    b. 對稱的,如果 y 有少於 t 個關鍵字(t-1個),則檢查結點 x 中後於 k 的子結點 z,如果 z 至少有 t 個關鍵字,則找出 k 在以 z 為根的子樹中的後繼 k’(子樹中最“小”的關鍵字)。遞迴的刪除 k’,並在 x 中用 k’ 代替 k 。
    c. 否則,如果 y 和 z 都只含有(t-1)個關鍵字,則將k和z的全部合併進 y ,這樣 x 就失去了 k 和指向 z 的指標,並且 y 現在包含(2t-1)個關鍵字。然後釋放 z 並遞迴從 y 中刪除 k 。

由於一棵B樹中的大部分關鍵字都在葉結點中,所以在實際中,刪除操作經常是從葉結點刪除關鍵字。

最後,推薦一個B樹的視覺化網站,方便對B樹的理解。

以上就是二叉樹的介紹,如有不對的地方,感謝指正!

參考《演算法導論》