初入數據結構的二叉搜索樹(Binary Search Tree)以及Java實現
如果覺得對你有幫助,能否點個贊或關個注,以示鼓勵筆者呢?!博客目錄 | 先點這裏
-
二叉搜索樹
- 什麼是二叉搜索樹
- 二叉搜索樹的特性
- 二叉搜索樹的缺陷
-
Java代碼實現
- 二叉搜索樹的日常方法
二叉樹搜索樹
什麼是二叉搜索樹?
二叉搜索樹
,又叫二叉查找樹
,二叉排序樹
,二分搜索|查找|排序樹
。其實都是一樣的東西,我們這裏就統一一下用詞“二叉搜索樹”。
二叉樹搜索樹的定義:
- 首先二叉搜索樹也是一棵二叉樹
- 二叉搜索樹的任意結點A, 其左子樹的所有結點的值都小於結點A的值,其右子樹的所有結點都大於結點A的值;前提是任意結點A的左右子樹不爲空
- 二叉搜索樹的左右子樹也是一棵二叉搜索樹
- 二叉搜索樹沒有值相等的結點
所以總結起來,二叉樹搜索樹就是一棵,左孩子小,右孩子大,且沒有相同大小值的有序樹
二叉搜索樹的特性
- 二叉搜索樹的基礎操作與樹的高成正比,通常是二分查找算法的實踐,如果一棵二叉搜索樹是一棵
完全二叉樹
的話,查找值的最壞時間複雜度就是O(logn)
, 但是如果這棵二叉搜索樹是一棵線性樹
(斜樹
),那就是O(n)
- 二叉搜索樹所存儲的元素必須具有可比較性。能比較是二叉搜索樹的基礎,不能比較,就無從談起二叉樹搜索樹
二叉搜索樹的缺陷
我們從上圖可以看出,左邊的樹和右邊的樹,都能滿足二叉樹搜索樹的定義。從代碼實現角度來看,如果後續插入的元素都是一致的,想成爲左邊的樹,還是右邊的樹,完全取決於整顆樹的根結點的大小。如果整顆樹的傾向是一顆右邊的樹,那麼這棵樹的結點就不太平衡,也不利於做二分搜索,當查看一個元素時,時間複雜度會退回成O(n)
所以一顆二叉搜索樹的好壞,往往就取決了根結點開頭的取值,但是我們又無法左右用戶對根結點取什麼值。這就是普通二叉搜索樹的一個很大的缺陷,容易造成樹的傾斜或層級過深。
爲了解決二叉搜索樹傾斜等因素造成查詢效率降低的問題,就此引進了平衡二叉搜索樹AVL
和紅黑樹
等概念
Java代碼實現
二叉搜索樹的日常方法
- 在二叉搜索樹中添加元素:
add()
| 遞歸 - 查看二叉搜索樹是否含有某個元素:
contains()
| 遞歸 - 找出二叉搜索樹中的最小值和最小值 :
minimum()
、maximum()
| 遞歸與迭代 - 刪除二叉搜索樹中的最小結點和最大結點:
removeMin()
、removeMax()
| 遞歸 - 刪除二叉搜索樹中的任意結點:
remove()
| 遞歸
如果你想了解二叉樹的前中後序遍歷以及層次遍歷的實現方式,你可以去這裏瞭解
在二叉搜索樹中添加元素:add()
| 遞歸
這裏提供了兩種實現方式,一種是常規版本
,代碼清晰簡單易懂,另一種是優化版
,代碼簡潔高效,但相對不直觀易懂; 之後的其他方法實現都是按照優化版的邏輯去實現的
直觀常規版本:
/**
* 添加一個元素
*
* @param data
*/
public void add(T data) {
//如果根結點爲空,那麼新結點就是根結點
if (this.rootNode == null) {
this.nodeCount++;
this.rootNode = new TreeNode<>(data);
}
//不然就遞歸以rootNode爲根結點的二叉搜索樹
add(this.rootNode, data);
}
/**
* 插入新元素到以root爲根結點的二叉樹搜索樹
*
* @param root
* @param data
*/
private void add(TreeNode<T> root, T data) {
//1. 二叉搜索樹的結點元素不允許有重複
if (root.data.compareTo(data) == 0) {
throw new RuntimeException("二叉搜索樹所存儲的元素不允許相等,已存在");
}
//2. 如果添加的元素大小小於相對根結點的值
if (data.compareTo(root.data) < 0) {
//且相對根結點的左孩子是null, 那我們直接將新結點賦予給相對根結點的左孩子,遞歸退出條件
if (root.lchild == null) {
this.nodeCount++;
root.lchild = new TreeNode<>(data);
return;
}
//否則繼續遞歸將左孩子作爲根結點的子樹
add(root.lchild, data);
}
//3. 如果新增元素的大小大於相對根結點的值
if (data.compareTo(root.data) > 0) {
//且相對根結點的右孩子爲null,那麼我們就將新結點賦予給相對根結點的右孩子,遞歸退出條件
if (root.rchild == null) {
this.nodeCount++;
root.rchild = new TreeNode<>(data);
return;
}
//否則繼續遞歸將右孩子作爲根結點的子樹
add(root.rchild, data);
}
}
優化版本:
/**
* 向二分搜索樹添加一個元素 | 遞歸
* 1. 統一操作,不用對根結點進行額外的判斷
* 2. 代碼更簡潔,只是相對不好理解
*
* @param data
*/
public void add(T data) {
this.rootNode = add(this.rootNode, data);
}
/**
* 向根結點爲root的二分搜索樹添加元素 | 改進型,不需要對根結點做特殊處理
* 1. 遞歸推出條件是,當遞歸到空樹時,那麼新元素就是該空樹的根結點
* 2. 每一次的遞歸返回的結點,都是用於給上層做關聯的,即遞歸是找到新元素要存儲的位置,然後將新元素結點返回給父結點去操作。parent.child = newNode
*
* @param root
* @param data
* @return
*/
private TreeNode<T> add(TreeNode<T> root, T data) {
//遞歸退出條件,只要相對根結點爲null,即代表目前我是一棵空樹,新增元素就要作爲我這棵空樹的根結點,直接返回新結點給上層操作
if (root == null) {
this.nodeCount++;
return new TreeNode<>(data);
}
//不允許存在相同元素
if (data.compareTo(root.data) == 0) {
throw new RuntimeException("二叉搜索樹所存儲的元素不允許相等,已存在");
//如果新增元素小於當前子樹的根結點的值,左遞歸
} else if (data.compareTo(root.data) < 0) {
root.lchild = add(root.lchild, data);
//如果新增元素大於當前子樹的根結點的值,右遞歸
} else if (data.compareTo(root.data) > 0) {
root.rchild = add(root.rchild, data);
}
//將當前子樹的根結點返回出去,讓上層做關聯
return root;
}
區別:
- 常規版本的子遞歸不需要返回結點給上層做關聯,代碼簡單直觀; 優化版本的每次遞歸都會得到一個結點用於返回,給上層調用做關聯
- 常規版本需要給樹的第一個結點做特殊處理,但優化版本不需要,可以將所有結點一視同仁
查看二叉搜索樹是否含有某個元素:contains()
| 遞歸
這裏的實現說白了就是一個二分查找
/**
* 二叉樹的查詢 | 遞歸
*
* @param data
* @return
*/
public boolean contains(T data) {
return contains(this.rootNode, data);
}
/**
* 查詢以root爲根結點的子樹
*
* @param root
* @param data
* @return
*/
private boolean contains(TreeNode<T> root, T data) {
//空樹是肯定不含有目標元素,直接返回false , 遞歸退出條件
if (root == null) {
return false;
}
//如果相等,說明找到了,直接返回true
if (data.compareTo(root.data) == 0) {
return true;
//如果當前子樹的根結點不是目標元素,判斷是當前根結點的左邊還是右邊
} else if (data.compareTo(root.data) < 0) {
return contains(root.lchild, data);
} else {
return contains(root.rchild, data);
}
}
找出二叉搜索樹中的最小值和最小值 :minimum()
、maximum()
| 遞歸與迭代
迭代的方式:
/**
* 查詢二叉搜索樹中的最小值 | 迭代
*
* @return
*/
public T minimumWithIterator() {
if (this.rootNode == null) {
return null;
}
//找到二叉搜索樹最左結點,即最左沒有左孩子的結點,不一定是葉子結點
TreeNode<T> node = this.rootNode;
while (node.lchild != null) {
node = node.lchild;
}
return node.data;
}
/**
* 查詢二叉搜索樹中的最大值 | 迭代
*
* @return
*/
public T maximumWithIterator() {
if (this.rootNode == null) {
return null;
}
//找到二叉搜索樹最右結點,即最右沒有右孩子的結點,不一定是葉子結點
TreeNode<T> node = this.rootNode;
while (node.rchild != null) {
node = node.rchild;
}
return node.data;
}
遞歸的方式:
/**
* 查找二叉搜索樹的最小值 | 遞歸
*
* @return
*/
public T minimumWithRecursion() {
if (this.rootNode == null) {
return null;
}
return minimumWithRecursion(this.rootNode).data;
}
/**
* 查找二叉樹搜索樹的最小值 | 遞歸
* 1. 每一步就是查找左子樹的最小值是什麼,如果沒有左子樹,則最小值就是當前結點所代表的值
*
* @param node
* @return
*/
private TreeNode<T> minimumWithRecursion(TreeNode<T> node) {
if (node.lchild == null) {
return node;
}
return minimumWithRecursion(node.lchild);
}
/**
* 查找二叉樹搜索樹的最大值 | 遞歸
*
* @return
*/
public T maximumWithRecursion() {
if (this.rootNode == null) {
return null;
}
return maximumWithRecursion(this.rootNode).data;
}
/**
* 查找二叉樹搜索樹的最大值 | 遞歸
* 1. 每一步就是查找右子樹的最大值是什麼,如果沒有右子樹,則最大值就是當前結點所代表的值
*
* @param node
* @return
*/
private TreeNode<T> maximumWithRecursion(TreeNode<T> node) {
if (node.rchild == null) {
return node;
}
return maximumWithRecursion(node.rchild);
}
刪除二叉搜索樹中的最小結點和最大結點: removeMin()
、removeMax()
| 遞歸
要注意的問題:
- 無論是刪除最大還是最小結點,只要刪除結點沒有孩子,那就直接刪除。
- 刪除結點如果有左孩子或者右孩子,即有單個孩子結點時。就要判斷是刪除最小還是最大模式,再來解決後續
- 不過在這種模式下,刪除結點不可能既有左孩子或右孩子
刪除最小值:
/**
* 刪除二叉樹搜索樹中的最小值 | 遞歸
*
* @return
*/
public T removeMin() {
if (this.rootNode == null) {
return null;
}
//1. 先獲得最小值
T minResult = minimumWithRecursion();
//2. 再刪除最小值的結點
this.rootNode = removeMin(this.rootNode);
return minResult;
}
/**
* 刪除以Node結點爲根結點的子樹的最小值 | 遞歸
* 1. 該代碼屬於優化代碼,類似於add()操作,優化的好處就是簡潔,但不好理解
* 2. 先判斷當前子樹的根結點是否有左孩子,如果有,繼續遞歸
* 3. 如果沒有左孩子,即找到最小值結點,判斷最小值結點是否有右孩子
* 4. 如果沒有,直接刪除,如果有,讓刪除結點父結點的左孩子指向刪除結點的右孩子
*
* @param node
* @return
*/
private TreeNode<T> removeMin(TreeNode<T> node) {
//1. 如果當前結點的左孩子爲null , 則當前結點就是最小值結點,就是要刪除的結點
if (node.lchild == null) {
/**
* 兩種情況:
* 1. 無右孩子,直接讓上層的結點.lchild = null即可
* 2. 有右孩子,讓上層結點.lchild = 刪除結點.rchild;
* 這裏在代碼上可以將兩種情況進行合併(優化後,可能不好理解),如果右結點爲null,返回null給上層結點的左孩子
* 如果不爲null,返回刪除結點的右孩子給上層結點左孩子
*/
TreeNode<T> rNode = node.rchild;
node.rchild = null;
nodeCount--;
return rNode;
}
//2. 如果當前結點有左孩子,則遞歸左孩子
node.lchild = removeMin(node.lchild);
//3. 返回當前結點
return node;
}
刪除最大值:
/**
* 刪除二叉搜索樹的最大值 | 遞歸
*
* @return
*/
public T removeMax() {
//1. 獲得最大值
T maxResult = maximumWithRecursion();
//2. 刪除最大值結點
this.rootNode = removeMax(this.rootNode);
return maxResult;
}
/**
* 刪除二叉搜書樹的最大值結點 | 遞歸
*
* @param node
* @return
*/
private TreeNode<T> removeMax(TreeNode<T> node) {
if (node.rchild == null) {
TreeNode<T> lNode = node.lchild;
node.lchild = null;
nodeCount--;
return lNode;
}
node.rchild = removeMax(node.rchild);
return node;
}
刪除二叉搜索樹中的任意結點: remove()
| 遞歸
要注意的問題:
- 如果刪除結點沒有孩子,直接刪除
- 如果刪結點只有左孩子或只有右孩子,只要讓左或右孩子去替代刪除結點即可
- 如果刪除結點即有左孩子,又有右孩子,要做更特殊複雜的邏輯 |
Hibbard Deletion
方法
Hibbard Deletion
方法:
爲了解決刪除結點即有左孩子,又有右孩子的情況,我們就需要給刪除結點找到一個替代結點
這個替代結點,可以是刪除結點的 後繼結點
, 也可以是前趨結點
- 後繼結點是刪除結點的右子樹的最小值結點
- 前趨結點是刪除結點的左子樹的最大值結點
因爲刪除結點的替代結點可以是前趨結點
,也可以是後繼節點
,所以我們的代碼中採用後繼結點
去代替刪除結點
代碼:
/**
* 二叉搜索樹刪除目標值結點(任意結點)
* 1. 刪除結點沒有孩子的情況
* 2. 刪除結點只有一個孩子的情況
* 3. 刪除結點有兩個孩子的情況
* 4. 刪除結點的替代結點可以是前趨結點,也可以是後繼節點,我們這裏採用後繼結點
*
* @param data
*/
public void remove(T data) {
this.rootNode = remove(data, this.rootNode);
}
/**
* 二叉搜索刪除目標結點
*
* @param data
* @param node
* @return
*/
private TreeNode<T> remove(T data, TreeNode<T> node) {
//如果結點爲null, 直接返回null,代表沒有找到目標值結點
if (node == null) {
return null;
}
//二分查找遞歸,目標是找到目標值結點
if (data.compareTo(node.data) < 0) {
node.lchild = remove(data, node.lchild);
return node;
} else if (data.compareTo(node.data) > 0) {
node.rchild = remove(data, node.rchild);
return node;
} else { //data == node.data,找到刪除結點
//(1) 如果刪除結點沒有孩子,直接刪除,爽快,其實這個步驟可以省略,因爲下面的做法已經囊括了,但是爲了直觀
if (node.lchild == null && node.rchild == null) {
nodeCount--;
return null;
} else if (node.lchild == null) {
//(2) 如果刪除結點沒有左孩子 | 等價刪除結點只有右孩子
TreeNode<T> rNode = node.rchild;
nodeCount--;
node.rchild = null;
return rNode;
} else if (node.rchild == null) {
//(3) 如果刪除結點沒有右孩子 | 等價刪除結點只有左孩子
TreeNode<T> lNode = node.lchild;
nodeCount--;
node.lchild = null;
return lNode;
} else {
/**
* (4) 如果刪除結點左右孩子都不爲空,Hibbard Deletion
* 1. 找到刪除結點右子樹的最小值結點(刪除結點的後繼結點) minimum(node.rchild) ,作爲刪除結點的替代結點
* 2. 刪除結點的後繼結點的左右指針替換成刪除結點的左右指針的指向
* 3. 剔除刪除結點
* 4. 返回替代結點給上層,重新關聯
*/
//獲得後繼結點,將後繼結點作爲刪除結點的代替結點
TreeNode<T> successorNode = minimumWithRecursion(node.rchild);
//後繼結點的左指針指向刪除結點的左指針
successorNode.lchild = node.lchild;
//後繼結點的右指針指向刪除結點的右指針
//爲什麼removeMin, 因爲後繼結點要從刪除結點的右子樹最小結點位置移動到當前刪除結點的位置,先刪除,再移動
//這裏因爲涉及到nodeCount的問題,爲什麼這裏不nodeCount--,是因爲removeMin中已經nodeCount--了,曲線做了,因爲不直觀,容易誤導
successorNode.rchild = removeMin(node.rchild);
//釋放刪除結點的左右孩子
node.lchild = null;
node.rchild = null;
return successorNode;
}
}
完整代碼
TreeNode.java
/**
* 二叉搜索樹的結點構造
* 跟其他普通的二叉樹一樣
*
* @param <E>
*/
public class TreeNode<E extends Comparable<T>> {
public E data;
public TreeNode<E> lchild, rchild;
public TreeNode(E data) {
this.data = data;
this.lchild = null;
this.rchild = null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TreeNode)) return false;
TreeNode<?> treeNode = (TreeNode<?>) o;
if (data != null ? !data.equals(treeNode.data) : treeNode.data != null) return false;
if (lchild != null ? !lchild.equals(treeNode.lchild) : treeNode.lchild != null) return false;
return rchild != null ? rchild.equals(treeNode.rchild) : treeNode.rchild == null;
}
@Override
public int hashCode() {
int result = data != null ? data.hashCode() : 0;
result = 31 * result + (lchild != null ? lchild.hashCode() : 0);
result = 31 * result + (rchild != null ? rchild.hashCode() : 0);
return result;
}
}
BinarySearchTree.java
/**
* 二叉搜索樹
*
* @author liwenjie
*/
public class BinarySearchTree<T extends Comparable<T>>
/**
* 二叉搜索樹根結點
*/
private TreeNode<T> rootNode;
/**
* 二叉搜索樹的結點個數
*/
private int nodeCount;
public BinarySearchTree(T data) {
this.rootNode = new TreeNode<>(data);
this.nodeCount = 1;
}
/**
* 向二分搜索樹添加一個元素 | 遞歸
* 1. 統一操作,不用對根結點進行額外的判斷
* 2. 代碼更簡潔,只是相對不好理解
*
* @param data
*/
public void add(T data) {
this.rootNode = add(this.rootNode, data);
}
/**
* 向根結點爲root的二分搜索樹添加元素 | 改進型,不需要對根結點做特殊處理
* 1. 遞歸推出條件是,當遞歸到空樹時,那麼新元素就是該空樹的根結點
* 2. 每一次的遞歸返回的結點,都是用於給上層做關聯的,即遞歸是找到新元素要存儲的位置,然後將新元素結點返回給父結點去操作。parent.child = newNode
*
* @param root
* @param data
* @return
*/
private TreeNode<T> add(TreeNode<T> root, T data) {
//遞歸退出條件,只要相對根結點爲null,即代表目前我是一棵空樹,新增元素就要作爲我這棵空樹的根結點,直接返回新結點給上層操作
if (root == null) {
this.nodeCount++;
return new TreeNode<>(data);
}
//不允許存在相同元素
if (data.compareTo(root.data) == 0) {
throw new RuntimeException("二叉搜索樹所存儲的元素不允許相等,已存在");
//如果新增元素小於當前子樹的根結點的值,左遞歸
} else if (data.compareTo(root.data) < 0) {
root.lchild = add(root.lchild, data);
//如果新增元素大於當前子樹的根結點的值,右遞歸
} else if (data.compareTo(root.data) > 0) {
root.rchild = add(root.rchild, data);
}
//將當前子樹的根結點返回出去,讓上層做關聯
return root;
}
/**
* 二叉樹的查詢 | 遞歸
*
* @param data
* @return
*/
public boolean contains(T data) {
return contains(this.rootNode, data);
}
/**
* 查詢以root爲根結點的子樹
*
* @param root
* @param data
* @return
*/
private boolean contains(TreeNode<T> root, T data) {
//空樹是肯定不含有目標元素,直接返回false , 遞歸退出條件
if (root == null) {
return false;
}
//如果相等,說明找到了,直接返回true
if (data.compareTo(root.data) == 0) {
return true;
//如果當前子樹的根結點不是目標元素,判斷是當前根結點的左邊還是右邊
} else if (data.compareTo(root.data) < 0) {
return contains(root.lchild, data);
} else {
return contains(root.rchild, data);
}
}
/**
* 查詢二叉搜索樹中的最小值 | 迭代
*
* @return
*/
public T minimumWithIterator() {
if (this.rootNode == null) {
return null;
}
//找到二叉搜索樹最左結點,即最左沒有左孩子的結點,不一定是葉子結點
TreeNode<T> node = this.rootNode;
while (node.lchild != null) {
node = node.lchild;
}
return node.data;
}
/**
* 查詢二叉搜索樹中的最大值 | 迭代
*
* @return
*/
public T maximumWithIterator() {
if (this.rootNode == null) {
return null;
}
//找到二叉搜索樹最右結點,即最右沒有右孩子的結點,不一定是葉子結點
TreeNode<T> node = this.rootNode;
while (node.rchild != null) {
node = node.rchild;
}
return node.data;
}
/**
* 查找二叉搜索樹的最小值 | 遞歸
*
* @return
*/
public T minimumWithRecursion() {
if (this.rootNode == null) {
return null;
}
return minimumWithRecursion(this.rootNode).data;
}
/**
* 查找二叉樹搜索樹的最小值 | 遞歸
* 1. 每一步就是查找左子樹的最小值是什麼,如果沒有左子樹,則最小值就是當前結點所代表的值
*
* @param node
* @return
*/
private TreeNode<T> minimumWithRecursion(TreeNode<T> node) {
if (node.lchild == null) {
return node;
}
return minimumWithRecursion(node.lchild);
}
/**
* 查找二叉樹搜索樹的最大值 | 遞歸
*
* @return
*/
public T maximumWithRecursion() {
if (this.rootNode == null) {
return null;
}
return maximumWithRecursion(this.rootNode).data;
}
/**
* 查找二叉樹搜索樹的最大值 | 遞歸
* 1. 每一步就是查找右子樹的最大值是什麼,如果沒有右子樹,則最大值就是當前結點所代表的值
*
* @param node
* @return
*/
private TreeNode<T> maximumWithRecursion(TreeNode<T> node) {
if (node.rchild == null) {
return node;
}
return maximumWithRecursion(node.rchild);
}
/**
* 刪除二叉樹搜索樹中的最小值 | 遞歸
*
* @return
*/
public T removeMin() {
if (this.rootNode == null) {
return null;
}
//1. 先獲得最小值
T minResult = minimumWithRecursion();
//2. 再刪除最小值的結點
this.rootNode = removeMin(this.rootNode);
return minResult;
}
/**
* 刪除以Node結點爲根結點的子樹的最小值 | 遞歸
* 1. 該代碼屬於優化代碼,類似於add()操作,優化的好處就是簡潔,但不好理解
* 2. 先判斷當前子樹的根結點是否有左孩子,如果有,繼續遞歸
* 3. 如果沒有左孩子,即找到最小值結點,判斷最小值結點是否有右孩子
* 4. 如果沒有,直接刪除,如果有,讓刪除結點父結點的左孩子指向刪除結點的右孩子
*
* @param node
* @return
*/
private TreeNode<T> removeMin(TreeNode<T> node) {
//1. 如果當前結點的左孩子爲null , 則當前結點就是最小值結點,就是要刪除的結點
if (node.lchild == null) {
/**
* 兩種情況:
* 1. 無右孩子,直接讓上層的結點.lchild = null即可
* 2. 有右孩子,讓上層結點.lchild = 刪除結點.rchild;
* 這裏在代碼上可以將兩種情況進行合併(優化後,可能不好理解),如果右結點爲null,返回null給上層結點的左孩子
* 如果不爲null,返回刪除結點的右孩子給上層結點左孩子
*/
TreeNode<T> rNode = node.rchild;
node.rchild = null;
nodeCount--;
return rNode;
}
//2. 如果當前結點有左孩子,則遞歸左孩子
node.lchild = removeMin(node.lchild);
//3. 返回當前結點
return node;
}
/**
* 刪除二叉搜索樹的最大值 | 遞歸
*
* @return
*/
public T removeMax() {
//1. 獲得最大值
T maxResult = maximumWithRecursion();
//2. 刪除最大值結點
this.rootNode = removeMax(this.rootNode);
return maxResult;
}
/**
* 刪除二叉搜書樹的最大值結點 | 遞歸
*
* @param node
* @return
*/
private TreeNode<T> removeMax(TreeNode<T> node) {
if (node.rchild == null) {
TreeNode<T> lNode = node.lchild;
node.lchild = null;
nodeCount--;
return lNode;
}
node.rchild = removeMax(node.rchild);
return node;
}
/**
* 二叉搜索樹刪除目標值結點(任意結點)
* 1. 刪除結點沒有孩子的情況
* 2. 刪除結點只有一個孩子的情況
* 3. 刪除結點有兩個孩子的情況
* 4. 刪除結點的替代結點可以是前趨結點,也可以是後繼節點,我們這裏採用後繼結點
*
* @param data
*/
public void remove(T data) {
this.rootNode = remove(data, this.rootNode);
}
/**
* 二叉搜索刪除目標結點
*
* @param data
* @param node
* @return
*/
private TreeNode<T> remove(T data, TreeNode<T> node) {
//如果結點爲null, 直接返回null,代表沒有找到目標值結點
if (node == null) {
return null;
}
//二分查找遞歸,目標是找到目標值結點
if (data.compareTo(node.data) < 0) {
node.lchild = remove(data, node.lchild);
return node;
} else if (data.compareTo(node.data) > 0) {
node.rchild = remove(data, node.rchild);
return node;
} else { //data == node.data,找到刪除結點
//(1) 如果刪除結點沒有孩子,直接刪除,爽快,其實這個步驟可以省略,因爲下面的做法已經囊括了,但是爲了直觀
if (node.lchild == null && node.rchild == null) {
nodeCount--;
return null;
} else if (node.lchild == null) {
//(2) 如果刪除結點沒有左孩子 | 等價刪除結點只有右孩子
TreeNode<T> rNode = node.rchild;
nodeCount--;
node.rchild = null;
return rNode;
} else if (node.rchild == null) {
//(3) 如果刪除結點沒有右孩子 | 等價刪除結點只有左孩子
TreeNode<T> lNode = node.lchild;
nodeCount--;
node.lchild = null;
return lNode;
} else {
/**
* (4) 如果刪除結點左右孩子都不爲空,Hibbard Deletion
* 1. 找到刪除結點右子樹的最小值結點(刪除結點的後繼結點) minimum(node.rchild) ,作爲刪除結點的替代結點
* 2. 刪除結點的後繼結點的左右指針替換成刪除結點的左右指針的指向
* 3. 剔除刪除結點
* 4. 返回替代結點給上層,重新關聯
*/
//獲得後繼結點,將後繼結點作爲刪除結點的代替結點
TreeNode<T> successorNode = minimumWithRecursion(node.rchild);
//後繼結點的左指針指向刪除結點的左指針
successorNode.lchild = node.lchild;
//後繼結點的右指針指向刪除結點的右指針
//爲什麼removeMin, 因爲後繼結點要從刪除結點的右子樹最小結點位置移動到當前刪除結點的位置,先刪除,再移動
//這裏因爲涉及到nodeCount的問題,爲什麼這裏不nodeCount--,是因爲removeMin中已經nodeCount--了,曲線做了,因爲不直觀,容易誤導
successorNode.rchild = removeMin(node.rchild);
//釋放刪除結點的左右孩子
node.lchild = null;
node.rchild = null;
return successorNode;
}
}
public static void main(String[] args) {
BinarySearchTree<Integer> bsTree = new BinarySearchTree<>(5);
bsTree.add(1);
bsTree.add(4);
bsTree.add(9);
bsTree.add(7);
System.out.println(bsTree.contains(9));
System.out.println(bsTree.minimumWithRecursion());
System.out.println(bsTree.maximumWithRecursion());
bsTree.removeMin();
bsTree.removeMax();
bsTree.add(10);
}
要注意的地方就是, 二叉搜索樹的結點的元素必須可以比較,所以無論是結點的泛型還是二叉樹結構本身的泛型都必須是一個實現了Comparable接口的元素。不能比較的元素是無法構造成一棵搜索樹的
參考資料
-
RongleXie/Play-with-Data-Structures-Ronglexie - @作者:RongleXie
-
如果覺得對你有幫助,能否點個贊或關個注,以示鼓勵筆者呢?!