LeetCode 95 | 構造出所有二叉搜尋樹

TechFlow2019發表於2020-09-01

今天是LeetCode專題第61篇文章,我們一起來看的是LeetCode95題,Unique Binary Search Trees II(不同的二叉搜尋樹II)。

這道題的官方難度是Medium,點贊2298,反對160,通過率40.5%。我也仿照steam當中遊戲評論的分級,給LeetCode中的題目也給出一個評級標準。按照這個點贊和反對的比例,這道題可以評到特別好評。從題目內容上來說,這是一道不可多得基礎拷問的演算法題,看著不簡單,做起來也不簡單,但看了題解之後,你會發現也沒你想象得那麼難。

題意

給定一個n,表示1到n這n個數字,要求用這n個數構建二叉搜尋樹(Binary Search Tree)簡稱BST,要求我們構建出所有不同的二叉搜尋樹。

樣例

Input: 3
Output:
[
  [1,null,3,2],
  [3,2,null,1],
  [3,1,null,null,2],
  [2,1,3],
  [1,null,2,null,3]
]
Explanation:
The above output corresponds to the 5 unique BST's shown below:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

從這個case當中我們可以看到,當n=3的時候,二叉搜尋樹一共有5中不同的情況。為了方便展示,Output當中展示的內容是這些樹中序遍歷的結果。但實際上我們要返回的是樹根節點構成的list。

哦哦,對了題目當中還有一個n <= 8的條件,所以如果你是一個狼人的話,也可以把所有的情況都手動實現。

解法

這道題我感覺官方難得給的有點低了,應該可以算得上是Hard了。拿到手我們思路沒多少,但是發現的問題卻一大堆。比如說我們怎麼構建這些BST,並且怎麼判斷兩顆BST是否重複呢?難道要整個遍歷一遍之後,一個節點一個節點地判斷是否相同嗎?顯然這也太耗時了,而且編碼也不容易。舉個例子[2, 1, 3]和[2, 3, 1]生成的BST是一樣的,這種情況很難解決。

即使我們解決了這個問題,那麼又怎麼樣保證我們可以順利找到所有的答案,而不會有所遺漏呢?這兩個核心的問題很難回答,並且你會發現越想越複雜。

這個有點像什麼呢?就好像是古代行軍打仗,攻打一個異常堅固的堡壘,正面攻堅可能非常困難,我們想出來的辦法都在敵人的預料之中,總能找到破解之道。這個時候就需要我們有敏銳的意識,就好像是一個經驗豐富的老將,觀察地形之後發現強攻不可為,那麼自然就會退下來想一想其他的辦法。

我們做題也是一樣,正面硬剛做不出來,再耗下去也不會有好辦法,往往就需要出奇制勝了。

我們試著把問題縮小,化整為零,如果n=1,那麼很簡單,BST就只有一種,這個是我們都知道的東西。如果n=2呢,也不難,只有兩種,無非是[1, 2]和[2, 1]。這時候我們停住,來思考一個問題,n=2的情況和n=1的情況有什麼區別呢?

如果仔細想,會發現其實沒什麼區別, 我們只不過是在n=1的情況當中加入了2這個數字而已。同理,我們發散一下n=k和n=k+1的時候生成的BST之間有什麼關係呢?如果我們知道了n=k時候的所有BST,可不可以利用這個關係生成n=k+1時的所有結果呢?

當然是可以的,實際上這也是一個很好的做法。這是一種最基本的二叉樹,假設我們要往其中插入一個最大的節點,我們很容易發現,一共有三種方法。

image-20200809155208137
image-20200809155208137

第一種方法將原搜尋樹作為新節點的左孩子節點。

第二種方法是將新的節點插入根節點的右側,代替根節點的右孩子。由於這個新加入的節點大於其他所有節點,所以根節點的右孩子會變成它的左孩子。

最後一種方法就是將它變成葉子節點,也就是放在最底層。

