【完虐演算法】自頂向下專題類目 全覆盤

技術gogogo發表於2021-11-09

刷題覆盤進度

大家好,我是Johngo!

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

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

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

基本上,絕大多數關於「樹」的題目,會有很大一類屬於「自頂向下」型別的。

什麼意思?就是計算結果的時候,通常會涉及從樹根到葉子節點的計算過程,比如說最大深度、路徑總和、從根結點到葉子結點的所有路徑等等,都屬於「自頂向下」這類題目。

涉及到的題目

104.二叉樹的最大深度: https://leetcode-cn.com/problems/maximum-depth-of-binary-tree
112.路徑總和: https://leetcode-cn.com/problems/path-sum
113.路徑總和 II: https://leetcode-cn.com/problems/path-sum-ii
437.路徑總和 III: https://leetcode-cn.com/problems/path-sum-iii
257.二叉樹的所有路徑: https://leetcode-cn.com/problems/binary-tree-paths
129.求根節點到葉節點數字之和: https://leetcode-cn.com/problems/sum-root-to-leaf-numbers
988.從葉結點開始的最小字串: https://leetcode-cn.com/problems/smallest-string-starting-from-leaf

然後這類題目的解決方法基本又會有兩類:

第一類:BFS,廣度優先搜尋,利用層次遍歷的方式進行解決

第二類:DFS,深度優先搜尋,利用前中後序遍歷樹的方式進行問題的解決

既然先說的 BFS,我們們就從 BFS 先說起,後面再描述 DFS 的解決方式。

BFS 思路

BFS(Breadth First Search):廣度優先搜尋

回憶一下經典二叉樹的層序遍歷問題,把需要的圖放出來先看看。

很簡單的一個過程。迴圈判斷佇列 queue 中是否有元素,如果有,訪問該元素並且判斷該結點元素是否有孩子結點,如果有,孩子結點依次入隊 queue,否則,繼續迴圈執行。

再來看看程式碼:

res = []
while queue:
    node = queue.pop()
    res.append(node.val)
    if node.left:
        queue.appendleft(node.left)
    if node.right:
        queue.appendleft(node.right)

很順暢很經典的一個層次遍歷的程式碼。

現在想要丟擲 2 個引例,往上述程式碼中新增點作料,看是否可以很容易就解答。

引例一

遍歷過程中能否記錄根結點到當前結點的一些資訊?

包括:

1、根結點到當前結點的路徑資訊

2、根結點到當前結點的路徑和

把上述圖中結點中的字母對應為數字,達到引例一中的要求情況,看下圖:

在遍歷過程中,不斷的進行結點值【包括結點物件、根結點到當前結點路徑、根結點到當前結點路徑和】的記錄。

node 表示結點物件

node_path 表示根結點到當前結點路徑

node_val 表示根結點到當前結點路徑和

Python 中使用元祖進行表示結點的三元組資訊:(node, node_path, node_val)

程式碼實現:

res = []

while queue:
    node, node_path, node_val = queue.pop()
    res.append((node, node_path, node_val))
    if node.left:
        queue.appendleft((node.left, node_path+str(node.left), node_val+node.left.val))
    if node.right:
        queue.appendleft((node.right, node_path+str(node.right), node_val+node.right.val))

這樣,在遍歷過程中,就會將三元組的資訊隨時攜帶。完美解決!

引例二

能否在層序遍歷過程中,攜帶一個值進行層序的記錄?

對,就是這樣,利用一個額外的變數來記錄層序。

