1. 程式人生 > >樹堆(Treap = tree+heap)

樹堆(Treap = tree+heap)

Treap是用來排序(Sort)的一種資料結構(Data Structure)。
reap是隨機查詢二叉平衡樹。 Treap發音為tree+ Heap。顧名思義, Treap把 BST和 Heap結合了起來。它和 BST一樣滿足許多優美的性質,而引入堆目的就是為了維護平衡。 Treap在 BST的基礎上,添加了一個修正值。在滿足 BST性質的上,Treap節點的修正值還滿足最小堆性質。最小堆性質可以被描述為每個子樹根節點都小於等於其子節點。
(1) Treap的特點 
1.  Treap簡明易懂。Treap只有兩種調整方式,左旋和右旋。而且即使沒有嚴密的數學證明和分析,Treap的構造方法啊,平衡原理也是不難理解的。只要能夠理解 BST和堆的思想,理解 Treap當然不在話下。 
2.  Treap易於編寫。Treap只需維護一個滿足堆序的修正值,修正值一經生成無需修改。相
比較其他各種平衡樹, Treap擁有最少的調整方式,僅僅兩種相互對稱的旋轉。所以 Treap當之無愧是最易於編碼除錯的一種平衡樹。 
3.  Treap穩定性佳。Treap的平衡性雖不如 AVL,紅黑樹, SBT等平衡樹,但是 Treap也不會退化,可以保證期望 O(logN)的深度。Treap的穩定性取決於隨機數發生器。 
4.  Treap具有嚴密的
數學證明
。Treap期望 O(logN)的深度,是有嚴密的數學證明的。但這不是介紹的重點,大多略去。 5. Treap具有良好的實踐效果。各種實際應用中, Treap的穩定性表現得相當出色,沒有因為任何的構造出的資料而退化。於是在資訊學競賽中,不少選手習慣於使用 Treap,均取得了不俗的表現。

一棵treap是一棵修改了結點順序的二叉查詢樹,如圖,顯示一個例子,通常樹內的每個結點x都有一個關鍵字值key[x],另外,還要為結點分配priority[x],它是一個獨立選取的隨機數。

假設所有的優先順序是不同的,所有的關鍵字也是不同的。treap的結點排列成讓關鍵字遵循二叉查詢樹性質,並且優先順序(有的地方也叫修正值,是一個隨機數)遵循最小堆順序性質:

1.如果left是u的左孩子,則key[left] < key[u].
2.如果right是u的右孩子,則key[right] > key[u].

3.如果child是u的孩子,則priority[child] > priority[u].

4.如果priority[vi] < priority[vj].則vi相對於vj而言更接近根節點。

這兩個性質的結合就是為什麼這種樹被稱為“treap”的原因,因為它同時具有二叉查詢樹和堆的特徵。(在關鍵字上它滿足二叉排序樹,在優先順序上他滿足小頂堆)。

用以下方式考慮treap會有幫助。假設插入關聯關鍵字的結點x1,x2,...,xn到一棵treap內。結果的treap是將這些結點以它們的優先順序(隨機選取)的順序插入一棵正常的二叉查詢樹形成的,亦即priority[xi] < priority[xj]表示xi在xj之前被插入。
在演算法導論的12.4節中,其證明了隨機構造的二叉查詢樹的期望高度為O(lgn),因而treap的期望高度亦是O(lgn)。

1.treap插入操作:
1.按照二叉樹的插入方法,將結點插入到樹中
2.根據堆的性質(我們這裡為最小堆)和優先順序的大小調整結點位置。

2.treap刪除操作:
1.找到相應的結點
2.若該結點為葉子結點,則直接刪除;
若該結點為只包含一個葉子結點的結點,則將其葉子結點賦值給它;
若該結點為其他情況下的節點,則進行相應的旋轉,直到該結點為上述情況之一,然後進行刪除。

3、如何使treap平衡