我們稍加思考就可以想明白,如果要把一個最大的元素插入BST,那麼它只能夠放在最右側的樹分支上。也就是說當我們從n=k的情況推導k+1時,根據最右側鏈路長度的不同,會衍生出多個解來。只要抓住了這一點,這其中的遞推關係就很明顯了。

我們用程式碼來實現這個想法,思路雖然簡單,但是實現起來要複雜一些,有很多細節需要考慮。我在這裡不一一列舉了,大家檢視程式碼當中的註釋吧。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def generateTrees(self, n: int) -> List[TreeNode]:
        ret = []
        
        # 拷貝二叉樹
        def copyTree(node):
            if node is None:
                return None
            u = TreeNode(node.val)
            u.left = copyTree(node.left)
            u.right = copyTree(node.right)
            return u
        
        def dfs(n):
            # n=1只有一種情況
            if n == 1:
                ret.append(TreeNode(1))     
                return
            
            dfs(n-1)
            # 遍歷n=k時的所有情況
            for i in range(len(ret)):
                u = ret[i]
                node = TreeNode(n)
                node.left = u
                ret[i] = node
                
                it = u
                rank = 0
                # 將n插入最右側鏈路當中,有幾種可以選擇的位置,就會誕生幾種新的放法
                while it is not None:
                    node = TreeNode(n)
                    # 為了防止答案之間互不影響,所以需要把樹拷貝一份
                    new = copyTree(u)
                    cur = new
                    
                    # rank記錄的是每一個解對應的n放入的深度
                    for _ in range(rank):
                        cur = cur.right
                    
                    node.left = cur.right
                    cur.right = node
                    
                    ret.append(new)
                    
                    it = it.right
                    rank += 1
            
        if n == 0:
            return ret
        dfs(n)
        return ret

這種方法當然是可行的, 我們也成功做了出來。但是它也有很多問題,最大的問題就是細節太多,而且處理起來太麻煩了。那麼有沒有簡單一點的方法呢?

我們來思考一個問題,我們通過遞推和迭代從n=k構造出了n=k+1的情況,這一種構造和遞推的思路非常巧妙。但問題是,我們構造和遞推的方法難道只有這一種嗎?能不能想出其他簡便一些的構造和遞推的方法呢?

既然我這麼說,那麼很顯然,它是可以的,怎麼做呢?

這要用到BST另外一個性質,我們都知道對於BST來說,它有一個性質是對於根節點來說,所有比它小的元素都出現在它的左側,比它大的元素都在它的右側。那麼假如我們知道根節點是i,那麼我們可以確定1到i-1出現在它的左子樹,i+1到n出現在它的右子樹。假設說我們已經得到了左右子樹的所有情況,我們只需要把它們兩兩組合在一起,是不是就得到了答案了呢?

我這麼說你們理解起來可能還是會覺得有些費勁,但是一旦檢視程式碼,你們一定會為這段邏輯的簡易性而折服,看起來實在是太簡明也太巧妙了。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def generateTrees(self, n: int) -> List[TreeNode]:
        ret = []
        
        def dfs(l, r):
            cur = []
            if r < l:
                cur.append(None)
                return cur
            
            # 列舉作為樹根的元素
            for i in range(l, r+1):
                # 列舉左右子樹的所有子樹的構成情況
                for u in dfs(l, i-1):
                    for v in dfs(i+1, r):
                        node = TreeNode(i)
                        node.left = u
                        node.right = v
                        cur.append(node)
            return cur 
            
        if n == 0:
            return ret
        return dfs(1, n)

和上面的方法一樣,這也是遞迴和構造方法的結合,但顯然無論從執行效率上還是程式碼的簡易性上,這種做法都要好不少,實實在在地體現了程式碼和邏輯之美。

今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、轉發、點贊)。

- END -

原文連結,求個關注

LeetCode 95 | 構造出所有二叉搜尋樹

相關文章