1. 程式人生 > >【資料結構和演算法06】2-3-4樹

【資料結構和演算法06】2-3-4樹

    從第4節的分析中可以看出,二叉搜尋樹是個很好的資料結構,可以快速地找到一個給定關鍵字的資料項,並且可以快速地插入和刪除資料項。但是二叉搜尋樹有個很麻煩的問題,如果樹中插入的是隨機資料,則執行效果很好,但如果插入的是有序或者逆序的資料,那麼二叉搜尋樹的執行速度就變得很慢。因為當插入數值有序時,二叉樹就是非平衡的了,它的快速查詢、插入和刪除指定資料項的能力就喪失了。

    2-3-4樹是一個多叉樹,它的每個節點最多有四個子節點和三個資料項。2-3-4樹和紅-黑樹一樣,也是平衡樹,它的效率比紅-黑樹稍差,但是程式設計容易。2-3-4樹名字中的2、3、4的含義是指一個節點可能含有的子節點的個數。對非葉節點有三種可能的情況:

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

    ·有兩個資料項的節點總是有三個子節點;

    ·有三個資料項的節點總是有四個位元組點。

    簡而言之,非葉節點的子節點總是比它含有的資料項多1。如下圖所示:

    為了方便起見,用0到2給資料項編號,用0到3給子節點鏈編號。樹的結構中很重要的一點就是它的鏈與自己資料項的關鍵字值之間的關係。二叉樹所有關鍵字值比某個節點值小的都在這個節點的左子節點為根的子樹上,所有關鍵字值比某個及誒的那值大的節點都在這個節點右子節點為根的子樹上。2-3-4樹中規則是一樣的,還加上了以下幾點:

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

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

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

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

    這種關係如下圖所示,2-3-4樹中一般不允許出現重複關鍵字值,所以不用考慮比較相同的關鍵字值的情況。

    2-3-4樹中插入節點有時比較簡單,有時比較複雜。當沒有碰到滿節點時插入很簡單,找到合適的葉節點後,只要把新資料項插入進去即可,插入可能會涉及到在一個節點中移動一個或兩個其他的資料項,這樣在新的資料項插入後關鍵字值仍保持正確的順序。如下圖:

    如果往下尋找要插入的位置的路途中,節點已經滿了,插入就變得複雜了。這種情況下,節點必須分裂。正是這種分裂過程保證了樹的平衡。設要分裂節點中的資料項為A、B、C,下面是分裂時的情況(假設分裂的節點不是根節點):

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

    ·資料項C移到新節點中;

    ·資料項B移動到要分裂節點的父節點中;

    ·資料項A保留在原來的位置;

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

    下圖顯示了一個節點分裂的過程。另一種描述節點分裂的方法是說4-節點變成了兩個2-節點。

    如果一開始查詢插入點時就碰到滿根時,插入過程就更復雜一點:

    ·建立新的根。它是要分裂節點的父節點;

    ·建立第二個新的節點。它是要分裂節點的兄弟節點;

    ·資料項C移動到新的兄弟節點中;

    ·資料項B移動到新的根節點中;

    ·資料項A保留在原來的位置上;

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

    下圖是根分裂的過程。過程中建立新的根,比舊的高一層,因此整個樹的高度就增加了一層。

    下面是2-3-4樹的程式碼:

public class Tree234 {
	private Node2 root = new Node2();
	public int find(long key) {
		Node2 currentNode = root;
		int childNumber;
		while(true) {
			if((childNumber = currentNode.findItem(key)) != -1) {
				return childNumber;
			}
			else if(currentNode.isLeaf()) {
				return -1;
			}
			else {
				currentNode = getNextChild(currentNode, key);
			}
		}
	}
	//insert a DataItem
	public void insert(long data) {
		Node2 currentNode = root;
		DataItem tempItem = new DataItem(data);
		while(true) {
			if(currentNode.isFull()) {
				split(currentNode);	//if node is full, split it
				currentNode = currentNode.getParent();	//back up
				currentNode = getNextChild(currentNode, data);	//search once
			}
			else if(currentNode.isLeaf()) { //if node if leaf
				break;	//go insert
			}
			else {
				currentNode = getNextChild(currentNode, data);
			}
		}
		currentNode.insertItem(tempItem);
	}
	//display tree
	public void displayTree() {
		recDisplayTree(root, 0, 0);
	}
	public Node2 getNextChild(Node2 currentNode, long key) {
		int j;
		//assumes node is not empty, not full and not leaf
		int numItems = currentNode.getNumItems();
		for(j = 0; j < numItems; j++) {
			if(key < currentNode.getItem(j).dData) {
				return currentNode.getChild(j);
			}
		}
		return currentNode.getChild(j);
	}
	public void split(Node2 currentNode) {
		//assumes node is full
		DataItem itemB, itemC;	//儲存要分裂節點的後兩個DataItem
		Node2 parent, child2, child3;	//儲存要分裂節點的父節點和後兩個child
		int itemIndex;
		itemC = currentNode.removeItem();
		itemB = currentNode.removeItem();	//remove items from this node
		child2 = currentNode.disconnectChild(2);
		child3 = currentNode.disconnectChild(3); //remove children from this node
		Node2 newRight = new Node2(); //make a new node
		if(currentNode == root) {
			root = new Node2(); //make a new root
			parent = root;	//root is our parent
			root.connectChild(0, currentNode);//connect currentNode to parent
		}
		else {
			parent = currentNode.getParent();
		}
		//deal with parent
		itemIndex = parent.insertItem(itemB);	//insert B to parent
		int n = parent.getNumItems();	//total items
		for(int j = n-1; j > itemIndex; j--) {
			Node2 temp = parent.disconnectChild(j);
			parent.connectChild(j+1, temp);
		}
		parent.connectChild(itemIndex+1, newRight);
		//deal with newRight
		newRight.insertItem(itemC);
		newRight.connectChild(0, child2);
		newRight.connectChild(1, child3);
	}
	public void recDisplayTree(Node2 thisNode, int level, int childNumber) {
		System.out.print("level = " + level + " child = " + childNumber + " ");
		thisNode.displayNode();
		//call ourselves for each child of this node
		int numItems = thisNode.getNumItems();
		for(int j = 0; j < numItems+1; j++) {
			Node2 nextNode = thisNode.getChild(j);
			if(nextNode != null) {
				recDisplayTree(nextNode, level+1, j);
			}
			else 
				continue;
		}
	}
}

