給你一個數列 7, 3, 10, 12, 5, 1, 9
,要求能夠高效的完成對數據的查詢和添加。
在 為什么需要樹這種數據結構 中講解了數組、鏈表數據結構的優缺點,簡單說:
-
數組訪問快,增刪慢
新增或移除時,需要整體移動數據
-
鏈表增刪快,訪問慢
只能從頭開始遍歷查找
那么利用 二叉排序樹(Binary Sort/Search Tree),既可以保證數據的檢索速度,同時也可以保證數據的插入、刪除、修改 的速度
二叉排序樹介紹
二叉排序樹(Binary Sort/Search Tree),簡稱 BST ,又稱二叉查找樹(Binary Search Tree),亦稱二叉搜索樹。。
對於二叉排序樹的任何一個 非葉子節點,要求如下:
- 左節點,比父節點
小
- 右節點,比父節點
大
特殊說明:如果有相同的值,可以將該節點放在左節點或右節點。當然,最理想的是沒有重復的值,比如 Mysql 中的 B 樹索引,就是以主鍵 ID 來排序的。
創建二叉排序樹的動圖:
比如對下面這個二叉排序樹增加一個節點:
- 從根節點開始,發現比 7 小,直接往左子樹查找,相當於直接折半了
- 比 3 小,再次折半
- 比 1 大:直接掛在 1 的右節點
這里提出一個疑問,如果添加元素為 4,不是應該掛在 3 的右側嗎?帶着這個疑問往下看。
創建與遍歷
在我前面的博客中講解了很多的二叉樹知識點,添加和遍歷相對簡單(如果不懂的就去學習前面的知識再來看,就簡單很多了),下面直接上代碼
/**
* 二叉排序樹
*/
public class BinarySortTreeTest {
/**
* 二叉排序樹 添加和遍歷 測試
*/
@Test
public void addTest() {
BinarySortTree tree = new BinarySortTree();
int[] arr = {7, 3, 10, 12, 5, 1, 9};
for (int i = 0; i < arr.length; i++) {
tree.add(new Node(arr[i]));
}
tree.infixOrder();
int item = 2;
tree.add(new Node(item));
System.out.println("\n添加新節點:" + item + " 到二叉排序樹中");
System.out.println("添加之后的中序順序:");
tree.infixOrder();
item = 4;
tree.add(new Node(item));
System.out.println("\n添加新節點:" + item + " 到二叉排序樹中");
System.out.println("添加之后的中序順序:");
tree.infixOrder();
}
}
/**
* 排序二叉樹
*/
class BinarySortTree {
Node root;
/**
* 添加節點
*
* @param node
*/
public void add(Node node) {
if (root == null) {
root = node;
return;
}
root.add(node);
}
/**
* 中序遍歷
*/
public void infixOrder() {
if (root == null) {
return;
}
root.infixOrder();
}
}
/**
* 節點類
*/
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
/**
* 添加節點:按照二叉排序樹的要求添加
*
* @param node
*/
public void add(Node node) {
if (node == null) {
return;
}
// 如果添加的值小於當前節點,則往左走
if (node.value < this.value) {
// 左節點為空,則直接掛在上面
if (this.left == null) {
this.left = node;
} else {
// 否則繼續往下查找
this.left.add(node);
}
} else {
// 往右走
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
}
/**
* 中序遍歷:剛好是從小到大的順序
*/
public void infixOrder() {
if (this.left != null) {
left.infixOrder();
}
System.out.println(this.value);
if (this.right != null) {
this.right.infixOrder();
}
}
}
輸出測試
1
3
5
7
9
10
12
添加新節點:2 到二叉排序樹中
添加之后的中序順序:
1
2
3
5
7
9
10
12
添加新節點:4 到二叉排序樹中
添加之后的中序順序:
1
2
3
4
5
7
9
10
12
現在來回答這個疑問,如果添加元素為 4,不是應該掛在 3 的右側嗎?
看輸出結果,沒有做任何的判定,對於 中序來說就是從小到大的輸出,所以這里針對的是 某一棵子樹,是如下規則:
對於二叉排序樹的任何一個 非葉子節點,要求如下:
- 左節點,比父節點小
- 右節點,比父節點大
所以並不需要針對已經存在的節點進行調整。
刪除(重點)
由於節點只有 left 和 right,是單向節點,要刪除一個節點:
-
先找到這個要刪除 目標節點
-
找到這個目標節點的 父節點
只有一種情況沒有父節點,那就是目標節點就是 root 節點
找到父節點之后,我們才可以刪掉目標節點,那么就有如下三種情況需要考慮:
-
目標節點是 葉子節點
①如果目標節點是 父節點的 left 節點,那么父節點的 left 置空
②如果目標節點是 父節點的 right 節點,那么父節點的 rigt 置空
-
目標節點是 有一個子節點 left 或則 right 的樹,那么就需要將目標節點的子節點提升到目標節點位置上
①如果目標節點是 父節點 的 left 節點,那么將目標節點的 left 或 right 節點設置為 父節點的 left 節點
②如果目標節點是 父節點 的 right 節點,那么將目標節點的 left 或 right 節點設置為父節點的 right 節點
簡單說:因為目標節點有一子節點,將目標節點刪除,將目標節點的子節點放到被刪除的位置上。
-
目標節點有 兩個子節點
-
以目標節點為根節點,往右子樹的,左子樹一直 找到最小的節點,刪除它,並持有它(保存它)
為什么要這么操作呢?原因是二叉排序樹的規則是 左節點,比父節點
小
,右節點,比父節點大
,因此樹的左邊的數是恆小於右邊的數的,所以你找左邊樹的最小的值替換到要刪除的目標節點是符合排序樹的規則的。注意!!不一定是找到左子樹中的一個 葉子節點,這一點一定要明白 -
把 目標節點 從 父節點 的 left 或 right 中刪掉(說是刪掉實則替換)
①刪掉的位置:替換上第 1 步中刪掉的最小節點。
②將 最小節點的 left 和 right 節點 重置為 目標節點的 left 和 right 節點
如上圖所示:目標節點是 10
1. 先往右側為起點:12 2. 再往左側找,且一直往左側找(也就是找最小):11,這個時候 11 的左已經為空了,那么 11 就是最小節點 3. 用臨時變量保存 11 這個節點,並刪除 4. 將 10 刪掉(用 11 這個節點替換該節點) 4. 將 11 掛在原來 10 的位置
-
動圖演示:
以上描述注意事項:
-
省略了需要判斷目標節點是父的 left 還是 right 節點,因為涉及到你刪除的時候,置空的是 父節點的 left 還是 right;這一步算是一個公共的描述步驟吧,重置的時候都需要,記得寫代碼的時候需要判斷下。
-
當要刪除的節點是:「有兩個子節點」和「只有一個子節點」的時候,要考慮到要刪除的是否是 root 節點,如果不做考慮,當要刪除的是 root 節點,直接操作「父節點」就會空指針異常。 這一點要注意到!!!
/**
* 二叉排序樹
*/
public class BinarySortTreeTest {
/**
* 刪除:葉子節點
*/
@Test
public void delete1() {
System.out.println("\n\n刪除葉子節點:2,5,9,12");
BinarySortTree tree = new BinarySortTree();
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
for (int i = 0; i < arr.length; i++) {
tree.add(new Node(arr[i]));
}
tree.infixOrder();
// 當只實現了刪除葉子節點時,這步驟是刪除不成功的
// tree.delete(1);
// System.out.println("刪除非葉子節點后的內容:");
// tree.infixOrder();
tree.delete(2);
tree.delete(5);
tree.delete(9);
tree.delete(12);
System.out.println("刪除后的內容:");
tree.infixOrder();
}
/**
* 刪除:只有一顆葉子節點的節點
*/
@Test
public void delete2() {
System.out.println("\n\n只有一顆葉子節點的節點:1");
BinarySortTree tree = new BinarySortTree();
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
for (int i = 0; i < arr.length; i++) {
tree.add(new Node(arr[i]));
}
tree.infixOrder();
tree.delete(1);
System.out.println("刪除后的內容:");
tree.infixOrder();
}
/**
* 刪除:有兩顆子節點的 節點
*/
@Test
public void delete3() {
System.out.println("\n\n有兩顆子節點的節點: 10");
BinarySortTree tree = new BinarySortTree();
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
for (int i = 0; i < arr.length; i++) {
tree.add(new Node(arr[i]));
}
tree.infixOrder();
tree.delete(10);
System.out.println("刪除節點后的內容:");
tree.infixOrder();
}
/**
* 刪除 root 節點
*/
@Test
public void deleteRoot() {
System.out.println("\n\n刪除 root 節點:7");
BinarySortTree tree = new BinarySortTree();
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
for (int i = 0; i < arr.length; i++) {
tree.add(new Node(arr[i]));
}
tree.infixOrder();
tree.delete(7);
System.out.println("刪除節點后的內容:");
tree.infixOrder();
}
/**
* 排序二叉樹
*/
class BinarySortTree {
Node root;
/**
* 添加節點
*
* @param node
*/
public void add(Node node) {
if (root == null) {
root = node;
return;
}
root.add(node);
}
/**
* 中序遍歷
*/
public void infixOrder() {
if (root == null) {
return;
}
root.infixOrder();
}
/**
* 查找目標節點
*
* @param value
* @return
*/
public Node searchTarget(int value) {
if (root == null) {
return null;
}
return root.searchTarget(value);
}
/**
* 查找目標節點的父節點
*
* @param value
* @return
*/
public Node searchParent(int value) {
if (root == null) {
return null;
}
if (root.value == value) {
return null;
}
return root.searchParent(value);
}
/**
* 刪除節點
*
* 注意:刪除節點的思路是找到 目標節點 和 父節點,利用這兩個節點就可以完成刪除了,
* 而不是去遞歸查找的。這一點需要明白,而且很重要。否則你將不知道遞歸如何寫
*
*
* @param value
*/
public void delete(int value) {
if (root == null) {
return;
}
Node target = searchTarget(value);
// 如果沒有找到目標節點,則返回
if (target == null) {
return;
}
// 如果找到了節點
// 並且,root 沒有子節點,則說明當前只有 root 一個節點,而且root就是目標節點
if (root.left == null && root.right == null) {
root = null;
return;
}
//找目標節點的父節點
Node parent = searchParent(value);
// 1. 如果目標節點是葉子節點
if (target.left == null && target.right == null) {
// 如果目標節點是 父節點的 左節點
if (parent.left != null && target.value == parent.left.value) {
parent.left = null;
return;
}
// 如果目標節點是 父節點的 右節點
if (parent.right != null && target.value == parent.right.value) {
parent.right = null;
return;
}
}
// 2. 如果目標節點有兩個子節點
else if (target.left != null && target.right != null) {
// 1. 以目標節點為 root 節點,往右子樹的左子樹中找最小的節點,用臨時變量保存,並刪掉;
// 2. 並把目標節點使用這個最小節點替換掉
// 可以有一個更簡單的方式實現,刪掉最小節點之后,直接將目標節點的 value 值替換為最小節點的值。 下面的實現沒有采用替換值的方式,而是采用替換節點的方式,看起來就麻煩一點,但這個方法是適合很多場景的。
// 以目標節點為 root 節點,往右子樹的左子樹中找最小的節點,用臨時變量保存,並刪掉;注意!!不一定是找到左子樹中的一個 葉子節點,這一點一定要明白
Node min = deleteRightTreeMin(target);
// 如果刪除的是 root 節點,全程不要操作 parent
if (parent == null) {
root = min;
min.right = target.right;
min.left = target.left;
return;
}
// 如果是父節點的 左節點
if (parent.left != null && target.value == parent.left.value) {
parent.left = min;
min.right = target.right;
min.left = target.left;
return;
}
// 如果是父節點的 右節點
if (parent.right != null && target.value == parent.right.value) {
parent.right = min;
min.right = target.right;
min.left = target.left;
return;
}
}
// 3. 如果目標節點有 1 個子節點
else {
// 注意!!!如果刪除的是 root 節點,全程不要操作 parent,否則會出現空指針異常
// 由於目標節點有一個節點,先拿到這個要替換掉目標節點的 節點
Node replaceNode = null;
// 要替換的節點,由於只有一個,不是左就是右
if (target.left != null) {
replaceNode = target.left;
} else {
replaceNode = target.right;
}
// 如果要刪除的是 root 節點
if (parent == null) {
root = replaceNode;
return;
}
// 如果是父節點的 左節點
if (parent.left != null && target.value == parent.left.value) {
parent.left = replaceNode;
return;
}
if (parent.right != null && target.value == parent.right.value) {
parent.right = replaceNode;
}
}
return;
}
/**
* 以目標節點為 root 節點,找到左子樹中最小的節點,並刪掉;也就是找到左子樹中的一個 葉子節點
*
* @param target
* @return
*/
private Node deleteRightTreeMin(Node target) {
Node min = target.right;
while (min.left != null) {
min = min.left;
}
delete(min.value);
return min;
}
}
}
/**
* 節點
*/
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
/**
* 搜索目標節點
*
* @param value
* @return
*/
public Node searchTarget(int value) {
if (value == this.value) {
return this;
} else if (value < this.value) {
if (left != null) {
return left.searchTarget(value);
}
} else {
if (right != null) {
return right.searchTarget(value);
}
}
return null;
}
/**
* 查找目標值的父節點
*
* @param value
* @return
*/
public Node searchParent(int value) {
// 本節點能匹配到左右兩節點其中一個等於,則父節點是本節點
if (left != null && left.value == value
|| right != null && right.value == value
) {
return this;
}
if (value < this.value && left != null) {
return left.searchParent(value);
}
if (value >= this.value && right != null) {
return right.searchParent(value);
}
return null;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
測試輸出
刪除葉子節點:2,5,9,12
1
2
3
5
7
9
10
12
刪除后的內容:
1
3
7
10
只有一顆葉子節點的節點:1
1
2
3
5
7
9
10
12
刪除后的內容:
2
3
5
7
9
10
12
有兩顆子節點的節點: 10
1
2
3
5
7
9
10
12
刪除節點后的內容:
1
2
3
5
7
9
12
刪除 root 節點:7
1
2
3
5
7
9
10
12
刪除節點后的內容:
1
2
3
5
9
10
12
看懂上面的代碼后再來看下面的完整代碼。
完整代碼
/**
* 二叉排序樹
*/
public class BinarySortTreeTest {
/**
* 二叉排序樹添加和遍歷測試
*/
@Test
public void addTest() {
BinarySortTree tree = new BinarySortTree();
int[] arr = {7, 3, 10, 12, 5, 1, 9};
for (int i = 0; i < arr.length; i++) {
tree.add(new Node(arr[i]));
}
tree.infixOrder();
int item = 2;
tree.add(new Node(item));
System.out.println("\n添加新節點:" + item + " 到二叉排序樹中");
System.out.println("添加之后的中序順序:");
tree.infixOrder();
item = 4;
tree.add(new Node(item));
System.out.println("\n添加新節點:" + item + " 到二叉排序樹中");
System.out.println("添加之后的中序順序:");
tree.infixOrder();
}
/**
* 刪除:葉子節點
*/
@Test
public void delete1() {
System.out.println("\n\n刪除葉子節點:2,5,9,12");
BinarySortTree tree = new BinarySortTree();
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
for (int i = 0; i < arr.length; i++) {
tree.add(new Node(arr[i]));
}
tree.infixOrder();
// 當只實現了刪除葉子節點時,這步驟是刪除不成功的
// tree.delete(1);
// System.out.println("刪除非葉子節點后的內容:");
// tree.infixOrder();
tree.delete(2);
tree.delete(5);
tree.delete(9);
tree.delete(12);
System.out.println("刪除后的內容:");
tree.infixOrder();
}
/**
* 刪除:只有一顆葉子節點的節點
*/
@Test
public void delete2() {
System.out.println("\n\n只有一顆葉子節點的節點:1");
BinarySortTree tree = new BinarySortTree();
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
for (int i = 0; i < arr.length; i++) {
tree.add(new Node(arr[i]));
}
tree.infixOrder();
tree.delete(1);
System.out.println("刪除后的內容:");
tree.infixOrder();
}
/**
* 刪除:有兩顆子節點的 節點
*/
@Test
public void delete3() {
System.out.println("\n\n有兩顆子節點的節點: 10");
BinarySortTree tree = new BinarySortTree();
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
for (int i = 0; i < arr.length; i++) {
tree.add(new Node(arr[i]));
}
tree.infixOrder();
tree.delete(10);
System.out.println("刪除節點后的內容:");
tree.infixOrder();
}
/**
* 刪除 root 節點
*/
@Test
public void deleteRoot() {
System.out.println("\n\n刪除 root 節點:7");
BinarySortTree tree = new BinarySortTree();
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
for (int i = 0; i < arr.length; i++) {
tree.add(new Node(arr[i]));
}
tree.infixOrder();
tree.delete(7);
System.out.println("刪除節點后的內容:");
tree.infixOrder();
}
/**
* 排序二叉樹
*/
class BinarySortTree {
Node root;
/**
* 添加節點
*
* @param node
*/
public void add(Node node) {
if (root == null) {
root = node;
return;
}
root.add(node);
}
/**
* 中序遍歷
*/
public void infixOrder() {
if (root == null) {
return;
}
root.infixOrder();
}
/**
* 查找目標節點
*
* @param value
* @return
*/
public Node searchTarget(int value) {
if (root == null) {
return null;
}
return root.searchTarget(value);
}
/**
* 查找父節點
*
* @param value
* @return
*/
public Node searchParent(int value) {
if (root == null) {
return null;
}
if (root.value == value) {
return null;
}
return root.searchParent(value);
}
/**
* 刪除節點
*
* 注意:刪除節點的思路是找到 目標節點 和 父節點,利用這兩個節點就可以完成刪除了,
* 而不是去遞歸查找的。這一點需要明白,而且很重要。否則你將不知道遞歸如何寫
*
*
* @param value
*/
public void delete(int value) {
if (root == null) {
return;
}
Node target = searchTarget(value);
// 如果沒有找到目標節點,則返回
if (target == null) {
return;
}
// 如果找到了節點
// 並且,root 沒有子節點了,則說明當前只有 root 一個節點,並且root是目標節點
if (root.left == null && root.right == null) {
root = null;
return;
}
Node parent = searchParent(value);
// 1. 如果目標節點是葉子節點
if (target.left == null && target.right == null) {
// 如果目標節點是 父節點的 左節點
if (parent.left != null && target.value == parent.left.value) {
parent.left = null;
return;
}
// 如果目標節點是 父節點的 右節點
if (parent.right != null && target.value == parent.right.value) {
parent.right = null;
return;
}
}
// 2. 如果目標節點有兩顆子節點
else if (target.left != null && target.right != null) {
// 以目標節點為 root 節點,找到左子樹中最小的節點,並刪掉;也就是找到左子樹中的一個 葉子節點
Node min = deleteRightTreeMin(target);
// 如果刪除的是 root 節點,全程不要操作 parent
if (parent == null) {
root = min;
min.right = target.right;
min.left = target.left;
return;
}
// 如果是父節點的 左節點
if (parent.left != null && target.value == parent.left.value) {
parent.left = min;
min.right = target.right;
min.left = target.left;
return;
}
// 如果是父節點的 右節點
if (parent.right != null && target.value == parent.right.value) {
parent.right = min;
min.right = target.right;
min.left = target.left;
return;
}
}
// 3. 如果目標節點有 1 顆子節點
else {
// 如果刪除的是 root 節點,全程不要操作 parent
// 因為只有一顆節點,不是左就是右邊
/* if (target.left != null) {
// 刪除的如果是 root 節點
if (parent == null) {
root = target.left;
return;
}
// 如果是父節點的 左節點
if (parent.left != null && target.value == parent.left.value) {
parent.left = target.left;
return;
}
if (parent.right != null && target.value == parent.right.value) {
parent.right = target.left;
}
} else {
// 刪除的如果是 root 節點
if (parent == null) {
root = target.right;
return;
}
// 如果是父節點的 右節點
if (parent.left != null && target.value == parent.left.value) {
parent.left = target.right;
return;
}
if (parent.right != null && target.value == parent.right.value) {
parent.right = target.right;
}
}
*/
// 上面的寫法重構后為下面這樣
// 由於目標節點有一顆節點,先拿到這個要替換掉目標節點的 節點
Node replaceNode = null;
// 要替換的節點,由於只有一個,不是左就是右
if (target.left != null) {
replaceNode = target.left;
} else {
replaceNode = target.right;
}
// 如果要刪除的是 root 節點
if (parent == null) {
root = replaceNode;
return;
}
// 如果是父節點的 左節點
if (parent.left != null && target.value == parent.left.value) {
parent.left = replaceNode;
return;
}
if (parent.right != null && target.value == parent.right.value) {
parent.right = replaceNode;
}
}
return;
}
/**
* 以目標節點為 root 節點,找到左子樹中最小的節點,並刪掉;也就是找到左子樹中的一個 葉子節點
*
* @param target
* @return
*/
private Node deleteRightTreeMin(Node target) {
Node min = target.right;
while (min.left != null) {
min = min.left;
}
delete(min.value);
return min;
}
}
}
/**
* 節點
*/
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
/**
* 添加節點:按照排序二叉樹的要求添加
*
* @param node
*/
public void add(Node node) {
if (node == null) {
return;
}
// 如果添加的值小於當前節點,則往左走
if (node.value < value) {
// 左節點為空,則直接掛在上面
if (left == null) {
left = node;
} else {
// 否則繼續往下查找
left.add(node);
}
} else {
// 往右走
if (right == null) {
right = node;
} else {
right.add(node);
}
}
}
/**
* 中序遍歷:剛好是從小到大的順序
*/
public void infixOrder() {
if (left != null) {
left.infixOrder();
}
System.out.println(value);
if (right != null) {
right.infixOrder();
}
}
/**
* 搜索目標節點
*
* @param value
* @return
*/
public Node searchTarget(int value) {
if (value == this.value) {
return this;
} else if (value < this.value) {
if (left != null) {
return left.searchTarget(value);
}
} else {
if (right != null) {
return right.searchTarget(value);
}
}
return null;
}
/**
* 查找目標值的父節點
*
* @param value
* @return
*/
public Node searchParent(int value) {
// 本節點能匹配到左右兩節點其中一個等於,則父節點是本節點
if (left != null && left.value == value
|| right != null && right.value == value
) {
return this;
}
if (value < this.value && left != null) {
return left.searchParent(value);
}
if (value >= this.value && right != null) {
return right.searchParent(value);
}
return null;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}