講透樹4 | 非自頂向下類別題目覆盤專題

迷失技術de小豬發表於2021-07-28

大家好,我是Johngo!

這篇文章是「講透樹」系列的第 4 篇文章,也是「樹」專題中非自頂向下這類題目的一個覆盤總結。

前 3 講的連結地址在這裡了:

講透樹1 | 樹的基礎遍歷專題 https://mp.weixin.qq.com/s/nTB41DvE7bfrT7_rW_gfXw

講透樹2 | 樹的遍歷覆盤專題 https://mp.weixin.qq.com/s/MkCF5TaR1JD3F3E2MKlgVw

講透樹3 | 自頂向下類別題目覆盤專題 https://mp.weixin.qq.com/s/9U4P5zZIFppiJO_XK2QFDA

不同的型別已經都進行了各自的總結,相信在後面記憶不太清晰的時候,返回頭來看看,這些文件又會和思維激起靈魂碰撞!

階段說明

一起刷題的小夥伴們,覆盤還是要嘮叨一句,記錄思路,在記錄的過程中,又一次深刻體會!比如說

相信在後面記憶不太清晰的時候,返回頭來看看,這些文件又會和思維激起靈魂碰撞!

直觀的先看看本文的所處的一個進度

相比較於「自頂向下」的題目,「非自頂向下」的題目相比較下來不太適合用 BFS 來解決,非常適合於用 DFS 來解決。而且相較於「自頂向下」的題目,「非自頂向下」的題目難度會大一點。

關於 BFS 的解題思路對於「自頂向下」這型別題目是非常友好的,思路清晰。可檢視這裡https://mp.weixin.qq.com/s/9U4P5zZIFppiJO_XK2QFDA。

本篇文章涉及到的題目

687.最長同值路徑:https://leetcode-cn.com/problems/longest-univalue-path/

124.二叉樹中的最大路徑和:https://leetcode-cn.com/problems/binary-tree-maximum-path-sum

543.二叉樹的直徑:https://leetcode-cn.com/problems/diameter-of-binary-tree

652.尋找重複的子樹:https://leetcode-cn.com/problems/find-duplicate-subtrees/

236.二叉樹的最近公共祖先:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree

235.二叉搜尋樹的最近公共祖先:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree

DFS 解題思路

本篇著重說「非自頂向下」的思路以及涉及到的 LeetCode 題目。

在這裡總結一句,其實就是下面三種基礎遞迴遍歷的變形,這個變形來源於題目的要求,但本質都是遞迴遍歷。

這裡我再一次把三種二叉樹遞迴的程式碼貼出來:

二叉樹的先序遍歷

def pre_order_traverse(self, head):
    if head is None:
        return
    print(head.value, end=" ")
    self.pre_order_traverse(head.left)
    self.pre_order_traverse(head.right)

二叉樹的中序遍歷

def in_order_traverse(self, head):
    if head is None:
        return
    self.in_order_traverse(head.left)
    print(head.value, end=" ")
    self.in_order_traverse(head.right)

二叉樹的後續遍歷

def post_order_traverse(self, head):
    if head is None:
        return
    self.post_order_traverse(head.left)
    self.post_order_traverse(head.right)
    print(head.value, end=" ")

令人整潔的舒服...

令人難以理解的不舒服...

對的,就是這種整潔的程式碼,在處理起二叉樹的問題來,著實是遊刃有餘的。

案例剖析

LeetCode687.最長同值路徑

題目連結:https://leetcode-cn.com/problems/longest-univalue-path/

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/687.最長同值路徑.py

這就是一個後續的遞迴遍歷,後續遞迴遍歷完,不進行結點的列印,而是進行結點值和相關孩子結點的比對判斷:

def longestUnivaluePath_dfs(self, root):
    self.length = 0

    def dfs(root):
        if not root:
            return 0
        left_len = dfs(root.left)
        right_len = dfs(root.right)
        left_tag = right_tag = 0
        if root.left and root.left.val == root.val:
            left_tag = left_len + 1
        if root.right and root.right.val == root.val:
            right_tag = right_len + 1
        # max(最大長度, 左子樹最大長度+右子樹最大長度)
        self.length = max(self.length, left_tag + right_tag)
        return max(left_tag, right_tag)

    dfs(root)
    return self.length

看到了吧,其實就是在left_len = dfs(root.left)right_len = dfs(root.right) 之後,進行當前結點和左右孩子結點的結點值比對,如果相同了,很顯然是 +1

另外,就題論題。

下面會介紹一種很重要的思路,在很多題目中都會遇到。

這個題目有很關鍵的一點是self.length = max(self.length, left_tag + right_tag),由於題目不要求一定經過根結點。那麼,此時如果左孩子的結點值和根結點的結點值以及右孩子的結點值和根結點的結點值都相同,此時會是下面的一種情況。

談論紅色框內的結點:

灰色結點的左孩子length=1,灰色結點的右孩子length=0

再往上層看,灰色結點 5 的左孩子也是 5,那麼left_tag=2,灰色結點 5 的右孩子也是 5,那麼right_tag=1

所以,length = max(length, left_tag + right_tag),那麼,得到的結果是length=3,也就是黑色所示的粗邊。

以上,利用後續遍歷的遞迴思想,就可以把問題解決了!

強調:上面的思路經常會遇到,很重要!

再看一個例子:

