前言
這是這個系列上的第二篇文章,如果你還沒有了解二叉樹的話,可以先看我的文章中閱讀量最高的二叉樹(從建樹、遍歷到存儲)Java.
一、定義
滿是學究氣息的文字定義我們先不看,還是沿用看圖說話的慣例。
“數無形時少直覺,形少數時難入微”,所以文字上的定義也是不能少的,其實我們通過上圖可以自己總結出來。
對於二叉樹上的任意節點,若它的左子樹不爲空,那麼左子樹上所有節點的值均小於它的值,若它的右子樹不爲空,那麼右子樹上所有節點的值均大於它的值。
構造一棵二叉排序樹的目的,其實並不是爲了排序,而是爲了提高查找、增加和刪除數據的速度。因爲在一個有序的集合中查找數據的速度總是快於在無序集合中,而且二叉樹排序樹這種非線性的結構也有利於增加和刪除節點(類似鏈表)。
二、查找
在一本很暢銷的數據結構書上這一部分的算法實現用的遞歸,我不是很贊同,雖然遞歸寫起來代碼很簡潔,但是對於初學者而言比較難理解,隨着一層一層的遞歸調用下去,跟着跟着就滿腦漿糊了。
所以這裏我們還是用循環來寫,方便理解。
代碼
public boolean contains(int e) {
//用current記錄根節點
Node current = root;
while (current != null) {
//相等,說明找到了,返回true
if (e == current.data) {
return true;
//要查找的數比根節點大,指向根節點的右兒子
} else if (e > current.data) {
current = current.right;
//要查找的數比根節點下,指向根節點的左兒子
} else {
current = current.left;
}
}
//找不到,返回false
return false;
}
這只是一個代碼片段,最後提供完整代碼,更具有邏輯性。
圖解
圖一:初始狀態,若查找80。
圖二:
(1)node爲根節點,不爲空,進入while循環;
(2)要查找的是80,node.data的值是60,80大於60,node等於node.right。
圖三:
(1)此時node爲下圖中的紅色節點,不爲空,進入while循環;
(2)要查找的是80,node.data是75,80大於75,node等於node.right。
圖四:
(1)此時node爲下圖中的紅色節點,不爲空,進入while循環;
(2)要查找的80,node.data是80,80等於80,返回true。
三、增加節點
當要插入的數據在二叉排序樹中不存在時,插入成功並返回true,否則返回false。
代碼
public boolean add(int e) {
if (root == null) {
root = new Node(e);
return true;
}
//記錄current的父節點
Node father = null;
Node current = root;
while (current != null) {
if (e > current.data) {
father = current;
current = current.right;
} else if (e < current.data) {
father = current;
current = current.left;
} else {
return false;
}
}
if (e > father.data) {
father.right = new Node(e);
} else {
father.left = new Node(e);
}
return true;
}
請對比查找代碼:
public boolean contains(int e) {
Node current = root;
while (current != null) {
if (e == current.data) {
return true;
} else if (e > current.data) {
current = current.right;
} else {
current = current.left;
}
}
return false;
}
所以如果已經理解查找的代碼,對於增加的代碼也不難理解。
四、刪除節點
“請佛容易送佛難”,從上面的代碼我們知道增加節點的操作其實並不麻煩,一半代碼還和“查找”操作的代碼相同,但是要刪除節點的話就沒那麼輕鬆了,因爲要考慮到刪除該節點後整個二叉樹還滿住二叉排序樹的要求。
1.單身狗
即如下圖中的深色節點,這個要刪除的節點是一個可憐的單身狗。。。
那麼就把它直接刪除!我們可以看到直接刪除後對整個二叉樹排序樹並沒有什麼影響。
2.獨生子
這個節點的後代雖然只有一棵獨苗,但也是有的。
當他駕崩後,這皇位要傳給誰呢?當然是他的獨生子69。而且我們把69放到62的位置發現二叉排序樹的性質並沒有被破壞,如下圖。
當然,若這顆獨苗也有後代,情況是一樣的。
3.多子多孫
“哈哈哈哈!我可是多福多壽,多子多孫之人!”又來了一個節點。這個節點可是兒孫滿堂,那麼他駕崩後,“奪嫡”自然是不可避免的。
30他駕崩了,留下了大好河山,宗室諸王們蠢蠢欲動,誰都想過一把皇帝癮,經過一番比拼,發現25或者35都可以繼承皇位,那就二者選其一吧。
“等等!怎麼就25或者35了?”一個外國人不明所以地問道。原來在二叉排序樹內部皇位的繼承不是看誰最強而是看誰最像的。在30的衆多子孫中只有25和35是最像他的(和他的差距最小)。
但是在實際操作中我們不一定要真的把30刪掉,可以稍稍變通一下,先找到繼承人將他的值賦給要刪除的節點,再將繼承人刪除。
看到這裏有些人不禁會想這個方法聽上去簡單,實際中要怎麼用算法找到繼承人呢?之後會與大家分享兩句口訣,保證讓大家按圖索驥,順利找到這兩個節點。
(1)25繼承
口訣一:左轉,向右到盡頭。
我們再來看一個例子,如下圖:
注意:對於繼承人來說他只有單身狗和左獨生子這兩種情況,不存在右獨生子和多子多孫的情況,不明的話,再看看口訣就明白了,如果他存在右子,那麼皇位還能輪到他繼承嗎?口訣可是向右到盡頭!
(2)35繼承
口訣二:右轉,向左到盡頭。
同樣我們再來看一個例子,如下圖:
注意:同理,對於第二種情況的繼承人來說他只有單身狗和右獨生子這兩種情況,不存在左獨生子和多子多孫的情況,如果他存在左子,就不是他繼承皇位了。
五、完整代碼
這是我在學習過程中模仿Java集合類寫的一個二叉排序樹類,歡迎大家指導。
BinarySortTree類:
import java.util.LinkedList;
public class BinarySortTree {
//記錄根節點
Node root;
public BinarySortTree() {
}
//增
public boolean add(int e) {
if (root == null) {
root = new Node(e);
return true;
}
//記錄current的父節點
Node father = null;
Node current = root;
while (current != null) {
if (e > current.data) {
father = current;
current = current.right;
} else if (e < current.data) {
father = current;
current = current.left;
} else {
return false;
}
}
if (e > father.data) {
father.right = new Node(e);
} else {
father.left = new Node(e);
}
return true;
}
/* 刪
*
* */
public boolean remove(int e) {
Node father = null;
Node current = root;
while (current != null) {
if (current.data == e)
return removeNode(current, father, e);
else if (e > current.data) {
father = current;
current = current.right;
} else {
father = current;
current = current.left;
}
}
return false;
}
final boolean removeNode(Node current, Node father, int e) {
//若左兒子爲空,右兒子繼承
if (current.left == null) {
if (current.data > father.data) {
father.right = current.right;
} else {
father.left = current.right;
}
//方便垃圾回收器回收
current = null;
}
//若右兒子爲空,左兒子繼承
else if (current.right == null) {
if (current.data > father.data) {
father.right = current.left;
} else {
father.left = current.left;
}
current = null;
}
//多子多孫
else {
//用於記錄heir的父節點
Node heirFather = null;
/* heir(繼承人)
* heir = current.left即口訣一中的左轉
* */
Node heir = current.left;
//即口訣一中的向右到盡頭
while (heir.right != null) {
heirFather = heir;
heir = heir.right;
}
//繼承人的值賦給要被刪除的節點
current.data = heir.data;
if (heir.data > heirFather.data) {
heirFather.right = heir.left;
} else {
heirFather.left = heir.left;
}
heir = null;
}
return true;
}
//查找
public boolean contains(int e) {
Node current = root;
while (current != null) {
if (e == current.data) {
return true;
} else if (e > current.data) {
current = current.right;
} else {
current = current.left;
}
}
return false;
}
//層次遍歷
public void levelOrder() {
if (root == null) {
return;
}
LinkedList<Node> q = new LinkedList<Node>();
q.addFirst(root);
Node current;
while (!q.isEmpty()) {
current = q.removeLast();
System.out.print(current.data + " -> ");
if (current.left != null)
q.addFirst(current.left);
if (current.right != null)
q.addFirst(current.right);
}
}
private static class Node {
int data;
Node right;
Node left;
Node(int data) {
this.data = data;
}
}
}
測試類:
public class Demo {
public static void main(String[] args) {
BinarySortTree tree = new BinarySortTree();
tree.add(60);
tree.add(30);
tree.add(75);
tree.add(15);
tree.add(50);
tree.add(80);
tree.add(10);
tree.add(25);
tree.add(40);
tree.add(55);
tree.add(19);
tree.add(35);
System.out.print("層次遍歷(刪除前):");
tree.levelOrder();
System.out.println();
//刪除
tree.remove(30);
System.out.print("層次遍歷(刪除後):");
tree.levelOrder();
System.out.println();
System.out.print("是否已存在:");
System.out.print(tree.contains(80));
}
}
測試類中構造出的二叉樹排序樹長這樣:
結果:
層次遍歷(刪除前):60 -> 30 -> 75 -> 15 -> 50 -> 80 -> 10 -> 25 -> 40 -> 55 -> 19 -> 35 ->
層次遍歷(刪除後):60 -> 25 -> 75 -> 15 -> 50 -> 80 -> 10 -> 19 -> 40 -> 55 -> 35 ->
是否已存在:true
這裏我只測試了一種情況,有興趣的可以將代碼粘貼複製到編譯器上多測試幾種情況。
寫在最後
分享這篇文章的目的不僅僅是與大家一起討論二叉排序樹的使用,更大的目的是爲了對付JDK1.8中的HashMap,其中用到了紅黑樹這種數據結構。
所以接下來還會分享其他類型二叉樹的使用,總的路線爲:二叉樹——>二叉排序樹(二叉搜索樹)——>紅黑樹,再加一個哈希表(hash table)。
這是這條路線上的第二篇文章,之前還有一篇二叉樹的二叉樹(從建樹、遍歷到存儲)Java.有興趣的話可以看看,如果喜歡這個系列的話可以持續關注哦。