二叉查找樹及B-樹、B+樹、B*樹變體


動態查找樹主要有二叉查找樹(Binary Search Tree),平衡二叉查找樹(Balanced Binary Search Tree), 紅黑樹 (Red-Black Tree ),

都是典型的二叉查找樹結構,查找的時間復雜度 O(log2-N) 與樹的深度相關,降低樹的深度會提高查找效率,於是有了多路的B-tree/B+-tree/ B*-tree (B~Tree)。

二叉查找樹

二叉查找樹即搜索二叉樹,或者二叉排序樹(BSTree)。

一、關於二叉查找樹

二叉查找樹(Binary Search Tree)是指一棵空樹或者具有下列性質的二叉樹:
1. 若任意節點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
2. 若任意節點的右子樹不空,則右子樹上所有節點的值均大於它的根節點的值;
3. 任意節點的左、右子樹也分別為二叉查找樹。
4. 沒有鍵值相等的節點,這個特征很重要,可以幫助理解二叉排序樹的很多操作。
二叉查找樹具有很高的靈活性,對其優化可以生成平衡二叉樹,紅黑樹等高效的查找和插入數據結構。

二、基本性質

(1)二叉查找樹是一個遞歸的數據結構,對二叉查找樹進行中序遍歷,可以得到一個遞增的有序序列。
(2)二叉查找樹上基本操作的執行時間和樹的高度成正比。

對一棵n個節點的完全二叉樹來說,樹的高度為lgn,這些操作的最壞情況運行時間為O(lg n),而如果是線性鏈表結構,這些操作的最壞運行時間是O(n)。
一棵隨機構造的二叉查找樹的期望高度為O(lg n),但實際中並不能總是保證二叉查找樹是隨機構造的,
有些二叉查找樹的變形能保證各種基本操作的最壞情況性能,比如紅黑樹的高度為O(lg n),而B樹對維護隨機訪問的二級存儲器上的數據庫特別有效。

注意對復雜度的理解,所謂的O(lg n)就是指復雜度是對數級別,是數量級的比較,和對數的底數其實沒關系,

只要底數是大於1的,就是相同的數量級,有些書上說二叉查找樹的復雜度是O(log2-n),指的是相同的時間復雜度。

三、前驅和后繼節點

一個節點的后繼是該節點的后一個,即比該節點鍵值稍大的節點。
給定一個二叉查找樹中的節點,找出在中序遍歷順序下某個節點的前驅和后繼。
如果樹中所有關鍵字都不相同,則某一節點x的前驅就是小於key[x]的所有關鍵字中最大的那個節點,后繼即是大於key[x]中的所有關鍵字中最小的那個節點。根據二叉查找樹的結構和性質,不用對關鍵字做任何比較,就可以找到某個節點的前驅和后繼。

四、查找、插入與刪除

(1)查找
利用二叉查找樹左小右大的性質,可以很容易實現查找任意值和最大/小值。

在二叉查找樹中查找一個給定的關鍵字k的過程與二分查找很類似

首先是關鍵字k與樹根的關鍵字進行比較,如果k比根的關鍵字大,則在根的右子樹中查找,否則在根的左子樹中查找,重復此過程,直到找到與遇到空節點為止。

在二叉查找樹中查找x的過程如下:
1.若二叉樹是空樹,則查找失敗。
2.若x等於根節點的數據,則查找成功,否則。
3.若x小於根節點的數據,則遞歸查找其左子樹,否則。
4.遞歸查找其右子樹。

(2)插入
二叉樹查找樹b插入操作x的過程如下:
1.若b是空樹,則直接將插入的節點作為根節點插入。
2.x等於b的根節點的數據的值,則直接返回,否則。
3.若x小於b的根節點的數據的值,則將x要插入的節點的位置改變為b的左子樹,否則。
4.將x要出入的節點的位置改變為b的右子樹。

(3)刪除


假設從二叉查找樹中刪除給定的結點z,分三種情況討論:
1.節點z為葉子節點,沒有孩子節點,那么直接刪除z,修改父節點的指針即可。
2.節點z只有一個子節點或者子樹,將節點z刪除,根據二叉查找樹的性質,將z的父節點與子節點關聯就可以了。
3.節點Z有兩個子節點,刪除Z該怎樣將Z的父結點與這兩個孩子結點關聯呢?
在刪去節點Z之后,為保持其它元素之間的相對位置不變,可按中序遍歷保持有序進行調整。
這種情況下可以用Z的后繼節點來替代Z。
實現方法就是將后繼從二叉樹中刪除,將后繼的數據覆蓋到Z中。

五、代碼實現

public class BinarySearchTree <T extends Comparable<? super T>>{