這個的思路,其實很容易就讓我想到之前二叉樹按照 LeetCode 形式列印的一個過程(不太記得的小夥伴可以檢視 https://mp.weixin.qq.com/s/MkCF5TaR1JD3F3E2MKlgVw 回憶下關於LeetCode的層序遍歷)

下面我又把 LeetCode 中要求層次遍歷的圖解過程放出來,作為回憶參考!

「點選下圖檢視高清原圖」?

即,在每一層遍歷的時候,進行 node_depth+=1 的操作。先來看最初程式碼的樣子(還。。記得嗎~?):

def levelOrder(self, root):
    res = []
    if not root:
        return res
    queue = [root]
    while queue:
        level_queue = []      # 臨時記錄每一層結點
        level_res = []        # 臨時記錄每一行的結點值
        for node in queue:
            level_res.append(node.val)
            if node.left:
                level_queue.append(node.left)
            if node.right:
                level_queue.append(node.right)
        queue = level_queue
        res.append(level_res)
    return res

每一層的遍歷,都是 queue 被賦予新的一個佇列 level_queue,即新的一層的所有結點集。

在此思路的基礎上

首先,初始化變數用作記錄層序值 node_depth = 0

其次,在每一次while queue: 之後進行 node_depth+=1

最後 node_depth 的值就是你想要的某一層的值

看程式碼小改動後的實現

def levelOrder(self, root):
    res = []
    # 層序記錄
    node_depth = 0
    if not root:
        return res
    queue = [root]
    while queue:
	      node_depth += 1				# 層序值+1
        level_queue = []
        level_res = []
        for node in queue:
            level_res.append(node.val)
            if node.left:
                level_queue.append(node.left)
            if node.right:
                level_queue.append(node.right)
        queue = level_queue
        res.append(level_res)
    return res

哈哈對,不要找了!就是有註釋的那兩行,只不過要取的 node_depth 的值是你所需要的那個值。

比如說,最大深度的計算,那就是最後 node_depth 的值;如果是根結點到某一結點路徑和你給到的 target 值一致的時候的那個深度,那就是被滿足結點所在層序的 node_depth

好!

兩個重要的問題被引出來,有沒有什麼感覺,是不是真的是比較簡單的一個思路。下面就來看看這些簡單思路,能處理哪些問題!

利用上述「引例一」和「引例二」的思路舉例看看對 LeetCode 中部分題目有什麼幫助?

LeetCode104.二叉樹的最大深度

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

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/104.二叉樹的最大深度.py

其實就是「引例二」中攜帶一個值進行層序的記錄,最後返回的 node_depth 就是二叉樹的最大深度!

def maxDepth_bfs(self, root):
    if not root:
        return 0
    queue = collections.deque()
    # 初始化深度為 0
    node_depth = 0
    # 初始化佇列中的結點元素 root
    queue.appendleft(root)
    while queue:
        # 每一層的遍歷,深度 +1
        node_depth += 1
        # 記錄每一層的結點集合
        level_queue = []
        for node in queue:
            if node.left:
                level_queue.append(node.left)
            if node.right:
                level_queue.append(node.right)
        queue = level_queue

    return node_depth

是不是很容易就解決了!

再來看一個:

LeetCode112.路徑總和:

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

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/112.路徑總和.py

LeetCode112題目是從根結點到葉子結點,是否存在路徑和為 target 的一個路徑。

如下圖,如果我們們要找路徑和為 target=16 的一個路徑,利用「引例一」中的思路,很容易就可以判斷,在最後一個結點中的三元組 (9, 1->2->4->9, 16) 中能夠得到路徑為 1->2->4->9

程式碼實現起來也很容易

def hasPathSum_bfs(self, root, targetSum):
    if not root:
        return False
    queue = [(root, root.val)]
    while queue:
        node, node_sum = queue.pop(0)
        if not node.left and not node.right and node_sum == targetSum:
            return True
        if node.left:
            queue.append((node.left, node_sum+node.left.val))
        if node.right:
            queue.append((node.right, node_sum+node.right.val))
    return False

這裡返回了存在該路徑,為 True,如果想要返回路徑,那麼直接將路徑返回就可以了!

再來看一個:

LeetCode257.二叉樹的所有路徑:

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

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/257.二叉樹的所有路徑.py

就是要把所有從根結點開始到葉子結點所有的路徑遍歷出來。其實還是「引例一」中的思路,當遍歷到葉子結點的時候,將所有葉子結點中的三元組中的路徑值取出。例如葉子結點 (9, 1->2->4->9, 16) 中的路徑為 1->2->4->9取出。

可以得到一個路徑集合:

[[1->3->6], [1->3->7], [1->2->4->8], [1->2->4->9]]

程式碼很類似

def binaryTreePaths_bfs(self, root):
    res = []
    if not root:
        return res
    queue = collections.deque()
    queue.appendleft((root, str(root.val)+"->"))
    while queue:
        node, node_val = queue.pop()
        if not node.left and not node.right:
            res.append(node_val[0:-2])
        if node.left:
            queue.appendleft((node.left, node_val + str(node.left.val) + "->"))
        if node.right:
            queue.appendleft((node.right, node_val + str(node.right.val) + "->"))
    return res

核心還是記錄遍歷過程中對路徑的記錄情況,最後得到想要的結果!

再來看最後一個例子:

LeetCode129.求根節點到葉節點數字之和

題目連結:https://leetcode-cn.com/problems/sum-root-to-leaf-numbers

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/129.求根節點到葉節點數字之和.py

這個題目依然延續了「引例一」中的思路,就是將路徑中的數字一次記錄,轉為數字,進行相加。

舉例說,圖中路徑分別為[[1->3->6], [1->3->7], [1->2->4->8], [1->2->4->9]],對應的數字為 [10, 11, 15, 16],那麼,數字之和為 10+11+15+16=52

上一題目是將路徑完整的遍歷出來,這個題目就是增加了一個步驟,那就是將每個路徑轉為數字,並且相加。

def sumNumbers_bfs(self, root):
    res = []
    sum = 0
    if not root:
        return 0
    queue = collections.deque()
    queue.append((root, str(root.val)))
    while queue:
        node, node_val = queue.pop()
        if node and not node.left and not node.right:
            res.append(node_val)
        if node.left:
            queue.appendleft((node.left, node_val+str(node.left.val)))
        if node.right:
            queue.appendleft((node.right, node_val+str(node.right.val)))
    for item in res:
        sum += int(item)
    return sum

就是最後一個步驟,將陣列中的數字型字串轉為整數並且相加。得到最終的結果!

還有一些其他類似的題目,這裡就先不說了,文章最開頭給出的「自頂向下」這類題目都在 github:https://github.com/xiaozhutec/share_leetcode/tree/master/樹 上進行了記錄,細節程式碼可以參考。

關於這部分題目,重點想說的就是「引例一」和「引例二」兩方面的思路,這兩方面的思路已經可以把這類題目的絕大多數都可以解決了!

下面再總結下利用 DFS 的思路進行問題的解決。

DFS 思路

DFS(Depth First Search):深度優先搜尋

回憶下之前的二叉樹的遞迴遍歷,也可以說是 DFS 的思路。之前在這篇文章中詳細闡述過 https://mp.weixin.qq.com/s/nTB41DvE7bfrT7_rW_gfXw

利用遞迴進行二叉樹的遍歷,很簡單但是不太容易理解。在之前也詳細說過這方面的理解方式。

很多時候我會利用一個很 easy 的思路是,將二叉樹的遞迴遍歷利用在「二叉樹」的葉子結點以及再向上一層進行理解和問題的解決。

下面先來看看各個遍歷的程式碼。

二叉樹的先序遍歷:

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=" ")

