1. 程式人生 > >Java數據結構和算法(十二)——2-3-4樹

Java數據結構和算法(十二)——2-3-4樹

oid 樹的高度 n+1 tno != val post 節點數據 isp

  通過前面的介紹,我們知道在二叉樹中,每個節點只有一個數據項,最多有兩個子節點。如果允許每個節點可以有更多的數據項和更多的子節點,就是多叉樹。本篇博客我們將介紹的——2-3-4樹,它是一種多叉樹,它的每個節點最多有四個子節點和三個數據項。

1、2-3-4 樹介紹

  2-3-4樹每個節點最多有四個字節點和三個數據項,名字中 2,3,4 的數字含義是指一個節點可能含有的子節點的個數。對於非葉節點有三種可能的情況:

  ①、有一個數據項的節點總是有兩個子節點;

  ②、有二個數據項的節點總是有三個子節點;

  ③、有三個數據項的節點總是有四個子節點;

  簡而言之,非葉節點的子節點數總是比它含有的數據項多1。如果子節點個數為L,數據項個數為D,那麽:L = D + 1

  技術分享圖片

  葉節點(上圖最下面的一排)是沒有子節點的,然而它可能含有一個、兩個或三個數據項。空節點是不會存在的。

  樹結構中很重要的一點就是節點之間關鍵字值大小的關系。在二叉樹中,所有關鍵字值比某個節點值小的節點都在這個節點左子節點為根的子樹上;所有關鍵字值比某個節點值大的節點都在這個節點右子節點為根的子樹上。2-3-4 樹規則也是一樣,並且還加上以下幾點:

  為了方便描述,用從0到2的數字給數據項編號,用0到3的數字給子節點編號,如下圖:

  技術分享圖片

  ①、根是child0的子樹的所有子節點的關鍵字值小於key0;

  ②、根是child1的子樹的所有子節點的關鍵字值大於key0並且小於key1;

  ③、根是child2的子樹的所有子節點的關鍵字值大於key1並且小於key2;

  ④、根是child3的子樹的所有子節點的關鍵字值大於key2。

  簡化關系如下圖,由於2-3-4樹中一般不允許出現重復關鍵值,所以不用考慮比較關鍵值相同的情況。

  技術分享圖片

2、搜索2-3-4樹

  查找特定關鍵字值的數據項和在二叉樹中的搜索類似。從根節點開始搜索,除非查找的關鍵字值就是根,否則選擇關鍵字值所在的合適範圍,轉向那個方向,直到找到為止。

  比如對於下面這幅圖,我們需要查找關鍵字值為 64 的數據項。

  技術分享圖片

  首先從根節點開始,根節點只有一個數據項50,沒有找到,而且因為64比50大,那麽轉到根節點的子節點child1。60|70|80 也沒有找到,而且60<64<70,所以我們還是找該節點的child1,62|64|66,我們發現其第二個數據項正好是64,於是找到了。

3、插入

  新的數據項一般要插在葉節點裏,在樹的最底層。如果你插入到有子節點的節點裏,那麽子節點的編號就要發生變化來維持樹的結構,因為在2-3-4樹中節點的子節點要比數據項多1。

  插入操作有時比較簡單,有時卻很復雜。

  ①、當插入沒有滿數據項的節點時是很簡單的,找到合適的位置,只需要把新數據項插入就可以了,插入可能會涉及到在一個節點中移動一個或其他兩個數據項,這樣在新的數據項插入後關鍵字值仍保持正確的順序。如下圖:

  技術分享圖片

  ②、如果往下尋找插入位置的途中,節點已經滿了,那麽插入就變得復雜了。發生這種情況時,節點必須分裂,分裂能保證2-3-4樹的平衡。

  ps:這裏討論的是自頂向下的2-3-4樹,因為是在向下找到插入點的路途中節點發生了分裂。把要分裂的數據項設為A,B,C,下面是節點分裂的情況(假設分裂的節點不是根節點):

  1、節點分裂

  一、創建一個新的空節點,它是要分裂節點的兄弟,在要分裂節點的右邊;

  二、數據項C移到新節點中;

  三、數據項B移到要分裂節點的父節點中;

  四、數據項A保留在原來的位置;

  五、最右邊的兩個子節點從要分裂處斷開,連到新節點上。

  技術分享圖片

  上圖描述了節點分裂的例子,另一種描述節點分裂的說法是4-節點變成了兩個 2- 節點。節點分裂是把數據向上和向右移動,從而保持了數的平衡。一般插入只需要分裂一個節點,除非插入路徑上存在不止一個滿節點時,這種情況就需要多重分裂。

  2、根的分裂

  如果一開始查找插入節點時就碰到滿的根節點,那麽插入過程更復雜:

  ①、創建新的根節點,它是要分裂節點的父節點。

  ②、創建第二個新的節點,它是要分裂節點的兄弟節點;

  ③、數據項C移到新的兄弟節點中;

  ④、數據項B移到新的根節點中;

  ⑤、數據項A保留在原來的位置;

  ⑥、要分裂節點最右邊的兩個子節點斷開連接,連到新的兄弟節點中。

  技術分享圖片

  上圖便是根分裂的情況,分裂完成之後,整個樹的高度加1。另外一種描述根分裂的方法是說4-節點變成三個2-節點。

  註意:插入時,碰到沒有滿的節點時,要繼續向下尋找其子節點進行插入。如果直接插入該節點,那麽還要進行子節點的增加,因為在2-3-4樹中節點的子節點個數要比數據項多1;如果插入的節點滿了,那麽就要進行節點分裂。下圖是一系列插入過程,有4個節點分裂了,兩個是根,兩個是葉節點:

  技術分享圖片

  

