演算法面試(七) 廣度和深度優先演算法

天澄發表於2019-04-05

系列文章導圖:

演算法面試(七) 廣度和深度優先演算法

檢視所有系列文章: xiaozhuanlan.com/leetcode_tc

演算法面試(一) 連結串列

演算法面試(三) 優先佇列

1. 知識點

本章節重點介紹二叉樹的廣度和深度優先搜尋。

1.1 二叉樹的遍歷

前序遍歷、中序遍歷、後序遍歷在之前將二叉樹的時候已經提到過,這裡不再重複,需要再次瞭解的請看下面連結。 二叉樹的遍歷

1.2 BFS 廣度優先搜尋

下面介紹一下BFS,即廣度優先搜尋,下面圖解示例看BFS是如何遍歷的:

演算法面試(七) 廣度和深度優先演算法

上面四幅圖,1-2-3-4,比較清晰的描述了BFS的是如何遍歷的,可以理解為是按層進行掃蕩。

1.3 DFS 深度優先搜尋

跟BFS一樣,同樣給出圖解示例,帶大家理解DFS的遍歷過程:

演算法面試(七) 廣度和深度優先演算法

可以看出DFS,是一般先遍歷左子樹,並且將左子樹的子孫全部遍歷完,直到葉子節點,然後回到根節點,繼續遍歷右子樹。

1.4 BFS vs DFS

根據上面對BFS和DFS的介紹,給出一張直觀圖來對比二者的區別:

演算法面試(七) 廣度和深度優先演算法

2. 題目

2.1 108. Convert Sorted Array to Binary Search Tree

  • 題目要求 將一個按照升序排列的有序陣列,轉換為一棵高度平衡二叉搜尋樹。

  • Example

Given the sorted array: [-10,-3,0,5,9],

One possible answer is: [0,-3,9,-10,null,5], which represents the following height balanced BST:

      0
     / \
   -3   9
   /   /
 -10  5
複製程式碼
  • 解題思路 這題目主要考察對二叉搜尋樹的瞭解以及中序遍歷的知識點。 容易想到和理解的一條思路,就是利用二叉搜尋樹按中序遍歷是有序的特性。而中序遍歷的規則是 左-根-右。即陣列的中間值則是根節點,左右兩邊則分別是左子樹和右子樹,按遞迴的思維,左子樹和右子樹也按照上面的邏輯進行樹的轉換。

  • 程式碼演示

class Solution(object):
   def sortedArrayToBST(self, nums):
       """
       :type nums: List[int]
       :rtype: TreeNode
       """
       if len(nums) == 0:
           return None
       mid = int(len(nums) / 2)
       root = TreeNode(nums[mid])
       root.left = self.sortedArrayToBST(nums[:mid])
       root.right = self.sortedArrayToBST(nums[mid + 1:])
       return root
複製程式碼

2.2 111. Minimum Depth of Binary Tree

  • 題目要求 Given a binary tree, find its minimum depth.找出二叉樹的最小深度。

  • Example

    Given binary tree [3,9,20,null,null,15,7],
        3
       / \
      9  20
        /  \
       15   7
    return its minimum depth = 2.
    複製程式碼

2.2.1 解題思路一: 遞迴

  • 思路 上一篇講了二叉樹的最高的深度,用的就是遞迴思維,所以找出二叉樹的最小深度,我們先來用遞迴解題,思路就是遞迴查詢左子樹和右子樹的深度,然後取二者的小者即可。

  • 程式碼演示

class Solution(object):
   def minDepth(self, root):
       """
       :type root: TreeNode
       :rtype: int
       """
       if not root:
           return 0
       if root.left is None:
           return self.minDepth(root.right) + 1
       if root.right is None:
           return self.minDepth(root.left) + 1
       left = self.minDepth(root.right) + 1
       right = self.minDepth(root.left) + 1
       return min(left, right)
複製程式碼

程式碼也比較清晰,可以跟找出二叉樹的最大深度進行對比。也就是分別找出左子樹和右子樹的深度,取小者返回即可。

時間複雜度O(N)

2.2.2 解題思路二: DFS

  • 思路 思路二則採用今天的主題,DFS來解,按照知識點的描述,DFS是先將左子樹的自身遍歷完畢,再回來遍歷右子樹。DFS非遞迴解法一般需要採用棧Stack資料結構,按層將右子樹和左子樹依次入棧,下一次先出左子樹(後入),然後進行下一輪,判斷左子樹的子孫,直到左子樹遍歷完畢,獲取到左子樹的最高深度;右子樹也是如此。如果該節點是葉子節點,則說明已經到底,將此深度與最小深度進行比較。

  • 程式碼演示

class Solution(object):
   def minDepth(self, root):
       """
       :type root: TreeNode
       :rtype: int
       DFS O(n)
       """
       if not root:
           return 0
       stack = [(root, 1)]
       min_depth = float("inf")
       while stack:
           cur_node, cur_depth = stack.pop()
           children = [cur_node.right, cur_node.left]
           if not any(children):
               min_depth = min(min_depth, cur_depth)
           for child in children:
               if not child:
                   continue
               stack.append((child, cur_depth + 1))
       return min_depth
複製程式碼

先定義一個min_depth,初始化為正無窮,python里正無窮的表達方式是float("inf")。然後檢查該節點是否是葉子節點,any([])是表示該陣列中是否全為空,如果是則返回True,否則返回False。如果左節點和右節點全為空,則表示是葉子節點,則取該深度與最小深度的小者。然後按先右後左的順序依次入棧即可。

時間複雜度O(N)

