257. Binary Tree Paths

257. Binary Tree Paths

題目給定一個二元樹,要找出從跟節點到最終的葉節點的所有路徑。

既然這個題目要求要找出所有可能的路徑,那就一定是需要窮舉的題目,既然需要窮舉,就可以用 backtrack 來思考這個題目。

首先我們可以知道,找出路徑的方式一定是夠過深度優先的方式來遍歷整棵樹,這時候可以先建立出 DFS 的方式

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:

        # DFS
        def traverse(node):
            if not node:
                return
            traverse(node.left)
            traverse(node.right)
    
        traverse(root)
        
        return res

既然上面已經建立了遍歷的方式,接著就是要怎麼透過 backtrack 來窮舉出所有的路徑。我心中大致上的方向是這樣的:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:

        # DFS
        def traverse(node, curr):
            if not node:
                return
            # When to stop??
            # TODO

            # Is it correct we append here and pop only once?
            curr.append(str(node.val))
            traverse(node.left, curr)
            traverse(node.right, curr)
            curr.pop()
    
        traverse(root, [])
        
        return res

但是其中有幾個點需要考慮

  1. 還有何時可以算是停止的時機呢?
  2. 目前的遞迴方程式總共會向左子樹和右子樹進行遞迴, 上述的方式可以很好的處理目前的記憶方式嗎?

因為這是個遞迴,說實在透過程式的方式來思考是有點難度的,不過回到用樹的方式來思考會比較簡單一點,那就是何時可以視為一個路徑已經結束?那就是我們已經在葉節點的時候,但這個地方和原先遞迴終止的地方是不是一樣的呢?

答案是不同的,因為遞迴終止的條件,已經是葉節點向左右某個子樹去探索並發現該子樹是個空值,也就是那並不是實際意義上的葉節點。

而找到葉節點後就比較像是完成 backtrack 的終止條件了。我們就可以把探索到的路徑,放到紀錄窮舉路徑的記憶體中如下:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:

        res = []
        # DFS
        def traverse(node, curr):
            if not node:
                return
            if not node.left and not node.right:
                curr.append(str(node.val))
                res.append("->".join(curr))
                curr.pop()
                return
            # Is it correct we append here and pop only once?
            curr.append(str(node.val))
            traverse(node.left, curr)
            traverse(node.right, curr)
            curr.pop()
    
        traverse(root, [])
        
        return res

後一個問題是上面就完成了嗎?我一開始對於 curr 處理的方式有點遲疑,因為我把根節點放進去後,接著進行了兩次的遞迴,但是 curr 的值在進行左子樹遞迴的時候,curr 的值一直有在改變,我想確保的是再次進入到右子樹遞迴時,curr 所含有的內容,和要進行左子樹遞迴的時候一樣,這樣才能確保我們是真的在進行 backtrack 。

這個地方需要一點想像力去完成 backtrack 的過程,答案當然是會的,但是站在最後的端點來思考會比較好思考。

例如:目前有一個節點 x,其兩個左右子樹剛好都是葉節點,兩個葉節點都會把當前節點給移除後,才會返回到我現在這個節點,剛好我在最後一步也把我自己移除了,這時候才會返回上一層,這時候我們再思考一次,如果 x 剛好是一個左字樹,跟他同一層的右側,剛好也有一個子樹,不管該子樹是一個樹的根節點、葉節點又或是沒有節點,都會從記憶體移除,因此兩個子樹的內容都會移除後才返回,因此目前處理 curr 的處理方式是正確的。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
        
        res = []

        def traverse(node, curr):
            if not node:
                return
            if not node.left and not node.right:
                curr.append(str(node.val))
                res.append("->".join(curr))
                curr.pop()
                return
            curr.append(str(node.val))
            traverse(node.left, curr)
            traverse(node.right, curr)
            curr.pop()
    
        traverse(root, [])
        
        return res