LeetCode124.二叉樹中的最大路徑和

題目連結:https://leetcode-cn.com/problems/binary-tree-maximum-path-sum

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/124.二叉樹中的最大路徑和.py

從二叉樹中找出一個最大的路徑,就是找到一個連續結點值最大的一個路徑。

可以參考上一個題目的思路,依然採用後續遍歷的思路,進行解決。

def maxPathSum(self, root):
    self.length = float("-inf")

    def dfs(root):
        if not root:
            return 0
        left_len = max(dfs(root.left), 0)   # 只有貢獻值大於 0 的,才會選取對應的子結構
        right_len = max(dfs(root.right), 0)
        inner_max = left_len + root.val + right_len

        self.length = max(self.length, inner_max)   # 計算當前結點所在子樹的最大路徑
        return max(left_len, right_len) + root.val  # 返回當前結點左右子結構的最大路徑

    dfs(root)
    return self.length

思路依然還是比較輕鬆的吧,後續遍歷的典型變形。

看這句self.length = max(self.length, inner_max),是不是和上一題異曲同工,也是比較上一層遞迴的 length 和本層中 inner_max作比較,找出最大值。

兩點注意

  1. 每次遞迴,需要計算 length 的值,以保證每次遞迴得到最大路徑和
  2. 每次遞迴的返回值一定是左子樹和右子樹中的最大值+當前結點值

再來看一道題目:

LeetCode543.二叉樹的直徑

題目連結:https://leetcode-cn.com/problems/diameter-of-binary-tree

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/543.二叉樹的直徑.py

題目要求返回一顆二叉樹的直徑,其實還是一個後續遞迴遍歷。

依然還是採用上述「LeetCode687」中介紹的重要思路進行求解。

def diameterOfBinaryTree(self, root):
    self.path_length = 0

    def dfs(root):
        if not root:
            return 0
        left_len = dfs(root.left)
        right_len = dfs(root.right)
        left_tag = right_tag = 0
        if root.left:
            left_tag = left_len + 1
        if root.right:
            right_tag = right_len + 1
        self.path_length = max(self.path_length, left_tag + right_tag)
        return max(left_tag, right_tag)

    dfs(root)
    return self.path_length

依然是後續遍歷,遞迴呼叫後,採用max(self.path_length, left_tag + right_tag),比較上一層返回的 length和左右子樹中的最大值。

依然是兩點:

  1. 每次遞迴計算 length 的值,找當前結點最長路徑

  2. 返回左右子樹的最大路徑長度

上面說了三個題目,大致思路都相同,不同的是一個題目計算路徑和、兩個題目是計算路徑長度。

比較重要的那個點,在「LeetCode687」中介紹的重要思路,這個是最重要的一環,務必理解加以運用!

見得太多,用的也太多!

最後我們們再看兩個題目:

LeetCode236.二叉樹的最近公共祖先

題目連結:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/236.二叉樹的最近公共祖先.py

這個是給定兩個結點,求解他們的最近公共祖先,也是一個遞迴的問題。

def lowestCommonAncestor(self, root, p, q):
    if not root or root == p or root == q:
        return root
    left = self.lowestCommonAncestor(root.left, p, q)
    right = self.lowestCommonAncestor(root.right, p, q)
    if not left: return right
    if not right: return left
    return root

其實說白了還是遞迴的思路進行求解,不過這裡需要注意四種情況:

​ 1.無左孩子 and 無右孩子,直接返回根結點

​ 2.無左孩子 and 有右孩子,直接返回根結點

​ 3.有左孩子 and 無右孩子,直接返回根結點

​ 4.有左孩子 and 有右孩子,繼續遞迴遍歷

所以,還是遞迴的變形進行解決,唯一有一點就是需要深入思考,深入體會。

LeetCode235.二叉搜尋樹的最近公共祖先

題目連結:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/235.二叉搜尋樹的最近公共祖先.py

這個題目是上一個題目的變形,難度降低,因為要考察的二叉樹由一般結構變為了二叉搜尋樹。

因此,利用搜尋二叉樹的性質,稍進行判斷就可以解決。

def lowestCommonAncestor(self, root, p, q):
    if p.val < root.val and q.val < root.val:
        return self.lowestCommonAncestor(root.left, p, q)
    if p.val > root.val and q.val > root.val:
        return self.lowestCommonAncestor(root.right, p, q)
    return root.val

是不是很簡單整潔的程式碼。

第一個if,就是如果pq都小於當前結點,那麼直接到左子樹進行遞迴呼叫;

第二個if,就是如果pq都大於當前結點,那麼直接到右子樹進行遞迴呼叫;

如果都不滿足,即就是所需要的答案。

想想看,是不是?利用搜尋二叉樹的結構,其實思路很清晰簡單。

好了,「樹」這一階段的刷題已經進入尾聲,但我理解在第一輪刷題完畢之後,還會繼續回來重新來過。

下一階段就到了「動態規劃」的一個刷題時間視窗了,遞迴和動態規劃都是出了名的比較難學,但是它們的思想深入貫徹整個演算法的各個節點。所以,堅持把這幾部分內容都進行一個總結,讓大家把思路逐漸清晰起來!

程式碼和本文的文件都在 https://github.com/xiaozhutec/share_leetcode,需要的小夥伴可以自行下載程式碼執行跑起來!方便的話給個 star。謝過大家!

相關文章