搜索算法的简单学习笔记
1、算法解释
常见的优先搜索算法:深度优先搜索(DFS)和广度优先搜索(BFS),广泛在图和树等结构中进行搜索
回过头小结:
- DFS是逮住一个符合的元素后,疯狂递归直至不符合条件,需要辅函数
- BFS是一层一层慢慢来,不需要辅函数
- 但很多时候需要两者结合,各自完成一部分计算
2、深度优先搜索
总是对新节点调用遍历,看起来向着“深度”方向前进
- 搜索到一个新的节点后,立即对该新节点进行遍历
- 遍历需要先入后出的栈;也可以使用与栈等价的递归
栈与递归调用的原理相同
- 递归便于实现,同时方便进行回溯(刷题推荐)
- 栈便于理解,不易出现递归栈满(工程推荐)
可用来检测环路
- 记录每个遍历过的节点的父节点
- 若一个节点被再次遍历且父节点不同,则说明有环
对已经搜索过的节点进行标记,防止在遍历时重复搜索某个节点——状态记录或记忆化
方法
- 一般分为主函数和辅函数
- 主函数用于遍历所有的搜索位置,判断是否可以开始搜索,如果可以即在辅函数进行搜索
- 辅函数则负责深度优先搜索的递归调用
- 最重要的是递归边界的确定
解题思路
- 从上到下,从左到右,逐个判断是否为岛屿
- 首先确定边界条件,即不能超出二维数组大小
- 遍历过的岛屿置零,防止再次计算
- 主函数用于从上到下、从左到右;辅函数用于判断当前岛屿的周围位置情况
Java解答
class Solution {
// 主函数
public int maxAreaOfIsland(int[][] grid) {
int ans = 0;
if(grid.length == 0 || grid[0].length == 0) return 0;
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < grid[0].length; j++){
if(grid[i][j] == 1) {
ans = Math.max(ans, IslandDFS(grid, i, j));
}
}
}
return ans;
}
// 辅函数
public int IslandDFS(int[][] grid, int r, int c){
if((r < grid.length) && (r >= 0) && (c < grid[0].length) && c >= 0){
if(grid[r][c] == 0){
return 0;
} else {
grid[r][c] = 0;
return 1 + IslandDFS(grid, r-1, c) + IslandDFS(grid, r+1, c) + IslandDFS(grid, r, c-1) + IslandDFS(grid, r, c+1);
}
} else {
return 0;
}
}
}
解题思路
- 从左到右,从上到下遍历
- 对每一列,从上到下判断是否联通,联通的标记为真
- 每改变一列时,计数加一
- 主函数用于更换列以及计数;辅函数用于判断该列有多少联通并设置为真
Java解答
class Solution {
// 主函数
public int findCircleNum(int[][] isConnected) {
int provinces = isConnected.length;
boolean[] visited = new boolean[provinces];
int circles = 0;
for (int i = 0; i < provinces; i++) {
if (!visited[i]) {
dfs(isConnected, visited, provinces, i);
circles++;
}
}
return circles;
}
// 辅函数
public void dfs(int[][] isConnected, boolean[] visited, int provinces, int i) {
for (int j = 0; j < provinces; j++) {
if (isConnected[i][j] == 1 && !visited[j]) {
visited[j] = true;
dfs(isConnected, visited, provinces, j);
}
}
}
}
解题思路
- 从两大洋的方向向中间汇聚
- 标记两大洋都会经过的共同地方
- 主函数负责分布从两大洋的方向;辅函数负责每一块的上下左右的大小判断
Java解答
class Solution {
// 用来返回的返回值
private List<List<Integer>> ans = new ArrayList<>();
// 方向转换的数组
private int[][] dirs = {
{
0, -1}, {
1, 0}, {
0, 1}, {
-1, 0}};
// 大西洋和太平洋共享的访问数组
private boolean[][] visited = null;
public List<List<Integer>> pacificAtlantic(int[][] heights) {
int n = heights.length, m = heights[0].length;
visited = new boolean[n][m];
// temp 是用来记录当前深度优先搜索访问过的点
boolean[][] temp = new boolean[n][m];
// 首先从太平洋出发,看看都能遇到哪些点
for (int x = 0; x < n; ++x) {
for (int y = 0; y < m; ++y) {
// x == 0 || y == 0 表示要从太平洋出发需要满足的条件,flag == false 意味着是从太平洋出发的
if ((x == 0 || y == 0) && !temp[x][y]) dfs(heights, x, y, temp, n, m, false);
}
}
// 同上,temp 是用来标记当前深度优先搜索访问到的点
temp = new boolean[n][m];
// 然后再从大西洋出发,看看能遇到哪些点,如果遇到的点 在 visited 中之前已经被标记为 true, 那么说明双方都可到达
for (int x = 0; x < n; ++x) {
for (int y = 0; y < m; ++y) {
// x == n - 1 || y == m - 1 表示从大西洋出发
if ((x == n - 1 || y == m - 1) && !temp[x][y]) dfs(heights, x, y, temp, n, m, true);
}
}
return ans;
}
/**
* @param x 深度优先搜索的起始点坐标 x
* @param y 起始点坐标 y
* @param temp 用来标记当前深度优先搜索已经访问过哪些点了
* @param flag 为 true 时意味着是大西洋来的,为 false 意味着是太平洋来的
*/
private void dfs(int[][] heights, int x, int y, boolean[][] temp, int n, int m, boolean flag) {
// 如果是大西洋来的,而且 太平洋已经访问过 {x, y} 了,就放到返回值中
if (flag && visited[x][y]) {
List<Integer> buf = new ArrayList<>();
buf.add(x);
buf.add(y);
ans.add(buf);
// 顺便把该点置为 false,防止重复记录
visited[x][y] = false;
}
// 如果是从太平洋来的,需要将 {x, y} 标记为已来过
if (!flag) visited[x][y] = true;
// 然后切换四个方向,逐个检查
for (int i = 0; i < 4; ++i) {
int nx = x + dirs[i][0];
int ny = y + dirs[i][1];
// 检查新的坐标是否合法,以及当前深度优先搜索是否来过,最后还要满足 逆向 条件
if (nx >= 0 && nx < n && ny >= 0 && ny < m && !temp[nx][ny] && heights[nx][ny] >= heights[x][y]) {
temp[nx][ny] = true; // 然后在当前深度优先搜索中标记为已来过
dfs(heights, nx, ny, temp, n, m, flag); // 继续深度优先搜索
}
}
}
}
3、回溯法(DFS特殊情况)
优先搜索的特殊情况,又称试探法
- 用于需要记录节点状态的深度优先搜素
- 常用于排列、组合、选择类的问题
两个小诀窍
- 按引用传状态
- 所有状态修改在递归完成后进行回改
- 修改最后一位输出,如排列组合;
- 或修改访问标记,如矩阵里搜素字符串
解题思路
- 每次遍历到数组末尾,加入列表中
- 从后往前,两两元素交换位置后,从当前指针指向的位置开始往后遍历
Java解答
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if (nums == null) return res;
permute(nums, 0, res);
return res;
}
public void permute(int[] nums, int index, List<List<Integer>> res) {
// 当指针此时指向最后,说明前面已经将nums数组的元素换过位置了
// 将换过元素位置的nums加入列表中
if (index == nums.length - 1) {
List<Integer> ans = new ArrayList<>();
for(int i = 0; i < nums.length; i++) {
ans.add(nums[i]);
}
res.add(ans);
}
// 以下为深度遍历数组进行位置交换,结束后回溯
// 相当于从后往前的两两交换
for(int i = index; i < nums.length; i++) {
swap(nums, i, index);
permute(nums, index + 1, res);
swap(nums, i, index);
}
}
public void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
解题思路
- 如n=4,k=2,即从[1, 2, 3, 4]中选择两个
- [1]—>[1, 2]—>[1]—>[1, 3]—>[1]—>[1, 4]—>[2]—>[2, 3]—>[2]—>[2, 4]—>[3]—>[3, 4]
- 从前到后,顺序添加足够数量的元素;去除末端元素,更换为下一个
Java解答
class Solution {
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
if(k <= 0 || n < k){
return res;
}
dfs(n, k, 1, new ArrayList<>());
return res;
}
public void dfs(int n, int k, int index, List<Integer> ans) {
if(k == 0) {
res.add(new ArrayList<>(ans));
return;
}
for (int i = index; i <= n - k + 1; i++) {
ans.add(i);
System.out.println(ans);
dfs(n, k-1, i+1, ans);
ans.remove(ans.size()-1);
}
}
}
解题思路
- 双重循环遍历board二维数组,寻找与word字符串(转为数组)相同的元素
- 对相同的元素,在考虑边界的情况下,对其四周进行判断
Java解答
class Solution {
public boolean exist(char[][] board, String word) {
int row = board.length, col = board[0].length;
boolean[][] visited = new boolean[row][col];
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++