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

image

刷題覆盤進度node

你們好,我是Johngo!python

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

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

仍是直觀的先看看本文的所處的一個進度。數組

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

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

涉及到的題目oop

104.二叉樹的最大深度: leetcode-cn.com/problems/ma…post

112.路徑總和: leetcode-cn.com/problems/pa…學習

113.路徑總和 II: leetcode-cn.com/problems/pa…

437.路徑總和 III: leetcode-cn.com/problems/pa…

257.二叉樹的全部路徑: leetcode-cn.com/problems/bi…

129.求根節點到葉節點數字之和: leetcode-cn.com/problems/su…

988.從葉結點開始的最小字符串: leetcode-cn.com/problems/sm…

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

第一類: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 個引例,往上述代碼中添加點做料,看是否能夠很容易就解答。

引例一

遍歷過程當中可否記錄根結點到當前結點的一些信息?

包括:

一、根結點到當前結點的路徑信息

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

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

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

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 形式打印的一個過程(不太記得的小夥伴能夠查看 mp.weixin.qq.com/s/MkCF5TaR1… 回憶下關於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.二叉樹的最大深度

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

GitHub解答:github.com/xiaozhutec/…

其實就是「引例二」中攜帶一個值進行層序的記錄,最後返回的 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.路徑總和:

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

GitHub解答:github.com/xiaozhutec/…

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.二叉樹的全部路徑:

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

GitHub解答:github.com/xiaozhutec/…

就是要把全部從根結點開始到葉子結點全部的路徑遍歷出來。其實仍是「引例一」中的思路,當遍歷到葉子結點的時候,將全部葉子結點中的三元組中的路徑值取出。例如葉子結點 (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.求根節點到葉節點數字之和

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

GitHub解答:github.com/xiaozhutec/…

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

舉例說,圖中路徑分別爲[[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:github.com/xiaozhutec/… 上進行了記錄,細節代碼能夠參考。

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

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

DFS 思路

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

回憶下以前的二叉樹的遞歸遍歷,也能夠說是 DFS 的思路。以前在這篇文章中詳細闡述過 mp.weixin.qq.com/s/nTB41DvE7…

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

不少時候我會利用一個很 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.二叉樹的最大深度

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

GitHub解答:github.com/xiaozhutec/…

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

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.路徑總和:

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

GitHub解答:github.com/xiaozhutec/…

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.二叉樹的全部路徑:

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

GitHub解答:github.com/xiaozhutec/…

這個題目用 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.求根節點到葉節點數字之和

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

GitHub解答:github.com/xiaozhutec/…

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

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 的狀況是,代碼簡潔,可是思路有時候會有些混亂,須要大量的練習才能逐漸的清晰起來!

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

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

相關文章
相關標籤/搜索