java語言實現二叉樹

咕咚0203發表於2020-10-31

一、概念

對於大量的輸入資料,連結串列的線性訪問時間太慢,不宜使用。樹這種結構極大的縮短了資料的訪問時間。什麼是樹呢?它是怎麼做到提高訪問效率的呢?樹可以用幾種方式定義,定義樹的一種自然方式是遞迴方式,一棵樹是一些節點的集合,這些集合可以是空集;若不是空集,則樹由root)節點以及n(n>=0)個child)節點組成,這些子節點每一個都有來自根節點的一條有向邊(edge)所連線。

如上圖,這是一棵樹。【A】為根節點,【B】、【C】、【D】、【E】、【F】、【G】為【A】的子(child)節點,另外每個節點下面都可能有任意多child,也有可能沒有child,對於沒有child 的節點,我們稱為leaf)節點。從根節點到任意節點經過的邊(edge)數稱為該節點的深度。上圖中【E】的深度是1,【P】的深度是3。一棵樹的深度等於該樹最深葉節點的深度。

 


二、應用

樹的應用有很多,流行的用法之一是目錄結構,如圖是UNIX檔案系統中一個典型的目錄。

這個目錄的根是/usr(* 號表示此為目錄),/usr下面有三個子目錄:mark、alex、bill,alex目錄下面有一個junk的檔案。上圖的所有檔案都可以通過根節點遞迴遍歷得到,如下虛擬碼

private void listAll(int depth){
    printName(depth); // 列印出該深度下的節點名稱
    if(isDirectory()){ 
    // 如果該節點是資料夾,則繼續遍歷該資料夾下檔案
    for(File c:thisDiretory){
        listAll(depth+1);
    }
}
private void listAll(){
    listAll(0);
}

通過這種listAll的遞迴,就可以列印出根節點下的所有節點。

 


三、二叉樹

二叉樹是一種特殊的樹,其每個節點下最多有兩個子節點。如下圖顯示的是普通樹和二叉樹

上圖普通樹的節點【6】下面有三個子節點,所以它並不屬於二叉樹。

二叉樹有許多和搜尋無關的應用,其主要用處之一就是在編譯器的設計領域。例:表示式樹

上圖是一個算術運算的表示式,我們可以通過遞迴計算左子樹和右子樹所得到的值,應用在根處的運算子算出整棵樹的值。其左子樹的值是a+(b*c),右子樹的值是((d*e)+f)*g,因此整棵樹表示:(a+(b*c))+(((d*e)+f)*g)

 


四、二叉查詢樹

二叉樹的一個重要的應用是它們在查詢中的使用。於是便衍生出二叉查詢樹這種結構。使二叉樹成為二叉查詢樹的性質是:對於樹中的任意節點X,它的左子樹中所有節點的值都要小於的值,而它的右子樹中所有節點的值都要大於的值

上圖所示,左邊的為二叉查詢樹,右邊的就不是,因為它不滿足二叉查詢樹的性質。下面我們用程式碼實現二叉查詢樹,並對其進行資料插入和資料刪除的操作。

程式碼實現思路分析:首先定義樹的節點,二叉樹只有一個儲存的值和兩個子節點,所以我們定義的節點物件如下:

 private static class BinaryNode<T> {
        // 節點中儲存的資料
        T element;
        // 左子樹
        BinaryNode<T> left;
        // 右子樹
        BinaryNode<T> right;
        BinaryNode(T element) {
            this(element, null, null);
        }
        BinaryNode(T element, BinaryNode<T> left, BinaryNode<T> right) {
            this.element = element;
            this.left = left;
            this.right = right;
        }
        @Override
        public String toString() {
            return "BinaryNode{" +
                    "element=" + element +
                    ", left=" + left +
                    ", right=" + right +
                    '}';
        }
    }

然後建立二叉查詢樹物件(BinarySearchTree),為了要寫出一個一般的類,我們讓這個類繼承Comparable類,然後定義一個根節點root,新增無參構造器。

public class BinarySearchTree<T extends Comparable<? super T>> {
    // 根節點
    private BinaryNode<T> root;
    BinarySearchTree() {
        root = null;
    }
}

構造器和根節點定義好,接下來我們就開始新增新增(insert)方法

private void insert(T t){
     root = insert(t, root);
 }
 private BinaryNode<T> insert(T t,BinaryNode<T> n){
     if(n == null)
         return new BinaryNode(t, null, null);
     // 比較插入資料和n節點的元素大小
     int compareResult = x.compareTo(n.element);
     if(compareResult < 0) // 插入到左子樹中
         t.left = insert(t, n.left);
     else if(compareResult > 0)  // 插入到右子樹中
         t.right = insert(t, n.right);
     return t;
 }

insert方法新增成功以後,我們開始新增刪除(remove)方法,刪除的業務相對比新增要複雜一點,下面我們獻上程式碼:

private void remove(T t){
    root = remove(t,BinaryNode root);
}
private BinaryNode<T> remove(T t,BinaryNode<T> n){
      if(n == null)
          return n;
      int compareResult = t.compareTo(n.element);
      if(compareResult < 0)
          t.left = remove(t, n.left);
      else if(compareResult  > 0)
          t.right = remove(t.right);
      else if(t.left !=null &&  t.right != null){
          t.element = findMin(t.right).element;
          t.right = remove(t.element, t.right);
      }else{
          t = ( t.left == null ) ? t.right : t.left
      }
}
​
private BinaryNode<T> findMin(BinaryNode<T> n){
    if(n == null)
        return null;
    else if(n.left == null)
        return n
     return findMin(n.left);
}

以上,我們已經實現了一棵二叉查詢樹,下面我們對程式碼進行測試

    /**
     * 二叉查詢樹測試
     */
    @Test
    public void BinarySearchTreeTest() {
        BinarySearchTree<Integer> bt = new BinarySearchTree<>();
        bt.insert(6);
        bt.insert(2);
        bt.insert(8);
        bt.insert(1);
        bt.insert(4);
        bt.insert(3);
        bt.printTree();
        bt.remove(2);
        bt.printTree();
    }

該測試新增和刪除之後的資料如下圖顯示

從圖上我們可以看出,新增和刪除符合我們的預期,至此,我們的二叉查詢樹已經實現。

 


五、存在的缺陷

從二叉樹的定義我們可以知道,每個節點下的子節點最多有兩個,那麼考慮極端情況:每個節點下都只有一個子節點

我們通過剛剛完成的程式碼進行測試

    /**
     * 極端情況下二叉查詢樹測試
     */
    @Test
    public void BinarySearchTreeTest1() {
        BinarySearchTree<String> bt = new BinarySearchTree<>();
        bt.insert("A");
        bt.insert("B");
        bt.insert("C");
        bt.insert("D");
        bt.insert("E");
        bt.printTree();
    }

以上程式碼執行的結果會產生如下圖的“樹”

從圖上我們可以看出,二叉樹退化了,退化成了一個連結串列,而我們期望的對二叉樹的操作平均耗時在O(log N) ,但這種極端情況下操作耗時為O(N)

為了避免這種極端情況的效能損耗,我們必須要對這樣的二叉樹結構進行改進。

本章篇幅有限,我們下章再介紹如何改進才能有效的避免這種極端情況的發生,感興趣的讀者朋友可以 關注本公眾號,和我們一起學習探究。


本人因所學有限,如有錯誤之處,望請各位指正!

相關文章