看完這幾段程式碼,它的整潔性令人舒服,但是它的可讀性確實不太高...

下一期,還會覆盤一期《講透樹 | 非自頂向下題目專題》,與這一期「自頂向下」題目不同,思路也會有些差別。

這一期先把「自頂向下」這類題目運用 DFS 的思路說明白了!

利用二叉樹的遞迴思路,其實很容易就可以解決這類問題,把 BFS 說到的題目用 DFS 思路解決一下,程式碼看起來更加的簡潔,美觀!

LeetCode104.二叉樹的最大深度

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

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/104.二叉樹的最大深度.py

簡單到氣人,理解到想打人!

def maxDepth_dfs(self, root):
    if not root:
        return 0
    else:
        max_left = self.maxDepth_dfs(root.left)
        max_right = self.maxDepth_dfs(root.right)
        return max(max_left, max_right) + 1

太簡單了叭!!~~

一個後續遍歷,很快就把問題解決了!

由於使用了遞迴呼叫,那麼還是從葉子結點開始考慮:

a.當遞迴到葉子的時候,程式return 0,也就是遞迴使用 self.maxDepth_dfs(root.left)以及 self.maxDepth_dfs(root.right)的時候,返回值為 0;

b.往上考慮一層,遞迴使用 self.maxDepth_dfs(root.left)或者 self.maxDepth_dfs(root.right)的時候,返回值是return max(max_left, max_right) + 1, 是 【a.】的返回值 0+1

