深度優先搜索屬於圖算法的一種,是一個針對圖和樹的遍歷算法,英文縮寫爲 DFS 即 Depth First Search。深度優先搜索是圖論中的經典算法,利用深度優先搜索算法能夠產生目標圖的相應拓撲排序表,利用拓撲排序表能夠方便的解決不少相關的圖論問題,如最大路徑問題等等。通常用堆數據結構來輔助實現 DFS 算法。其過程簡要來講是對每個可能的分支路徑深刻到不能再深刻爲止,並且每一個節點只能訪問一次。html
(1)對於下面的樹而言,DFS 方法首先從根節點1開始,其搜索節點順序是 1,2,3,4,5,6,7,8(假定左分枝和右分枝中優先選擇左分枝)。算法
(2)從 stack 中訪問棧頂的點;數組
(3)找出與此點鄰接的且還沒有遍歷的點,進行標記,而後放入 stack 中,依次進行;數據結構
(4)若是此點沒有還沒有遍歷的鄰接點,則將此點從 stack 中彈出,再按照(3)依次進行;框架
(5) 因爲與節點 5 相連的的節點都被訪問過了,因而5被彈出,查找與 4 相鄰但沒有被訪問過的節點:oop
(6)直到遍歷完整個樹,stack 裏的元素都將彈出,最後棧爲空,DFS 遍歷完成。優化
// 用於記錄某個節點是否訪問過
private Map<String, Boolean> status = new HashMap<String, Boolean>();
// 用於保存訪問過程當中的節點 private Stack<String> stack = new Stack<String>();
// 入口,這裏選擇 1 爲入口 public void DFSSearch(String startPoint) { stack.push(startPoint); status.put(startPoint, true); dfsLoop(); } private void dfsLoop() {
// 到達終點,結束循環 if(stack.empty()){ return; } // 查看棧頂元素,但並不出棧 String stackTopPoint = stack.peek(); // 找出與此點鄰接的且還沒有遍歷的點,進行標記,而後所有放入list中。 List<String> neighborPoints = graph.get(stackTopPoint); for (String point : neighborPoints) { if (!status.getOrDefault(point, false)) { //未被遍歷 stack.push(point);
// 加上已訪問標記 status.put(point, true); dfsLoop(); } }
// 若是鄰接點都被訪問了,那麼就彈出,至關因而恢復操做,也就是在遞歸後面作的。 String popPoint = stack.pop(); System.out.println(popPoint); }
經過上面的示例,基本瞭解 dfs 使用。spa
其通常框架原理以下:指針
void dfs() { if(到達終點狀態) { ... //根據題意添加 return; } if(越界或不合法狀態) return; if(特殊狀態) // 剪枝 return; for(擴展方式) { if(擴張方式所到達狀態合法) { 修改操做; // 根據題意添加 標記; dfs(); (還原標記); //是否加上還原標記根據題意 //若是加上還原標記就是回溯法 } } }
經過這個 dfs 框架能夠看出該方法主要有如下幾個規律:code
訪問路徑的肯定。根據不一樣的題目思考怎麼纔算是一條訪問路徑,如何去實現遍歷。
起點條件。從哪一個點開始訪問?是否每一個點都須要看成起點?第一次 dfs 調用相當重要。
遞歸參數。也就是 dfs 遞歸怎麼在上一個節點的基礎上繼續遞歸,實現遞歸依賴什麼參數?須要知道一條路徑上各個節點之間的關係,當前訪問節點。
終結條件。訪問的終結條件是什麼?好比到達邊界點,全部點已經都訪問過了。終結條件須要在下一次遞歸前進行判斷。
訪問標誌。當一條路走不通的時候,會返回上一個節點,嘗試另外一個節點。爲了不重複訪問,須要對已經訪問過的節點加上標記,避免重複訪問。
下面將結合幾道算法題來加深對深度優先搜索算法的理解。
問題:給定大於0的數字n,輸出數字 1 ~ n 之間的全排列。
對於這道題目,有些人可能會好奇爲啥這到題目可使用 dfs 算法。對於全排列,其實能夠經過樹的形式來進行理解:
能夠發現就是一個 n 叉樹,總共是 n 層,下面採用前面總結的規律來看看算法實現原理:
訪問路徑:從起始位置到葉節點就是一個排列,也就是一條路徑
起點條件:start 下面有 n 個節點,每一個點均可以被看成起始點,說明須要採用 for 循環方式,。
遞歸參數:當前訪問的節點位置,定位下一個遞歸節點。須要一個變量記錄數字的排列,須要輸出。節點總數 n,便於知道什麼時候遞歸結束。
終結條件:遞歸訪問到節點數到達 n 層的時候中止遞歸。
訪問標誌:不須要,可重複訪問;
剪枝:不須要,沒有其餘須要提早終止遞歸的條件。
下面就是算法實現:
// 調用入口,起始點
dfs(total, 0, "");
// 遞歸參數:tatal 表示數字n, index 當前訪問節點,s 記錄排列方式 public void dfs(int total, int index, String s) {
// 終結條件 if (index == total) { System.out.println(s); return; }
// 對於每一個節點,當前有 total 種選擇 for (int i= 1;i<=total;i++) { dfs(total, index+1, s+i); } }
能夠發現,代碼仍是很簡單的。
給定一個包含了一些 0 和 1 的非空二維數組 grid 。
一個 島嶼 是由一些相鄰的 1 (表明土地) 構成的組合,這裏的「相鄰」要求兩個 1 必須在水平或者豎直方向上相鄰。你能夠假設 grid 的四個邊緣都被 0(表明水)包圍着。
找到給定的二維數組中最大的島嶼面積。(若是沒有島嶼,則返回面積爲 0 。)
示例 1:
[[0,0,1,0,0,0,0,1,0,0,0,0,0], [0,0,0,0,0,0,0,1,1,1,0,0,0], [0,1,1,0,1,0,0,0,0,0,0,0,0], [0,1,0,0,1,1,0,0,1,0,1,0,0], [0,1,0,0,1,1,0,0,1,1,1,0,0], [0,0,0,0,0,0,0,0,0,0,1,0,0], [0,0,0,0,0,0,0,1,1,1,0,0,0], [0,0,0,0,0,0,0,1,1,0,0,0,0]]
對於上面這個給定矩陣應返回 6。注意答案不該該是 11 ,由於島嶼只能包含水平或垂直的四個方向的 1 。
示例 2:
[[0,0,0,0,0,0,0,0]]
對於上面這個給定的矩陣, 返回 0。
注意: 給定的矩陣grid 的長度和寬度都不超過 50。
對於這道題目仍是採用以前的分析方式:
訪問路徑:節點中相鄰的1構成一條路徑。0 直接無視。
起點條件:二維數組的每一個點均可以看成起點。因此兩個 for 循環來進行調用。
遞歸參數:當前訪問的節點位置(x,y),二維數組表,從表中查找下一個節點
終結條件:到達二維數組的邊界,節點爲0
訪問標誌:須要,不可重複訪問;能夠將訪問過的節點置爲0,避免再次訪問,重複計算。
剪枝:只有在節點等於1的時候,才調用dfs。這樣能夠減小調用次數。
題目解答以下:
class Solution { public int maxAreaOfIsland(int[][] grid) { if (grid == null || grid.length <1 || grid[0].length<1) { return 0; } int rx = grid.length; int cy = grid[0].length; int max = 0; for (int x =0; x< rx; x++) { for (int y= 0;y<cy; y++) { if (grid[x][y]==1) { //只有節點等於1才調用,這裏就能夠算做是剪枝,算法的優化 int num = dfs(grid,x,y); max = Math.max(max, num); } } } return max; } // 遞歸參數:節點位置x,y, 二維數組 private int dfs (int[][] grid, int x, int y){ int rx = grid.length; int cy = grid[0].length;
// 邊界條件,節點爲0 if (x >= rx || x < 0 || y>=cy || y<0 || grid[x][y]==0 ) { return 0; }
// 直接修改原數組來標記已訪問 grid[x][y]=0;
// 每次遞歸就表示面積多了一塊 int num = 1;
// 每一個節點有四種不一樣的選擇方向 num += dfs(grid, x-1, y); num += dfs(grid, x, y-1); num += dfs(grid, x+1, y); num += dfs(grid, x, y+1); return num; } }
給你一個由 '1'(陸地)和 '0'(水)組成的的二維網格,請你計算網格中島嶼的數量。
島嶼老是被水包圍,而且每座島嶼只能由水平方向或豎直方向上相鄰的陸地鏈接造成。
此外,你能夠假設該網格的四條邊均被水包圍。
示例 1:
// 輸入: 11110 11010 11000 00000 // 輸出: 1
示例 2:
// 輸入: 11000 11000 00100 00011 // 輸出: 3
解釋: 每座島嶼只能由水平和/或豎直方向上相鄰的陸地鏈接而成。
能夠發現,這道題目與前面的題目很相似,關於 dfs 規則這裏就不在分析了,留給你們本身去分析。
題目解答以下:
class Solution { public int numIslands(char[][] grid) { if (grid == null || grid.length < 1 || grid[0].length<1) { return 0; } int num = 0; int rx = grid.length; int cy = grid[0].length;
// 起始點 for (int x =0;x<rx;x++) { for (int y =0;y<cy;y++) {
// 題目要求,'0'不符合路徑條件 if (grid[x][y]=='1') { dfs(grid,x,y); num++; } } } return num; } // 遞歸條件 private void dfs(char[][] grid, int x, int y) { int rx = grid.length; int cy = grid[0].length;
// 終結條件 if (x<0 || x>=rx || y<0 || y>= cy || grid[x][y] == '0') { return; }
// 訪問方向實質是由訪問路徑來決定的,就是你得想清楚怎麼纔算一條路徑 grid[x][y]='0'; dfs(grid,x-1,y); dfs(grid,x,y-1); dfs(grid,x+1,y); dfs(grid,x,y+1); return ; } }
到這裏,深度優先搜索的理論和實踐就講完了,相信看到這裏的小夥伴應該也掌握了其算法的原理,以及如何去書寫。