在学习二叉树的时候,我们了解到了遍历二叉树的方式:层序遍历,前序遍历,中序遍历,后序遍历。其中层序遍历本质上属于广度优先搜索,而后三种本质上属于深度优先搜索。所以今天我们就来简单学习广度优先搜索和深度优先搜索。
一、深度优先搜索(DFS)
1.什么是深度优先搜索?
深度优先搜索简单来说就是一条路走到黑。拿二叉树的前序遍历举例,我们遍历时从根节点向子节点遍历,先遍历左子树,再遍历右子树。
如图,我们会从编号为1的节点开始,遍历2,5,到达末尾时回溯到2,6,再回溯到1...
简而言之,就是我们一头扎进去,撞了南墙,我就退一步,但是决不放弃,在原基础上做出局部的改变去尝试第二条路,直到所有的情况我都试了,实在没有其他情况了,那我就回到1,从头出发,再做选择,再一头扎进去,直到成功。
//前序遍历
void preOrder(TreeNode *root,int *size)
{
if(root==NULL)
{
return;
}
//访问优先级:根节点->左子树->右子树
arr[(*size)++]=root->val;
preOrder(root->left,size);
preOrder(root->right,size);
}
在实现前序遍历时,通常基于递归实现。
2.dfs代码框架
void dfs(参数){
if(终止条件){
存放结果;
return;
}
for(选择:本节点所连接的其他节点){
处理节点;
dfs(图,选择的节点);
回溯,撤销处理的结果
}
}
dfs简单可以分为三步:
(1)确认递归函数,参数
通常我们递归的时候,我们递归搜索需要了解哪些参数。一般情况,深搜需要二维数组数据结构保存所有路径,需要一维数组保存单一路径。
(2)确认终止条件
终止条件在dfs中非常重要,没有确立好终止条件就容易导致死循环,栈溢出等问题。
(3)处理目前搜索节点出发的路径
一般这里是一个for循环,去遍历目前搜索节点所能到的所有节点。
3.dfs题目实例
原题链接:
这道题我使用了深度优先搜索,思路就是遍历数组中的元素,如果为1,则以这个点向上下左右四个方向搜索,直 到搜索到0或者到达边界(终止条件)。
每当进行一次深搜岛屿数量就要加1。需要注意的是每次搜索要把这个点赋 为0(标记遍历过的陆地),避免重复搜索导致陷入死循环。
void dfs(char** grid, int gridSize, int* gridColSize, int i, int j) {
if (grid[i][j] == '1') {
grid[i][j] = '0';
} else {
return;
}
if ((i - 1) >= 0) {
dfs(grid, gridSize, gridColSize, i - 1, j);
}
if ((i + 1) < gridSize) {
dfs(grid, gridSize, gridColSize, i + 1, j);
}
if ((j - 1) >= 0) {
dfs(grid, gridSize, gridColSize, i, j - 1);
}
if ((j + 1) < gridColSize[i]) {
dfs(grid, gridSize, gridColSize, i, j + 1);
}
}
int numIslands(char** grid, int gridSize, int* gridColSize) {
int num = 0;
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridColSize[i]; j++) {
if (grid[i][j] == '1') {
dfs(grid, gridSize, gridColSize, i, j);
num++;
}
}
}
return num;
}
二、广度优先搜索(BFS)
1.什么是广度优先搜索?
如果说DFS是一条路走到黑的话,BFS就完全相反了。BFS会在每个岔路口都各向前走一步。如图所示。
我们发现每次搜索的位置都是距离当前节点最近的点。因此,BFS是具有最短路径的性质的。
继续以二叉树的遍历为例,在层序遍历时从顶部到底部逐层遍历二叉树,每一层从左到右访问节点。通常借用队列实现。
为什么借用队列实现呢?
BFS要保证的第一件事就是我们需要先走最近的,因此,队列的作用就是基于此的。
我们先让根节点1入队,与根节点同一层的节点已经搜索完毕,让根节点出队。
之后以根节点为起点搜索到离其最近的3个的节点,再将其储存起来,便于后续将它们作为新的姐节点去继续搜索最近的点。
由于点2周围的搜索,我们找到了距离2最近的5和6,我们让它们进队,但是它们之前还有3和4,因此只有34出队后,才轮得到下一层的56。
这就体现了队列的作用:保证本层搜索结束才轮得到下一层的点。
//层序遍历
int *levelOrder(TreeNode *root, int *size)
{
int front, rear;
int index;
TreeNode *node;
TreeNode **queue;
queue = (TreeNode**)malloc(sizeof(TreeNode*)*MAX_SIZE);
front = 0;
rear=0;
queue[rear++]=root;
index=0;
while(front<rear)
{
node = queue[front++];
arr[index++]=node->val;
if(node->left!=NULL)
{
queue[rear++]=node->left;
}
if(node->right!=NULL)
{
queue[rear++]=node->right;
}
}
*size=index;
arr=realloc(arr,sizeof(int)*(*size));
free(queue);
return arr;
}
2.bfs题目实例及代码分析
原题链接:
这道题要求从extrance到最近出口的最短路径的步数这就让我们想到了bfs的最短路径性质。
我们用结构体node保存x,y,step,分别代表当前点的横纵坐标和距离extrance的步数。
当我们遍历至点(x,y)时,我们枚举它上下左右的相邻坐标(x+dx[i],y+dy[i])。
此时可能有三种情况:
(1)(x+dx[i],y+dy[i])不合法或对应的坐标为墙,此时无需进行任何操作;
(2) (x+dx[i],y+dy[i])为迷宫的出口(在迷宫边界且不为墙),此时应返回 cur.step + 1,即该出口相对入口的距离作为答案;
(3) (x+dx[i],y+dy[i]) 为空格子且不为出口,此时应将新坐标对应的节点 加入队列。
最终,如果不存在到达出口的路径,我们返回 −1 作为答案。
为了避免重复遍历,我们可以将所有遍历过的坐标对应迷宫矩阵的值改为墙所对应的字符 ‘+’。
struct node {
int x;
int y;
int step;
};
const int dx[] = {1, -1, 0, 0};
const int dy[] = {0, 0, 1, -1};
int nearestExit(char** maze, int mazeSize, int* mazeColSize, int* entrance,
int entranceSize) {
int m = mazeSize;
int n = mazeColSize[0];
struct node* queue = (struct node*)malloc(sizeof(struct node) * 10001);
int i = 0;
int front = 0, rear = 0;
queue[rear].x = entrance[0];
queue[rear].y = entrance[1];
queue[rear++].step = 0;
maze[entrance[0]][entrance[1]] = '+';//原点不能算出口
while (front != rear) {
struct node cur = queue[front];
front = front+1;//已经结算过的节点出对
for (int i = 0; i < 4; i++) {
int xi = cur.x + dx[i];
int yi = cur.y + dy[i];
if (xi >= m || yi >= n || xi < 0 || yi < 0) {
continue;//不能出迷宫
}
if (maze[xi][yi] == '+') {
continue;
} else if (maze[xi][yi] == '.') {
if (xi == m - 1 || yi == n - 1 || xi == 0 || yi == 0) {
return cur.step + 1;
} else {
maze[xi][yi] = '+';//避免走回头路
queue[rear].x = xi;
queue[rear].y = yi;
queue[rear++].step = cur.step + 1;
}
}
}
}
return -1;
}