4、完整源碼實現

  分為節點類Node,表示每個節點的數據項類DataItem,以及最後的2-3-4樹類Tree234.class

package com.ys.tree.twothreefour;

public class Tree234 {
	private Node root = new Node() ;
	/*public Tree234(){
		root = new Node();
	}*/
	//查找關鍵字值
	public int find(long key){
		Node curNode = root;
		int childNumber ;
		while(true){
			if((childNumber = curNode.findItem(key))!=-1){
				return childNumber;
			}else if(curNode.isLeaf()){//節點是葉節點
				return -1;
			}else{
				curNode = getNextChild(curNode,key);
			}
		}
	}
	
	public Node getNextChild(Node theNode,long theValue){
		int j;
		int numItems = theNode.getNumItems();
		for(j = 0 ; j < numItems ; j++){
			if(theValue < theNode.getItem(j).dData){
				return theNode.getChild(j);
			}
		}
		return theNode.getChild(j);
	}
	
	//插入數據項
	public void insert(long dValue){
		Node curNode = root;
		DataItem tempItem = new DataItem(dValue);
		while(true){
			if(curNode.isFull()){//如果節點滿數據項了,則分裂節點
				split(curNode);
				curNode = curNode.getParent();
				curNode = getNextChild(curNode, dValue);
			}else if(curNode.isLeaf()){//當前節點是葉節點
				break;
			}else{
				curNode = getNextChild(curNode, dValue);
			}
		}//end while
		curNode.insertItem(tempItem);
	}
	
	public void split(Node thisNode){
		DataItem itemB,itemC;
		Node parent,child2,child3;
		int itemIndex;
		itemC = thisNode.removeItem();
		itemB = thisNode.removeItem();
		child2 = thisNode.disconnectChild(2);
		child3 = thisNode.disconnectChild(3);
		Node newRight = new Node();
		if(thisNode == root){//如果當前節點是根節點,執行根分裂
			root = new Node();
			parent = root;
			root.connectChild(0, thisNode);
		}else{
			parent = thisNode.getParent();
		}
		//處理父節點
		itemIndex = parent.insertItem(itemB);
		int n = parent.getNumItems();
		for(int j = n-1; j > itemIndex ; j--){
			Node temp = parent.disconnectChild(j);
			parent.connectChild(j+1, temp);
		}
		parent.connectChild(itemIndex+1, newRight);
		
		//處理新建的右節點
		newRight.insertItem(itemC);
		newRight.connectChild(0, child2);
		newRight.connectChild(1, child3);
	}
	
	//打印樹節點
	public void displayTree(){
		recDisplayTree(root,0,0);
	}
	private void recDisplayTree(Node thisNode,int level,int childNumber){
		System.out.println("levle="+level+" child="+childNumber+" ");
		thisNode.displayNode();
		int numItems = thisNode.getNumItems();
		for(int j = 0; j < numItems+1 ; j++){
			Node nextNode = thisNode.getChild(j);
			if(nextNode != null){
				recDisplayTree(nextNode, level+1, j);
			}else{
				return;
			}
		}
	}

	//數據項
	class DataItem{
		public long dData;
		public DataItem(long dData){
			this.dData = dData;
		}
		public void displayItem(){
			System.out.println("/"+dData);
		}
	}
	
	//節點
	class Node{
		private static final int ORDER = 4;
		private int numItems;//表示該節點有多少個數據項
		private Node parent;//父節點
		private Node childArray[] = new Node[ORDER];//存儲子節點的數組,最多有4個子節點
		private DataItem itemArray[] = new DataItem[ORDER-1];//存放數據項的數組,一個節點最多有三個數據項
		