	//節點數據結構 靜態內部類
	static class BinaryNode<T>{
		T data;
		BinaryNode<T> left;
		BinaryNode<T> right;
		public BinaryNode(){
			data=null;
		}
		public BinaryNode(T data) {
			this(data,null,null);
		}
		public BinaryNode(T data,BinaryNode<T> left,BinaryNode<T> right){
			this.data=data;
			this.left=left;
			this.right=right;
		}
	}
	
	//私有的頭結點
	private BinaryNode<T> root;
	
	//構造一棵空二叉樹
	public BinarySearchTree(){
		root=null;
	}
	//二叉樹判空
	public boolean isEmpty(){
		return root==null;
	}
	//清空二叉樹
	public void clear(){
		root=null;
	}
	
	//檢查某個元素是否存在
	public boolean contains(T t){
		return contains(t,root);
	}
	
	/**
	 * 從某個節點開始查找某個元素是否存在
	 * 在二叉查找樹中查找x的過程如下:
	 * 1、若二叉樹是空樹,則查找失敗。
	 * 2、若x等於根結點的數據,則查找成功,否則。
	 * 3、若x小於根結點的數據,則遞歸查找其左子樹,否則。
	 * 4、遞歸查找其右子樹。
	 */
	public boolean contains(T t,BinaryNode<T> node){
		if(node==null){
			return false;
		}
		/**
		 * 這就是為什么使用Comparable的泛型
		 * compareTo的對象也必須是實現了Comparable接口的泛型,
		 * 所以參數必須是BinaryNode<T> node格式
		 */
		int result=t.compareTo(node.data);
		if(result>0){//去右子樹查找
			return contains(t,node.right);
		}else if(result<0){//去左子樹查找
			return contains(t,node.left);
		}else{
			return false;			
		}
	}
	
	//插入元素
	public void insert(T t){
		root=insert(t,root);
	}
	
	/**
	 * 將節點插入到以某個節點為頭的二叉樹中
	 * 這個插入其實也是一個遞歸的過程
	 * 遞歸最深層的返回結果一個包含要插入的節點子樹的頭節點
	 */
	public BinaryNode insert(T t,BinaryNode<T> node){
		//如果是空樹,直接構造一棵新的二叉樹
		if(node==null){
			return new BinaryNode<T>(t);
		}
		
		int result=t.compareTo(node.data);
		
		if(result<0){
			node.left=insert(t,node.left);
		}else if(result>0){
			node.right=insert(t,node.right);
		}else{
			;//即要插入的元素和頭節點值相等,直接返回即可
		}
		return node;
	}
	
	
	/**
	 * 刪除元素
	 * 返回調整后的二叉樹頭結點
	 */
	public BinaryNode delete(T t){
		return delete(t,root);
		
	}
	
	/**
	 * 在以某個節點為頭結點的樹結構中刪除元素
	 * 首先需要找到該關鍵字所在的節點p,然后具體的刪除過程可以分為幾種情況:
	 * p沒有子女,直接刪除p
	 * p有一個子女,直接刪除p
	 * p有兩個子女,刪除p的后繼q(q至多只有一個子女)
	 * 確定了要刪除的節點q之后,就要修正q的父親和子女的鏈接關系,
	 * 然后把q的值替換掉原先p的值,最后把q刪除掉
	 */
	public BinaryNode delete(T t,BinaryNode<T> node){
		if(node==null){//節點為空還要啥自行車
			return node;
		}
		/**
		 * 首先要找到這個節點,所以還是得比較
		 */
		int result=t.compareTo(node.data);
		
		/**
		 * 去左半部分找這個節點,
		 * 找到節點result==0,這個遞歸就停止
		 */
		if(result<0){
			node.left=delete(t,node.left);
		}else if(result>0){//去右半部分找這個節點
			node.right=delete(t,node.right);
		}
		/**
		 * 如果這個節點的左右孩子都不為空,那么找到當前節點的后繼節點,
		 * 
		 */
		if(node.left!=null && node.right!=null){
			/**
			 * node節點的右子樹部分的最小節點,實際上就是它的后繼節點
			 * 得到后繼節點的值
			 */
			node.data = findMin(node.right).data;  
			/**
			 * 這個過程並不是刪除后繼節點,是一步一步的把所有的節點都替換上來
			 */
	        node.right = delete(node.data,node.right);  
		}else{
			/**
			 * 如果二叉搜索樹中一個節點是完全節點,
			 * 那么它的前驅和后繼節點一定在以它為頭結點的子樹中,應該是這樣的
			 * 來到了只有一個頭節點和一個子節點的情況
			 */
			node = (node.left!=null)?node.left:node.right; 
		}
		//此處的node,是經過調整后的傳入的root節點
		return node;
		
	}
	
