什么是二叉查找樹(BST)
1. 什么是BST
對於二叉樹中的每個節點X,它的左子樹中所有項的值都小於X中的項,它的右子樹中所有項的值大於X中的項。這樣的二叉樹是二叉查找樹。
以上是一顆二叉查找樹,其特點是:
(1)若它的左子樹不為空,則左子樹上的所有節點的值都小於它的根節點的值;
(2)若它的右子樹不為空,則右子樹上所有節點的值都大於它的根節點的值;
(3)其他的左右子樹也分別為二叉查找樹;
(4)二叉查找樹是動態查找表,在查找的過程中可見添加和刪除相應的元素,在這些操作中需要保持二叉查找樹的以上性質。
2. 二叉查找樹的定義
根據二叉查找樹的要求,我們首先讓BinaryNode實現Comparable接口。
// 樹節點
class BinaryNode implements Comparable {
Integer element;
BinaryNode left;
BinaryNode right;
public BinaryNode{
}
public BinaryNode(Integer element, BinaryNode left, BinaryNode right) {
this.element = element;
this.left = left;
this.right = right;
}
@Override
public int compareTo(@NonNull Object o) {
return this.element - (Integer) o;
}
}
下面我們完成對BinarySearchTree的定義(只定義關鍵輪廓,具體接口后續進行分析)。
public class BinarySearchTree {
//定義樹的根節點
BinaryNode root;
public BinarySearchTree() {
this.root = null;
}
public void makeEmpty() {
this.root = null;
}
public boolean isEmpty() {
return this.root == null;
}
// 判斷是否包含某個元素
public boolean contains(Integer x) {
//TODO:后續講解
return false;
}
// 查找最小值
public BinaryNode findMin(){
//TODO:后續講解
return null;
}
// 查找最大值
public BinaryNode findMax(){
//TODO:后續講解
return null;
}
// 按照順序插入值
public void insert(Integer x){
//TODO:后續講解
}
// 刪除某個值
public void remove(Integer x){
//TODO:后續講解
}
// 打印樹
public void printTree(){
//TODO:后續講解
}
}
3. contaions操作
如果在樹T中包含項X的節點,那么該操作返回true,否則返回false。
樹的結構使得這種操作變得非常簡單,如果T為空集,直接返回false;否則我們就對T的左子樹或右子樹進行遞歸查找,直到找到該項X。
代碼實現如下:
/**
* 是否包含某個元素
* @param x 待查找對象
* @return 查找結果
*/
public boolean contains(Integer x) {
// 首次查找從根節點開始
return contains(x,root);
}
private boolean contains(Integer x, BinaryNode node) {
// 根節點為空的情況,不需要再查找
if (node == null) {
return false;
}
// 與當前節點進行比較
int compareResult = x.compareTo(node.element);
// 小於當前節點的值,就遞歸遍歷左子樹
if (compareResult < 0) {
return contains(x, node.left);
}
// 大於當前節點的值,就遞歸遍歷右子樹
else if (compareResult > 0) {
return contains(x, node.right);
}
// 等於當前節點值,直接返回
else {
return true;
}
}
4. 查找最小節點
從根開始並且只要有左子樹就向左進行,終止點就是最小元素節點。
// 查找最小值
public BinaryNode findMin() {
return findMin(root);
}
private BinaryNode findMin(BinaryNode node) {
// 當前節點為null,直接返回null
if (node == null) {
return null;
}
// 不存在左子樹,返回當前節點
if (node.left == null) {
return node;
}
// 遞歸遍歷左子樹
return findMin(node.left);
}
5. 查找最大節點
從根開始並且只要有右子樹就向右進行,終止點就是最大元素節點。作為與findMin的對比,findMax方法我們拋棄了常用的遞歸,使用了常見的while循環來查找。
// 查找最大值
public BinaryNode findMax() {
return findMax(root);
}
private BinaryNode findMax(BinaryNode node) {
if (node != null) {
while (node.right != null) {
node = node.right;
}
}
return node;
}
6. insert操作
插入操作在概念上是簡單的,為了將X插入樹T中,我們像contains一樣沿着樹查找。
如果找到X,那么我們可以什么都不做,也可以做一些更新操作。
否則,就將X插入到遍歷路徑上的最后一個節點。(這個做法有待商榷)
代碼實現如下:
public void insert(Integer x) {
root = insert(x, root);
}
// 返回的插入節點的根節點
private BinaryNode insert(Integer x, BinaryNode node) {
// 如果當前節點為null,新建節點返回
if (node == null) {
return new BinaryNode(x, null, null);
}
// 與當前節點比較
int compareResult = x.compareTo(node.element);
// 小於當前節點值,遞歸插入左子樹,並將返回值設置為當前節點的left
if (compareResult < 0) {
node.left = insert(x, node.left);
}
// 大於當前節點值,遞歸插入右子樹,並將返回值設置為當前節點的right
if (compareResult > 0) {
node.right = insert(x, node.right);
}
// 等於當前的值,不做任何處理
if (compareResult == 0) {
// do some update or do noting
}
return node;
}
7. 刪除操作
正如許多數據結構一樣,最難的操作是remove,一旦我們發現了要刪除的元素,就要考慮幾種可能的情況:
當待刪除的節點為葉子節點時,直接刪除。
當待刪除節點只有一個兒子節點時,把兒子節點代替該節點的位置,然后刪除該節點。
當待刪除的節點有兩個兒子節點時,一般的刪除策略是用其右子樹的最小的數據代替該節點的數據並遞歸刪除那個節點(現在它是空的);因為右子樹的最小節點不可能有左兒子,所以第二次Delete要容易。
// 刪除某個值
public void remove(Integer x) {
remove(x, root);
}
private BinaryNode remove(Integer x, BinaryNode node) {
if (node == null) {
return null;
}
int compareResult = x.compareTo(node.element);
if (compareResult < 0) {
node.left = remove(x, node.left);
}
if (compareResult > 0) {
node.right = remove(x, node.right);
}
if (compareResult == 0) {
if (node.left != null && node.right != null) {
node.element = findMin(node.right).element;
node.right = remove(node.element, node.right);
} else {
node = (node.left != null) ? node.left : node.right;
}
}
return node;
}
8. 二叉查找樹的局限
同樣的數據,可以對應不同的二叉搜索樹,如下:
二叉搜索樹可能退化成鏈表,相應的,二叉搜索樹的查找操作是和這棵樹的高度相關的,而此時這顆樹的高度就是這顆樹的節點數n,同時二叉搜索樹相應的算法全部退化成 O(n) 級別。
顯然,說二叉搜索樹的查找、插入、刪除這三個操作都是O(lgn) 級別的,只是一個大概的估算,具體要和二叉搜索樹的形狀相關。但總的來說時間復雜度在o(logn)
到o(n)之間。
9. 簡單總結
二叉搜索樹的節點查詢、構造和刪除性能,與樹的高度相關,如果二叉搜索樹能夠更“平衡”一些,避免了樹結構向線性結構的傾斜,則能夠顯著降低時間復雜度。二叉搜索樹的存儲方面,相對於線性結構只需要保存元素值,樹中節點需要額外的空間保存節點之間的父子關系,所以在存儲消耗上要高於線性結構。