200. 岛屿数量
695. 岛屿的最大面积
注意深度优先遍历,对一格陆地(=='1')遍历, 就会把与它连通的所有陆地遍历到,全部标记为2, 完成一个岛屿。从而这一次标记到的作为一个岛屿数量 +1
for (int i=0; i<grid.length;i++) {
for (int j=0;j<grid[0].length;j++) {
// 深度优先遍历,对一格陆地(=='1')遍历,
// 就会把与它连通的所有陆地遍历到,全部标记为2。从而这一次标记到的作为一个岛屿数量 +1
if (grid[i][j] == '1') {
dfs(grid, i, j);
islandNum++;
}
}
}
岛屿数量
class Solution { public int numIslands(char[][] grid) { int islandNum = 0; for (int i=0; i<grid.length;i++) { for (int j=0;j<grid[0].length;j++) { // 深度优先遍历,对一格陆地(=='1')遍历, // 就会把与它连通的所有陆地遍历到,全部标记为2。从而这一次标记到的作为一个岛屿数量 +1 if (grid[i][j] == '1') { dfs(grid, i, j); islandNum++; } } } return islandNum; } private void dfs(char[][] grid, int x, int y) { if (!isInGrid(grid, x, y)) { return; } if (grid[x][y] != '1') { return; } // 访问过的标记为2,不再重复访问 grid[x][y] = '2'; // 对这个格子的上下左右分别dfs // 上 dfs(grid, x, y+1); // 下 dfs(grid, x, y-1); // 左 dfs(grid, x-1, y); // 右 dfs(grid, x+1, y); } // 判断这个格子是否已经超出了 grid 的范围 private boolean isInGrid(char[][] grid, int x, int y) { // 注意是 >= 0 不是 >0 return x >= 0 && x < grid.length && y >= 0 && y < grid[0].length; } }
岛屿的最大面积
class Solution { public int maxAreaOfIsland(int[][] grid) { int maxAreaOfIsland = 0; for (int i=0;i<grid.length;i++) { for (int j=0;j<grid[0].length;j++) { // 每次 dfs 完的都是一整块岛屿 if (grid[i][j] == 1) { int thisIslandArea = dfs(grid, i, j); maxAreaOfIsland = Math.max(maxAreaOfIsland, thisIslandArea); } } } return maxAreaOfIsland; } private int dfs(int[][] grid, int x, int y) { if (!isInGrid(grid, x, y)) { return 0; } if (grid[x][y] != 1) { return 0; } grid[x][y] = 2; // 自己1 + 上下左右 return 1 + dfs(grid, x, y+1) + dfs(grid, x, y-1) + dfs(grid, x-1, y) + dfs(grid, x+1, y); } private boolean isInGrid(int[][] grid, int x, int y) { return x>=0 && x<grid.length && y>=0 && y<grid[0].length; } }
207. 课程表
DFS 解法
关于 dfs,请看这个题解里的说明图
1、怎么样的有向图可以有一个拓扑排序?即怎样的有向图可以学完所有课程?
- 没有环的有向图都可以有一个拓扑排序
- 只要有环,就没法得到一个拓扑排序(因为环里一定存在相互依赖的两个课程,A 需要先学完 B 才能学,B 又需要先学完 A 才能学)
2、怎么表示图?
- 用 List<List<Integer>> graph 表示,index 是起始节点的课程的标志
- graph.get(index) 这个 List 就是这个起始课程可以指向的课程
- 用 prerequisites 初始化这个 graph ,[ai, bi] 学习 ai 必须要先学课程 bi,即有一条从 bi (prerequisites[i][1]) 指向 ai (prerequisites[i][0]) 的有向边
3、visited 访问数组有什么用?visited = new int[numCourses];
- visited = [0] ,这个课程节点还未被访问过,可以进行 dfs 访问
- visited = [1] ,dfs 刚进来时会置为 1。表示正在被本轮 dfs 访问,如果在一轮 dfs 中访问到了 visited 为 1 的,说明它在一轮中被访问了两次,说明图里存在环,将结果置为 false ,并返回
- visited = [2] ,dfs 末尾时会置为 2(题解的说明图里为-1)。本轮 dfs 访问完了置为 2,之后的 dfs 访问到,就表示它已经被别的轮次的 dfs 访问过,不必再重复访问。
4、整个过程是怎么样的?
先初始化好邻接表
从 0 开始遍历 numCourses(条件要 && res,res 一有 false 就可以停了),当 visited = 0 时进行 dfs,表示对以每个没访问过的课程节点为起点,开始进行 dfs
在 dfs 里面:
-
- 一进来,先将 visited 置为 1
- 对每个分支进行 dfs,在这里即为对当前节点邻接表里的所有边进行 dfs
- 如果 visited = 0 ,没被访问过,进行 dfs
- 如果 visited = 1,表示有环,结果置为 false 并返回
- 如果 visited = 2,表示已经被别的轮次的 dfs 访问过了,不必再访问,什么都不用做
- dfs 结束,将 visited 置为 2 ,本轮次已经结束。
class Solution { // 邻接表 List<List<Integer>> graph; // 表示是否访问过? int[] visited; boolean res; public boolean canFinish(int numCourses, int[][] prerequisites) { graph = new ArrayList(); visited = new int[numCourses]; // 只有这个有向图存在环,就不存在拓扑排序(因为有相互依赖的) // 只有这个有向图没有环,就存在拓扑排序 // 所以默认为 true,只在判断存在环的地方改为 false res = true; // 初始化邻接表 for(int i=0;i<numCourses;i++) { visited[i] = 0; List<Integer> courseList = new ArrayList(); graph.add(courseList); } for(int[] link: prerequisites) { // [ai, bi] 学习 ai 必须要先学课程 bi // 即有一条从 bi (prerequisites[i][1])指向 ai (prerequisites[i][0]) 的有向边 graph.get(link[1]).add(link[0]); } // 只要有一个环,结果就为 false,不用再继续遍历了。所以要加上条件 && res // 以所有课程为起点,进行 dfs for(int i=0;i<numCourses && res;i++) { if (visited[i] == 0) { dfs(i); } } return res; } private void dfs(int nowCourse) { // 被当前轮次的 dfs 访问这个节点 visited[nowCourse] = 1; List<Integer> nextCourseList = graph.get(nowCourse); // 对于一个节点所有的分支进行 dfs,这里是对以这个节点为起点的所有有向边进行 dfs for (int i=0;i<nextCourseList.size();i++) { int nextCourse = nextCourseList.get(i); // 访问那些没有访问过的 if (visited[nextCourse] == 0) { dfs(nextCourse); } else if (visited[nextCourse] == 1) { // 这个节点被当前轮次的 dfs 访问了两次,存在环,直接返回 // 只有这个有向图存在环,就不存在拓扑排序(因为有相互依赖的) // 只有这个有向图没有环,就存在拓扑排序 res = false; return; } // 对于 visited[i] == 2,被别的轮次的 dfs 访问过的,不必重复访问 } // 只有一次DFS完整结束了,才能执行到这一步,这个 dfs 轮次已经结束。改为 2表示被别的轮次的 dfs 访问过了 visited[nowCourse] = 2; } }
另一种写法,与之前 dfs 的风格更一致
class Solution { // 邻接表 List<List<Integer>> graph; // 表示是否访问过? int[] visited; boolean res; public boolean canFinish(int numCourses, int[][] prerequisites) { graph = new ArrayList(); visited = new int[numCourses]; // 只有这个有向图存在环,就不存在拓扑排序(因为有相互依赖的) // 只有这个有向图没有环,就存在拓扑排序 // 所以默认为 true,只在判断存在环的地方改为 false res = true; // 初始化邻接表 for(int i=0;i<numCourses;i++) { visited[i] = 0; List<Integer> courseList = new ArrayList(); graph.add(courseList); } for(int[] link: prerequisites) { // [ai, bi] 学习 ai 必须要先学课程 bi // 即有一条从 bi (prerequisites[i][1])指向 ai (prerequisites[i][0]) 的有向边 graph.get(link[1]).add(link[0]); } // 只要有一个环,结果就为 false,不用再继续遍历了。所以要加上条件 && res // 以所有课程为起点,进行 dfs for(int i=0;i<numCourses && res;i++) { if (visited[i] == 0) { dfs(i); } } return res; } private void dfs(int nowCourse) { if (visited[nowCourse] == 1) { // 这个节点被当前轮次的 dfs 访问了两次,存在环,直接返回 // 只有这个有向图存在环,就不存在拓扑排序(因为有相互依赖的) // 只有这个有向图没有环,就存在拓扑排序 res = false; return; } else if (visited[nowCourse] == 2) { // 对于 visited[i] == 2,被别的轮次的 dfs 访问过的,不必重复访问 return; } // 被当前轮次的 dfs 访问这个节点 visited[nowCourse] = 1; List<Integer> nextCourseList = graph.get(nowCourse); // 对于一个节点所有的分支进行 dfs,这里是对以这个节点为起点的所有有向边进行 dfs for (int i=0;i<nextCourseList.size();i++) { int nextCourse = nextCourseList.get(i); dfs(nextCourse); } // 只有一次DFS完整结束了,才能执行到这一步,这个 dfs 轮次已经结束。改为 2表示被别的轮次的 dfs 访问过了 visited[nowCourse] = 2; } }
BFS 解法
关于 bfs,请看这个题解里的说明图
1、怎么样的有向图可以有一个拓扑排序?即怎样的有向图可以学完所有课程?
- 没有环的有向图都可以有一个拓扑排序
- 只要有环,就没法得到一个拓扑排序(因为环里一定存在相互依赖的两个课程,A 需要先学完 B 才能学,B 又需要先学完 A 才能学)
2、怎么表示图?
- 用 List<List<Integer>> graph 表示,index 是起始节点的课程的标志
- graph.get(index) 这个 List 就是这个起始课程可以指向的课程
- 用 prerequisites 初始化这个 graph ,[ai, bi] 学习 ai 必须要先学课程 bi,即有一条从 bi (prerequisites[i][1]) 指向 ai (prerequisites[i][0]) 的有向边
3、InDegree 入度数组有什么用?队列怎么用?
- 先初始化所有节点的入度,把入度为 0 的节点加入队列
- 每次从队列取出(poll)一个入度为 0 的节点,将其指向的所有节点(从邻接表获得)的入度 -1,这时要判断如果有节点入度减为 0 了,要将其入队
- 如果最后所有节点的入度都为 0,说明不存在环,可以有一个拓扑排序
- 如果最后有节点的入度不为 0,说明存在环,总有节点入度不为 0,不能有拓扑排序
- 要有一个节点总数的计数,每次有节点入度减到0,除了要将其加入队列,还要将这个计数减一。这样在最后通过这个数值是否为0判断是都存在环。
class Solution { // 邻接表 List<List<Integer>> graph;public boolean canFinish(int numCourses, int[][] prerequisites) { graph = new ArrayList(); int[] inDegree = new int[numCourses]; // 初始化邻接表 for(int i=0;i<numCourses;i++) { inDegree[i] = 0; List<Integer> courseList = new ArrayList(); graph.add(courseList); } for(int[] link: prerequisites) { // [ai, bi] 学习 ai 必须要先学课程 bi // 即有一条从 bi (prerequisites[i][1])指向 ai (prerequisites[i][0]) 的有向边 graph.get(link[1]).add(link[0]); // bi 的入度 +1 inDegree[link[0]] = ++inDegree[link[0]]; } Queue<Integer> queue = new LinkedList(); for (int i=0;i<inDegree.length;i++) { if (inDegree[i] == 0) { // 入度为 0 的节点入队 queue.offer(i); // 只要有入度为0的节点,就减一 numCourses--; } } while(!queue.isEmpty() && numCourses != 0) { // 队列里入度为0的节点出队 int indegree0Course = queue.poll(); List<Integer> nextCourseList = graph.get(indegree0Course); for (int nextCourse : nextCourseList) { // 对于入度为 0 的节点,将其指向的节点的入度 -1 inDegree[nextCourse] = --inDegree[nextCourse]; if (inDegree[nextCourse] == 0) { queue.offer(nextCourse); // 只要有入度为0的节点,就减一 numCourses--; } } } if (numCourses != 0) { // 最后还有入度不为 0 的节点,说明存在环,不能拓扑排序 return false; } else { return true; } }
}
994. 腐烂的橘子
用 BFS 类似于树的层序遍历
刚开始腐烂的橘子是第一层,这第一层可以同时发起感染,如果是新鲜橘子,就会被感染到。这次感染到的就是第二层,第二层的再同时发起感染,这次感染到的就是第三层.........
所以最后的结果就是看,层序遍历至少到第几层的时候,全部的新鲜橘子都被感染了。
代码过程如下:
- 先遍历一下grid(1)统计所有新鲜橘子的数量 goodOrangeCnt(2)再把初始时的所有腐烂橘子加入 queue ,作为层序遍历的第一层
- 开始层序遍历,注意要统计层数,所以每层的遍历要分开,外层 while 循环获取 queue.size ,然后再内层 for 循环 queue.size 次
- 在四个方向上,如果是新鲜的橘子,(1)就进行感染(值设为2),(2)同时新鲜橘子计数 goodOrangeCnt--,(3)再将这个新被感染的加入队列queue,作为下一层
- 最后如果新鲜橘子计数 goodOrangeCnt 大于0,说明有不能被感染的,返回 -1。否则就返回层数。
注意 while 循环的条件,还有一个goodOrangeCnt>0,也就是说如果橘子已经感染完了,但层序遍历还没完,也要退出循环
while (goodOrangeCnt>0 && !queue.isEmpty()) {
class Solution { private static int res = 0; Solution() { res = 0; } public int orangesRotting(int[][] grid) { int R = grid.length; int C = grid[0].length; int goodOrangeCnt = 0; Queue<int[]> queue = new LinkedList<>(); for (int r=0;r<R;r++) { for (int c=0;c<C;c++) { // 统计所有新鲜橘子的数量 if (grid[r][c]==1) { goodOrangeCnt++; } // 将初始时就腐烂的橘子加入队列。它们是层序遍历的第一层 if (grid[r][c]==2) { queue.add(new int[]{r,c}); } } } int layer = 0; // 注意条件还有一个goodOrangeCnt>0,也就是说如果橘子已经感染完了,但层序遍历还没完,也要退出循环 while (goodOrangeCnt>0 && !queue.isEmpty()) { // 为了分开层序遍历的每一层,应该在开始时获得这一层的数量 int curLayerSize = queue.size(); // 同一层的腐烂橘子应该是一起去感染的,所以最后的结果就是层数 layer++; // 然后每次循环是这一层的数量,把这一层搞完 for (int i=0;i<curLayerSize;i++) { // 从队列里取出腐烂的橘子 int[] rotOrange = queue.poll(); int rotOrangeR = rotOrange[0]; int rotOrangeC = rotOrange[1]; // 它四周的四个橘子,如果是新鲜的,就去感染它,并且把新鲜橘子计数-1 if (rotOrangeR-1>=0 && grid[rotOrangeR-1][rotOrangeC]==1) { goodOrangeCnt--; grid[rotOrangeR-1][rotOrangeC]=2; queue.add(new int[] {rotOrangeR-1, rotOrangeC}); } if (rotOrangeC-1>=0 && grid[rotOrangeR][rotOrangeC-1]==1) { goodOrangeCnt--; grid[rotOrangeR][rotOrangeC-1]=2; queue.add(new int[] {rotOrangeR, rotOrangeC-1}); } if (rotOrangeR+1<R && grid[rotOrangeR+1][rotOrangeC]==1) { goodOrangeCnt--; grid[rotOrangeR+1][rotOrangeC]=2; queue.add(new int[] {rotOrangeR+1, rotOrangeC}); } if (rotOrangeC+1<C && grid[rotOrangeR][rotOrangeC+1]==1) { goodOrangeCnt--; grid[rotOrangeR][rotOrangeC+1]=2; queue.add(new int[] {rotOrangeR, rotOrangeC+1}); } } } if (goodOrangeCnt>0) { return -1; } else { return layer; } } }
208. 实现 Trie (前缀树)
包含三个单词 "sea","sells","she" 的 Trie 会长啥样呢?
下标隐式的定义了对应的字符,同时对于末端的字符结束,要有一个 isEnd 来表示是否是一个字符的结束
根节点,就是 Trie root = this
char c = word.charAt(i);
int cindex = c - 'a';
root.children[cindex]
class Trie { private Trie[] children; private boolean isEnd; public Trie() { // 每个下标是否有 child 表示这个字符是否有 children = new Trie[26]; isEnd = false; } public void insert(String word) { // 从根节点开始,根节点 = this Trie root = this; for (int i=0;i<word.length();i++) { char c = word.charAt(i); int cindex = c - 'a'; // 往下找,没有的话就添加新节点 if (root.children[cindex] == null){ Trie newTrie = new Trie(); root.children[cindex] = newTrie; } root = root.children[cindex]; } // 无论最后的这个是新加的,还是以前就有的。isEnd 都要新设为 true root.isEnd = true; } public boolean search(String word) { Trie node = searchNode(word); // 有这样的节点,并且该节点是最后一个 return node != null && node.isEnd; } public boolean startsWith(String prefix) { return searchNode(prefix) != null; } public Trie searchNode(String prefix) { // 从根节点开始,根节点 = this Trie root = this; for (int i=0;i<prefix.length();i++) { char c = prefix.charAt(i); int cindex = c - 'a'; if (root.children[cindex] == null){ return null; } // 往下找 root = root.children[cindex]; } return root; } } /** * Your Trie object will be instantiated and called as such: * Trie obj = new Trie(); * obj.insert(word); * boolean param_2 = obj.search(word); * boolean param_3 = obj.startsWith(prefix); */
79. 单词搜索
进行 dfs
- dfs 的起点是什么?
- 棋盘上所有元素值为 word.charAt(0) 的元素
- 除了board和dfs到的当前元素i,j,还需要什么变量?
- 由于同一个单元格里的字母不允许被重复使用,所以需要一个 boolean[][] visited 访问数组,来标志一次dfs中,这个元素有没有被访问过(初始false,访问过了标为true)
- 需要一个变量k来表示前面已经完成了word从0到k-1的字母,现在需要寻找的是word.charAt(k)的字母
- 怎样判断这一步的dfs不是有效的?
- 超过了board边界
- 已经被访问过
- 不等于当前期望的字符 board[i][j] != word.charAt(k)
- 怎么判断当前dfs已经找到棋盘上等于word的路径?
- 当 k==word.length 时
class Solution { Boolean res = false; Solution() { res = false; } public boolean exist(char[][] board, String word) { // 从那些和 word 第一个字符一样的元素开始 dfs for(int i=0;i<board.length;i++) { for(int j=0;j<board[0].length;j++) { if(word.charAt(0) == board[i][j]) { dfs(board, getInitVisited(board), word, i, j, 0); // 每次 dfs 完都判断一下,如果已经是true了,可以提前退出 if (res) { break; } } } } return res; } private void dfs(char[][] board, boolean[][] visited, String word, int i, int j, int k) { // 不是有效的,直接返回 if (!isValid(board, visited, word, i, j, k)) { return; } // 到最后一个字符了,与word的相等, 就说明获得了结果,可以返回了 if (k == word.length()-1) { res = true; return; } visited[i][j] = true; dfs(board, visited, word, i-1, j, k+1); dfs(board, visited, word, i+1, j, k+1); dfs(board, visited, word, i, j-1, k+1); dfs(board, visited, word, i, j+1, k+1); // 回溯 visited[i][j] = false; } private boolean isValid(char[][] board, boolean[][] visited, String word,int i, int j, int k) { // 1.超出边界 if (i<0 || i>=board.length) { return false; } else if (j<0 || j>=board[0].length) { return false; } // 2.已经参观过 else if (visited[i][j]) { return false; } // 3.不是期望的那个字符 else if (word.charAt(k) != board[i][j]) { return false; } return true; } private boolean[][] getInitVisited(char[][] board) { boolean[][] visited = new boolean[board.length][board[0].length]; for (int i=0;i<board.length;i++) { for (int j=0;j<board[0].length;j++) { visited[i][j] = false; } } return visited; } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库