在图和树的题中,经常会碰到需要遍历的情况。解决需要遍历的问题,就需要DFS或者BFS。这两种方法的思路都是很简单很自然的
- DFS:从一个点出发,接着进入与它相连的任意一个点,不断深入向下搜索,直到搜索无法进行下去就回退到上一个节点,处理与之相连的其他未搜索的节点。在树中体现为先序遍历。
- BFS:从一个点出发,找到与他相连的所有节点,依次访问这些节点。在树中体现为层次遍历。
由于DFS和BFS在行为逻辑上的不同,所以BFS一般用队列来实现,而DFS一般用栈来实现。而函数递归的本质就是栈,所以DFS可以很容易地写成递归函数。
DFS的递归一般形式
void dfs(node, visited){
if(到达边界条件){
相应的操作;
return;
}
else{
if(check满足继续搜索的条件)){
标记当前点已经访问过,修改visited;
DFS(与该节点相邻的所有节点); //如果是一维的只有下一个点,如果是二维的依题意是四个方向上或八个方向上的点
恢复未标记前的状态,恢复visited; //回溯法需要恢复状态,如果题意是不需要回溯的就不需要这一步
}
}
}
在对应到具体问题的时候需要具体的分析,有些不需要回溯的就不需要恢复visited。在参数传递的时候,如果要在遍历的过程中保存需要返回的值的,那么通常还需要再传一个参数的引用,以便在内层的递归能够最终修改这个需要返回的值。
对于不同的问题通常是check是否满足条件那里的不同,如果check部分过于复杂,可以另写一个函数来实现。
DFS解决一维全排列问题
class Solution {
public:
vector<string> Permutation(string str) {
int length = str.length();
vector<string> result;
if (length == 0)
return result;
vector<bool> visited(length, 0);
string s = "";
DFS(0, s, str, visited, result);
return result;
}
void DFS(int step, string s, string str, vector<bool> visited, vector<string>& result) {
int length = str.size();
if (step == length) {
result.push_back(s);
return;
}
for (int i = 0; i < length; i++) {
if (visited[i] == 0) {
s = s + str[i];
visited[i] = 1;
DFS(step + 1, s, str, visited, result);
visited[i] = 0;
s = s.substr(0, s.size() - 1);
}
}
}
};
以上是采用仿照模板的编程思路。使用了visited来记录哪些位置上的字符访问过,同时也用了额外的空间来存储当前搜索的字符串。另外一种更简洁的方式是直接搜索到某个节点的时候,和的节点上的字符进行交换。由于只和后面的字符交换,前面访问过的不交换,所以实际上也实现了visited数组的功能。另外,由于交换是就地的,也就不需要额外一个字符串来存储结果。同时这样还有一个优点是当存在重复字符的时候,能够很方便地去重,只要相同就不交换。代码如下:
class Solution {
public:
vector<string> Permutation(string str) {
int length = str.length();
vector<string> result;
if (length == 0)
return result;
placeKthChar(str, result, 0);
return result;
}
void placeKthChar(string str, vector<string>& result, int k) {
int length = str.length();
if (k == length) {
result.push_back(str);
//cout << str << endl;
return;
}
for (int i = k; i < length; i++) {
if (i != k && str[i] == str[k])
continue;
char c = str[k];
str[k] = str[i];
str[i] = c;
placeKthChar(str, result, k + 1);
}
}
};
DFS解决二维路径遍历问题
在做这部分题目的时候,我就会想这题目的DFS解法怎么和剑指offer里的回溯法这么像。后来想一想,回溯法是建立在DSF的基础上,要回溯必须要先实现以DFS的方式遍历图中的节点。因此其实回溯法是DFS的一种应用。当然在某些场景下,DFS只需要搜索,而不用回溯,那就是单纯的DFS。
二维的情况下,DFS的整体框架还是和上述的模板类似。一般是从一个节点出发,向它的四个方向(有时候是8个)搜索。当然在进入搜索下一个节点前,需要先判断是否到达边界条件,以及check当前节点是否可进入。
例题:
给定一个由'1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 1:
输入:
11110
11010
11000
00000输出: 1
示例2:
输入:
11000
11000
00100
00011输出: 3
这一题的思路很简单,只需要遍历整个图,从一个是'1'的点出发,搜索到所有通过一定路径它能达到的所有'1'的节点。直到图中所有的'1'都被找到,假设一共出发过n次,那么就有n个岛屿。可以发现这里的搜索既可以是DFS也可以是BFS。另外,对于已经搜索过的点不需要回溯的操作。因此代码如下:
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int rows = grid.size();
if(rows == 0)
return 0;
int cols = grid[0].size();
int count = 0;
vector<vector<bool>> visited(rows, vector<bool>(cols, 0) );
for(int i = 0; i < rows; i++){
for(int j = 0; j < cols; j++){
if(visited[i][j] == 0 && grid[i][j] == '1'){
DFS(i, j, visited, grid);
count++;
}
}
}
return count;
}
void DFS(int x, int y, vector<vector<bool>>& visited, vector<vector<char>>& grid){
int rows = grid.size();
int cols = grid[0].size();
if(x >= 0 && x < rows && y >= 0 && y < cols && grid[x][y] == '1' && visited[x][y] == 0){
visited[x][y] = 1;
DFS(x - 1, y, visited, grid);
DFS(x + 1, y, visited, grid);
DFS(x, y - 1, visited, grid);
DFS(x, y + 1, visited, grid);
}
}
};
总结
用递归法来实现DFS,比较好理解,就一直往下找,知道走不通后在回来尝试其它的地方。一个DFS一般要判断边界,check来判断是否符合相应条件,visited来记录是否已经被用过,递归进行下一步操作。如果场景是需要回溯的话,那么就需要恢复一些变量的值,通常是visited。是否要回溯取决于应用的场景。