1. 程式人生 > 實用技巧 >虛樹 入門

虛樹 入門

問題描述

輸入一個整數陣列,判斷該陣列是不是某二叉搜尋樹的後序遍歷結果。如果是則返回 true,否則返回 false。假設輸入的陣列的任意兩個數字都互不相同。

參考以下這顆二叉搜尋樹:

     5
    / \
   2   6
  / \
 1   3

示例 1:

輸入: [1,6,3,2,5]
輸出: false

示例 2:

輸入: [1,3,2,6,5]
輸出: true

提示:陣列長度 <= 1000

解法一(遞迴):

如果這題說的是判斷該陣列是不是某二叉樹的中序遍歷結果,那麼這道題就非常的簡單了,因為二叉樹搜尋樹的中序遍歷結果一定是有序的,我們只需要判斷陣列是否有序就行了,但這道題要判斷的是不是某二叉搜尋樹的後序遍歷結果,這樣就有點難辦了。

二叉搜尋樹的特點是左子樹的值<根節點<右子樹的值。而後續遍歷的順序是:左子節點→右子節點→根節點;

比如下面這棵二叉樹,他的後續遍歷是

[3,5,4,10,12,9]

我們知道後續遍歷的最後一個數字一定是根節點,所以陣列中最後一個數字9就是根節點,我們從前往後找到第一個比9大的數字10,那麼10後面的[10,12](除了9)都是9的右子節點,10前面的[3,5,4]都是9的左子節點,後面的需要判斷一下,如果有小於9的,說明不是二叉搜尋樹,直接返回false。然後再以遞迴的方式判斷左右子樹。

再來看一個,他的後續遍歷是[3,5,13,10,12,9]

我們來根據陣列拆分,第一個比9大的後面都是9的右子節點[13,10,12]。然後再拆分這個陣列,12是根節點,第一個比12大的後面都是12的右子節點[13,10],但我們看到10是比12小的,他不可能是12的右子節點,所以我們能確定這棵樹不是二叉搜尋樹。搞懂了上面的原理我們再來看下程式碼。

 
    public boolean verifyPostorder(int[] postorder){
        return helper(postorder, 0, postorder.length - 1);
    }

    private boolean helper(int[] postorder, int left, int right) {
        //如果left==right,就一個節點不需要判斷了,如果left>right說明沒有節點,
        //也不用再看了,否則就要繼續往下判斷
        if (left>=right)return
true; //因為陣列中最後一個值postorder[right]是根節點,這裡從左往右找出第一個比 // 根節點大的值,他後面的都是根節點的右子節點(包含當前值,不包含最後一個值, //因為最後一個是根節點),他前面的都是根節點的左子節點 int mid = left; //根節點 int root = postorder[right]; //找到左右節點臨界點 while (postorder[mid] < root) mid++; int temp = mid; //因為postorder[mid]前面的值都是比根節點root小的, //我們還需要確定postorder[mid]後面的值都要比根節點root大, //如果後面有比根節點小的直接返回false while (temp < right) { if (postorder[temp++] < root) return false; } //然後對左右子節點進行遞迴呼叫 return helper(postorder,left,mid-1)&&helper(postorder,mid,right-1); }

解法二(迭代):

我們先來畫一個節點多一些的二叉搜尋樹,然後觀察一下他的規律

他的後續遍歷結果是

[3,6,5,9,8,11,13,12,10]

從前往後不好看,我們來從後往前看

[10,12,13,11,8,9,5,6,3]

如果你仔細觀察會發現一個規律,就是挨著的兩個數如果arr[i]<arr[i+1],那麼arr[i+1]一定是arr[i]的右子節點,這一點是毋庸置疑的,,我們可以看下上面的10和12是挨著的並且10<12,所以12是10的右子節點。同理12和13,8和9,5和6,他們都是挨著的,並且前面的都是小於後面的,所以後面的都是前面的右子節點。如果想證明也很簡單,因為比arr[i]大的肯定都是他的右子節點,如果還是挨著他的,肯定是在後續遍歷中所有的右子節點最後一個遍歷的,所以他一定是arr[i]的右子節點。

我們剛才看的是升序的,再來看一下降序的(這裡的升序和降序都是基於後續遍歷從後往前看的,也就是上面的陣列)。

如果arr[i]>arr[i+1],那麼arr[i+1]一定是arr[0]……arr[i]中某個節點的左子節點,並且這個值是大於arr[i+1]中最小的。我們來看一下上面的陣列,比如13,11是降序的,那麼11肯定是他前面某一個節點的左子節點,並且這個值是大於11中最小的,我們看到12和13都是大於11的,但12最小,所以11就是12的左子節點。同理我們可以觀察到11和8是降序,8前面大於8中最小的是10,所以8就是10的左子節點。9和5是降序,6和3是降序,都遵守這個規律。

根據上面分析的過程,很容易想到使用棧來解決。遍歷陣列的所有元素,如果棧為空,就把當前元素壓棧。如果棧不為空,並且當前元素大於棧頂元素,說明是升序的,那麼就說明當前元素是棧頂元素的右子節點,就把當前元素壓棧,如果一直升序,就一直壓棧。當前元素小於棧頂元素,說明是倒序的,說明當前元素是某個節點的左子節點,我們目的是要找到這個左子節點的父節點,就讓棧頂元素出棧,直到棧為空或者棧頂元素小於當前值為止,其中最後一個出棧的就是當前元素的父節點。我們來看下程式碼

 public boolean verifyPostorder1(int[] postorder) {
        Stack<Integer> stack = new Stack<>();
        int parent = Integer.MAX_VALUE;
        //注意for迴圈是倒敘遍歷的
        for (int i = postorder.length - 1; i >= 0; i--) {
            int cur = postorder[i];
            //當如果前節點小於棧頂元素,說明棧頂元素和當前值構成了倒敘,
            //說明當前節點是前面某個節點的左子節點,我們要找到他的父節點
            while (!stack.isEmpty() && stack.peek() > cur) {
                parent = stack.pop();
            }
            //只要遇到了某一個左子節點,才會執行上面的程式碼,才會更
            //新parent的值,否則parent就是一個非常大的值,也就
            //是說如果一直沒有遇到左子節點,那麼右子節點可以非常大
            if (cur > parent) return false;
            //入棧
            stack.add(cur);
        }
        return true;
    }

上面程式碼可能大家有點蒙的是if(cur>parent)這一行的判斷。二叉搜尋樹應該是左子節點小於根節點,右子節點大於根節點,但上面為什麼大於父節點的時候要返回false,注意這裡的parent是在什麼情況下賦的值,parent並不一定都是父節點的值,相對於遇到了左子節點的時候他是左子節點的父節點。如果是右子節點,parent就是他的某一個祖先節點,並且這個右子節點是這個祖先節點的一個左子樹的一部分,所以不能超過他,有點繞,慢慢體會。

總結:

這題第一種方式是最容易想到的,每次把陣列劈兩半,因為通過第一個while迴圈,左邊的都是小於根節點的,然後再判斷右邊的是不是都大於根節點,然後左右兩邊再以同樣的方式計算……。第二種方式也能解決,但比較繞,相對來說不太好容易理解,但如果真的搞懂了,會豁然開朗,也會有很大的收穫。