資料結構學習系列之二叉搜尋樹詳解!

矢澤妮可發表於2019-01-08

寫在前面

近期準備補一下資料結構,尤其是關於Tree系列的,其中,二叉樹(Binary Tree)可以算是最簡單的之一,所以打算從之入手,將各種Tree的結構和操作都進一步瞭解一遍,以來充實自己的閒餘時間!

本文主要圍繞二叉樹中最簡單的實現:二叉搜尋樹

介紹

二叉搜尋樹(Binary Search Tree),也叫作二叉排序樹(Binary Sort Tree),後文中統一簡稱為BST,顧名思義,它的每個結點的葉子數量不得超過兩個:

資料結構學習系列之二叉搜尋樹詳解!

BST插入都是按照一定的規則順序的,例如圖中的結點,F的左邊一定比F的值小,F右邊一定比F的值大,當然這些都是可以自定義的,不過到頭來,總是要有一定的規則進行儲存,這樣才會方便我們用來檢索需要的內容。

BST的基本操作相對於平衡樹或者紅黑樹來講算是非常簡單的,BST並不在乎整個樹的平衡性,僅僅保證了結點的有序規則不被打亂,這種問題將會導致大量結點下的查詢效能下降,因此便引出了更高階的樹種(之後也將會進一步學習)。因此,實現一個BST並不難,我們可以通過Java去實現一個簡單的BST來加深對二叉樹的瞭解!

實現

首先是實現思路,我們主要實現插入、查詢和刪除的功能,其中查詢是最簡單的,因為不需要變動整個樹的結構,刪除是較複雜的,接下來將會一一解析其實現思路。

插入

被插入結點與root結點做比較,如果大於,則取root的右結點繼續比較,反之則取左結點繼續比較,如果相等,則更新當前結點的值。

public Object insert(int index, Object value, Node cur) {
		while(true) {
			if(cur.index == index) {
				//命中則更新
				cur.value = value;
				break;
			}else {
				if(index < cur.index) {
					if(null == cur.left) {
						//無命中則新增
						cur.left = new Node(index, value, cur);
						cur.left.isLeft = true;
						size ++;
						break;
					}else {
						//指向左結點
						cur = cur.left;
					}
				}else{
					if(null == cur.right) {
						//無命中則新增
						cur.right = new Node(index, value, cur);
						size ++;
						break;
					}else {
						//指向右結點
						cur = cur.right;
					}
				}
			}
		}
		return value;
	}
複製程式碼

這就是一個簡單的深度查詢過程,如果使用遞迴可能更好理解一些,但是為了防止StackOverFlow,並沒用採用之。

程式碼中從cur這個結點開始,首先判斷是否命中目標,也就是cur.index == index如果為true,則代表命中,命中的話則直接更新。

如果沒有命中,則要向cur結點的分支方向搜尋,如果到了葉子結點還未命中,則要進行插入操作(直接加結點),否則就修改cur指標,繼續重複當前動作。

查詢

查詢類似於插入操作,不同的是查詢更為簡單:

public Node find(int index, Node cur) {
	while(cur != null && cur.index != index) {
		cur = index < cur.index ? cur.left : cur.right;
	}
	return cur;
}
複製程式碼

深度搜尋即可!

刪除

BST的刪除是比較複雜的,不像插入那般,只需要操作一個結點的left或者right變數引用就可以了,它涉及著多個結點的變化:

@Override
public Object remove(int index) {
	//首先尋找該結點
	Node target = find(index, root);
	if(target == null) {
		//該結點為空則直接返回null
		return null;
	}else {
		//優先考慮將被移除結點的左結點連線到右結點的左分支
		Node n = target.right != null ? target.right : target.left;

		if(target.left != null && target.right != null) {
			Node leftLoopStarter = n;
			//尋找目標結點的右結點下的最左結點
			while(leftLoopStarter.left != null) {
				leftLoopStarter = leftLoopStarter.left;
			}
			leftLoopStarter.setLeft(target.left);
		}

		if(target.parent == null) {
			//只剩下root結點不允許刪除
			if(target.left == null && target.right == null) {
				throw new RuntimeException("再刪就沒了!");
			}else {
				//當刪除root結點時,優先考慮將它的右結點作為新的root結點
				root = n;
				n.parent = null;
			}
		}else {
			//移除響應的結點
			if(target.isLeft) {
				target.parent.setLeft(n);
			}else {
				target.parent.setRight(n);
			}
		}
		size --;
		return target.value;
	}
}
複製程式碼

