圖解二分搜索樹

1. 樹結構基礎

樹和鏈表一樣是屬於動態數據結構。

二叉樹(二分樹)的本意就是對於每個節點最多有左右兩個節點,這就是分成二叉。當然也有三叉,四叉等多叉樹。

二叉樹的特點:

  • 具有唯一一個根節點。
  • 每個節點最多有左右兩個節點(孩子)。
  • 每個節點最多隻有一個父親節點,除了根節點。
  • 除最後一層無任何子節點外,每一層上的所有節點都有兩個子節點的二叉樹叫做滿二叉樹。下圖就是一個滿二叉樹。
  • 每個節點的左節點那一分支的子樹其實也是一棵二叉樹,叫做左子樹,右節點也是,叫做右子樹。

在這裏插入圖片描述

二叉樹經常使用遞歸實現:
在這裏插入圖片描述

(其實一個節點也可以看成一棵二叉樹)
在這裏插入圖片描述

二分搜索樹(Binary Search Tree)是二叉樹,它要求每個節點的值(value):

  • 大於其左子樹的所有節點的值。
  • 小於其右子樹的所有節點的值。
  • 每個節點的值具有可比較性(Comparable)。

2. 創建二分搜索樹

先來看看節點的結構,它需要一個來存儲值的變量,還需要左右兩個節點:

    class Node {
        private E e;
        private Node left, right;

        public Node(E e) {
            this.e = e;
            this.left = null;
            this.right = null;
        }
    }

那麼對於樹來說,需要有一個根節點,額外的還可以定義一個size來記錄一顆樹有多少個節點。把Node封裝到二分搜索樹中:

/**
 * 存儲的值具有可比較性,所以需要指定傳入的值是Comparable子類
 * @param <E>
 */
public class BST<E extends Comparable<E>> {
    private class Node {
        private E e;
        private Node left, right;

        public Node(E e) {
            this.e = e;
            this.left = null;
            this.right = null;
        }
    }

    private Node root;
    private int size;

    public BST() {
        root = null;
        size = 0;
    }

    public int getSize() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }
}

3. 二分搜索樹的添加

注意:根據定義,該二分搜索樹是不包含重複元素的。所以如果想包含重複元素,則需要修改定義:左子樹小於等於節點;或右子樹小於等於節點。

