今天是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時的所有結果呢?
當然是可以的,實際上這也是一個很好的做法。這是一種最基本的二叉樹,假設我們要往其中插入一個最大的節點,我們很容易發現,一共有三種方法。
第一種方法將原搜尋樹作為新節點的左孩子節點。
第二種方法是將新的節點插入根節點的右側,代替根節點的右孩子。由於這個新加入的節點大於其他所有節點,所以根節點的右孩子會變成它的左孩子。
最後一種方法就是將它變成葉子節點,也就是放在最底層。
我們稍加思考就可以想明白,如果要把一個最大的元素插入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 -原文連結,求個關注