通過以上【a. b.】兩點鎖構造的思路進行程式碼的設計,一定是正確的。

重點重點重點:以上的【b.】,不是太容易理解,用心思考,恍然大悟的時候,真的很巧妙!

下一個題目:

LeetCode112.路徑總和:

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

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/112.路徑總和.py

LeetCode112題目是從根結點,尋找路徑和為 target 的一個路徑。

又是一個程式碼過分簡潔的例子:

def hasPathSum(self, root, targetSum):
    if not root:
        return False
    if not root.left and not root.right:
        return root.val == targetSum
    return self.hasPathSum(root.left, targetSum - root.val) or self.hasPathSum(root.right, targetSum - root.val)

思路點:遞迴將 targetSum-遞迴到的結點值 ,直到遇到葉子結點的時候,剛好被完全減去,得到0。即存在該路徑。

上述是一個很簡潔的先序遍歷過程。

由於使用了遞迴呼叫,那麼依然從葉子結點開始考慮:

a.當遞迴到葉子的時候,程式判斷葉子結點的值和tagetSum被減的剩餘的值是否相等;

b.往上考慮一層,遞迴使用 self.hasPathSum(root.left, targetSum - root.val)以及self.hasPathSum(root.right, targetSum - root.val)的時候,返回值是【a.】返回的值。

再來看一個:

LeetCode257.二叉樹的所有路徑:

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

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/257.二叉樹的所有路徑.py

這個題目用 DFS 解決起來,同樣是非常簡潔的,但是中間多了一個步驟的記錄,所以會多幾行程式碼:

    def binaryTreePaths_dfs(self, root):
        res = []
        if not root:
            return res

        def dfs(root, path):
            if not root:
                return
            if root and not root.left and not root.right:
                res.append(path + str(root.val))
            if root.left:
                dfs(root.left, path + str(root.val) + "->")
            if root.right:
                dfs(root.right, path + str(root.val) + "->")
        dfs(root, "")
        return res

核心還是一個遞迴的先序遍歷,依然我們們用遞迴解決思路步驟來分析一下:

a.當遞迴到葉子的時候,沒有左右孩子,直接將該結點加入到路徑中來;

b.往上考慮一層,遞迴使用 dfs(root.left, path + str(root.val) + "->")以及dfs(root.right, path + str(root.val) + "->")的時候,就是將當前結點值加入到路徑中。

還是依照這樣的思路,就可以很容易的將題目解決!

再來看最後一個例子:

LeetCode129.求根節點到葉節點數字之和

題目連結:https://leetcode-cn.com/problems/sum-root-to-leaf-numbers

GitHub解答:https://github.com/xiaozhutec/share_leetcode/blob/master/樹/129.求根節點到葉節點數字之和.py

這個題目與上一個題目,很類似,就是將計算好的路徑值轉為整型進行相加,看看程式碼:

def sumNumbers_dfs(self, root):
    res = []    # 所有路徑集合
    sum = 0     # 所有路徑求和

    def dfs(root, path):
        if not root:
            return
        if root and not root.left and not root.right:
            res.append(path + str(root.val))
        if root.left:
            dfs(root.left, path + str(root.val))
        if root.right:
            dfs(root.right, path + str(root.val))

    dfs(root, "")
    for item in res:
        sum += int(item)

    return sum

核心本質還是一個遞迴實現的先序遍歷,兩個步驟就先不分析了,參考上個題目。

嗯。。大概本篇的「樹-自頂向下」已經接近尾聲,尋找刷題組織的小夥伴們可以一起參與進來,私信我就OK!我們們一起堅持!

總結嘮叨幾句

個人刷題經驗,難免會出現思路上的欠缺,如果大家有發現的話,一定提出來,一起交流學習!

關於「樹-自頂向下」這類題目,既適合用 BFS 思路解決,又適合使用 DFS 的思路進行解決。

用 BFS 解決問題的時候,思路清晰,程式碼稍微看起來會有些多!

可是用 DFS 的情況是,程式碼簡潔,但是思路有時候會有些混亂,需要大量的練習才能逐漸的清晰起來!

下一期是講透樹 | 非自頂向下題目專題》,專門說說「樹-非自頂向下」這一類,不知道會不會再有最後覆盤的一期,看思路而定吧。

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

相關文章