最直觀的寫法如下:

  1. 先判斷根節點,如果爲空,直接new一個新的節點。
  2. 否則,新插入的值就與根節點的值判斷大小,如果大於根節點的值,則新插入的值肯定是在右子樹,此時判斷根節點的右節點是否爲null,如果爲null,說明新值可以插入到該位置,那麼就可以結束了添加操作。如果小於根節點的值,則新插入的值肯定是在左子樹,此時判斷根節點的左節點是否爲null,如果爲null,說明新值可以插入到該位置,那麼就可以結束了添加操作。如果遇到的值與根節點的值相等,那麼不操作,直接結束添加操作。(遞歸終止條件)
  3. 如果第二步不成立,那麼說明新插入的值得在根節點的左子樹或右子樹中。此時還得判斷新插入的值與根節點的值判斷大小,如果小於該根節點的值,則新插入的值肯定在左子樹,那麼遞歸跳轉到左子樹中,否則肯定在右子樹插入。
    public void add(E e) {
        if(root == null) {
            root = new Node(e);
            size++;
            return;
        } else {
            add(root, e);
        }
    }

  private void add(Node node, E e) {
        // 終止條件
        // 每次遞歸,跟根節點的值比較
        if(e.compareTo(node.e) == 0) {
            return;
        } else if(e.compareTo(node.e) < 0 && node.left == null) {
            // 如果根節點的值小於e
            // 加到左子節點
            node.left = new Node(e);
            size++;
            return;
        } else if(e.compareTo(node.e) > 0 && node.right == null) {
            // 如果根節點的值大於e
            // 加到右子節點
            node.right = new Node(e);
            size++;
            return;
        }

        if(e.compareTo(node.e) < 0) {
            // 如果根節點的值小於e
            // 跳轉到左子樹
            add(node.left, e);
        } else {
            // 如果根節點的值大於e
            // 跳轉到右子樹
            add(node.right, e);
        }

現在看看上面的寫法,是非常清晰的,但是對於代碼量還可以再優化,上面冗餘的地方就是遞歸的終止條件,每次遞歸一次就要先進行三次判斷。通過跳轉子樹的語句可以發現,最終會遞到新值插入的位置,也就是遞歸一直遞到樹底,那麼此時樹底肯定是null,供我們插入新的節點,所以可以這樣修改:

    public void add(E e) {
        root = add(root, e);
    }

    /**
     * 每次遞歸會返回一顆新樹
     * 直接遞歸到樹底,此時該位置就是新插入的節點的最終位置
     * 因爲每次遞歸時,會把當前的根節點的值和要插入的值比較:
     * 1. 如果大於根節點,那麼新插入的值肯定是要插入到根節點的右子樹中
     * 2. 如果小於根節點,那麼新插入的值肯定是要插入到根節點的左子樹中
     *
     * 因爲新插入的節點會改變原先樹的結構,因爲相比原先多了個節點嘛
     * 所以必須維護把舊樹替換成新的樹
     * @param root
     * @param e
     * @return 返回新節點插入後新樹的根
     */
    private Node add(Node node, E e) {
        // 一個null也可以看成一個節點,也可以看成一顆樹
        // 遞歸到底部,直到遇到null就創建節點,把它當成一棵樹,然後就返回該樹的根
        if(node == null) {
            size++;
            return new Node(e);
        }

      if(e.compareTo(node.e) < 0) {
            // 如果是左子樹,add返回新插入節點後的新左子樹的根,那麼替換掉舊樹
            node.left = add(node.left, e);
        } else if(e.compareTo(node.e) > 0) {
            // 如果是右子樹,add返回新插入節點後的新右子樹的根,那麼替換掉舊樹
            node.right = add(node.right, e);
        }

        // 返回根
        return node;
    }

圖解:

在這裏插入圖片描述

4. 二分搜索樹的查詢

對於節點的查詢需要結合鍵值對,以鍵來查值,後續講。

打印整顆二分搜索樹有4種方式:前序遍歷,中序遍歷,後續遍歷,層序遍歷。

4.1 前序遍歷

說白了就是先訪問根節點,再去訪問左節點,然後去訪問右結點。

   /**
     * 前序遍歷
     */
    public void preOrder() {
        preOrder(root);
    }
    private void preOrder(Node node) {
        if(node != null) {
            System.out.println(node.e);
            preOrder(node.left);
            preOrder(node.right);
        }
    }

在這裏插入圖片描述

非遞歸寫法:

  /**
     * 非遞歸的前序遍歷
     * 藉助棧,注意是先壓入右節點再壓入左節點
     * 這樣左節點就在棧頂,下次就先取出左節點
     */
    public void preOrderNB() {
        Stack<Node> stack = new Stack<>();
        stack.push(root);
        while(!stack.isEmpty()) {
            Node node = stack.pop();
            System.out.println(node);
            // 先壓入右節點
            if(node.right != null) {
                stack.push(node.right);
            }
            // 再壓入左節點
            if(node.left != null) {
                stack.push(node.left);
            }
        }
    }

4.2 中序遍歷

先訪問左節點,然後去訪問根節點,再去訪問右結點。**中序遍歷的結果其實樹中所有值排序後的結果。**因爲左子樹的所有值小於根節點,根節點小於右子樹所有值。

    /**
     * 中序遍歷
     */
    public void inOrder() {
        inOrder(root);
    }
    private void inOrder(Node node) {
        if(node == null) {
            return;
        }
        inOrder(node.left);
        System.out.println(node.e);
        inOrder(node.right);
    }

在這裏插入圖片描述

中序遍歷的非遞歸實現:

    /**
     * 中序遍歷非遞歸寫法
     * 還是藉助棧,但這次我們是先打印左節點。
     * 1. 因爲是要先打印根節點的左節點,那麼第一個打印的肯定是樹中的最小值,即左下角的節點,當然提前是存在左節點。
     * 2. 那麼我們可以先把根節點的全部左節點壓入棧,然後再彈出。
     * 3. 對於根節點的右節點有兩種情況:
     *  - 如果根節點的右節點存在,則重複上面的操作。
     *  - 如果根節點的右節點不存在,則結束。
     * 4. 當棧爲空時結束。
     */
    public void inOrderNR() {
        inOrder(root);
    }
    private void inOrderNR(Node node) {
        Stack<Node> stack = new Stack<>();
        Node cur = node;
        while(!stack.isEmpty()) {
            // 以cur爲根的樹,先把它的全部左節點先壓入棧
            while(cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            // 棧頂彈出
            cur = stack.pop();
            // 打印左節點
            System.out.println(cur.e);
            // 跳到剛剛打印左節點的右子節點中
            // 不用判斷存不存在,因爲根據上面的邏輯,如果不存着右節點,則棧會彈出上一個左節點。
            cur = cur.right;
        }
    }

4.3 後序遍歷

先訪問左節點,然後去訪問右結點,再去訪問根節點。

後序遍歷的一個應用是:爲二分搜索樹釋放內存。java中是自動釋放內存的。C++就可能需要手動釋放。

   /**
     * 後序遍歷
     */
    public void postOrder() {
        postOrder(root);
    }
    private void postOrder(Node node) {
        if(node != null) {
            postOrder(node.left);
            postOrder(node.right);
            System.out.println(node.e);
        }
    }

在這裏插入圖片描述

後序遍歷的非遞歸實現更復雜。後序遍歷的複雜地方就是:必須保證根節點的左子樹和右子樹被訪問後纔可以打印根節點。所以需要一個標記位pos,來保存右節點已經被訪問的情況。

   /**
     * 後序遍歷非遞歸寫法
     * 後序遍歷的複雜地方就是:必須保證根節點的左子樹和右子樹被訪問後纔可以打印根節點
     * 訪問根節點的可能有兩種情況:根節點的右節點爲空,或者右節點已經被訪問過
     * 所以需要一個標記位pos,來保存右節點已經被訪問的情況
     */
    public void postOrderNR() {
        postOrderNR(root);
    }
    private void postOrderNR(Node node) {
        Stack<Node> stack = new Stack<>();
        Node cur = node;
        Node pos = null;
        while(cur != null || !stack.isEmpty()) {
            // 以cur爲根的樹,先把它的全部左節點先壓入棧
            while(cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            // 查看棧頂,這裏得申請一個臨時變量,如果使用cur來存儲,那麼當打印根節點時,下次循環的cur不爲空會出錯
            Node temp = stack.peek();
            // 如果根節點的右節點爲空,或者右節點已經被訪問過
            if(temp.right == null || temp.right == pos) {
                // 打印根節點
                System.out.println(temp.e);
                // 保存已經被訪問過的點
                pos = stack.pop();
            } else {
                cur = temp.right;
            }

        }
    }

還是不懂的建議拿個例子試試,我也有圖解,但是圖片太多了發在這佔太多空間,可以下載我寫的PPT:藍奏雲

也可以看B站視頻,我不小心看到的,雖然跟我的寫法有點不一樣,但是思路一樣的:點我跳轉

4.4 層序遍歷

把數分層,然後一層一層地打印。

    /**
     * 層序遍歷/廣度遍歷
     *
     * queue:
     * 1. offer 添加元素,隊列滿則返回false,poll 移除隊頭且返回隊頭,隊列爲空返回true
     * 2. add 添加元素,隊列滿則拋出一個IIIegaISlabEepeplian異常, remove 移除隊頭且返回隊頭,隊列爲空則拋出一個NoSuchElementException異常
     */
    public void levelOrder() {
        Queue<Node> queue = new LinkedList<>();
        queue.add(root);
        while(!queue.isEmpty()) {
            Node cur = queue.remove();
            System.out.println(cur.e);

            if(cur.left != null) {
                queue.add(cur.left);
            }
            if(cur.right != null) {
                queue.add(cur.right);
            }
        }
    }

在這裏插入圖片描述

5.二叉搜索樹的刪除

因爲要刪除任意節點有點複雜,那麼先來說說如何查詢最小/大值,然後再引入刪除最小/大值,最後再來刪除任意節點就比較好理解。

5.1 查詢最小/最大值

  • 二叉搜索樹的最小值一定是在樹的左下角。所以要查詢時,一直往樹的左節點找,直到左節點的下一個左節點爲null,說明此時的左節點就是最小值。
  • 而最大值一定在樹的右下角。查詢一直往樹的右節點找,直到右節點的下一個右節點爲null,說明此時的右節點就是最大值。
  /**
     * 查詢二叉搜索樹的最小值
     * 一直向左節點走,直到左節點的下一個左節點爲null,說明該左節點就是最小值
     * @return
     */
    public E minimun() {
        if(size == 0) {
            throw new IllegalArgumentException("Tree is empty.");
        }
        return minimun(root).e;
    }
    private Node minimun(Node node) {
        if(node.left == null) {
            return node;
        } else {
            return minimun(node.left);
        }
    }

    /**
     * 查詢二叉搜索樹的最大值
     * 一直向右節點走,直到右節點的下一個右節點爲null,說明該右節點就是最大值
     * @return
     */
    public E minimax() {
        if(size == 0) {
            throw new IllegalArgumentException("Tree is empty.");
        }
        return minimax(root).e;
    }
    private Node minimax(Node node) {
        if(node.right == null) {
            return node;
        } else {
            return minimax(node.right);
        }
    }

5.2 刪除最小/大值

知道了如何查詢最小/大值,那麼要刪除就容易了,但是要注意:

  • 對於刪除最小值,刪除後還要注意刪除的節點有沒有右節點,如果有,那得把右節點接到原樹中,以免數據丟失,至於怎麼接,可以把要刪除的節點替換成要刪除的節點的右節點。這一步也間接地處理了對於要刪除的節點沒有左右節點的情況,因爲對於這種情況我們得把要刪除的節點的父親節點的left指向null,表示刪除。
  • 對於刪除最大值也如此,如果要刪除的節點有左節點,那麼必須把左節點接回樹中。
   /**
     * 按照常規的,刪除後返回刪除的元素,
     * 1. 先把要刪除的最小值查出來
     * 2. 然後就刪除,因爲刪除後會改變樹的結構,所以得更新root
     * 刪除細節:刪除最小元素,注意要刪除的節點有右節點的情況,因爲如果刪除的節點不把右節點接到樹中,會丟失數據。
     * @return
     */
    public E removeMin() {
        E ret = minimun();
        root = removeMin(root);
        return ret;
    }

    /**
     * 刪除最小值
     * @param node
     * @return 刪除後返回以node爲根的樹
     */
    private Node removeMin(Node node) {
        if(node.left == null) {
            // 此時把要刪除的節點的右節點保存起來,不用管有沒有
            Node rightNode = node.right;
            // 這裏斷開要刪除的右節點,防止成環
            node.right = null;
            size--;
            // 然後把要刪除的右節點返回,替換掉要刪除的節點,這就成功刪除了
            return rightNode;
        }

        // 在最後一步,removeMin會把要刪除的節點的右節點返回到這,把node.left替換掉,完成刪除
        // 因爲此時是改變左子樹的結構,所以要把新樹的根返回給左子樹
        node.left = removeMin(node.left);

        return node;
    }

    /**
     * 按照常規的,刪除後返回刪除的元素,
     * 1. 先把要刪除的最大值查出來
     * 2. 然後就刪除,因爲刪除後會改變樹的結構,所以得更新root
     * 刪除細節:刪除最大元素,注意要刪除的節點有左節點的情況,因爲如果刪除的節點不把左節點接到樹中,會丟失數據。
     * @return
     */
    public E removeMax() {
        E ret = minimax();
        root = removeMax(root);
        return ret;
    }

    /**
     * 刪除最大值
     * @param node
     * @return 刪除後返回以node爲根的樹
     */
    private Node removeMax(Node node) {
        if(node.right == null) {
            // 此時把要刪除的節點的左節點保存起來,不用管有沒有
            Node leftNode = node.left;
            // 這裏斷開要刪除的左節點,防止成環
            node.left = null;
            size--;
            // 然後把要刪除的左節點返回,替換掉要刪除的節點,這就成功刪除了
            return leftNode;
        }

        // 在最後一步,removeMin會把要刪除的節點的左節點返回到這,把node.right替換掉,完成刪除
        // 因爲此時是改變右子樹的結構,所以要把新樹的根返回給右子樹
        node.right = removeMax(node.right);

        return node;
    }

刪除最小值圖解:

在這裏插入圖片描述

刪除最大值的圖解相反,不放了。

5.3 刪除任意節點

刪除任意節點,其實有三種情況,即要刪除的節點有左右節點,或者有左右節點之一,或者沒有左右節點。對於有左右節點之一和沒有左右節點的這兩種情況可以一起處理,用替換法,上面刪除最小值也有說了。

主要是要刪除的節點有左右節點,因爲對於這種情況,刪除後還要去處理刪除的節點的左右節點如何接上原樹中。藉助二叉搜索樹的定義,我們可以找一個節點來做刪除節點的左右子樹的根節點,該節點的元素必須滿足大於左子樹中任意元素和小於右子樹中任意元素。而且節點是要在這左右子樹中找到,不是自己隨便創個新的。如此一想,那麼只能找到要刪除的節點的右子樹中的最小值。因爲右子樹的最小值滿足:大於要刪除的節點的左子樹中的任意元素,並且也小於右子樹的任意元素啊。這個可以畫圖試試。

(尋找的節點可稱爲要刪除的節點的後驅節點,即要刪除的節點的右子樹中的最小值。其實,找到要刪除的節點的左子樹中的最大值也是可以的,稱爲前驅節點)

小結:要刪除的節點有左右節點的刪除步驟:

  • 找到要刪除的節點,記爲d
  • 在d的右子樹中查找最小值,運用上面寫的方法:minimin(d.right),並保存,記爲s
  • 那麼此時要從d的右子樹中刪除最小值,運用上面寫的方法:removeMin(d.right)
  • 此時讓s來替換掉d,即把d的左右子樹都給s
  • 最後返回以s爲根的新樹

(第一種寫法:尋找的節點可稱爲要刪除的節點的後驅節點,即要刪除的節點的右子樹中的最小值)

    /**
     * 刪除任意元素
     * @param e
     */
    public void remove(E e) {
        // 刪除後會改變樹結構,所以要更新原先的樹
        root = remove(root, e);
    }
    private Node remove(Node node, E e) {
        if(node == null) {
            return null;
        }

        // 當e小於根節點時,從左子樹找
        if(e.compareTo(node.e) < 0) {
            // 可能改變左子樹的結構
            node.left = remove(node.left, e);
        } else if(e.compareTo(node.e) > 0) { // 當e大於根節點時,從右子樹找
            node.right = remove(node.right, e);
        } else { // 命中
            // 刪除只有右子樹的節點的情況,不管右子樹有沒有,有也沒影響
            if(node.left == null) {
                // 步驟跟刪除最小值一樣
                Node rightNode = node.right;
                node.right = null;
                size--;
                return rightNode;
            }
            // 刪除只有左子樹的節點的情況,不管左子樹有沒有
            if(node.right == null) {
                // 步驟跟刪除最大值一樣
                Node leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }
            // 對於有左右子樹的節點
            // 在要刪除的節點的右子樹中查詢最小值
            Node successor = minimun(node.right);
            // 讓要刪除的節點的左右子樹指向successor,,並移除右子樹中的最小值
            successor.left = node.left;
            successor.right = removeMin(node.right);
            // 注意小陷阱:此時還需要size--嗎?想想,調用removeMin做了什麼,刪了一個節點,那麼該方法會size--,雖然刪除的不是我們要刪除的節點
            // 在刪除後,我們可以先size++,當真正刪除要刪除的節點時,在size--。所以抵消了。
            // 刪除
            node.left = null;
            node.right = null;

            return successor;
        }

        return node;
    }

圖解:在這裏插入圖片描述

(第二種寫法:找到要刪除的節點的左子樹中的最大值也是可以的,稱爲前驅節點)

    private Node remove(Node node, E e) {
        if(node == null) {
            return null;
        }

        // 當e小於根節點時,從左子樹找
        if(e.compareTo(node.e) < 0) {
            // 可能改變左子樹的結構
            node.left = remove(node.left, e);
        } else if(e.compareTo(node.e) > 0) { // 當e大於根節點時,從右子樹找
            node.right = remove(node.right, e);
        } else { // 命中
            // 刪除只有右子樹的節點的情況,不管右子樹有沒有,有也沒影響
            if(node.left == null) {
                // 步驟跟刪除最小值一樣
                Node rightNode = node.right;
                node.right = null;
                size--;
                return rightNode;
            }
            // 刪除只有左子樹的節點的情況,不管左子樹有沒有
            if(node.right == null) {
                // 步驟跟刪除最大值一樣
                Node leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }
            // 對於有左右子樹的節點
            // 在要刪除的節點的左子樹中查詢最大值
            Node predecessor = minimun(node.left);
            // 讓要刪除的節點的左右子樹指向predecessor,,並移除右子樹中的最小值
            predecessor.left = removeMax(node.left);
            predecessor.right = node.right;
            // 注意小陷阱:此時還需要size--嗎?想想,調用removeMin做了什麼,刪了一個節點,那麼該方法會size--,雖然刪除的不是我們要刪除的節點
            // 在刪除後,我們可以先size++,當真正刪除要刪除的節點時,在size--。所以抵消了。
            // 刪除
            node.left = null;
            node.right = null;

            return predecessor;
        }

        return node;
    }

6. 向上取整(floor)和向下取整(ceil)

floor(e): 小於等於e的最大值,簡單講就是最接近於e且小於或等於e的值。

思路,有三種情況:

  1. 如果e等於根節點的值,那麼直接返回;
  2. 如果e小於根節點的值,那麼尋找的節點肯定在左子樹中;
  3. 如果e大於根節點的值,那麼尋找的節點可能根節點,也有可能在根節點的右子樹中,所以還需要去根節點的右子樹尋找。
    /**
     * 小於等於e的最大值(e可以不用在樹中存在)
     * 如果不存在返回null
     * @param e
     * @return
     */
    public E floor(E e) {
        Node node = floor(root, e);
        return node != null ? node.e : null;
    }
    public Node floor(Node node, E e) {
        if(node == null) {
            return null;
        }
        // 如果等於根節點,則返回
        if(e.compareTo(node.e) == 0) {
            return node;
        }
        // 如果e小於根節點,則小於等於e的節點一定在根節點的左子樹
        if(e.compareTo(node.e) < 0) {
            return floor(node.left, e);
        }

        // 否則,e大於根節點,那麼要尋找的節點有可能就是當前的節點,也有可能在當前節點的右子樹中
        Node rightTree = floor(node.right, e);
        if(rightTree != null) {
            return rightTree;
        } else {
            // 找不到就返回根節點
            return node;
        }
    }

圖解:

在這裏插入圖片描述

ceil(e): 大於等於e的最大值,簡單講就是最接近於e且大於或等於e的值。

思路,有三種情況:

  1. 如果e等於根節點的值,那麼直接返回;
  2. 如果e大於根節點的值,那麼尋找的節點肯定在右子樹中;
  3. 如果e小於根節點的值,那麼尋找的節點可能根節點,也有可能在根節點的左子樹中,所以還需要去根節點的左子樹尋找。
   /**
     * 大於等於e的最小值(e可以不用在樹中存在)
     * 如果不存在返回null
     * @param e
     * @return
     */
    public E ceil(E e) {
        Node node = ceil(root, e);
        return node != null ? node.e : null;
    }
    public Node ceil(Node node, E e) {
        if(node == null) {
            return  null;
        }
        if(e.compareTo(node.e) == 0) {
            return node;
        }
        // 如果e大於根節點,那麼尋找的節點一定在右子樹
        if(e.compareTo(node.e) > 0) {
            return ceil(node.right, e);
        }

        // 否則,e小於根節點,那麼尋找的節點可能是根節點,也可能是在根節點的左子樹中
        Node leftNode = ceil(node.left, e);
        if(leftNode != null) {
            return leftNode;
        } else {
            return node;
        }
    }

7. 排名(rank)

rank(e):e在樹中的排名(小於e的元素的數量)。

思路:

  1. 如果e等於根節點元素,則返回:根節點的左子樹的全部元素數量。
  2. 如果e小於根節點元素,則就跳到左子樹中。
  3. 如果e大於根節點元素,那麼需要跳到右子樹中,並記錄:根節點的左子樹的全部元素數量 + 根節點。
    /**
     * e是排名第幾元素
     * @param e
     * @return
     */
    public int rank(E e) {
        return rank(root, e);
    }
    private int rank(Node node, E e) {
        if(node == null) {
            return 0;
        }
        if(e.compareTo(node.e) == 0) {
            return getNodeSize(node.left);
        } else if(e.compareTo(node.e) < 0) {
            return rank(node.left, e);
        } else {
            return rank(node.right, e) + 1 + getNodeSize(node.left);
        }
    }

    /**
     * 查詢一棵樹的節點數量
     * @param node
     * @return
     */
    private int getNodeSize(Node node) {
        if(node == null) {
            return 0;
        }

        int leftSize = 0, rightSize = 0;
        leftSize = getNodeSize(node.left);
        rightSize = getNodeSize(node.right);

        return leftSize + rightSize + 1;
    }

這是一種做法(不是很推薦使用,主要是還要去查節點數量),還有另一種做法,就是在Node類添加一個size的屬性,該屬性記錄該節點的左右子樹的節點數量加上自己。
相應的就需要去更改添加操作,維護每個節點的size屬性。然後樹中的size屬性就可以不要了,因爲節點已經維護了。

這個方法後續在搞,因爲要改動的地方很多。

圖解:

在這裏插入圖片描述

8. 選擇(select)

select(e):找到排名爲e的節點(即樹中正好有e個小於該節點)。

後續也在搞,主要是跟鍵值對聯合起來,通過鍵找值,現在樹只是一個元素而已。

思路:跟排名方法的思路一樣。

9. 使用BST實現Set集合

很容易:

public interface Set<E> {
    void add(E e);
    boolean isEmpty();
    int getSize();
    void remove(E e);
    boolean contains(E e);
}
public class BSTSet<E extends Comparable<E>> implements Set<E> {

    private BST<E> bst;

    public BSTSet() {
        this.bst = new BST<>();
    }

    @Override
    public void add(E e) {
        bst.add(e);
    }

    @Override
    public boolean isEmpty() {
        return bst.isEmpty();
    }

    @Override
    public int getSize() {
        return bst.getSize();
    }

    @Override
    public void remove(E e) {
        bst.remove(e);
    }

    @Override
    public boolean contains(E e) {
        return bst.contains(e);
    }

}

10. 二叉搜索樹的複雜度分析

二叉搜索樹中只有增刪查三個操作。三個操作的時間複雜度都是一樣的,因爲每次都是去跟根節點比較,所以每比較一次就可以拋棄另一半子樹,一直到葉子節點,可得這跟樹的高度有關,設高度爲h,那麼時間複雜度爲O(h)。

設n爲元素個數,h跟n有什麼關係?假設二叉樹是一個滿二叉樹(即除葉子節點外的節點都有左右節點):

在這裏插入圖片描述

可以看出,對於第h層的元素個數有2^h。那麼對於一顆滿二叉樹的元素個數總共有:

在這裏插入圖片描述

所以對於滿二叉樹的時間複雜度爲O(logn),這只是平均的複雜度,因爲二叉樹不一定是滿二叉樹。

二叉樹最壞的情況就是:

在這裏插入圖片描述

這種情況會退化成鏈表,即高度等於元素個數,最差時間複雜度爲O(n)。可以發現,其實是因爲順序插入導致的原因。

後面有平衡二叉樹來解決。

11. 使用二分搜索樹實現Map

接口:

public interface Map<K, V> {
    void add(K key, V value);
    boolean isEmpty();
    V remove(K key);
    boolean containsKey(K key);
    void set(K key, V newValue);
    V get(K key);
    int getSize();
}

實現:

public class BSTMap<K extends Comparable, V> implements Map<K, V> {

    private class Node {
        private K key;
        private V value;
        private Node left, right;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.left = null;
            this.right = null;
        }
    }

    private Node root;
    private int size;

    public BSTMap() {
        root = null;
        size = 0;
    }

    /**
     * 返回key所在的節點
     * @param key
     * @return
     */
    private Node getNode(Node node, K key) {
       if(node == null) {
           return null;
       }

       if(key.compareTo(node.key) == 0) {
           return node;
       } else if(key.compareTo(node.key) < 0) {
           return getNode(node.left, key);
       } else {
           return getNode(node.right, key);
       }
    }

    /**
     * 添加操作
     * @param key 要添加的鍵
     * @param value 要添加的值
     */
    @Override
    public void add(K key, V value) {
        root = add(root, key, value);
    }

    private Node add(Node node, K key, V value) {

        if(node == null) {
            size++;
            return new Node(key, value);
        }

        if(key.compareTo(node.key) < 0) {
            // 如果是左子樹,add返回新插入節點後的新左子樹的根,那麼替換掉舊樹
            node.left = add(node.left, key, value);
        } else if(key.compareTo(node.key) > 0) {
            // 如果是右子樹,add返回新插入節點後的新右子樹的根,那麼替換掉舊樹
            node.right = add(node.right, key, value);
        } else {
            node.value = value;
        }

        // 返回根
        return node;
    }

    /**
     *
     * @return 樹是否爲空
     */
    @Override
    public boolean isEmpty() {
        return size == 0;
    }
    
    /**
     * 查詢鍵是否包含在樹中
     * @param key
     * @return
     */
    @Override
    public boolean containsKey(K key) {
        return getNode(root, key) != null;
    }

    /**
     *
     * @param key 要修改值的鍵
     * @param newValue 新值
     */
    @Override
    public void set(K key, V newValue) {
        Node node = getNode(root, key);

        if(node == null) {
            throw new IllegalArgumentException(key + " doesn't exist!");
        }

        node.value = newValue;
    }

    /**
     *
     * @param key
     * @return 獲取key的值
     */
    @Override
    public V get(K key) {
        Node node = getNode(root, key);
        return node != null ? node.value : null;
    }

    /**
     *
     * @return 樹的大小
     */
    @Override
    public int getSize() {
        return size;
    }

    /**
     *
     * @param key
     * @return 移除以key的節點
     */
    @Override
    public V remove(K key) {
        Node delNode = getNode(root, key);
        if(delNode != null) {
            root = remove(root, key);
            return delNode.value;
        }
        return null;
    }

    /**
     * 移除元素
     * @param node
     * @param key
     * @return
     */
    private Node remove(Node node, K key) {
        if(node == null) {
            return null;
        }

        if(key.compareTo(node.key) < 0) {
            node.left = remove(node.left, key);
        } else if(key.compareTo(node.key) > 0) {
            node.right = remove(node.right, key);
        } else {
            if(node.left == null) {
                Node rightNode = node.right;
                node.right = null;
                size--;
                return rightNode;
            }
            if(node.right == null) {
                Node leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }
            // node.left != null && node.right != null
            Node newNode = minimin(node.right);
            // removeMin已經size--了
            newNode.right = removeMin(node.right);
            newNode.left = node.left;
            node.left = null;
            node.right = null;

            return newNode;
        }

        return node;
    }

    /**
     *
     * @param node
     * @return 要查詢的最小值的節點
     */
    private Node minimin(Node node) {
        if(node.left == null) {
            return node;
        } else {
            return minimin(node.left);
        }
    }

    /**
     *
     * @param node
     * @return 要移除的最小值的節點
     */
    private Node removeMin(Node node) {
        if(node.left == null) {
            Node rightNode = node.right;
            node.right = null;
            size--;
            return rightNode;
        }
        node.left = removeMin(node.left);

        return node;
    }

}

現在來修改一下,在Node類中添加size屬性,並把Map中的size屬性去掉:

public class BSTMap<K extends Comparable, V> implements Map<K, V> {

    private class Node {
        private K key;
        private V value;
        private Node left, right;
        // 記錄節點的左右子樹的節點數加上當前節點的總數量
        private int size;
        public Node(K key, V value, int size) {
            this.key = key;
            this.value = value;
            this.size = size;
            this.left = null;
            this.right = null;
        }
    }

    private Node root;


    public BSTMap() {
        root = null;
    }

    /**
     * 得到節點的左右子樹的節點數加上當前節點的總數量
     * @param node
     * @return
     */
    private int getSize(Node node) {
        if(node == null) {
            return 0;
        } else {
            return node.size;
        }
    }

    /**
     * 返回key所在的節點
     * @param key
     * @return
     */
    private Node getNode(Node node, K key) {
       if(node == null) {
           return null;
       }

       if(key.compareTo(node.key) == 0) {
           return node;
       } else if(key.compareTo(node.key) < 0) {
           return getNode(node.left, key);
       } else {
           return getNode(node.right, key);
       }
    }

    /**
     * 添加操作
     * @param key 要添加的鍵
     * @param value 要添加的值
     */
    @Override
    public void add(K key, V value) {
        root = add(root, key, value);
    }

    /**
     * 遞歸
     * @param node
     * @param key
     * @param value
     * @return
     */
    private Node add(Node node, K key, V value) {

        if(node == null) {
            return new Node(key, value, 1);
        }

        if(key.compareTo(node.key) < 0) {
            // 如果是左子樹,add返回新插入節點後的新左子樹的根,那麼替換掉舊樹
            node.left = add(node.left, key, value);
        } else if(key.compareTo(node.key) > 0) {
            // 如果是右子樹,add返回新插入節點後的新右子樹的根,那麼替換掉舊樹
            node.right = add(node.right, key, value);
        } else {
            node.value = value;
        }
        // 維護以node爲根的節點總數
        node.size = getSize(node.left) + getSize(node.right) + 1;
        // 返回根
        return node;
    }

    /**
     *
     * @return 樹是否爲空
     */
    @Override
    public boolean isEmpty() {
        return root.size == 0;
    }

    /**
     * 查詢鍵是否包含在樹中
     * @param key
     * @return
     */
    @Override
    public boolean containsKey(K key) {
        return getNode(root, key) != null;
    }

    /**
     *
     * @param key 要修改值的鍵
     * @param newValue 新值
     */
    @Override
    public void set(K key, V newValue) {
        Node node = getNode(root, key);

        if(node == null) {
            throw new IllegalArgumentException(key + " doesn't exist!");
        }

        node.value = newValue;
    }

    /**
     *
     * @param key
     * @return 獲取key的值
     */
    @Override
    public V get(K key) {
        Node node = getNode(root, key);
        return node != null ? node.value : null;
    }

    /**
     *
     * @return 樹的大小
     */
    @Override
    public int getSize() {
        return root.size;
    }

    /**
     *
     * @param key
     * @return 移除以key的節點
     */
    @Override
    public V remove(K key) {
        Node delNode = getNode(root, key);
        if(delNode != null) {
            root = remove(root, key);
            return delNode.value;
        }
        return null;
    }

    /**
     * 移除元素
     * @param node
     * @param key
     * @return
     */
    private Node remove(Node node, K key) {
        if(node == null) {
            return null;
        }

        if(key.compareTo(node.key) < 0) {
            node.left = remove(node.left, key);
        } else if(key.compareTo(node.key) > 0) {
            node.right = remove(node.right, key);
        } else {
            if(node.left == null) {
                Node rightNode = node.right;
                node.right = null;
                return rightNode;
            }
            if(node.right == null) {
                Node leftNode = node.left;
                node.left = null;
                return leftNode;
            }
            // node.left != null && node.right != null
            Node newNode = minimin(node.right);
            newNode.right = removeMin(node.right);
            newNode.left = node.left;
            node.left = null;
            node.right = null;

            return newNode;
        }
        node.size = getSize(node.left) + getSize(node.right) + 1;
        return node;
    }


    /**
     *
     * @param node
     * @return 要查詢的最小值的節點
     */
    private Node minimin(Node node) {
        if(node.left == null) {
            return node;
        } else {
            return minimin(node.left);
        }
    }

    /**
     *
     * @param node
     * @return 要移除的最小值的節點
     */
    private Node removeMin(Node node) {
        if(node.left == null) {
            Node rightNode = node.right;
            node.right = null;
            return rightNode;
        }
        // 維護每個節點的子節點數
        node.left = removeMin(node.left);
        node.size = getSize(node.left) + getSize(node.right) + 1;
        return node;
    }

    /**
     * 排名
     * @param key
     * @return
     */
    public int rank(K key) {
        return rank(root, key);
    }
    private int rank(Node node, K key) {
        if(node == null) {
            return 0;
        }
        if(key.compareTo(node.key) == 0) {
            return getSize(node.left);
        } else if(key.compareTo(node.key) < 0) {
            return rank(node.left, key);
        } else {
            return rank(node.right, key) + 1 + getSize(node.left);
        }
    }

    /**
     *
     * @param rank
     * @return 樹中排名第幾元素的鍵
     */
    public K select(int rank) {
        return select(root, rank).key;
    }
    private Node select(Node node, int rank) {
        if(node == null) {
            return null;
        }
        int leftSize = getSize(node.left);
        if(leftSize > rank) {
            return select(node.left, rank);
        } else if(leftSize < rank) {
            return select(node.right, rank-leftSize-1) ;
        } else {
            return node;
        }
    }

}

在Node添加了size,然後在add方法和remove方法添加:

node.size = getSize(node.left) + getSize(node.right) + 1;

該語句來維護每個節點的總節點數,我們使用的是遞歸寫法,剛開始遞(從上往下)時沒有效果,因爲此時樹的結構還沒改變,但歸(從下往上),每個節點就會因爲該語句而重新更新自己的總節點數。然後還實現了sank方法和select方法。

在這裏插入圖片描述

其他樹結構待續。

參考:

慕課網liuyubobobo老師,突然發現網上好多教程都是參考他的,我也補充了liuyubobobo老師說的一些沒有實現的方法,比如中序和後序的非遞歸寫法,floor等。

推薦看《算法第四版》,樹的知識講得賊好。

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