刷題覆盤進度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。謝過你們!