//資料項
class DataItem {
	public long dData;
	public DataItem(long data) {
		dData = data;
	}
	public void displayItem() {
		System.out.print("/" + dData);
	}
}
//節點
class Node2 {
	private static final int ORDER = 4;
	private int numItems; //表示該節點存有多少個數據項
	private Node2 parent;
	private Node2 childArray[] = new Node2[ORDER]; //儲存子節點的陣列,最多四個子節點
	private DataItem itemArray[] = new DataItem[ORDER-1];//該節點中存放資料項的陣列,每個節點最多存放三個資料項
	//連線子節點
	public void connectChild(int childNum, Node2 child) {
		childArray[childNum] = child;
		if(child != null) {
			child.parent = this;
		}
	}
	//斷開與子節點的連線,並返回該子節點
	public Node2 disconnectChild(int childNum) {
		Node2 tempNode = childArray[childNum];
		childArray[childNum] = null;
		return tempNode;
	}
	public Node2 getChild(int childNum) {
		return childArray[childNum];
	}
	public Node2 getParent() {
		return parent;
	}

	public boolean isLeaf() {
		return (childArray[0] == null);
	}
	public int getNumItems() {
		return numItems;
	}
	public DataItem getItem(int index) {
		return itemArray[index];
	}
	public boolean isFull() {
		return (numItems == ORDER-1);
	}
	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) {
		//assumes node is not full
		numItems++;
		long newKey = newItem.dData;
		for(int j = ORDER-2; j >= 0; j--) { 	//start on right
			if(itemArray[j] == null) { 		//item is null
				continue; 					//get left one cell
			}
			else {							//not null
				long itsKey = itemArray[j].dData;	//get its key
				if(newKey < itsKey) {				//if it's bigger
					itemArray[j+1] = itemArray[j]; 	//shift it right
				}
				else {
					itemArray[j+1] = newItem; 		//insert new item
					return j+1;						//return index to new item
				}
			}
		}
		itemArray[0] = newItem;
		return 0;
	}
	public DataItem removeItem() {
		//assumes node not empty
		DataItem tempItem = itemArray[numItems-1];	//save item
		itemArray[numItems-1] = null;				//disconnect it
		numItems--;
		return tempItem;
	}
	public void displayNode() {
		for(int i = 0; i < numItems; i++) {
			itemArray[i].displayItem();
		}
		System.out.println("/");
	}
}

    和紅-黑樹一樣,2-3-4樹同樣要訪問每層的一個節點,但2-3-4樹有比相同資料項的紅-黑樹短(層數少)。更特別的是,2-3-4樹中每個節點最多可以有4個子節點,如果每個節點都是滿的,樹的高度應該和log4N成正比。以2為底的對數和以4為底的對數底數相差2,因此,在所有節點都滿的情況下,2-3-4樹的高度大致是紅-黑樹的一般。不過它們不可能都是滿的,2-3-4樹的高度就大致在log2(N+1)和log2(N+1)/2之間。

    另一方面,每個節點要檢視的資料項就更多了,這會增加查詢時間。因為節點中用線性搜尋來檢視資料項,使查詢時間增加的倍數和M成正比,即每個節點資料項的平均數量。總的查詢時間和M*log4N成正比。有些節點有1個數據項,有些有2個,有些有3個,如果按照平均兩個來計算,查詢時間和2*log4N成正比。

    因此,2-3-4樹中增加每個節點的資料項數量可以抵償樹的高度的減少。2-3-4樹中的查詢時間與平衡二叉樹(如紅-黑樹)大致相等,都是O(logN)。

    2-3-4樹就討論到這,如果有問題歡迎留言指正~

    文末福利:“程式設計師私房菜”,一個有溫度的公眾號~ 
    程式設計師私房菜

_____________________________________________________________________________________________________________________________________________________

-----樂於分享,共同進步!