	/**
	 * 返回二叉樹中的最小值節點
	 * 此時無比想念大根堆和小根堆
	 */
	public BinaryNode<T> findMin(BinaryNode node){
		if(node==null)
			return null;	
		/**
		 * 如果node不為空,就遞歸的去左邊找
		 * 最小值節點肯定是左孩子為空的節點
		 */
		if(node.left!=null)
			node=findMin(node.left);
		return node;
	}
	
}

  

B-樹、B+樹、B*樹變體

關於這B樹以及B樹的兩種變體,其實很好區分,

相比B樹,B+樹不維護關鍵字具體信息,不考慮value的存儲,所有的我們需要的信息都在葉子節點上,

B*樹在B+樹的基礎上增加了非葉子節點兄弟間的指針,在某些場景效率更高,

主要掌握B樹的操作,也就掌握了這兩種變體樹的操作。

1.B樹(B-tree),即B-樹

B-樹是為了磁盤或其它存儲設備而設計的一種多叉平衡查找樹。

(1)B-Tree的接點結構
B-tree中,每個結點包含:
本結點所含關鍵字的個數;
指向父結點的指針;
關鍵字;
指向子結點的指針數組;

#define Max l000 //結點中關鍵字的最大數目:Max=m-1,m是B-樹的階
#define Min 500 //非根結點中關鍵字的最小數目:Min=m/2-1
typedef int KeyType; //KeyType關鍵字類型由用戶定義
typedef struct node{ //結點定義中省略了指向關鍵字代表的記錄的指針
   int keynum; //結點中當前擁有的關鍵字的個數,keynum<<Max
   KeyType key[Max+1]; //關鍵字向量為key[1..keynum],key[0]不用。
   struct node *parent; //指向雙親結點
   struct node *son[Max+1];//指向孩子結點的指針數組,孩子指針向量為son[0..keynum]
 }BTreeNode;
typedef BTreeNode *BTree;

(2)B-tree的特點

  • B-tree是一種多路搜索樹(並不是二叉的),對於一棵M階樹:
  • 定義任意非葉子結點最多只有M個孩子;且M>2;
  • 根結點的孩子數為[2, M],除非根結點為葉子節點;
  • 除根結點以外的非葉子結點的兒子數為[M/2, M];
  • 非葉子結點的關鍵字個數=指向兒子的指針個數-1;
  • 每個非葉子結點存放至少M/2-1(取上整)和至多M-1個關鍵字;
  • 非葉子結點的關鍵字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
  • 非葉子結點的指針:P[1], P[2], …, P[M];其中P[1]指向關鍵字小於K[1]的子樹,P[M]指向關鍵字大於K[M-1]的子樹,其它P[i]指向關鍵字屬於(K[i-1], K[i])的子樹;
  • 所有葉子結點位於同一層;

以M=3的一棵3階B樹為例:

一棵包含了24個英文字母的5階B樹的結構:

(3)B-tree高度與復雜度

B樹的高度是,而不是其它幾種樹的H=log2n,其中T為度數(每個節點包含的元素個數),即所謂的階數,n為總元素個數或總關鍵字數。

B樹查找的時間復雜度為O(Log2-N),下面是參考推導過程:



其中M為設定的非葉子結點最多子樹個數,N為關鍵字總數;所以B-樹的性能總是等價於二分查找(與M值無關),也就沒有AVL樹平衡的問題。

 

2.B-tree的基本操作

(1)查找操作

在B-樹中查找給定關鍵字的方法類似於二叉排序樹上的查找。不同的是在每個結點上確定向下查找的路徑不一定是二路而是keynum+1路的。
對結點內的存放有序關鍵字序列的向量key[l..keynum] 用順序查找或折半查找方法查找。若在某結點內找到待查的關鍵字K,則返回該結點的地址及K在key[1..keynum]中的位置;否則,確定K在某個key[i]和key[i+1]之間結點后,從磁盤中讀son[i]所指的結點繼續查找。直到在某結點中查找成功;或直至找到葉結點且葉結點中的查找仍不成功時,查找過程失敗。

 

 BTreeNode *SearchBTree(BTree T,KeyType K,int *pos)
 { //在B-樹T中查找關鍵字K,成功時返回找到的結點的地址及K在其中的位置*pos
//失敗則返回NULL,且*pos無定義
  int i;
  T→key[0]=k; //設哨兵.下面用順序查找key[1..keynum]
  for(i=T->keynum;K<t->key[i];i--); //從后向前找第1個小於等於K的關鍵字
  if(i>0 && T->key[i]==1){ //查找成功,返回T及i
    *pos=i;
    return T;
   } //結點內查找失敗,但T->key[i]<K<T->key[i+1],下一個查找的結點應為
     //son[i]
  if(!T->son[i]) //*T為葉子,在葉子中仍未找到K,則整個查找過程失敗
    return NULL;
    //查找插入關鍵字的位置,則應令*pos=i,並返回T,見后面的插入操作
  DiskRead(T->son[i]); //在磁盤上讀人下一查找的樹結點到內存中
  return SearchBTree(T->Son[i],k,pos); //遞歸地繼續查找於樹T->son[i]
 }

  

