1. 程式人生 > >查詢樹之二叉查詢樹

查詢樹之二叉查詢樹

寫在前面

最近一直在專心學習資料結構,剛好看到樹這一章。之前大一學習的資料結構是以C語言為基礎的,然而當時也沒有好好聽講,現在也是比較後悔,通過接下來的這段時間準備好好學習一下樹的知識,也通過部落格來記錄一下對於其中的理解。

概述

先簡單介紹一下樹以及二叉樹的概念。

樹的概念

樹(Tree)是一些節點的集合,這個集合可以是空集,則該樹就是一棵空樹。而對於一棵非空樹而言,這棵樹是由一個稱為根root的節點以及0個或多個非空的子樹T1、T2、T3…組成,這些子樹中每一棵的根都被來自根root的一條有向的邊所連線。

每一棵子樹的根都是根root的兒子,而根root是每一棵子樹的根的父親。沒有兒子的節點稱為樹葉。具有相同父親的節點為兄弟。

在上圖中,節點A是根,節點E有一個父親A並且有一個兒子I,每一個節點可以有多個兒子,因此節點L有兒子M、N、P。沒有其中節點B、J、K、M、N、P、I、F都沒有兒子,因此它們都是樹葉。節點M、N、P擁有共同的父親,所有它們是兄弟。

樹的結構與生活中見到的樹是相反的,我們這裡的樹最上面的樹根,而樹葉則在下面。

對於一棵樹而言,某個節點的深度為該節點到根中間連線的數目,因此圖中根A的深度為0,節點G的深度為2。某個節點的高為該節點到它所能到達最遠的一片樹葉的長,所以一棵樹所有的樹葉的高都是0。一棵樹的高等於它的根的高,一棵樹的深度等於它的最深的樹葉的深度。該深度總是等於這棵樹的高。

二叉樹的概念

二叉樹(Binary Tree)的特點是每一個結點都不能有多於兩個的兒子。

二叉樹的每個結點最多有兩個子節點,所以我們可以直接它到兩個子節點的鏈。以及它還應該擁有一個element元素用來儲存在該節點的資料。

class BinaryTreeNode<T>{
	T element;
	BinaryTreeNode<T> left;  //左孩子
	BinaryTreeNode<T> right;  //右孩子
}

二叉查詢樹的概念

對於一棵二叉樹而言,樹中的每個節點X,它的左子樹中所有項的值小於X中的值,而它的右子樹中所有項的值大於X中的值,那麼就是一棵二叉查詢樹。

上圖中只有左邊的樹是二叉查詢樹,右邊的樹不是。右邊的樹的根節點的值為6,而它的左子樹中有一個節點的值為7。

在二叉查詢樹中我們會使用到一個排序的介面,這個介面就是Comparable。我們將會使用到該介面來進行樹中項的比較來確定它們之間的關係。

程式碼詳解

該程式碼塊中將會有以下類和方法:

  • BinaryTreeNode,該類用於建立每一個結點。
  • 方法makeEmpty,將樹置為空樹。
  • 方法isEmpty,判斷是否為空樹。
  • 方法contains,判斷樹中是否包含某元素。
  • 方法findMin,查詢樹中最小值。
  • 方法findMax,查詢樹中最大值。
  • 方法insert,在樹中合適位置插入元素。
  • 方法remove,移除樹中某元素。
  • 方法printTree,列印該二叉查詢樹。
/**
 * @author: zhangocean
 * @Date: 2018/10/7 12:44
 */
public class BinarySearchTree<T extends Comparable<? super T>> {

    private BinaryTreeNode<T> root;

    public BinarySearchTree() {
        root = null;
    }

    public void makeEmpty(){
        root = null;
    }

    public boolean isEmpty(){
        return root == null;
    }

    public boolean contains(T element){
        return contains(element, root);
    }

    private boolean contains(T element, BinaryTreeNode<T> root){
		//基準條件
        if(root == null){
            return false;
        }

        int compareResult = element.compareTo(root.element);
        if(compareResult > 0){
            return contains(element, root.right);
        } else if (compareResult < 0){
            return contains(element, root.left);
        } else {
            return true;
        }
    }

