200. 岛屿数量 https://leetcode.cn/problems/number-of-islands/?envType=study-plan-v2&envId=top-100-liked 给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [ [‘1’,’1’,’1’,’1’,’0’], [‘1’,’1’,’0’,’1’,’0’], [‘1’,’1’,’0’,’0’,’0’], [‘0’,’0’,’0’,’0’,’0’] ] 输出:1 示例 2:
输入:grid = [ [‘1’,’1’,’0’,’0’,’0’], [‘1’,’1’,’0’,’0’,’0’], [‘0’,’0’,’1’,’0’,’0’], [‘0’,’0’,’0’,’1’,’1’] ] 输出:3
提示:
m == grid.length n == grid[i].length 1 <= m, n <= 300 grid[i][j] 的值为 ‘0’ 或 ‘1’
表格每个格子看作一个图的节点,记为(i,j),i,j分别是行列坐标
如果两个相邻的节点都是1,意味着这两个节点直接有一条边。
每当读取到一个新的未访问过的1,岛屿数目就加1,遍历和他相邻的所有节点,所有遍历到的节点都记为访问过(可以二维布尔数组,也可以直接原地改为-1)
整体上来说,每个节点都被访问一次。每次访问的各种操作都是O(1)
因此复杂度O(mn)
深度优先遍历
考虑深度优先遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution { public int numIslands (char [][] grid) { int result = 0 ; for (int i = 0 ; i < grid.length; i++) { for (int j = 0 ; j < grid[0 ].length; j++) { if (grid[i][j] == '1' ){ result++; dfs(i,j,grid); } } } return result; } private void dfs (int row, int col, char [][] grid) { if (grid[row][col] == '2' || grid[row][col] == '0' ){ return ; } grid[row][col] = '2' ; int [][] direction = new int [][]{{1 ,0 },{0 ,1 },{-1 ,0 },{0 ,-1 }}; for (int [] dir : direction) { int newRow = row + dir[0 ]; int newCol = col + dir[1 ]; if (newRow >= 0 && newRow < grid.length && newCol >= 0 && newCol<grid[0 ].length){ if (grid[newRow][newCol] == '1' ){ dfs(newRow,newCol,grid); } } } } }
代码逻辑可以优化如下,dfs把判断都放在开头,同时没必要引入’2’这个状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution { public int numIslands (char [][] grid) { if (grid == null || grid.length == 0 ) { return 0 ; } int result = 0 ; for (int i = 0 ; i < grid.length; i++) { for (int j = 0 ; j < grid[0 ].length; j++) { if (grid[i][j] == '1' ) { result++; dfs(i, j, grid); } } } return result; } private void dfs (int row, int col, char [][] grid) { if (row < 0 || row >= grid.length || col < 0 || col >= grid[0 ].length || grid[row][col] != '1' ) { return ; } grid[row][col] = '0' ; dfs(row + 1 , col, grid); dfs(row - 1 , col, grid); dfs(row, col + 1 , grid); dfs(row, col - 1 , grid); } }
时间O(mn),空间O(mn)(最大岛屿面积mn) 2. 广度优先
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import java.util.ArrayDeque;import java.util.Queue;class Solution { public int numIslands (char [][] grid) { int result = 0 ; for (int i = 0 ; i < grid.length; i++) { for (int j = 0 ; j < grid[0 ].length; j++) { if (grid[i][j] == '1' ){ result++; bfs(grid,i,j); } } } return result; } private class node { char value = '2' ; int row = -1 ; int col = -1 ; public node (char value,int row,int col) { this .value = value; this .col = col; this .row = row; } } private void bfs (char [][] grid, int row, int col) { Queue<node> queue = new ArrayDeque <>(); queue.offer(new node (grid[row][col],row,col) ); grid[row][col] = '0' ; int [][] direction = new int [][]{{1 ,0 },{0 ,1 },{-1 ,0 },{0 ,-1 }}; while (!queue.isEmpty()){ node node = queue.poll(); int rowThisLoop = node.row; int colThisLoop = node.col; for (int [] dir : direction) { int newRow = rowThisLoop + dir[0 ]; int newCol = colThisLoop + dir[1 ]; if (newRow >= 0 && newRow < grid.length && newCol >= 0 && newCol<grid[0 ].length){ if (grid[newRow][newCol] == '1' ) { queue.offer(new node (grid[newRow][newCol], newRow, newCol)); grid[newRow][newCol] = '0' ; } } } } } }
实际上上面可以不适用node类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import java.util.ArrayDeque;import java.util.Queue;class Solution { public int numIslands (char [][] grid) { if (grid == null || grid.length == 0 ) { return 0 ; } int result = 0 ; for (int i = 0 ; i < grid.length; i++) { for (int j = 0 ; j < grid[0 ].length; j++) { if (grid[i][j] == '1' ) { result++; bfs(grid, i, j); } } } return result; } private void bfs (char [][] grid, int row, int col) { Queue<int []> queue = new ArrayDeque <>(); queue.offer(new int []{row, col}); grid[row][col] = '0' ; int [][] direction = new int [][]{{1 , 0 }, {0 , 1 }, {-1 , 0 }, {0 , -1 }}; while (!queue.isEmpty()) { int [] current = queue.poll(); int rowThisLoop = current[0 ]; int colThisLoop = current[1 ]; for (int [] dir : direction) { int newRow = rowThisLoop + dir[0 ]; int newCol = colThisLoop + dir[1 ]; if (newRow >= 0 && newRow < grid.length && newCol >= 0 && newCol < grid[0 ].length) { if (grid[newRow][newCol] == '1' ) { queue.offer(new int []{newRow, newCol}); grid[newRow][newCol] = '0' ; } } } } } }
时间O(mn),空间O(min(m,n)).画图可得,队列queue是在不断扩散的(一个斜着的正方形),最大是2sqrt(ming(m,n)),因此最终空间复杂度是O(min(m,n))
994. 腐烂的橘子 https://leetcode.cn/problems/rotting-oranges/description/?envType=study-plan-v2&envId=top-100-liked 在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
值 0 代表空单元格; 值 1 代表新鲜橘子; 值 2 代表腐烂的橘子。 每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
示例 1:
输入:grid = [[2,1,1],[1,1,0],[0,1,1]] 输出:4 示例 2:
输入:grid = [[2,1,1],[0,1,1],[1,0,1]] 输出:-1 解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。 示例 3:
输入:grid = [[0,2]] 输出:0 解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。
提示:
m == grid.length n == grid[i].length 1 <= m, n <= 10 grid[i][j] 仅为 0、1 或 2
把每个格子看成一个节点
先遍历,找出所有为2的节点
之后,对于每个为2的节点,只要相邻的是1都认为他们是邻节点,进行第一次腐化,minute加一
把新腐化的节点都放入一个列表。
之后再次腐化循环
直到所有1都被腐化或者没有和2相邻的1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import java.util.ArrayDeque;import java.util.ArrayList;import java.util.List;import java.util.Queue;class Solution { public int orangesRotting (int [][] grid) { Queue<int []> rottedOrangesToProcess = new ArrayDeque <>(); int loop = 0 ; for (int i = 0 ; i < grid.length; i++) { for (int j = 0 ; j < grid[0 ].length; j++) { if (grid[i][j] == 2 ) { rottedOrangesToProcess.offer(new int []{i, j}); } } } while (!rottedOrangesToProcess.isEmpty()) { int size = rottedOrangesToProcess.size(); for (int i = 0 ; i < size; i++) { int [] place = rottedOrangesToProcess.poll(); List<int []> rottedPlace = rot(grid, place[0 ], place[1 ]); for (int [] newPlace : rottedPlace) { rottedOrangesToProcess.offer(newPlace); } } loop++; } boolean hasOne = false ; boolean hasTwo = false ; boolean hasZero = false ; for (int i = 0 ; i < grid.length; i++) { for (int j = 0 ; j < grid[0 ].length; j++) { if (grid[i][j] == 1 ) { hasOne = true ; } if (grid[i][j] == 2 ){ hasTwo = true ; } if (grid[i][j] == 0 ){ hasZero = true ; } } } if (hasOne){ return -1 ; } if (hasZero && !hasTwo && !hasOne){ return 0 ; } return loop - 1 ; } private List<int []> rot(int [][] grid, int row, int col) { List<int []> rottedPlace = new ArrayList <>(); int [][] direction = new int [][]{{1 , 0 }, {0 , 1 }, {-1 , 0 }, {0 , -1 }}; for (int [] dir : direction) { int newRow = row + dir[0 ]; int newCol = col + dir[1 ]; if (newRow >= 0 && newRow < grid.length && newCol >= 0 && newCol < grid[0 ].length) { if (grid[newRow][newCol] == 1 ) { grid[newRow][newCol] = 2 ; rottedPlace.add(new int []{newRow, newCol}); } } } return rottedPlace; } }
时间复杂度:O(n^2),遍历寻找2花费O(n^2),腐化过程最多访问O(n^2)个节点,每个节点的访问和其他操作都是O(1),最后检查结果也是O(n^2),最终O(n^2)
空间复杂度:主要是队列,这个队列最差O(m*n)。比如如下情况
1 2 3 4 5 2 1 2 1 2 1 ... 1 2 1 2 1 2 ... 2 1 2 1 2 1 ... 1 2 1 2 1 2 ... ...
只有在只有一个起点的时候,复杂度才是O(min(m,n)).
实际上上面的解法是个多源起点的BFS。
上面的代码可以优化。可以寻找2的时候记录1和0的个数。之后每次腐化一个节点1的个数就减少rottedPlace.size()。最终1,0的数据获取不必再次遍历。但是复杂度不变。
同时Loop的返回可以优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import java.util.ArrayDeque;import java.util.Queue;class Solution { public int orangesRotting (int [][] grid) { int m = grid.length; int n = grid[0 ].length; Queue<int []> queue = new ArrayDeque <>(); int freshCount = 0 ; for (int i = 0 ; i < m; i++) { for (int j = 0 ; j < n; j++) { if (grid[i][j] == 1 ) { freshCount++; } else if (grid[i][j] == 2 ) { queue.offer(new int []{i, j}); } } } if (freshCount == 0 ) { return 0 ; } int minutes = 0 ; int [][] directions = {{1 , 0 }, {-1 , 0 }, {0 , 1 }, {0 , -1 }}; while (!queue.isEmpty()) { int size = queue.size(); boolean rottedInThisMinute = false ; for (int i = 0 ; i < size; i++) { int [] current = queue.poll(); for (int [] dir : directions) { int newRow = current[0 ] + dir[0 ]; int newCol = current[1 ] + dir[1 ]; if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && grid[newRow][newCol] == 1 ) { grid[newRow][newCol] = 2 ; freshCount--; queue.offer(new int []{newRow, newCol}); rottedInThisMinute = true ; } } } if (rottedInThisMinute) { minutes++; } } return freshCount <= 0 ? minutes : -1 ; } }
207. 课程表 https://leetcode.cn/problems/course-schedule/description/?envType=study-plan-v2&envId=top-100-liked 你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]] 输出:true 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。 示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]] 输出:false 解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
提示:
1 <= numCourses <= 2000 0 <= prerequisites.length <= 5000 prerequisites[i].length == 2 0 <= ai, bi < numCourses prerequisites[i] 中的所有课程对 互不相同
每个课看成一个图得节点
需要整个图不能存在一个环
prerequisites就是边
这个图还是个有向图。
prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi
因此bi必然在ai前面
因此bi -> ai
似乎可以使用邻接矩阵先创造一个图,之后判断是否有环?还是邻接表?
事实上邻接表和邻接矩阵都能表示有向图和无向图
邻接表:每个节点对应的表对应的就是该节点指向的所有节点。如果无向则[i,j][j,i]都添加
邻接矩阵:[i,j]的值表示i指向j有边
邻接表空间复杂度:O(V + E)(O(V+E1+……En) = (V+E)),
邻接矩阵:O(V^2)
E对于无向图最多是C(V,2) = (v)(v-2)/2,有向图A(V,2) = V(V-1).
深度优先:
先创建邻接表,之后深度优先搜索判断是否有环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;class Solution { public boolean canFinish (int numCourses, int [][] prerequisites) { List<List<Integer>> adjacency = new ArrayList <>(); for (int i = 0 ; i < numCourses; i++) { adjacency.add(new ArrayList <>()); } for (int [] edge : prerequisites) { adjacency.get(edge[1 ]).add(edge[0 ]); } int [] visited = new int [numCourses]; for (int i = 0 ; i < numCourses; i++) { if (visited[i] == 0 ) { if (hasCycle(i, adjacency, visited)) { return false ; } } } return true ; } private boolean hasCycle (int course, List<List<Integer>> adjacency, int [] visited) { if (visited[course] == 1 ) { return true ; } if (visited[course] == 2 ) { return false ; } visited[course] = 1 ; for (int nextCourse : adjacency.get(course)) { if (hasCycle(nextCourse, adjacency, visited)) { return true ; } } visited[course] = 2 ; return false ; } }
时间复杂度: O(V+E)。对于每个节点和每个边都会访问一次,每次访问都进行了O(1)操作(访问相邻,标记等) 空间复杂度: O(V+E)
广度优先搜索
先回顾度的概念:度就是相邻节点个数。入度就是有向图中指向该节点的个数,出度反之
构造入度数组,入度为0的意味着没有先修课,可以学习,直接加入一个队列
只要队列不为空,学过的课程数目count++,并且把这个课程的所有后续课程的入度减一,如果有某个课程入度成为0把它入队
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import java.util.ArrayDeque;import java.util.ArrayList;import java.util.List;import java.util.Queue;class Solution { public boolean canFinish (int numCourses, int [][] prerequisites) { int [] inDegree = new int [numCourses]; List<List<Integer>> ad = new ArrayList <>(); for (int i = 0 ; i < numCourses; i++) { ad.add(new ArrayList <>()); } for (int [] edge : prerequisites){ ad.get(edge[1 ]).add(edge[0 ]); inDegree[edge[0 ]]++; } Queue<Integer> queue = new ArrayDeque <>(); for (int i = 0 ; i < inDegree.length; i++) { if (inDegree[i] == 0 ) { queue.offer(i); } } int count = 0 ; while (!queue.isEmpty()) { int course = queue.poll(); count++; for (int nextCourse : ad.get(course)) { inDegree[nextCourse]--; if (inDegree[nextCourse] == 0 ){ queue.offer(nextCourse); } } } return count == numCourses; } }
时间复杂度:每个顶点和边都要访问一次,每个边和顶点都是O(1)操作,因此时间复杂度O((V+E))
空间:邻接表O(V+E),队列O(V),因此O(V+E)
208. 实现 Trie (前缀树) https://leetcode.cn/problems/implement-trie-prefix-tree/description/?envType=study-plan-v2&envId=top-100-liked Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。 void insert(String word) 向前缀树中插入字符串 word 。 boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。 boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
示例:
输入 [“Trie”, “insert”, “search”, “search”, “startsWith”, “insert”, “search”] [[], [“apple”], [“apple”], [“app”], [“app”], [“app”], [“app”]] 输出 [null, null, true, false, true, null, true]
解释 Trie trie = new Trie(); trie.insert(“apple”); trie.search(“apple”); // 返回 True trie.search(“app”); // 返回 False trie.startsWith(“app”); // 返回 True trie.insert(“app”); trie.search(“app”); // 返回 True
提示:
1 <= word.length, prefix.length <= 2000 word 和 prefix 仅由小写英文字母组成 insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次
出现在图这个章节,应该要用图
使用哈希表可以方便的查询插入
这个树实际上如下这个样子:
上面实际上就是一个有向无环图
每个节点都是一个字母,并且有一个结束标志。
从根节点开始到每个结束表示,路径组成的对应的单词
具体实现可以使用邻接表
通常的邻接表包含两个部分:所有节点的集合+每个节点的邻接点集合
但这里不需要。
这个特殊的图,我们保证从root能访问到所有其他节点,同时我们总是从root开始操作,因此只需要给每个节点构造一个邻接点的集合即可
具体的节点实现:
因为我们要可能频繁增删查改,所以邻接点的集合使用哈希表
一个布尔遍历表示是否是结束标志
注意这里根节点root实际上代表的是空字符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 import java.util.HashMap;import java.util.Map;class Trie { private class TrieNode { Map<Character,TrieNode> map; boolean isAWOrdEnd = false ; public TrieNode () { this .map = new HashMap <>(); } } private TrieNode root; public Trie () { this .root = new TrieNode (); } public void insert (String word) { int cur = 0 ; TrieNode curNode = root; while (cur < word.length()){ char curChar = word.charAt(cur); if (!curNode.map.containsKey(curChar)){ curNode.map.put(curChar,new TrieNode ()); } cur++; curNode = curNode.map.get(curChar); } curNode.isAWOrdEnd = true ; } public boolean search (String word) { int cur = 0 ; TrieNode curNode = root; while (cur < word.length()){ if (!curNode.map.containsKey(word.charAt(cur))){ return false ; }else { curNode = curNode.map.get(word.charAt(cur)); cur++; } } return curNode.isAWOrdEnd; } public boolean startsWith (String prefix) { int cur = 0 ; TrieNode curNode = root; while (cur < prefix.length()){ if (!curNode.map.containsKey(prefix.charAt(cur))){ return false ; }else { curNode = curNode.map.get(prefix.charAt(cur)); cur++; } } return true ; } }
时空复杂度:
insert,时间O(l)
search,时间O(L)
startsWith,时间O(l)
空间:O(S),其中 S 是所有插入单词的字符总数。在最坏情况下(所有单词没有公共前缀)
关于通配符回顾一下:
PECS 。
PECS = Producer Extends, Consumer Super
Producer Extends (? extends T) : 如果你的泛型结构是一个生产者 (它只往外提供、产出数据),那么使用 extends。
Consumer Super (? super T) : 如果你的泛型结构是一个消费者 (它只往里获取、消费数据),那么使用 super。
1. ? extends V (Producer Extends)
含义 : “V 或者 V 的某个子类 ”。(extends常常表示继承,反正就是子类的意思,小于等于)
行为 : 你可以把它想象成一个只读的容器。
你能做什么 : 你可以安全地从这个结构中取出 数据,因为无论取出的具体是哪个子类,它至少 是一个 V 类型的对象。
你不能做什么 : 你不能 安全地向这个结构中存入 数据(除了 null)。为什么?因为你不知道这个容器的确切类型。如果它是一个 List<? extends Fruit>,它可能是 List<Apple>,也可能是 List<Orange>。你不能把一个 Orange 存入一个 List<Apple> 中。编译器为了保证类型安全,干脆禁止了所有存入操作。
一句话总结 ? extends V : 我不知道具体是什么,但我知道它上限是 V 。我只能从中读取 (Produce),不能写入 。
2. ? super K (Consumer Super)
含义 : “K 或者 K 的某个父类 (一直到 Object)”。(super常常表示调用父类,大于等于)
行为 : 你可以把它想象成一个只写的容器。
你能做什么 : 你可以安全地向这个结构中存入 K 类型的对象(以及 K 的子类对象)。为什么?因为无论这个容器的确切类型是 List<K>,还是 List<SomeParentOfK>,还是 List<Object>,它都能安全地容纳一个 K 类型的对象。
你不能做什么 : 你不能 安全地从中取出 特定类型的数据(除了 Object)。为什么?因为你不知道里面存的到底是什么。如果它是一个 List<? super Apple>,它可能是 List<Fruit>,里面可能存着 Orange。你取出一个元素,只能保证它是一个 Object,但不能保证它是一个 Apple。
一句话总结 ? super K : 我不知道具体是什么,但我知道它下限是 K 。我只能向其中写入 (Consume),不能保证读取 到特定类型。