2.2.3 解題思路三: BFS

  • 思路 思路三採用BFS,逐層掃蕩二叉樹,BFS在此題中,還有一點優勢是如果發現某一層出現葉子節點,則直接返回此節點所處的深度就是答案,因為逐層掃蕩下去,最先出現葉子節點的層肯定也是深度最小,肯定越往下深度越高。BFS非遞迴解法則需要藉助雙端佇列的資料結構,因為需要從首邊出,尾部進,滿足先進先出的特點。

  • 程式碼演示

from collections import deque

class Solution3(object):
    def minDepth(self, root):
        """
        :type root: TreeNode
        :rtype: int
        BFS O(n)
        """
        if not root:
            return 0
        queue = deque([(root, 1)])
        while queue:
            cur_node, cur_depth = queue.popleft()
            children = [cur_node.left, cur_node.right]
            if not any(children):
                return cur_depth
            for child in children:
                if not child:
                    continue
                queue.append((child, cur_depth + 1))
複製程式碼

deque則是Python中雙端佇列的實現。也是先檢查該節點是否是葉子節點,如果是則直接返回結果,否則進行將下一層的左右節點分別入佇列

時間複雜度 O(n)

2.2.4 思考題

嘗試將上一篇中求二叉樹的最高深度,leetcode第104題,也按照DFS和BFS的思路進行求解。如果能自由的寫出來,說明你已經掌握DFS和BFS了。

2.3 102. Binary Tree Level Order Traversal

  • 題目要求 Given a binary tree, return the level order traversal of its nodes' values. (ie, from left to right, level by level). 給定一顆二叉樹,按層進行遍歷,要求先左後右.

  • Example

    Given binary tree [3,9,20,null,null,15,7],
        3
       / \
      9  20
        /  \
       15   7
    return its level order traversal as:
    [
      [3],
      [9,20],
      [15,7]
    ]
    複製程式碼

2.3.1 解題思路一: BFS

  • 思路 按層進行遍歷,不就是BFS的特性嘛,所以最直接解法就是BFS了,上面題目已經講解了BFS是如何實現,接下來看程式碼講解.

  • 程式碼演示

from collections import deque

class Solution(object):
    def levelOrder(self, root):
        """
        :type root: TreeNode
        :rtype: List[List[int]]
        BFS
        """
        if not root:
            return []
        queue = deque([root])
        result = []
        while queue:
            cur_level = []
            for _ in range(len(queue)):
                cur_node = queue.popleft()
                cur_level.append(cur_node.val)
                if cur_node.left:
                    queue.append(cur_node.left)
                if cur_node.right:
                    queue.append(cur_node.right)
            result.append(cur_level)
        return result
複製程式碼

BFS非遞迴實現,首先初始化雙端佇列,然後判斷一層的節點個數,迴圈每一個節點,進行遍歷,同時將每一個節點的孩子進入佇列,進行下一輪遍歷。

2.3.2 解題思路二: DFS非遞迴實現

  • 思路 如果此題按DFS是否可以實現了?答案肯定是可以的,麻煩在於需要記住每一層的level,這樣遍歷左子樹後,遍歷右子樹時需要找到所屬的層數。即需要知道每一個節點所屬哪一層。

  • 程式碼實現

class Solution(object):
    def levelOrder(self, root):
        """
        :type root: TreeNode
        :rtype: List[List[int]]
        DFS
        """
        if not root:
            return []
        stack, result = [], []
        stack.append((root, 0))
        while stack:
            cur_node, level = stack.pop()
            if len(result) < level + 1:
                result.append([])
            result[level].append(cur_node.val)
            children = [cur_node.right, cur_node.left]
            if not any(children):
                continue
            for child in children:
                if not child:
                    continue
                stack.append((child, level + 1))
        return result
複製程式碼

DFS非遞迴實現採用棧,棧中的元素是一個元組,第一個元素是節點,第二個元素是元素的層數,這樣遍歷到該節點時,可以將該節點的值放入所屬的層次的陣列中。 需要注意的是孩子們的壓棧順序是先右孩子再左孩子,因為需要先遍歷左孩子,後進先出。

2.3.3 解題思路三: DFS遞迴實現

DFS這裡演示如何使用遞迴實現,思路跟解題思路二一致。

  • 程式碼演示
class Solution(object):
    def levelOrder(self, root):
        """
        :type root: TreeNode
        :rtype: List[List[int]]
        DFS
        """
        if not root:
            return []
        self.result = []
        self.dfs(root, 0)
        return self.result

    def dfs(self, node, level):
        if not node:
            return
        if len(self.result) < level + 1:
            self.result.append([])

        self.result[level].append(node.val)
        self.dfs(node.left, level + 1)
        self.dfs(node.right, level + 1)
複製程式碼

這裡需要藉助一個函式dfs,傳入2個引數,一個是節點,一個是節點所屬的層數,這樣可以知道該節點的值該加入哪一層。

2.3.4 思考題

leetcode 第107題,是該題的變種:107. Binary Tree Level Order Traversal II
Example:


    Given binary tree [3,9,20,null,null,15,7],
        3
       / \
      9  20
        /  \
       15   7
    return its bottom-up level order traversal as:
    [
      [15,7],
      [9,20],
      [3]
    ]
複製程式碼

從底部按層遍歷,解題思路也是DFS和BFS,歡迎大家在評論區交流。

可以看出DFS和BFS的解題思路是可以複製的,基本都是這個套路,如果你以完全理解,則相關題目是完全難不倒你。

檢視所有系列文章: xiaozhuanlan.com/leetcode_tc

更多文章請關注公眾號 『天澄技術雜談』

演算法面試(七) 廣度和深度優先演算法

相關文章