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

你們好,我是Johngo!python

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

前 3 講的連接地址在這裏了:github

講透樹1 | 樹的基礎遍歷專題 mp.weixin.qq.com/s/nTB41DvE7…算法

講透樹2 | 樹的遍歷覆盤專題 mp.weixin.qq.com/s/MkCF5TaR1…markdown

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

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

階段說明

一塊兒刷題的小夥伴們,覆盤仍是要嘮叨一句,記錄思路,在記錄的過程當中,又一次深入體會!好比說ui

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

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

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

關於 BFS 的解題思路對於「自頂向下」這類型題目是很是友好的,思路清晰。可查看這裏mp.weixin.qq.com/s/9U4P5zZIF…

本篇文章涉及到的題目

687.最長同值路徑:leetcode-cn.com/problems/lo…

124.二叉樹中的最大路徑和:leetcode-cn.com/problems/bi…

543.二叉樹的直徑:leetcode-cn.com/problems/di…

652.尋找重複的子樹:leetcode-cn.com/problems/fi…

236.二叉樹的最近公共祖先:leetcode-cn.com/problems/lo…

235.二叉搜索樹的最近公共祖先:leetcode-cn.com/problems/lo…

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.最長同值路徑

題目連接:leetcode-cn.com/problems/lo…

GitHub解答:github.com/xiaozhutec/…

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

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.二叉樹中的最大路徑和

題目連接:leetcode-cn.com/problems/bi…

GitHub解答:github.com/xiaozhutec/…

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

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

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.二叉樹的直徑

題目連接:leetcode-cn.com/problems/di…

GitHub解答:github.com/xiaozhutec/…

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

依然仍是採用上述「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.二叉樹的最近公共祖先

題目連接:leetcode-cn.com/problems/lo…

GitHub解答:github.com/xiaozhutec/…

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

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.二叉搜索樹的最近公共祖先

題目連接:leetcode-cn.com/problems/lo…

GitHub解答:github.com/xiaozhutec/…

這個題目是上一個題目的變形,難度下降,由於要考察的二叉樹由通常結構變爲了二叉搜索樹。

所以,利用搜索二叉樹的性質,稍進行判斷就能夠解決。

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都大於當前結點,那麼直接到右子樹進行遞歸調用;

若是都不知足,即就是所須要的答案。

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

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

下一階段就到了「動態規劃」的一個刷題時間窗口了,遞歸和動態規劃都是出了名的比較難學,可是它們的思想深刻貫徹整個算法的各個節點。因此,堅持把這幾部份內容都進行一個總結,讓你們把思路逐漸清晰起來!

代碼和本文的文檔都在 github.com/xiaozhutec/… star。謝過你們!

相關文章
相關標籤/搜索