Treap中的節點不僅滿足BST的性質,還滿足最小堆的性質。因此需要通過旋轉來調整二叉樹的結構,在維護Treap的旋轉操作有兩種:左旋和右旋,(注意:無論怎麼旋轉二叉查詢樹的性質是不能改變的)

4.查詢

和一般的二叉搜尋樹一樣,但是由於Treap的隨機化結構,可以證明Treap中查詢的期望複雜度是O(log n)。

5.分離

要把一個Treap按大小分成兩個Treap,只要在需要分開的位置加一個虛擬節點,然後旋至根節點刪除,左右兩個子樹就是得出的兩個Treap了。根據二叉搜尋樹的性質,這時左子樹的所有節點都小於右子樹的節點。時間相當於一次插入操作的複雜度,也就是O(log n)。

6、查詢最大值和最小值

根據Treap的性質可以看出最左非空子節點就是最小值,同理最右非空子節點就是最大值(同樣也是BST的性質)

7、前驅與後繼

定義:前驅,查詢該元素在平衡樹中不大於該元素的最大元素;後繼查詢該元素在平衡樹中不小於該元素的最小元素。 從定義中看出,求一個元素在平衡樹中的前驅和後繼,這個元素不一定是平衡樹中的值,而且如果這個元素就是平衡樹中的值,那麼它的前驅與後繼一定是它本身。 求前驅的基本思想:貪心逼近法。在樹中查詢,一旦遇到一個不大於這個元素的值的節點,更新當前的最優的節點,然後在當前節點的右子樹中繼續查詢,目的是希望能找到
一個更接近於這個元素的節點。如果遇到大於這個元素的值的節點,不更新最優值,節點的左子樹中繼續查詢。直到遇到空節點,查詢結束,當前最優的節點的值就是要求的前
驅。求後繼的方法與上述相似,只是要找不小於這個元素的值的節點。
演算法說明: 求前驅:
1. 從根節點開始訪問,初始化最優節點為空節點;
2. 如果當前節點的值不大於要求前驅的元素的值,更新最有節點為當前節點,訪問當前節點的右子節點;
3. 如果當前節點的值大於要求前驅的元素的值,訪問當前節點的左子節點;
4. 如果當前節點是空節點,查詢結束,最優節點就是要求的前驅。

求後繼:
1. 從根節點開始訪問,初始化最優節點為空節點;
2. 如果當前節點的值不小於要求前驅的元素的值,更新最有節點為當前節點,訪問當前節點的左子節點;
3. 如果當前節點的值小於要求前驅的元素的值,訪問當前節點的右子節點;
4. 如果當前節點是空節點,查詢結束,最優節點就是要求的後繼。

8、當前節點子樹的大小

Treap 是一種排序的資料結構,如果我們想查詢第 k 小的元素或者詢問某個元素在 Treap 中從小到大的排名時,我們就必須知道每個子樹中節點的個數。我們稱以一個子樹的所有節點的權值之和,為子樹的大小。由於插入、刪除、旋轉等操作,會使每個子樹的大小改變,所以我們必須對子樹的大小進行動態的維護。 對於旋轉,我們要在旋轉後對子節點和根節點分別重新計算其子樹的大小。 對於插入,新建立的節點的子樹大小為 1。在尋找插入的位置時,每經過一個節點,都要先使以它為根的子樹的大小增加 1,再遞迴進入子樹查詢。 對於刪除,在尋找待刪除節點,遞迴返回時要把所有的經過的節點的子樹的大小減少 1。要注意的是,刪除之前一定要保證待刪除節點存在於 Treap 中。 下面給出左旋操作如何計運算元樹大小的程式碼,右旋很類似。 //這裡需要注意的是,每個節點可能有重複的,重複的數目是用cnt來記錄的,因此最後需要加上cnt

9、查詢第K小元素

