1. 程式人生 > >深入理解二叉搜尋樹(BST)

深入理解二叉搜尋樹(BST)

一棵二叉搜尋樹(BST)是以一棵二叉樹來組織的,可以用連結串列資料結構來表示,其中,每一個結點就是一個物件,一般地,包含資料內容key和指向孩子(也可能是父母)的指標屬性。如果某個孩子結點不存在,其指標屬性值為空(NIL)。
二叉搜尋樹中的關鍵字key的儲存方式總是滿足二叉搜尋樹的性質:
設x是二叉搜尋樹中的一個結點。如果y是x左子樹中的一個結點,那麼會有y.key<=x.key;如果y是x右子樹中的一個節點,那麼有y.key>=x.key。
二叉搜尋樹查詢:
顧名思義,二叉搜尋樹很多時候用來進行資料查詢。這個過程從樹的根結點開始,沿著一條簡單路徑一直向下,直到找到資料或者得到NIL值。 如下圖所示: 由圖可以看出,對於遇到的每個結點x,都會比較x.key與k的大小,如果相等,就終止查詢,否則,決定是繼續往左子樹還是右子樹查詢。因此,整個查詢過程就是從根節點開始一直向下的一條路徑,若假設樹的高度是h,那麼查詢過程的時間複雜度就是O(h)。
BST查詢的遞迴演算法與非遞迴演算法虛擬碼分別如下:
//遞迴實現
Tree_Search(x, k):
if x == NIL or x.key == k :
	return x
if k < x.key
	return Tree_Search(x.left, k)
else return Tree_Search(x.right, k)
//非遞迴迭代實現
Tree_Search(x, k) :
while x!=NIL and k!=x.key:
	if k < x.key
		x = x.left
	else x = x.right
return x
一般來說,迭代方式的效率比遞迴方式高很多。

前驅和後繼:
對於給定的一棵二叉搜尋樹,如果所有結點的key均不相同,那麼結點x的前驅是指小於x.key的最大關鍵字的結點;而一個結點x的後繼是指大於x.key的最小關鍵字的結點。
現在,我們考慮如何求解一個結點x的後繼,(求前驅也類似,對稱的結構):
對於結點x,如果其右子樹不為空,那麼x的後繼一定是其右子樹的最左邊的結點。而如果x的右子樹為空,並且有一個後繼,那麼其後繼必然是x的最底層的祖先,並且後繼的左孩子也是x的一個祖先,因此,為了找到這樣的後繼結點,只需要從x開始沿著樹向上移動,直到遇到一個結點,這個結點是它的雙親的左孩子。(例如,在上圖的例子中,結點12的後繼結點是16.)
給出求後繼結點的虛擬碼:
Tree_Successor(x):
if x.right != NIL
	return Tree_MinNode(x.right)
y = x.p
while y!=NIL and x == y.right
	x = y
	y = y.p
return y
Tree_MinNode(x):
while x.left != NIL
	x = x.left
return x
BST插入
BST的插入過程非常簡單,很類似與二叉樹搜尋樹的查詢過程。當需要插入一個新結點時,從根節點開始,迭代或者遞歸向下移動,直到遇到一個空的指標NIL,需要插入的值即被儲存在該結點位置。這裡給出迭代插入演算法,遞迴方式的比較簡單。
Tree_Insert(T, z):
y = NIL
x = T.root
while x != NIL
	y = x
	if  z.key < x.key
		x = x.left
	else x = x.right
z.p = y
if y == NIL
	T.root = z
else if z.key < y.key
	y.left = z
else y.right = z
下圖給出插入結點17的示意圖: 同其他搜尋樹類似於,二叉搜尋樹(BST)的插入操作的時間複雜度為O(h). BST刪除
二叉搜尋樹的結點刪除比插入較為複雜,總體來說,結點的刪除可歸結為三種情況:
1、 如果結點z沒有孩子節點,那麼只需簡單地將其刪除,並修改父節點,用NIL來替換z;
2、 如果結點z只有一個孩子,那麼將這個孩子節點提升到z的位置,並修改z的父節點,用z的孩子替換z;
3、 如果結點z有2個孩子,那麼查詢z的後繼y,此外後繼一定在z的右子樹中,然後讓y替換z。
這三種情況中,1和2比較簡單,3相對棘手。
我們通過示意圖,描述這幾種情況:
情況1:
情況2: 情況3: 可分為兩種型別,一種是z的後繼y位於其右子樹中,但沒有左孩子,也就是說,右孩子y是其後繼。如下: 另外一種型別是,z的後繼y位於z的右子樹中,但並不是z的右孩子,此時,用y的右孩子替換y,然後再用y替換z。如下: 二叉樹的遍歷:
最後,我們考慮二叉搜尋樹的遍歷。
二叉搜尋樹的性質允許通過簡單的遞迴演算法來輸出樹中所有的關鍵字,有三種方式:先序遍歷、中序遍歷、後序遍歷。其中,先序遍歷中輸出根的關鍵字在其左右子樹的關鍵字之前;中序遍歷中輸出根的關鍵詞位於其左子樹的關鍵字和右子樹的關鍵字之間;後序遍歷中輸出根的關鍵字在左右子樹的關鍵字之後。
如果x是一棵有n個結點子樹的根,那麼呼叫Preorder_Tree_Walk(x)或者Inorder_Tree_Walk(x)或者Postorder_Tree_Walk(x)需要O(n)時間。
//先序遍歷
Preorder_Tree_Walk(x):
if x!=NIL:
	print x.key
	Preorder_Tree_Walk(x.left)
	Preorder_Tree_Walk(x.right)
//中序遍歷
Inorder_Tree_Walk(x):
	Inorder_Tree_Walk(x.left)
	print x.key
	Inorder_Tree_Walk(x.right)
//後序遍歷
Postorder_Tree_Walk(x):
	Postorder_Tree_Walk(x.left)
	Postorder_Tree_Walk(x.right)
	print x.key