文章目录
前言
- DFS 是线,BFS 是面
- 一般来说在找最短路径的时候使用 BFS,其他时候还是 DFS 使用得多一些(主要是递归代码好写)
- BFS空间复杂度高,DFS时间复杂度高。
1 BFS
- 无权图的最短路径
- 在程序实现 BFS 时需要考虑以下问题:
队列:用来存储每一轮遍历得到的节点
标记:对于遍历过的节点,应该将它标记,防止重复遍历 - 学习资料
1.1 BFS模板
int BFS(Node start, Node target) {
Queue<Node> q; // 核心数据结构
Set<Node> visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
int step = 0; // 记录扩散的步数
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 划重点:这里判断是否到达终点 */
if (cur is target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj())
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
/* 划重点:更新步数在这里 */
step++;
}
}
1.2 二叉树的最小深度
BFS
分析:更改终点情况;以为是子树不用考虑历史结点问题
int minDepth(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// root 本身就是一层,depth 初始化为 1
int depth = 1;
while (!q.isEmpty()) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
/* 判断是否到达终点 */
if (cur.left == null && cur.right == null)
return depth;
/* 将 cur 的相邻节点加入队列 */
if (cur.left != null)
q.offer(cur.left);
if (cur.right != null)
q.offer(cur.right);
}
/* 这里增加步数 */
depth++;
}
return depth;
}
DFS
public int minDepth(TreeNode root) {
if (root == null) return 0;
int left = minDepth(root.left);
int right = minDepth(root.right);
if (left == 0 || right == 0) return left + right + 1;
return Math.min(left, right) + 1;
}
1.3 打开转盘锁
分析
- 可看成8叉树,四位每一位有两个分支
- 八个结果看作一层,相当于一个操作数,step+1;
- 终止条件是结果==target,肯定是最短的先触发,最短路径。
- 字符串比较记得用equels。大坑
- == 基本类型比较的是数值相等,包装类比较是的是否同一个对象,,重写equels的类可能会比较数值。
代码
//往上拨
String plusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '9')
ch[j] = '0';
else
ch[j] += 1;
return new String(ch);
}
// 将 s[i] 向下拨动一次
String minusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '0')
ch[j] = '9';
else
ch[j] -= 1;
return new String(ch);
}
int openLock(String[] deadends, String target) {
// 记录需要跳过的死亡密码
Set<String> deads = new HashSet<>();
for (String s : deadends) deads.add(s);
// 记录已经穷举过的密码,防止走回头路
Set<String> visited = new HashSet<>();
Queue<String> q = new LinkedList<>();
// 从起点开始启动广度优先搜索
int step = 0;
q.offer("0000");
visited.add("0000");
while (!q.isEmpty()) {
int sz = q.size();
/* 将当前队列中的所有节点向周围扩散 */
for (int i = 0; i < sz; i++) {
String cur = q.poll();
/* 判断是否到达终点 */
if (deads.contains(cur))
continue;
if (cur.equals(target))
return step;
/* 将一个节点的未遍历相邻节点加入队列 */
for (int j = 0; j < 4; j++) {
//每个结点两种操作
String up = plusOne(cur, j);
if (!visited.contains(up)) {
q.offer(up);
visited.add(up);
}
String down = minusOne(cur, j);
if (!visited.contains(down)) {
q.offer(down);
visited.add(down);
}
}
}
/* 在这里增加步数 */
step++;
}
// 如果穷举完都没找到目标密码,那就是找不到了
return -1;
}
优化
可以不需要dead这个哈希集合,可以直接将这些元素初始化到visited集合中,要注意visit的添加位置,不能重复添加。
这种方法可以极限测试会超时
1.4 最小基因变化
- 典型BFS
- 知道了终点,双向BFS也可以做。
1. 常规BFScode:
public class 最小基因变化433 {
// bfs
// 输入:start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]
// 输出:2
public int minMutation(String start, String end, String[] bank) {
char[] all = {'A', 'C', 'G', 'T'};
// 存储路径字符串
Set<String> road = new HashSet<>();
for(String i:bank){
road.add(i);
}
// 输入数据判断
if(start.equals(end)){
return 1;
}
if(!road.contains(end)){
return -1;
}
// 队列
Queue<String> que = new LinkedList<>();
// 历史节点 防止回头
Set<String> visit = new HashSet<>();
que.add(start);
visit.add(start);
// 只要开始就默认找了一层
int step = 1;
while(!que.isEmpty()){
// 遍历一层
int size = que.size();
for(int i = 0;i < size;i++){
String poll = que.poll();
for(int j =0;j <8;j++){
for(int k =0 ;k <all.length;k++){
if(poll.charAt(j)!=all[k] ){
StringBuilder a = new StringBuilder(poll);
a.setCharAt(j,all[k]);
if(road.contains(a.toString()) && !visit.contains(a.toString())){
if(a.toString().equals(end)){
return step;
}
que.add(a.toString());
visit.add(a.toString());
}
}
}
}
}
// 遍历一层,层数加一
step ++;
}
// 最后返回,肯定是没找到
return -1;
}
}
2. 双向BFS做法
public int minMutation(String start, String end, String[] bank) {
char[] all = {'A', 'C', 'G', 'T'};
// 存储路径字符串
Set<String> road = new HashSet<>();
for(String i:bank){
road.add(i);
}
// 输入数据判断
if(start.equals(end)){
return 1;
}
if(!road.contains(end)){
return -1;
}
// 队列
Queue<String> que1 = new LinkedList<>();
Queue<String> que2 = new LinkedList<>();
// 历史节点 防止回头
Set<String> visit = new HashSet<>();
que1.add(start);
que2.add(end);
visit.add(start);
// 只要开始就默认找了一层
int step = 1;
// que1一直来做bfs,que2记录相遇的节点
while(!que1.isEmpty() && !que2.isEmpty()){
// 遍历一层
int size = que1.size();
// 记录que1 接下来的一层
Queue<String> temp = new LinkedList<>();
for(int i = 0;i < size;i++){
String poll = que1.poll();
for(int j =0;j <8;j++){
for(int k =0 ;k <all.length;k++){
if(poll.charAt(j)!=all[k] ){
StringBuilder a = new StringBuilder(poll);
a.setCharAt(j,all[k]);
//相遇点在于 que2是否包含目标字符串
if(que2.contains(a.toString())){
return step;
}
if(road.contains(a.toString()) && !visit.contains(a.toString())){
temp.add(a.toString());
visit.add(a.toString());
}
}
}
}
}
// 遍历一层,层数加一
step ++;
que1 = que2;
que2 =temp;
}
// 最后返回,肯定是没找到
return -1;
}
2 双向BFS
- 刚才讨论的二叉树最小高度的问题,你一开始根本就不知道终点在哪里,也就无法使用双向 BFS;但是第二个密码锁的问题,是可以使用双向 BFS 算法来提高效率的,代码稍加修改即可
- start结点和target结点的来回切换扫描,通过temp临时结点来切换两个结点,同时判断相交的触发条件
- visit集合做备忘录防止重复遍历,和deadends集合一样作用,排除无用结点。
- 双向的含义,一个结点去遍历下一层,一个结点存储上一次遍历的一层结点,做相遇的触发条件。
思想手绘图
int openLock(String[] deadends, String target) {
Set<String> deads = new HashSet<>();
for (String s : deadends) deads.add(s);
// 用集合不用队列,可以快速判断元素是否存在
Set<String> q1 = new HashSet<>();
Set<String> q2 = new HashSet<>();
Set<String> visited = new HashSet<>();
int step = 0;
q1.add("0000");
q2.add(target);
while (!q1.isEmpty() && !q2.isEmpty()) {
// 哈希集合在遍历的过程中不能修改,用 temp 存储扩散结果
Set<String> temp = new HashSet<>();
/* 将 q1 中的所有节点向周围扩散 */
for (String cur : q1) {
/* 判断是否到达终点 */
if (deads.contains(cur))
continue;
if (q2.contains(cur))
return step;
visited.add(cur);
/* 将一个节点的未遍历相邻节点加入集合 */
for (int j = 0; j < 4; j++) {
String up = plusOne(cur, j);
if (!visited.contains(up))
temp.add(up);
String down = minusOne(cur, j);
if (!visited.contains(down))
temp.add(down);
}
}
/* 在这里增加步数 */
step++;
// temp 相当于 q1
// 这里交换 q1 q2,下一轮 while 就是扩散 q2
q1 = q2;
q2 = temp;
}
return -1;
}
leetdoe752双向BFS解法
package com.zknode.BFS_and_DFS;
import java.util.HashSet;
import java.util.LinkedList;
public class ti_752 {
//单个字符的加操作
public String pulsStr(String str,int j){
char[] chars = str.toCharArray();
if(chars[j] =='9'){
chars[j] = '0';
}else {
chars[j] += 1;
}
return new String(chars);
}
public String minusStr(String str, int j){
char[] chars = str.toCharArray();
if(chars[j] == '0'){
chars[j] = '9';
}else{
chars[j]-=1;
}
return new String(chars);
}
public int openLock(String[] deadends, String target) {
//死亡集合
// HashSet<String> dead = new HashSet<>();
// for(String Str:deadends){
// dead.add(Str);
// }
//历史集合+死亡集合
HashSet<String> history = new HashSet<>();
for(String i:deadends){
history.add(i);
}
//遍历节点队列
LinkedList<String> que = new LinkedList<>();
LinkedList<String> que2 = new LinkedList<>();
que.offer("0000");
que2.offer(target);
//寻找的层数
int step = 0;
while(!que.isEmpty() && !que2.isEmpty()){
LinkedList<String> temp = new LinkedList<>();
//BFS的一层
for(String node:que){
//历史路径查询+死亡结点查询 ,不考虑该路径
if(history.contains(node)){
continue;
}
if(que2.contains(node)){
return step;
}
//统一添加历史结点位置,在出队列后加,也可以在入队列里加
history.add(node);
//四个字母分别动两下
for(int j =0 ; j<4 ;j++){
String plusnum = pulsStr(node,j);
//历史路径查询+死亡结点查询 ,不考虑该路径
if(!history.contains(plusnum)){
temp.offer(plusnum);
}
String minusnum = minusStr(node,j);
if(!history.contains(minusnum)){
temp.offer(minusnum);
}
}
}
step++;
que = que2;
que2 = temp;
}
return -1;
}
public static void main(String[] args) {
ti_752 ti_752 = new ti_752();
String[] str = new String[]{"0201","0101","0102","1212","2002"};
System.out.println(ti_752.openLock(str,"0202"));
}
}
3 DFS
- DFS算法的核心:做选择再递归,回溯算法就是DFS的一种表现形式
网格DFS框架模板
void dfs(int[][] grid, int r, int c) {
// 判断 base case
if (!inArea(grid, r, c)) {
return;
}
// 如果这个格子不是岛屿,直接返回
if (grid[r][c] != 1) {
return;
}
grid[r][c] = 2; // 将格子标记为「已遍历过」
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}
3.1 岛屿的数量
详情
问题
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
输入:grid =
[ [“1”,“1”,“1”,“1”,“0”],
[“1”,“1”,“0”,“1”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“0”,“0”,“0”] ]
输出:1
分析
- 岛屿问题最常用的就是DFS。当然也可以使用BFS和并查集。
代码
class Solution {
public void dfs(char[][] grid,int i,int j)
{
int nr = grid.length;
int nc = grid[0].length;
if(i>nr-1 || j>nc-1 ||i<0||j<0)
return;
if(grid[i][j]!='1')
return;
grid[i][j]='2';
dfs(grid,i+1,j);
dfs(grid,i,j+1);
dfs(grid,i,j-1);
dfs(grid,i-1,j);
}
public int numIslands(char[][] grid) {
int count=0;
if(grid.length==0 || grid ==null) return 0;
for(int i=0;i<grid.length;i++)
for(int j=0;j<grid[0].length;j++)
{
if(grid[i][j]!='1') continue;
else dfs(grid,i,j);
count++;
}
return count;
}
}
3.2 岛屿的最大面积
问题
示例 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 。
分析
- 依次扫描为1的网格,扫到之后DFS计算面积。
代码
public int maxAreaOfIsland(int[][] grid) {
int res = 0;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == 1) {
int a = area(grid, r, c);
res = Math.max(res, a);
}
}
}
return res;
}
int area(int[][] grid, int r, int c) {
if (!inArea(grid, r, c)) {
return 0;
}
if (grid[r][c] != 1) {
return 0;
}
grid[r][c] = 2;
return 1
+ area(grid, r - 1, c)
+ area(grid, r + 1, c)
+ area(grid, r, c - 1)
+ area(grid, r, c + 1);
}
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}
3.3 填海造陆问题
问题
分析
- 两遍 DFS:第一遍 DFS 遍历陆地格子,计算每个岛屿的面积并标记岛屿;第二遍 DFS 遍历海洋格子,观察每个海洋格子相邻的陆地格子
代码
3.4 岛屿的周长
问题
分析
代码