		//連接子節點
		public void connectChild(int childNum,Node child){
			childArray[childNum] = child;
			if(child != null){
				child.parent = this;
			}
		}
		//斷開與子節點的連接,並返回該子節點
		public Node disconnectChild(int childNum){
			Node tempNode = childArray[childNum];
			childArray[childNum] = null;
			return tempNode;
		}
		//得到節點的某個子節點
		public Node getChild(int childNum){
			return childArray[childNum];
		}
		//得到父節點
		public Node getParent(){
			return parent;
		}
		//判斷是否是葉節點
		public boolean isLeaf(){
			return (childArray[0] == null)?true:false;
		}
		//得到節點數據項的個數
		public int getNumItems(){
			return numItems;
		}
		//得到節點的某個數據項
		public DataItem getItem(int index){
			return itemArray[index];
		}
		//判斷節點的數據項是否滿了(最多3個)
		public boolean isFull(){
			return (numItems == ORDER-1) ? true:false;
		}
		
		//找到數據項在節點中的位置
		public int findItem(long key){
			for(int j = 0 ; j < ORDER-1 ; j++){
				if(itemArray[j]==null){
					break;
				}else if(itemArray[j].dData == key){
					return j;
				}
			}
			return -1;
		}
		
		//將數據項插入到節點
		public int insertItem(DataItem newItem){
			numItems++;
			long newKey = newItem.dData;
			for(int j = ORDER-2 ; j >= 0 ; j--){
				if(itemArray[j] == null){//如果為空,繼續向前循環
					continue;
				}else{
					long itsKey = itemArray[j].dData;//保存節點某個位置的數據項
					if(newKey < itsKey){//如果比新插入的數據項大
						itemArray[j+1] = itemArray[j];//將大數據項向後移動一位
					}else{
						itemArray[j+1] = newItem;//如果比新插入的數據項小,則直接插入
						return j+1;
					}
				}
			}
			//如果都為空,或者都比待插入的數據項大,則將待插入的數據項放在節點第一個位置
			itemArray[0] = newItem;
			return 0;
		}
		//移除節點的數據項
		public DataItem removeItem(){
			DataItem temp = itemArray[numItems-1];
			itemArray[numItems-1] = null;
			numItems--;
			return temp;
		}
		//打印節點的所有數據項
		public void displayNode(){
			for(int j = 0 ; j < numItems ; j++){
				itemArray[j].displayItem();
			}
			System.out.println("/");
		}
	}

	
}

5、2-3-4樹和紅黑樹  

  2-3-4樹是多叉樹,而紅黑樹是二叉樹,看上去可能完全不同,但是,在某種意義上它們又是完全相同的,一個可以通過應用一些簡單的規則變成另一個,而且使他們保持平衡的操作也是一樣,數學上稱他們為同構。

  ①、對應規則

  應用如下三條規則可以將2-3-4樹轉化為紅黑樹:

  一、把2-3-4樹中的每個2-節點轉化為紅-黑樹的黑色節點。

  二、把每個3-節點轉化為一個子節點和一個父節點,子節點有兩個自己的子節點:W和X或X和Y。父節點有另一個子節點:Y或W。哪個節點變成子節點或父節點都無所謂。子節點塗成紅色,父節點塗成黑色。

  三、把每個4-節點轉化為一個父節點和兩個子節點。第一個子節點有它自己的子節點W和X;第二個子節點擁有子節點Y和Z。和前面一樣,子節點塗成紅色,父節點塗成黑色。

  技術分享圖片

  下圖是一顆2-3-4樹轉化成對應的紅-黑樹。虛線環繞的子樹是由3-節點和4-節點變成的。轉化後符合紅-黑樹的規則,根節點為紅色,兩個紅色節點不會相連,每條從根到葉節點的路徑上的黑節點個數是一樣的。

  技術分享圖片

  ②、操作等價

  不僅紅-黑樹的結構與2-3-4樹對應,而且兩種樹操作也一樣。2-3-4樹用節點分裂保持平衡,紅-黑樹用顏色變換和旋轉保持平衡。

  技術分享圖片

  上圖是4-節點分裂。虛線環繞的部分等價於4-節點。顏色變換之後,40,60節點都為黑色的,50節點是紅色的。因此,節點 50 和它的父節點70 對於3-節點,如上圖虛線所示。

6、2-3-4 樹的效率

  分析2-3-4樹我們可以和紅黑樹作比較分析。紅-黑樹的層數(平衡二叉樹)大約是log2(N+1),而2-3-4樹每個節點可以最多有4個數據項,如果節點都是滿的,那麽高度和log4N。因此在所有節點都滿的情況下,2-3-4樹的高度大致是紅-黑樹的一半。不過他們不可能都是滿的,所以2-3-4樹的高度大致在log2(N+1)和log2(N+1)/2。減少2-3-4樹的高度可以使它的查找時間比紅-黑樹的短一些。

  但是另一方面,每個節點要查看的數據項就多了,這會增加查找時間。因為節點中用線性搜索來查看數據項,使得查找時間的倍數和M成正比,即每個節點數據項的平均數量。總的查找時間和M*log4N成正比。

Java數據結構和算法(十二)——2-3-4樹