寫在前面
近期準備補一下資料結構,尤其是關於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引匯入門是一個很不錯的選擇!
原始碼地址:傳送門