    /**
     * 查詢樹中最小值
     */
    public T findMin(){
        if(isEmpty()){
            try {
                throw new Exception("樹為空");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return (T) findMin(root).element;
    }

    private BinaryTreeNode findMin(BinaryTreeNode root){
        if(root == null){
            return null;
        }else if (root.left == null){
            return root;
        }
        return findMin(root.left);
    }

    /**
     * 查詢最大值
     */
    public T findMax() throws Exception {
        if(isEmpty()){
            throw new Exception("樹為空");
        }
        return (T) findMax(root).element;
    }

    private BinaryTreeNode findMax(BinaryTreeNode root){
        if(root == null){
            return null;
        }else if (root.right == null){
            return root;
        }
        return findMin(root.right);
    }

    /**
     * 插入節點
     */
    public void insert(T element){
        root = insert(element, root);
    }

    private BinaryTreeNode<T> insert(T element, BinaryTreeNode<T> root){
        if(root == null){
            return new BinaryTreeNode<>(element, null, null);
        }

        int compareResult = element.compareTo(root.element);
        if(compareResult > 0){
            root.right = insert(element, root.right);
        } else if (compareResult < 0){
            root.left = insert(element, root.left);
        }
        return root;
    }

    /**
     * 刪除節點
     */
    public void remove(T element){
        root = remove(element, root);
    }

    private BinaryTreeNode<T> remove(T element, BinaryTreeNode<T> root){
		//未找到需要刪除的節點
        if(root == null){
            return null;
        }
        int compareResult = element.compareTo(root.element);

        if(compareResult > 0){
            root.right = remove(element, root.right);
        } else if (compareResult < 0){
            root.left = remove(element, root.left);
        } else if (root.left != null && root.right != null){
			//將該節點替換為其右子樹中的最小項的值
            root.element = (T) findMin(root.right).element;
			//移除該右子樹中的最小項節點
            root.right = remove(root.element, root.right);
        } else {
            root = root.left != null ? root.left : root.right;
        }
        return root;
    }

    /**
     * 列印該二叉查詢樹
     */
    public void printTree(){
        if(isEmpty()){
            System.out.println("空樹");
        } else {
            printTree(root);
			System.out.println("");
        }
    }

    private void printTree(BinaryTreeNode<T> root){
        if(root != null){
            printTree(root.left);
            System.out.print(root.element + " ");
            printTree(root.right);
        }
    }

    private class BinaryTreeNode<T>{

        T element;
        BinaryTreeNode<T> left;
        BinaryTreeNode<T> right;

        public BinaryTreeNode(T element) {
            this.element = element;
            left = null;
            right = null;
        }

        public BinaryTreeNode(T element, BinaryTreeNode<T> left, BinaryTreeNode<T> right) {
            this.element = element;
            this.left = left;
            this.right = right;
        }
    }
}

通過呼叫第二個過載的contains方法,我們可以判斷該樹中是否有我們需要的元素element。首先我們通過compareTo方法獲得需要比較的元素和當前所處位置節點的值的大小,然後通過遞迴調用搜查子樹中的元素。注意使用遞迴時一定要確認基準條件的設定,不然就會陷入死遞迴而無法自拔,這裡我們的基準條件就是程式碼27行所示。

在查詢最大值以及最小值方法中我們根據二叉查詢樹的性質,即樹中的每個節點X,它的左子樹中所有項的值小於X中的值,而它的右子樹中所有項的值大於X中的值可以很容易的找出樹中的極值。

其中插入節點很簡單可以通過程式碼理解。但是當我們進行節點的刪除時會涉及到三種情況:

  • 當刪除的節點是樹葉時,直接刪除。
  • 當刪除的節點有一個兒子時,直接調整該節點的父親與兒子的鏈即可刪除該節點。
  • 當刪除的節點有兩個兒子時,一般的刪除策略是用該節點右子樹中的最小的節點的值代替該節點的資料,並將那個最小的節點刪除。因為該最小的節點一定沒有左孩子,所以很容易remove。具體刪除過程可看下圖:

下面我們進行測試環節:

public static void main(String[] args) {
        Random random = new Random(100);
        BinarySearchTree<Integer> tree = new BinarySearchTree<>();
        tree.printTree();
        for(int i=0;i<10;i++){
            tree.insert(random.nextInt(100));
        }
        tree.printTree();
        tree.makeEmpty();
        tree.printTree();
    }

這裡測試我們首先建立一個空樹,然後使用隨機數往樹中新增元素,再清空該樹,輸出結果如下所示。

空樹
13 15 23 36 50 66 74 88 91 
空樹

總結

我們這裡所講解的二叉查詢樹,它的時間複雜度為(log N)。因為我們用常數時間在樹中降低了一層,這樣一來,對其進行操作的樹大致減少一半左右的時間。因此,所欲操作的執行時間都是O(d),其中d是包含所訪問的項的節點的深度。至於其他的樹我也會在之後的部落格中寫到。