今天來接觸下專業術語——深度優先搜索算法(英語:Depth-First-Search,DFS)
深度優先搜索算法(英語:Depth-First-Search,DFS)是一種用於遍歷或搜索樹或圖的算法。沿着樹的深度遍歷樹的節點,儘可能深的搜索樹的分支。當節點v的所在邊都己被探尋過,搜索將回溯到發現節點v的那條邊的起始節點。這一過程一直進行到已發現從源節點可達的所有節點爲止。如果還存在未被發現的節點,則選擇其中一個作爲源節點並重復以上過程,整個進程反覆進行直到所有節點都被訪問爲止。屬於盲目搜索。
深度優先搜索是圖論中的經典算法,利用深度優先搜索算法可以產生目標圖的相應拓撲排序表,利用拓撲排序表可以方便的解決很多相關的圖論問題,如最大路徑問題等等。
鏈接:https://leetcode-cn.com/tag/depth-first-search/
來源:力扣(LeetCode)
這裏提到,該算法多用於遍歷或搜索樹或圖,那麼以二叉樹爲例,該算法即儘可能的從根節點向下直到葉節點才結束,到達葉節點後再由根節點重新向下延伸。
但是,怎麼實現在我們到達葉節點時回到根節點來重新開始呢?初接觸,我的理解是通過遞歸來實現。在對根節點的函數中調用關於其子節點的函數,以此建立父子間的關聯;同時其子節點不止一個的話,那麼所謂的回溯其實也就通過遞歸同步實現了。
舉三道 LeetCode 題目爲例,看看它們是如何實現深度優先搜索的吧!
題目一
第 100 題:相同的樹
難度:簡單
給定兩個二叉樹,編寫一個函數來檢驗它們是否相同。
如果兩個樹在結構上相同,並且節點具有相同的值,則認爲它們是相同的。
示例 1:
輸入: 1 1
/ \ / \
2 3 2 3
[1,2,3], [1,2,3]
輸出: true
示例 2:
輸入: 1 1
/ \
2 2
[1,2], [1,null,2]
輸出: false
示例 3:
輸入: 1 1
/ \ / \
2 1 1 2
[1,2,1], [1,1,2]
輸出: false
#來源:力扣(LeetCode)
#鏈接:https://leetcode-cn.com/problems/same-tree
題目分析
既然要深度優先搜索,那麼要從根節點起一直到不能再向下子節點爲止,這麼產生一條鏈;比較兩個二叉樹相同,那麼只要保證生成這條鏈的過程中每個節點都是相同的即可。
思路很簡單,到代碼怎麼實現呢?因爲這題目是檢測兩棵二叉樹是否相同,自然會定義檢測函數。二叉樹是由根節點和子樹組成的,檢測兩棵二叉樹是否相同,我們保證根節點相同的情況下,檢查子樹是否相同即可——注意,檢查子樹,又可以調用我們定義的檢測函數,以此形成遞歸用法,這樣通過遞歸便可實現深度優先搜索了。
代碼實現
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isSameTree(self, p: TreeNode, q: TreeNode) -> bool:
# 兩根節點均爲空,返回 True
if p is None and q is None:
return True
# 其中一個空、另一個非空,返回 False
elif p is None or q is None:
return False
# 兩根節點均非空時,若根節點值不同,返回 False
if p.val != q.val:
return False
# 若根節點相同,比較兩個左子樹以及兩個右子樹是否都相同
# 遞歸對相應的子節點調用函數本身
return self.isSameTree(p.left,q.left) and self.isSameTree(p.right,q.right)
提交測試表現:
執行用時 : 36 ms, 在所有 Python3 提交中擊敗了 80.96% 的用戶
內存消耗 : 13.5 MB, 在所有 Python3 提交中擊敗了 7.14% 的用戶
題目二
第 101 題:對稱二叉樹
難度:簡單
給定一個二叉樹,檢查它是否是鏡像對稱的。
例如,二叉樹 [1,2,2,3,4,4,3] 是對稱的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面這個 [1,2,2,null,3,null,3] 則不是鏡像對稱的:
1
/ \
2 2
\ \
3 3
題目分析
如果不用深度優先搜索,可以考慮層序遍歷,但即使是空子節點,也要遍歷記錄,這樣來檢測每層的節點列表是否對稱即可。
但倘若採用深度優先搜索,與比較兩棵樹是否相同類似,我們要設計下如何複用設計的函數來通過子節點來繼續比較是否對稱。
本題中我們只輸入一個根節點、一棵完整的樹,但檢查其是否對稱,則要根據其子樹是否對稱。在檢查子樹是否對稱的過程中,子樹的根節點位置是要相等的,再下層的子樹又要繼續與對應位置上的子樹對稱,這樣我們便可以通過檢測兩棵子樹是否對稱的函數實現遞歸。
代碼實現
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
# 如果根節點爲空,返回 True
if not root:
return True
# 自定義檢測子節點是否對稱
def check_sym(node1,node2):
# 若兩節點均空,返回 True
if node1 is None and node2 is None:
return True
# 若一空一非空,返回 False
elif node1 is None or node2 is None:
return False
# 兩子節點作爲對稱點,若不等,返回 False
if node1.val!=node2.val:
return False
# 繼續尋找子節點下面的對稱節點,形成遞歸
return check_sym(node1.left,node2.right) and check_sym(node1.right,node2.left)
# 對左右子節點
return check_sym(root.left,root.right)
提交測試表現:
執行用時 : 52 ms, 在所有 Python3 提交中擊敗了 29.42% 的用戶
內存消耗 : 13.7 MB, 在所有 Python3 提交中擊敗了 6.06% 的用戶
試着加一下複雜度分析:因爲我們是遍歷整個二叉樹一次,共 n 個節點,故時間複雜度爲 O(n);空間上,運氣好的話可能不用每層都檢測完找到不對稱就返回,但對稱的話則需要對所有節點進行遞歸調用,造成的空間複雜度爲 O(n)。
題目三
第 104 題:二叉樹的最大深度
難度:簡單
給定一個二叉樹,找出其最大深度。
二叉樹的深度爲根節點到最遠葉子節點的最長路徑上的節點數。
說明: 葉子節點是指沒有子節點的節點。
示例:
給定二叉樹 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
題目分析
根節點對應左右兩棵子樹,二叉樹高度爲大的子樹高度再加一,那麼子樹高度又可以遞歸地等於其更大的子樹高度加一,就這樣遞歸形成。
代碼實現
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def maxDepth(self, root: TreeNode) -> int:
# 空節點返回高度 0
if not root:
return 0
# 當前樹高度爲較大的子樹高度+1
return max(self.maxDepth(root.left),self.maxDepth(root.right))+1
提交測試表現:
執行用時 : 40 ms, 在所有 Python3 提交中擊敗了 97.63% 的用戶
內存消耗 : 15.5 MB, 在所有 Python3 提交中擊敗了 5.55% 的用戶
這個時間比例並不準確,差幾 ms 比例卻差得很多。爲了計算每個子樹的高度來比較,n 個節點都會被遍歷到,故時間複雜度 O(n);遞歸也將被調用 n 次,保持調用棧的存儲是 O(n)。
結論
明明昨天結束了二叉樹題型的,結果今天由於算法的特性三道題目又都是二叉樹的。在學習和理解這算法的過程中,對深度優先搜索可能只是概念上增強,反倒對遞歸的應用更熟練了。
簡單整理下深度優先搜索的思路,由根節點向葉節點的過程中,找到可以複用的函數來實現遞歸過程,這樣便非常省力地通過遞歸來實現由上到下的聯繫,以達到深度搜索的效果。
爲了快速學習,今天選取的都是簡單題目,之後還是不同難度搭配着來比較好些。