文章目录
面试经典150题0507
Leetcode200 岛屿数量
网格类问题的DFS遍历方法
网格问题的基本概念:由m×n个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。
岛屿问题是一类典型的网格问题,每个格子数字为0或者1。把0看作海洋,把1看作陆地,相邻的陆地就可以看作一个岛屿。
在这样的设定下,出现了各种岛屿问题的变种,包括岛屿的数量、面积、周长等。
DFS基本结构:
网格结构其实是一种简化版的图结构。
二叉树的DFS方法如下:
void traverse(TreeNode root){
// 判断结束条件
if(root == null){
return;
}
// 访问两个相邻节点
traverse(root.left);
traverse(root.right);
}
二叉树的DFS由两个要素组成:访问相邻节点和判断结束条件
第一个要素为访问相邻节点。二叉树结构简单,只有左子节点和右子节点两个。二叉树本身就是一个递归定义个结构:一颗二叉树,它的左子树和右子树也是一颗二叉树。那么我们的DFS遍历只需要递归调用左子树和右子树即可。
返回条件判断其实有两个含义:一方面,这表示 root
指向的子树为空,不需要再往下遍历了。另一方面,在 root == null
的时候及时返回,可以让后面的 root.left
和 root.right
操作不会出现空指针异常。
参考二叉树的DFS,写出网格DFS的两个要素:
首先,网格中的每个格子有上下左右四个节点。
其次,网格DFS的返回条件为网格中不需要继续遍历,grid[r][c]
会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。
不管当前是在哪个格子,先往四个方向走一步再说,如果发现走出了网格范围再赶紧返回。这跟二叉树的遍历方法是一样的,先递归调用,发现 root == null
再返回。
网格DFS的遍历框架:
void dfs(int[][] grid, int r, int c){
// 判断返回条件
// 如果坐标(r, c)超出网格范围,直接返回
if(!inArea(grid, r, c)){
return;
}
// 访问相邻节点
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;
}
如何避免重复遍历
网格结构的 DFS 与二叉树的 DFS 最大的不同之处在于,遍历中可能遇到遍历过的结点。这是因为,网格结构本质上是一个「图」,我们可以把每个格子看成图中的结点,每个结点有向上下左右的四条边。在图中遍历时,自然可能遇到重复遍历结点。
因此需要标记已经遍历过的格子。在岛屿问题中,需要在所有值为1的陆地格子上做DFS遍历。没走过一个陆地格子,就把值改为2;当再遇到2的时候,知道这是遍历过的格子。
void dfs(int[][] grid, int r, int c) {
// 返回条件
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;
}
参考链接:https://leetcode.cn/problems/number-of-islands/solutions/211211/dao-yu-lei-wen-ti-de-tong-yong-jie-fa-dfs-bian-li-/?envType=study-plan-v2&envId=top-interview-150
Leetcode463 岛屿的周长
对于DFS直接返回有下面几种情况:
!inArea(grid, r, c)
,坐标超出网格范围。grid != 1
,即当前格子不是岛屿格子,分为如下两种情况:grid[r][c] == 0
,当前格子是海洋格子grid[r][c] == 2
,当前格子是已经遍历的陆地格子
岛屿的周长是计算岛屿全部的边缘,而这些边缘就再DFS函数返回的位置。如下,黄色的边是与网格边界相邻的周长,蓝色的边是与海洋格子相邻的周长。
当因坐标超出网格范围就返回一条黄色的边,当因海洋格子返回的时候就返回一条蓝色的边。
public int islandPerimeter(int[][] grid) {
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == 1) {
// 题目限制只有一个岛屿,计算一个即可
return dfs(grid, r, c);
}
}
}
return 0;
}
int dfs(int[][] grid, int r, int c) {
// 函数因为「坐标 (r, c) 超出网格范围」返回,对应一条黄色的边
if (!inArea(grid, r, c)) {
return 1;
}
// 函数因为「当前格子是海洋格子」返回,对应一条蓝色的边
if (grid[r][c] == 0) {
return 1;
}
// 函数因为「当前格子是已遍历的陆地格子」返回,和周长没关系
if (grid[r][c] != 1) {
return 0;
}
grid[r][c] = 2;
return 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;
}
Leetcode695 岛屿的最大面积
找出给定二维数组中最大的岛屿面积。如果没有岛屿,则返回面积0
本题只需要对每个岛屿做DFS遍历,求出每个岛屿的面积。求面积即每遍历一个陆地格子就把面积+1;
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;
}
Leetcode130 被围绕的区域
本题说明了被包围的区域不会存在于边界上,边界上的O要做特殊处理,那么剩下的O直接替换为X即可。问题转化为:如何寻找和边界联通的O
X X X X
X O O X
X X O X
X O O X
这种情况下O是不做替换的,因为和边界是连通的。可以把这种情况下的O换成#作为占位符,待搜索结束后,遇到O则替换为X,遇到#则替换为O。
如何寻找和边界联通的O?从边界出发,对网格进行DFS或者BFS即可。
class Solution {
public void solve(char[][] board) {
if (board == null || board.length == 0) return;
int m = board.length;
int n = board[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 从边缘o开始搜索
boolean isEdge = i == 0 || j == 0 || i == m - 1 || j == n - 1;
if (isEdge && board[i][j] == 'O') {
dfs(board, i, j);
}
}
}
// 搜索完毕后对字符进行替换
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O') {
board[i][j] = 'X';
}
if (board[i][j] == '#') {
board[i][j] = 'O';
}
}
}
}
public void dfs(char[][] board, int i, int j) {
if (i < 0 || j < 0 || i >= board.length || j >= board[0].length || board[i][j] == 'X' || board[i][j] == '#') {
// board[i][j] == '#' 说明已经搜索过了.
return;
}
board[i][j] = '#';
dfs(board, i - 1, j); // 上
dfs(board, i + 1, j); // 下
dfs(board, i, j - 1); // 左
dfs(board, i, j + 1); // 右
}
}
Leetcode133 克隆图
遍历整个图,在遍历的时候,记录已经访问过的点,使用一个字典记录。
深度遍历
class Solution{
public Node cloneGraph(Node node){
// map记录已经遍历过的节点
Map<Node, Node> lookup = new HashMap<>();
return dfs(node, lookup);
}
private Node dfs(Node node, Map<Node, Node> lookup){
if(node == null){
return null;
}
if(lookup.containsKey(node)){
// 已经遍历过该节点
return lookup.get(node);
}
Node clone = new Node(node.val, new ArrayList<>());
lookup.put(node, clone);
for(Node n : node.neighbors){
clone.neighbors.add(dfs(n, lookup));
}
return clone;
}
}
Leetcode399 除法求值
有向图搜索问题,本质上求两个节点之间的距离。
- 首先定义邻接节点,里面有两个字段,分别表示邻接节点的名称和当前节点到达邻接节点所需要的倍数。
- 然后构造一个map来存储图,map的键就是节点名称,map的值就是节点的邻接节点列表
- 遍历给定所有算式,将节点和值都存到map中
- 遍历需要求的问题,深搜每个节点,为了防止重复搜索,用一个集合存储已经搜索过的节点
public class Solution {
// 构造一个map用于存储图
static Map<String, List<Node>> map;
public static double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries){
int n = equations.size();
map = new HashMap<>();
// 存储查询结果
double[] res = new double[queries.size()];
// 构建图
for (int i = 0; i < n; i++) {
// 获取被除数和除数的节点名称
String divided = equations.get(i).get(0);
String divisor = equations.get(i).get(1);
// 如果map中还没有创建某个节点字符串的键值对,则添加一个键值对
if(!map.containsKey(divided)){
map.put(divided, new ArrayList<>());
}
if(!map.containsKey(divisor)){
map.put(divisor, new ArrayList<>());
}
// 创建的图为有向图
// 除数和被除数的倍数关系为被除数和除数倍数的倒数
map.get(divided).add(new Node(divisor, values[i]));
map.get(divisor).add(new Node(divided, 1 / values[i]));
}
int cnt = 0;
// 遍历
for(List<String> q : queries){
// dfs,初始倍数为1
res[cnt] = dfs(q.get(0), q.get(1), 1.0, new HashSet<>());
cnt++;
}
return res;
}
// cur表示当前节点,dst表示目的节点, times表示计算的倍数,set存储已经访问过的节点
public static double dfs(String cur, String dst, double times, Set<String> set){
// 如果map不包括当前节点或者已经走过当前节点,说明这条路径不会产生答案
if(!map.containsKey(cur) || set.contains(cur)){
return -1.0;
}
// 走到了终点,返回计算过的倍数
if(cur.equals(dst)){
return times;
}
// 在set中添加当前访问的节点
set.add(cur);
// 遍历当前节点的邻接节点
for(Node node: map.get(cur)){
// dfs,倍数需要乘上下一个节点的倍数
double tmp = dfs(node.id, dst, times * node.num, set);
// 搜到结果,直接返回
if(tmp != -1.0){
return tmp;
}
}
return -1.0;
}
}
class Node{
// 邻接节点代表的字符串
public String id;
// 到达邻接节点所需的倍数
public double num;
public Node(String i, double n){
id = i;
num = n;
}
}
Leetcode207 课程表
本题可简化为课程安排图是否是有向无环图(DAG),即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件不能成立。
通过拓扑排序判断课程安排图是否是有向无环图。
**拓扑排序:**对DAG的顶点进行排序,使得对每一条有向边(u,v)
,均有u比v先出现。
同故课程前置条件列表prerequisites
可以得到课程安排图的 邻接表 adjacency
,以降低算法时间复杂度,以下两种方法都会用到邻接表。
**深度优先遍历:**通过DFS判断图中是否有环。
- 借助一个标志列表
flags
,用来判断每个节点(课程)i
的状态:- 未被DFS访问:
i==0
- 已经被其它节点启动的DFS访问:
i==-1
- 已经被当前节点启动的DFS访问:
i==1
- 未被DFS访问:
- 对
numCourses
个节点依次执行DFS,判断每个节点起步DFS是否存在环,如果存在直接返回False;DFS过程如下:- 终止条件
- 当
flag[i] == -1
,说明当前访问节点已经被其它节点启动的DFS访问,无需重复搜索,直接返回True - 当
flag[i] == 1
,说明在本轮DFS搜索中节点i
已经被二次访问,课程安排图有环,直接返回false
- 当
- 将当前访问节点
i
对应的flag[i]
置为1,即标记其被本轮DFS访问过。 - 递归访问当前节点
i
的所有邻接节点j
,当发现环直接返回false - 当前节点所有邻接节点已经被遍历,并没有发现环,则将当前节点
flag
置为-1并返回true
- 终止条件
- 若整个图的DFS都没有发现环,返回True
public class A0507canFinish {
public static boolean canFinish(int numCourses, int[][] prerequisites){
// 创建邻接表,表示图
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
adj.add(new ArrayList<>());
}
int[] flags = new int[numCourses];
for(int[] p: prerequisites){
adj.get(p[1]).add(p[0]);
}
for(int i = 0; i < numCourses; i++){
if(!dfs(adj, flags, i)){
return false;
}
}
return true;
}
public static boolean dfs(List<List<Integer>> adj, int[] flags, int i){
if(flags[i] == 1){
// 当前DFS已经第二次访问
return false;
}
if(flags[i] == -1){
// 之前节点的DFS访问过
return true;
}
// 标记访问过当前节点
flags[i] = 1;
// 遍历当前节点的邻接点
for(Integer j: adj.get(i)){
if(!dfs(adj, flags, j)){
return false;
}
}
// 记录当前节点被遍历过
flags[i] = -1;
return true;
}
}
Leetcode210 课程表Ⅱ
深度优先搜索
在Leetcode207 课程表
的基础上,添加一个栈用来保存遍历的节点。
class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites){
// 建立邻接表
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
adj.add(new ArrayList<>());
}
int[] flags = new int[numCourses];
for(int[] p: prerequisites){
adj.get(p[1]).add(p[0]);
}
// 用栈保存访问序列
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < numCourses; i++) {
if(!dfs(adj, flags, stack, i)){
return new int[0];
}
}
int[] res = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
res[i] = stack.pop();
}
return res;
}
public boolean dfs(List<List<Integer>> adj, int[] flags, Stack<Integer> stack, int i){
if(flags[i] == 1){
// 本轮DFS已经访问过,形成环
return false;
}
if(flags[i] == -1){
// 前面节点的DFS已经访问过
return true;
}
// 修改标志位
flags[i] = 1;
for (int j: adj.get(i)) {
// dfs当前课程的后续课程
if(!dfs(adj, flags, stack, j)){
return false;
}
}
// 修改标记位
flags[i] = -1;
stack.push(i);
return true;
}
}
r> stack, int i){
if(flags[i] == 1){
// 本轮DFS已经访问过,形成环
return false;
}
if(flags[i] == -1){
// 前面节点的DFS已经访问过
return true;
}
// 修改标志位
flags[i] = 1;
for (int j: adj.get(i)) {
// dfs当前课程的后续课程
if(!dfs(adj, flags, stack, j)){
return false;
}
}
// 修改标记位
flags[i] = -1;
stack.push(i);
return true;
}
}