上述程式碼不足表達整個過程,我們以具體的demo來演示刪除過程,假如初始的樹形狀如下:

       8               
   4       12       
 2   6   10   16   
1 3 5 7 # 11 14 17
複製程式碼

移除結點1,葉子結點,直接移除:

       8               
   4       12       
 2   6   10   16   
# 3 5 7 # 11 14 17
複製程式碼

移除結點2,用右結點代替之:

       8               
   4       12       
 3   6   10   16   
# # 5 7 # 11 14 17
複製程式碼

移除結點4,其左結點放於右結點最左結點之後,並用右結點代替之:

       8               
   6       12       
 5   7   10   16   
3 # # # # 11 14 17
複製程式碼

移除結點16,同上:

       8               
   6       12       
 5   7   10   17   
3 # # # # 11 14 #
複製程式碼

移除結點8,同上:

               12                               
       10              17               
   6       11      14      #       
 5   7   #   #   #   #   #   #   
3 # # # # # # # # # # # # # # #
複製程式碼

格式化

在除錯樹的正確性時,無法避免要去檢視當前樹結點的狀態,所以將樹格式化並顯示出來很有必要,再著手之前,我們首先要找規律

               a 			          //4-15  首距2^4-1	無結點間隔
       b               c 			  //3-7   首距2^3-1	結點間隔15
   d       e       f       g 		  //2-3   首距2^2-1	結點間隔7
 h   i   g   k   l   m   n   o 		//1-1   首距2^1-1	結點間隔3
a a a a a a a a a a a a a a a a 	   //0-0   首距2^0-1	結點間隔1

設高度為n
求出首距的公式:2^n-1
求出兩結點間隔的公式:2^(n+1)-1
複製程式碼

由上引出,二叉樹的格式化還是很有規律的,我們便可以依據以上公式去寫一個演算法來格式化輸出BST:

@Override
public String toString() {
	//儲存樹形
	StringBuilder builder = new StringBuilder();
	//按層序保留結點
	List<Object> cache = new ArrayList<>(50);
	//使用棧對樹做層序遍歷
	Queue<Node> queue = new LinkedBlockingQueue<Node>();
	queue.add(root);

	int depth = 0; //臨時深度
	int maxDepth = getMaxDepth(); //最大深度
	while(! queue.isEmpty()) {
		Node cur = queue.poll();
		if(cur.left != null) {
			queue.add(cur.left);
		}else if(cur.depth() < maxDepth){
			//填補空缺
			queue.add(new Node(0, '#', cur));
		}
		if(cur.right != null) {
			queue.add(cur.right);
		}else if(cur.depth() < maxDepth){
			//填補空缺
			queue.add(new Node(0, '#', cur));
		}
		if(depth != cur.depth()) {
			//深度切換,將高度儲存
			depth = cur.depth();
			cache.add(depth);
		}
		//加入該結點
		cache.add(cur);
	}
	//將保留結點渲染為樹形
	for(int index = 0; index < cache.size(); index ++) {
		Object o = cache.get(index);
		if(o instanceof Integer) {
			builder.append(System.lineSeparator());
			builder.append(getOffset(maxDepth - (Integer)o));
		}else {
			if(index == 0) {
				builder.append(getOffset(maxDepth));
			}
			builder.append(((Node)o).value);
			builder.append(getOffset(maxDepth - ((Node)o).depth() + 1));
		}
	}
	return builder.toString();
}

/**
 * @param height 當前結點高度 = 最大深度 - 當前結點深度
 * @return 首距或者兩結點的間隔距離
 */
public String getOffset(double height) {
	StringBuilder builder = new StringBuilder();
	int count = (int) Math.pow(2d, height) - 1;
	while(count -- > 0) {
		builder.append(" ");
	}
	return builder.toString();
}

/**
 * @return 當前樹的最大深度
 */
public int getMaxDepth() {
	Queue<Node> queue = new LinkedBlockingQueue<Node>();
	queue.add(root);
	int depth = 0;
	while(! queue.isEmpty()) {
		Node cur = queue.poll();
		if(cur.left != null) queue.add(cur.left);
		if(cur.right != null) queue.add(cur.right);
		if(depth != cur.depth()) depth = cur.depth();
	}
	return depth;
}
複製程式碼

總結

通過BST引匯入門是一個很不錯的選擇!

原始碼地址:傳送門

相關文章