零
刷題覆盤進度
大家好,我是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。謝過大家!