首先,在一個子樹中,根節點的排名取決於其左子樹的大小,如果根節點有權值 cnt,則根節點 P 的排名是一個閉區間 A,且 A = [P->left->size + 1,P->left->size + P->cnt]。根據此,我們可以知道,如果查詢排名第 k 的元素,k∈A,則要查詢的元素就是 P 所包含元素。如果 k<A,那麼排名第 k 的元素一定在左子樹中,且它還一定是左子樹的排名第 k 的元素。如果 k>A,則排名第 k 的元素一定在右子樹中,是右子樹排名第 k-(P->left->size + P->cnt)的元素(P->left->size指的是p節點左子樹的大小
演算法思想: 1. 定義 P 為當前訪問的節點,從根節點開始訪問,查詢排名第 k 的元素;                                                          
2. 若滿足 P->left->size + 1 <=k <= P->left->size + P->cnt,則當前節點包含的元素就是排名第 k 的元素;
3. 若滿足 k <P->left->size+ 1,則在左子樹中查詢排名第 k 的元素;
4. 若滿足 k >P->left->size + P->cnt,則在右子樹中查詢排名第 k-(P->left->size + P->cnt)的元素。

10、求某個元素的排名

演算法思想: 1. 定義 P 為當前訪問的節點,cur 為當前已知的比要求的元素小的元素個數。從根節點開始查詢要求的元素,初始化 cur 為 0;
2. 若要求的元素等於當前節點元素,要求的元素的排名為區間[P->left->size + cur + 1, P->left->size + cur + P->cnt]內任意整數;
3. 若要求的元素小於當前節點元素,在左子樹中查詢要求的元素的排名;
4. 若要求的元素大於當前節點元素,更新 cur 為 cur + P->left->size+P->cnt,在右子樹中查詢要求的元素的排名。

下面是程式碼實現:

package com.yc.tree;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Random;

public class THead <T extends Comparable<T>>{
	public class Node{
		T data;
		int priority; //隨機權值(修改值fix)
		Node parent;
		Node left;
		Node right;
		public Node(T data, int priority, Node parent, Node left, Node right){
			this.data = data;
			this.priority = priority;
			this.parent = parent;
			this.left = left;
			this.right = right;
		}
		public String toString(){
			return "[data="+data+",priority="+priority+"]";
		}
	}
	//根
	private Node root;
	//
	private Random rd;
	//
	private static final int DEFAULT_RD = 1 << 10;
	
	public THead(){
		root = null;
	}
	public THead(T data){
		rd = new Random();
		root = new Node(data, rd.nextInt(DEFAULT_RD), null, null, null);
	}
	public THead(T data, int priority){
		root = new Node(data, priority, null, null, null);
	}
	
	//按照隨機的修改值新增節點
	public void add(T data){
		rd = new Random();
		add(data, rd.nextInt(DEFAULT_RD));
	}
	//按照指定的修改值新增節點
	public void add(T data, int priority){
		Node newNode = new Node(data, priority, null, null, null);
		if(root == null){
			root = newNode;
		}else{
			//找新插入節點的所屬的父節點
			Node current = root;
			Node parent = current;
			int result = 0;
			while(current != null){
				parent = current;
				result = data.compareTo(current.data);
				if(result > 0){ //新插入節點的data域大於當前節點的data域(這裡也埋下了當data域相等時會把新節點往左子樹插)
					current = current.right;
				}else{
					current = current.left;
				}
			}
			
			if(result > 0){ 
				parent.right = newNode;
			}else{
				parent.left = newNode;
			}
			newNode.parent = parent;
			parent = null;  //釋放臨時節點
			
			reBalanceForAdd(newNode);
			
		}
	}
	//刪除節點
	public void remove(T data){
		Node node = getNode(data);
		if(node != null){
			if(node.left == null && node.right == null){ //如果是葉子節點
				removeNeitherLeftAndRight(node);
			}else if(node.left != null && node.right == null){ //只有左子節點
				removeOnlyHasLeft(node);
			}else if(node.right != null && node.left == null){ //只有右子節點
				removeOnlyHasRight(node);
			}else{ //既有左子樹又有右子樹
				removeHaveLeftAndRight(node);
			}
		}
	}
	//最大值
	public T maxData(){
		Node current = root;
		Node maxNode = current;
		while(current != null){
			maxNode = current;
			current = current.right;
		}
		if(maxNode != null){
			return maxNode.data;
		}else{
			return null;
		}
	}
	//最小值
	public T minData(){
		Node current = root;
		Node minNode = current;
		while(current != null){
			minNode = current;
			current = current.left;
		}
		if(minNode != null){
			return minNode.data;
		}else{
			return null;
		}
	}
	//前驅
	public T frontData(T data){
		Node current = root;//當前節點
		Node frontNode = null;//前驅節點
		int result = 0;
		while(current != null){
			result = data.compareTo(current.data);
			if(result == 0){
				return current.data;
			}else if(result > 0){
				frontNode = current;
				current = current.right;
			}else{
				current = current.left;
			}
		}
		if(frontNode != null){
			return frontNode.data;
		}
		return null;
	}
	//後繼
	public T descendantData(T data){
		Node current = root;//當前節點
		Node frontNode = null;//後繼節點
		int result = 0;
		while(current != null){
			result = data.compareTo(current.data);
			if(result == 0){
				return current.data;
			}else if(result < 0){
				frontNode = current;
				current = current.left;
			}else{
				current = current.right;
			}
		}
		if(frontNode != null){
			return frontNode.data;
		}
		return null;
	}
	//8、當前節點子樹的大小
	//9、查詢第K小元素
	//10、求某個元素的排名
	//對於上面的三個問題需要在Node節點新增兩個域Size(表示子樹中節點個數),ctn(表示子樹中重複Data域的節點個數)
	//這裡就不做了,希望有興趣的同學去完成
	private void removeNeitherLeftAndRight(Node node){
		if(root == node){
			root = null;
		}else{
			if(node == node.parent.left){
				node.parent.left = null;
				node.parent = null;
			}else{
				node.parent.right = null;
				node.parent = null;
			}
		}
	}
	private void removeOnlyHasLeft(Node node){
		node.left.parent = node.parent;
		if(node.parent != null){
			if(node == node.parent.left){
				node.parent.left = node.left;
			}else{
				node.parent.right = node.left;
			}
		}
		
		if(root == node){
			root = node.left;
		}
		node.parent = node.left = node.right = null;
	}
	private void removeOnlyHasRight(Node node){
		node.right.parent = node.parent;
		if(node.parent != null){
			if(node == node.parent.left){
				node.parent.left = node.right;
			}else{
				node.parent.right = node.right;
			}
		}
		
		if(root == node){
			root = node.right;
		}
		node.parent = node.left = node.right = null;
	}
	private void removeHaveLeftAndRight(Node node){
		int result = 0;
		while(node.left != null && node.right != null){
			result = node.left.priority - node.right.priority;
			if(result <= 0){ //左子節點的修改值小於等於右子節點,則進行右旋
				type_right(node.left);
			}else{
				type_left(node.right);
			}
		}
		if(node.left == null && node.right == null){ //如果是葉子節點
			removeNeitherLeftAndRight(node);
		}else if(node.left != null && node.right == null){ //只有左子節點
			removeOnlyHasLeft(node);
		}else if(node.right != null && node.left == null){ //只有右子節點
			removeOnlyHasRight(node);
		}
	}
	//新增節點後都THead進行修復
	private void reBalanceForAdd(Node node) {
		//1.遞迴
		if(node.parent != null){
			int result = node.priority - node.parent.priority;
			if(result < 0){
				if(node == node.parent.left){
					type_right(node);
					reBalanceForAdd(node);
				}else if(node == node.parent.right){
					type_left(node);
					reBalanceForAdd(node);
				}
			}else{
				return;
			}
		}
		//2.迴圈(樓主不會寫QAQ.)
		/*int result = node.priority - node.parent.priority;
		while(node.parent != null && result < 0){
			if(result < 0){
				if(node == node.parent.left){
					type_right(node);
				}else{
					type_left(node);
				}
			}
		}*/
	}

	/**
	 * 節點右旋
	 * @param node
	 * 
	 * 			 │			│
	 * 		   p─┘          └─l
	 * 		  ││     ->		  ││ 
	 * 		l─┘└  			 ─┘└─p
	 * 		││					 ││
	 * 	   ─┘└				    ─┘└
	 * 	
	 */
	private void type_right(Node l){
		Node p = l.parent;
		
		l.parent = p.parent;
		if(p.parent != null){
			if(p == p.parent.left){
				p.parent.left = l;
			}else{
				p.parent.right = l;
			}
		}
		
		p.left = l.right;
		if(l.right != null){
			l.right.parent = p;
		}
		
		l.right = p;
		p.parent = l;
		
		if(root == p){
			root = l;
		}
	}
	/**
	 * 節點左旋
	 * @param r
	 * 
	 * 	│						 │
	 * 	└─p					   r─┘
	 *    ││     	->        ││
	 *   ─┘└─r				p─┘└─
	 *      ││				││
	 *     ─┘└─			   ─┘└─
	 */
	private void type_left(Node r){
		Node p = r.parent;
		
		r.parent = p.parent;
		if(p.parent != null){
			if(p == p.parent.left){
				p.parent.left = r;
			}else{
				p.parent.right = r;
			}
		}
		
		p.right = r.left;
		if(r.left != null){
			r.left.parent = p;
		}
		
		r.left = p;
		p.parent = r;
		
		if(root == p){
			root = r;
		}
	}
	
	//獲得指定元素的節點
	private Node getNode(T data){
		Node current = root;
		if(current == null){
			return null;
		}else{
			int result = 0;
			while(current != null){
				result = data.compareTo(current.data);
				if(result > 0){
					current = current.right;
				}else if(result < 0){
					current = current.left;
				}else{
					return current;
				}
			}
			return null;
		}
	}
	//廣度優先遍歷
	public List<Node> breadthFirstSearch(){
		return cBreadthFirstSearch(root);
	}
	private List<Node> cBreadthFirstSearch(Node node) {
		List<Node> nodes = new ArrayList<Node>();
		Deque<Node> deque = new ArrayDeque<Node>();
		if(node != null){
			deque.offer(node);
		}
		while(!deque.isEmpty()){
			Node first = deque.poll();
			nodes.add(first);
			if(first.left != null){
				deque.offer(first.left);
			}
			if(first.right != null){
				deque.offer(first.right);
			}
		}
		return nodes;
	}
	public static void main(String[] args) {
		THead<Integer> tree = new THead<Integer>();
		tree.add(12, 6);
		tree.add(8, 12);
		tree.add(23, 20);
		tree.add(16, 3);
		tree.add(45, 18);
		tree.add(2, 7);
		tree.add(9, 42);
		tree.add(16, 15);
		System.out.println( tree.breadthFirstSearch());
		System.out.println("9的前驅是:" + tree.frontData(9));
		System.out.println("9的後繼是:" + tree.descendantData(9));
		tree.remove(12);
		System.out.println( tree.breadthFirstSearch());
	}
}



測試結果為:
[[data=16,priority=3], [data=12,priority=6], [data=45,priority=18], [data=2,priority=7], [data=16,priority=15], [data=23,priority=20], [data=8,priority=12], [data=9,priority=42]]
9的前驅是:9
9的後繼是:9
[[data=16,priority=3], [data=2,priority=7], [data=45,priority=18], [data=8,priority=12], [data=23,priority=20], [data=16,priority=15], [data=9,priority=42]]