定義
二叉搜尋樹(Binary Search Tree,BST),也稱為二叉排序樹或二叉查詢樹。
相較於普通的二叉樹,非空的二叉搜尋樹有如下性質:
- 非空左子樹的所有鍵值小於其根結點的鍵值;
- 非空右子樹的所有鍵值大於其根結點的鍵值;
- 左右子樹均為二叉搜尋樹;
- 樹中沒有鍵值相等的結點。
可以看到,二叉搜尋樹的性質很鮮明,這也使得二叉樹也有了實際意義。
二叉搜尋樹的常用操作
對於二叉搜尋樹,除了常規的4種遍歷之外,還有如下一些關鍵的操作值得我們去關注。
二叉樹的儲存結構實現
對於二叉樹,我們還是習慣的選擇採用鏈式儲存結構實現。
二叉樹結點定義
二叉搜尋樹最大的特點,就是他的元素是可以比較大小的。這一點是需要注意的地方。
/**
* Created by engineer on 2017/10/26.
* <p>
* 二叉搜尋樹樹結點定義
*/
public class TreeNode<T extends Comparable<T>> {
// 資料域
private T data;
// 左子樹
public TreeNode<T> leftChild;
// 右子樹
public TreeNode<T> rightChild;
public TreeNode(T data) {
this(null, data, null);
}
public TreeNode(TreeNode leftChild, T data, TreeNode rightChild) {
this.leftChild = leftChild;
this.data = data;
this.rightChild = rightChild;
}
public T getData() {
return data;
}
public TreeNode<T> getLeftChild() {
return leftChild;
}
public TreeNode<T> getRightChild() {
return rightChild;
}
public void setData(T data) {
this.data = data;
}
}複製程式碼
二叉搜尋樹插入
有了根節點,我們就可以根據二叉樹的性質,從根節點出發,構建出一顆二叉樹。
/**
* 樹中插入元素
*
* @param value
*/
void insert(T value) {
if (value == null) {
return;
}
root = insert(root, value);
}
private TreeNode<T> insert(TreeNode<T> node, T value) {
if (node == null) {
// 樹為空,則建立根節點
return new TreeNode<>(value);
} else {
if (compare(node, value) < 0) { // 插入值比根節點小,在左子樹繼續建立二叉搜尋樹
node.leftChild = insert(node.getLeftChild(), value);
} else if (compare(node, value) > 0) { // 插入值比根節點大,在右子樹繼續建立二叉搜尋樹
node.rightChild = insert(node.getRightChild(), value);
}
}
return node;
}
private int compare(TreeNode<T> node, T value) {
return value.compareTo(node.getData());
}複製程式碼
根據二叉搜尋樹的特性,我們很容易使用遞迴實現二叉樹的插入操作;總的來說,就是每次插入一個結點,從根節點出發作比較,小的就往左子樹插,大的就往右子樹插。這和二叉搜尋樹的定義時完全一致的。
我們可以簡單測試一下,這個insert方法的正確性。
測試二叉搜尋樹插入操作
public class BinarySearchTreeTest {
private static Integer[] arrays = new Integer[]{10, 8, 3, 12, 9, 4, 5, 7, 1,11, 17};
public static void main(String[] args) {
BinarySearchTree<Integer> mSearchTree = new BinarySearchTree<>();
for (Integer data : arrays) {
mSearchTree.insert(data);
}
// 列印二叉樹的三種遍歷順序
mSearchTree.printTree();
}
}複製程式碼
關於樹的遍歷已在上文中詳細分析,此處不再做深入探討
這裡定義了一個隨機陣列,這個將這個陣列的按序插入到樹中,並按照樹的三種遍歷結構列印樹。按照這個陣列我們將構建出如下所示的一顆二叉搜尋樹:
看一下程式輸出的遍歷結果。
前序遍歷:10 8 3 1 4 5 7 9 12 11 17
中序遍歷:1 3 4 5 7 8 9 10 11 12 17
後序遍歷:1 7 5 4 3 9 8 11 17 12 10複製程式碼
可以看到,遍歷結果和我們畫出來二叉樹是一致的,因此可以驗證插入方法是正確的。
查詢
通過插入操作,我們已經實現了一顆二叉搜尋樹,下面就來看看如何從樹中查詢元素。
- 查詢最大值與最小值
根據二叉搜尋樹的特點,我們知道在一顆二叉搜尋樹上,最小的值一定在最最左邊的結點上,而最大值一定在最最右邊的結點上。因此,查詢二叉樹最值就變得非常容易了。
/**
* 查詢最大值
*
* @return
*/
public T findMax() {
if (isEmpty()) return null;
return findMax(root);
}
/**
* 從特定結點開始尋找最大值
*
* @param node
* @return
*/
private T findMax(TreeNode<T> node) {
TreeNode<T> temp = node;
while (temp.getRightChild() != null) {
temp = temp.getRightChild();
}
return temp.getData();
}
/**
* 查詢最小值
*
* @return
*/
public T findMin() {
if (isEmpty()) return null;
return findMin(root);
}
/**
* 從特定結點開始尋找最小值
*
* @param node
* @return
*/
private T findMin(TreeNode<T> node) {
TreeNode<T> temp = node;
while (temp.getLeftChild() != null) {
temp = temp.getLeftChild();
}
return temp.getData();
}複製程式碼
可以看到,演算法實現非常簡單,就是不斷後移結點找到沒有子樹的結點,就是最邊界位置的結點了。
- 查詢特定值
在二叉搜尋樹中,怎樣快速找到一個值為特定元素的結點呢?想想我們是怎樣實現結點插入的?這個問題就很簡單了。
遞迴實現,查詢特定結點
**
/**
* find 特定值 遞迴實現
*
* @param value
* @return
*/
public TreeNode<T> find(T value) {
if (isEmpty()) {
return null;
} else {
return find(root, value);
}
}
private TreeNode<T> find(TreeNode<T> node, T value) {
if (node == null) {
// 當查詢一個不在樹中元素時,丟擲異常
throw new RuntimeException("the value must not in the tree");
}
if (compare(node, value) < 0) {
// 小於根節點時,從去左子樹找
return find(node.getLeftChild(), value);
} else if (compare(node, value) > 0) {
// 大於根節點時,從右子樹找
return find(node.getRightChild(), value);
} else {
// 剛好等於,找到了
return node;
// 剩下還有一種情況,就是不等於,也就是所查詢的元素不在樹中
}
}複製程式碼
查詢的實現思路,總體上和插入是一致的;無非就是做不同的操作;這裡需要注意的是,為了程式的健壯性,我們還得考慮如果查詢的元素不在樹中這種情況。
迭代實現,查詢特定值
有了前面查詢最大值、最小值的經驗,我們也可以考慮使用迭代演算法實現查詢指定元素的演算法。
/**
* 查詢特定值-非遞迴實現
*
* @param value
* @return 結點
*/
public TreeNode<T> findIter(T value) {
TreeNode<T> current = root;
while (current != null) {
if (compare(current, value) < 0) {
current = current.getLeftChild();
} else if (compare(current, value) > 0) {
current = current.getRightChild();
} else {
return current;
}
}
// current為null,說明所查詢的元素不在tree裡
return null;
}複製程式碼
這裡同樣測試一下,查詢方法的正確性:
System.out.printf("
find value %d in mSearchTree
", 12);
TreeNode mTreeNode = mSearchTree.find(12);
TreeNode mTreeNode_1 = mSearchTree.findIter(12);
System.out.println("遞迴實現結點 = :" + mTreeNode + ", value=" + mTreeNode.getData());
System.out.println("非遞迴實現結點= :" + mTreeNode_1 + ", value=" + mTreeNode_1.getData());
System.out.println("
find the max value in mSearchTree = " + mSearchTree.findMax());
System.out.println("find the min value in mSearchTree = " + mSearchTree.findMin());複製程式碼
輸出:
find value 12 in mSearchTree
遞迴實現結點 = :com.avaj.datastruct.tree.bst.TreeNode@4b67cf4d, value=12
非遞迴實現結點= :com.avaj.datastruct.tree.bst.TreeNode@4b67cf4d, value=12
find the max value in mSearchTree = 17
find the min value in mSearchTree = 1複製程式碼
我們分別用遞迴和迭代兩種方式去查詢 12,可以看到兩次找到是同一個物件,這個物件的值為12;找到的最大值和最小值也是正確的;因此查詢功能的實現是正確的。
刪除結點
從二叉搜尋樹中,刪除一個結點可以算是最複雜的操作了,主要是因為所要刪除的結點,所處的位置被刪除後,依然需要保持整棵樹依然為二叉樹,因此需要就不同的情況就像分析。
就拿我們上面建立的這顆二叉樹來說,如果要刪除的結點是1,7,11,17 這樣的葉子結點,就很容易了;讓其父結點指向為null即可;而如果是4,5 這樣包含一顆子樹的結點,換個角度來說,這其實就是單向連結串列,從單向連結串列中間位置刪除一個結點也比較容易;最麻煩的就是如果要刪除的結點是10,8,3,12 這類結點包含左右子樹,我們就需要從左子樹中找一個最大值,或者是右子樹中的最小值來替代這個值。總結一下刪除結點的操作:
- 葉子結點:直接刪除,其父結點指向null
- 包含一個孩子的結點 :父結點指向要刪除結點的自結點(相當於連結串列中間刪除一個元素);
- 包含左右子樹的結點:右子樹最小值或左子樹最大值替換此結點
結合以上分析,得出從二叉搜尋樹中刪除結點的實現。
/**
* 從樹中刪除值為value 的特定結點
*
* @param value
*/
public void delete(T value) {
if (value == null || isEmpty()) {
return;
}
root = delete(root, value);
}
private TreeNode<T> delete(TreeNode<T> node, T value) {
// 結點為空,要出刪除的元素不在樹中
if (node == null) {
return node;
}
if (compare(node, value) < 0) { // 去左子樹刪除
node.leftChild = delete(node.getLeftChild(), value);
} else if (compare(node, value) > 0) { // 去右子樹刪除
node.rightChild = delete(node.getRightChild(), value);
} else { // 要刪除的就是當前結點
if (node.getLeftChild() != null && node.getRightChild() != null) {// 被刪除的結點,包含左右子樹
T temp = findMin(node.getRightChild()); // 得到右子樹的最小值
node.setData(temp); //右子樹最小值替換當前結點
node.rightChild = delete(node.getRightChild(), temp); // 從右子樹刪除這個最小值的結點
} else {// 被刪除的結點,包含一個子樹或沒有子樹
if (node.getLeftChild() != null) {
node = node.getLeftChild();
} else {
node = node.getRightChild();
}
}
}
return node;
}複製程式碼
這裡選擇使用右子樹的最小值替換,是因為刪除這個最小值的結點會比較容易,因為他一定是不會是一個包含左右子樹的結點。
同樣,這裡測試一下刪除結點的功能:
// 刪除只帶一個子樹的結點
mSearchTree.delete(4);
mSearchTree.printTree();
System.out.println();
// 刪除帶左右子樹的根節點
mSearchTree.delete(10);
mSearchTree.printTree();複製程式碼
輸出:
前序遍歷:10 8 3 1 5 7 9 12 11 17
中序遍歷:1 3 5 7 8 9 10 11 12 17
後序遍歷:1 7 5 3 9 8 11 17 12 10
前序遍歷:11 8 3 1 5 7 9 12 17
中序遍歷:1 3 5 7 8 9 11 12 17
後序遍歷:1 7 5 3 9 8 17 12 11複製程式碼
通過和我們一開始畫出來的樹相比較,發現是對應的。
二叉搜尋樹的高度
最後,再來看看如何計算一顆二叉搜素樹的度。
public int getTreeHeight() {
if (isEmpty()) {
return 0;
}
return getTreeHeight(root);
}
private int getTreeHeight(TreeNode<T> node) {
if (node == null) {
return 0;
}
int leftHeight = getTreeHeight(node.getLeftChild());
int rightHeight = getTreeHeight(node.getRightChild());
int max = leftHeight > rightHeight ? leftHeight : rightHeight;
// 得到左右子樹中較大的返回.
return max + 1;
}複製程式碼
順便來算一算,到最後我們建立的樹,經過插入刪除操作高度變成了多少。
System.out.println("
Tree`s height =" + mSearchTree.getTreeHeight());複製程式碼
輸出:
Tree`s height =5複製程式碼
可以看到,由於結點4被刪除,樹由原來的6層變成了5層,結果是正確的!
好了,二叉搜尋樹的分析就是這些了!文中所有原始碼地址.