數據結構思維筆記(十三)二叉搜索樹

本章繼續承接上章的內容,具體實現TreeMap中的方

1.簡單的TreeMap

這裏比較核心的一個方法是findNode,用來尋找與鍵值相當的節點,下面是它的實現:

  private Node findNode(Object target) {
        if (target == null) {
            throw new IllegalArgumentException();
        }

        @SuppressWarnings("unchecked")
        Comparable<?super K> k = (Comparable<?super K>)target;

        Node node = root;
        while (node != null) {
            int cmp = k.compareTo(node.key);
            if (cmp <0)
                node = node.left;
            else if (cmp>0)
                node = node.right;
            else 
            return node;
        }
        return null;
    }
  • 在這個實現中,null不是鍵的合法值。
  • 在我們可以在target上調用compareTo之前,我們必須把它強制轉換爲某種形式的Comparable。這裏使用的“類型通配符”會儘可能允許;也就是說,它適用於任何實現Comparable類型,並且它的compareTo接受K或者任和K的超類(可以同任何類型做比較)。

之後,實際搜索比較簡單。我們初始化一個循環變量node來引用根節點。每次循環中,我們將目標與node.key比較。如果目標小於當前鍵,我們移動到左子樹。如果它更大,我們移動到右子樹。如果相等,我們返回當前節點(這裏用的是迭代,不斷賦值)。


2.搜索值

findNode運行時間與樹的高度成正比,而不是節點的數量,因爲我們不必搜索整個樹。但是對於containsValue,我們必須搜索值,而不是鍵;BST 的特性不適用於值,因此我們必須搜索整個樹。

下面是containsValue方法,這裏用遞歸實現:

public boolean containsValue(Object target) {
    return containsValueHelper(root, target);
}

private boolean containsValueHelper(Node node, Object target) {
    if (node == null) {
        return false;
    }
    if (equals(target, node.value)) {
        return true;
    }
    if (containsValueHelper(node.left, target)) {
        return true;
    }
    if (containsValueHelper(node.right, target)) {
        return true;
    }
    return false;
}

這是containsValueHelper的工作原理:

  • 第一個if語句檢查遞歸的邊界情況。如果nodenull,那意味着我們已經遞歸到樹的底部,沒有找到target,所以我們應該返回false。請注意,這隻意味着目標沒有出現在樹的一條路徑上;它仍然可能會在另一條路徑上被發現。
  • 第二種情況檢查我們是否找到了我們正在尋找的東西。如果是這樣,我們返回true。否則,我們必須繼續。
  • 第三種情況是執行遞歸調用,在左子樹中搜索target。如果我們找到它,我們可以立即返回true,而不搜索右子樹。否則我們繼續。
  • 第四種情況是搜索右子樹。同樣,如果我們找到我們正在尋找的東西,我們返回true。否則,我們搜索完了整棵樹,返回false

該方法“訪問”了樹中的每個節點,所以它的所需時間與節點數成正比


3.實現put

put方法比起get要複雜一些,因爲要處理兩種情況:

  1. 如果給定的鍵已經在樹中,則替換並返回舊值
  2. 否則必須在樹中添加一個新的節點,在正確的地方
public V put(K key, V value) {
    if (key == null) {
        throw new IllegalArgumentException();
    }
    if (root == null) {
        root = new Node(key, value);
        size++;
        return null;
    }
    return putHelper(root, key, value);
}

private V putHelper(Node node, K key, V value) {
    Comparable<? super K> k = (Comparable<? super K>) key;
    int cmp = k.compareTo(node.key);

    if (cmp < 0) {
        if (node.left == null) {
            node.left = new Node(key, value);
            size++;
            return null;
        } else {
            return putHelper(node.left, key, value);
        }
    }
    if (cmp > 0) {
        if (node.right == null) {
            node.right = new Node(key, value);
            size++;
            return null;
        } else {
            return putHelper(node.right, key, value);
        }
    }
    V oldValue = node.value;
    node.value = value;
    return oldValue;
}

第一個參數node最初是樹的根,但是每次我們執行遞歸調用,它指向了不同的子樹。就像get一樣,我們用compareTo方法來弄清楚,跟隨哪一條樹的路徑。如果cmp < 0,我們添加的鍵小於node.key,那麼我們要走左子樹。有兩種情況:

  • 如果左子樹爲空,那就是,如果node.leftnull,我們已經到達樹的底部而沒有找到key。這個時候,我們知道key不在樹上,我們知道它應該放在哪裏。所以我們創建一個新節點,並將它添加爲node的左子樹。
  • 否則我們進行遞歸調用來搜索左子樹。

如果cmp > 0,我們添加的鍵大於node.key,那麼我們要走右子樹。我們處理的兩個案例與上一個分支相同。最後,如果cmp == 0,我們在樹中找到了鍵,那麼我們更改它並返回舊的值。


4.中序遍歷

這裏我們還剩最後一個方法KeySet,它返回一個Set,按升序包含樹中的鍵。在其他Map實現中,keySet返回的鍵沒有特定的順序,但是樹形實現的一個功能是,對鍵進行簡單而有效的排序。下面是如何實現它的:

public Set<K> keySet() {
    Set<K> set = new LinkedHashSet<K>();
    addInOrder(root, set);
    return set;
}

private void addInOrder(Node node, Set<K> set) {
    if (node == null) return;
    addInOrder(node.left, set);
    set.add(node.key);
    addInOrder(node.right, set);        
}

keySet中,我們創建一個LinkedHashSet,這是一個Set實現,使元素保持有序,第一個參數node最初是樹的根,但正如你的期望,我們用它來遞歸地遍歷樹。addInOrder對樹執行經典的“中序遍歷”。

  1. 按順序遍歷左子樹。
  2. 添加node.key
  3. 按順序遍歷右子樹。

5.二叉搜索樹的問題

我們獲取最有查詢效率時,一般是O(log(n)),這種情況會在所搜索的樹爲平衡二叉樹時出現,若不是平衡二叉樹,搜索效率則會很低。

如果你思考put如何工作,你可以弄清楚發生了什麼。每次添加一個新的鍵時,它都大於樹中的所有鍵,所以我們總是選擇右子樹,並且總是將新節點添加爲,最右邊的節點的右子節點。結果是一個“不平衡”的樹,只包含右子節點。

這種樹的高度正比於n,不是logn,所以getput的性能是線性的,不是對數的


6.自平衡樹

這個問題有兩種可能的解決方案:

  • 你可以避免向Map按順序添加鍵。但這並不總是可能的。 你可以製作一棵樹,如果碰巧按順序處理鍵,那麼它會更好地處理鍵。(按順序添加會導致這是一個極不平衡的樹)
  • 第二個解決方案是更好的,有幾種方法可以做到。最常見的是修改put,以便它檢測樹何時開始變得不平衡,如果是,則重新排列節點。具有這種能力的樹被稱爲“自平衡樹”。普通的自平衡樹包括 AVL 樹(“AVL”是發明者的縮寫),以及紅黑樹,這是 JavaTreeMap所使用的。

總而言之,二叉搜索樹可以以對數時間實現getput,但是隻能按照使得樹足夠平衡的順序添加鍵。自平衡樹通過每次添加新鍵時,進行一些額外的工作來避免這個問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章