(2)查找操作的時間開銷
B-樹上的查找有兩個基本步驟:
1.在B-樹中查找結點,該查找涉及讀盤DiskRead操作,屬外查找;
2.在結點內查找,該查找屬內查找。
查找操作的時間為:
1.外查找的讀盤次數不超過樹高h,故其時間是O(h);
2.內查找中,每個結點內的關鍵字數目keynum<m(m是B-樹的階數),故其時間為O(nh)。
注意:
1.實際上外查找時間可能遠遠大於內查找時間。
2.B-樹作為數據庫文件時,打開文件之后就必須將根結點讀人內存,而直至文件關閉之前,此根一直駐留在內存中,故查找時可以不計讀入根結點的時間。

(3)插入操作

 插入一個元素時,首先在B樹中是否存在,如果不存在,即在葉子結點處結束,然后在葉子結點中插入該新的元素,注意:如果葉子結點空間足夠,這里需要向右移動該葉子結點中大於新插入關鍵字的元素,如果空間滿了以致沒有足夠的空間去添加新的元素,則將該結點進行“分裂”,將一半數量的關鍵字元素分裂到新的其相鄰右結點中,中間關鍵字元素上移到父結點中(當然,如果父結點空間滿了,也同樣需要“分裂”操作),而且當結點中關鍵元素向右移動了,相關的指針也需要向右移。如果在根結點插入新元素,空間滿了,則進行分裂操作,這樣原來的根結點中的中間關鍵字元素向上移動到新的根結點中,因此導致樹的高度增加一層。

(4)刪除操作

首先查找B樹中需刪除的元素,如果該元素在B樹中存在,則將該元素在其結點中進行刪除,如果刪除該元素后,首先判斷該元素是否有左右孩子結點,如果有,則上移孩子結點中的某相近元素到父節點中,然后是移動之后的情況;如果沒有,直接刪除后,移動之后的情況。

3.B+樹(B+-tree)

B+-tree是應文件系統所需而產生的一種B-tree的變形樹。

(1)B樹和B+樹的對比
一棵m階的B+樹和m階的B樹的異同點在於:
1.有n棵子樹的結點中含有n-1 個關鍵字;
2.所有的葉子結點中包含了全部關鍵字的信息,及指向含有這些關鍵字記錄的指針,且葉子結點本身依關鍵字的大小自小而大的順序鏈接。 (而B 樹的葉子節點並沒有包括全部需要查找的信息)
3.所有的非終端結點可以看成是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵字。 (而B 樹的非終節點也包含需要查找的有效信息)
(2)為什么說B+-tree比B 樹更適合實際應用中操作系統的文件索引和數據庫索引?

  •  B+-tree的磁盤讀寫代價更低

B+-tree的內部結點並沒有指向關鍵字具體信息的指針。因此其內部結點相對B 樹更小。

如果把所有同一內部結點的關鍵字存放在同一盤塊中,那么盤塊所能容納的關鍵字數量也越多。

一次性讀入內存中的需要查找的關鍵字也就越多。相對來說IO讀寫次數也就降低了。
舉個例子,假設磁盤中的一個盤塊容納16bytes,而一個關鍵字2bytes,一個關鍵字具體信息指針2bytes。

一棵9階B-tree(一個結點最多8個關鍵字)的內部結點需要2個盤快。而B+ 樹內部結點只需要1個盤快。當需要把內部結點讀入內存中的時候,B 樹就比B+ 樹多一次盤塊查找時間(在磁盤中就是盤片旋轉的時間)。

  • B+-tree的查詢效率更加穩定

由於非終結點並不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。

4.B*樹(B*-tree)

B*-tree是B+-tree的變體,在B+樹的基礎上(所有的葉子結點中包含了全部關鍵字的信息,及指向含有這些關鍵字記錄的指針),

B*樹中非根和非葉子結點再增加指向兄弟的指針;

B*樹定義了非葉子結點關鍵字個數至少為(2/3)*M,即塊的最低使用率為2/3(代替B+樹的1/2)。

下圖是一棵典型的B*樹:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM