目录
理论基础
详细的去看离散数学,以下快速回忆概念
有向图、无向图、加权图
入度、出度
强连通图、连通分量
邻接矩阵(二维矩阵)、邻接表(数组 + 链表)
深搜&广搜
深搜:二叉树递归遍历、回溯等等,原理通用
广搜:二叉树层序遍历,一般用队列,原理通用
depth = 0 # 记录遍历到第几层
while queue 非空:
depth++
n = queue 中的元素个数
循环 n 次:
node = queue.pop()
for node 的所有相邻结点 m:
if m 未访问过:
queue.push(m)
并查:两个元素在不在同一个集合
拓扑:一系列依赖关系,有向图转成线性的排序
最小生成树(p和k)、最短路径(d)hot100没有涉及
200.岛屿
给你一个由 '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'
class Solution {
public int numIslands(char[][] grid) {
int count = 0;
for(int i = 0; i < grid.length; i++) { // i: 当前行的索引
for(int j = 0; j < grid[0].length; j++) { // j: 当前列的索引
if(grid[i][j] == '1'){// 如果当前位置是 '1',说明找到了一个新的岛屿
dfs(grid, i, j);// 使用DFS将整个岛屿标记为访问过
count++;
}
}
}
return count;
}
//DFS
private void dfs(char[][] grid, int i, int j) {
// 检查边界条件,防止越界,以及是否遇到 '0'(水域),如果是,直接返回
if(i < 0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == '0') return;
grid[i][j] = '0';// 将当前格子标记为 '0',表示已访问
dfs(grid, i + 1, j); // 向下
dfs(grid, i, j + 1); // 向右
dfs(grid, i - 1, j); // 向上
dfs(grid, i, j - 1); // 向左
}
// BFS
private void bfs(char[][] grid, int i, int j){
Queue<int[]> list = new LinkedList<>(); // 使用队列存储需要访问的格子的坐标
list.add(new int[] { i, j });
while(!list.isEmpty()){
int[] cur = list.remove(); // 取出队列头部的元素,表示当前格子的坐标
i = cur[0]; j = cur[1]; // 获取当前格子的行和列索引
// 检查边界条件,防止越界,以及是否遇到 '0'(水域),如果是,则跳过
if(0 <= i && i < grid.length && 0 <= j && j < grid[0].length && grid[i][j] == '1') {
grid[i][j] = '0';// 将当前格子标记为 '0',表示已访问
list.add(new int[] { i + 1, j }); // 向下
list.add(new int[] { i - 1, j }); // 向上
list.add(new int[] { i, j + 1 }); // 向右
list.add(new int[] { i, j - 1 }); // 向左
}
}
}
}
994.烂橘子
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 10
grid[i][j] 仅为 0、1 或 2
为什么用广搜,因为相邻的
class Solution {
// DIRECTIONS: 四个方向的坐标偏移数组,分别表示上、下、左、右
private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int orangesRotting(int[][] grid) {
int m = grid.length; // 行
int n = grid[0].length; // 列
int fresh = 0; // 记录新鲜橘子的数量
List<int[]> q = new ArrayList<>(); // q: 用于存储当前腐烂橘子的队列
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) fresh++; // 统计新鲜橘子的数量
else if (grid[i][j] == 2) q.add(new int[]{i, j}); // 将腐烂橘子的坐标加入队列
}
}
int ans = -1; // ans: 记录经过的分钟数,初始化为-1,因为每轮开始时都会加1
while (!q.isEmpty()) {
ans++; // 每次进入新一轮,经过一分钟
List<int[]> tmp = q; // tmp: 暂存当前轮次腐烂的橘子位置
q = new ArrayList<>(); // 开始新一轮传播,初始化新的队列
for (int[] pos : tmp) { // 遍历当前轮次所有腐烂的橘子
for (int[] d : DIRECTIONS) { // 遍历四个方向
int i = pos[0] + d[0]; // 新的行坐标
int j = pos[1] + d[1]; // 新的列坐标
// 如果相邻位置在网格范围内且是新鲜橘子
if (0 <= i && i < m && 0 <= j && j < n && grid[i][j] == 1) {
fresh--; // 新鲜橘子减少
grid[i][j] = 2; // 将新鲜橘子变成腐烂橘子
q.add(new int[]{i, j}); // 将新腐烂的橘子加入队列
}
}
}
}
// 如果还有新鲜橘子剩余,则返回-1,否则返回经过的分钟数
return fresh > 0 ? -1 : Math.max(ans, 0);
}
}
207.课程表
你这个学期必须选修 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] 中的所有课程对 互不相同
想要学习课程 0
,你需要先完成课程 1 ->马上想到拓扑
通过拓扑排序判断此课程安排图是否是有向无环图
//BFS入度表
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] indegrees = new int[numCourses]; // indegrees: 每门课程的入度,表示有多少前置课程
List<List<Integer>> adjacency = new ArrayList<>(); // adjacency: 邻接表,用来存储每门课程的后继课程列表
Queue<Integer> queue = new LinkedList<>();// queue: 用于BFS的队列,存储入度为0的课程
// 初始化邻接表,每个课程对应一个空的列表
for(int i = 0; i < numCourses; i++)
adjacency.add(new ArrayList<>());
// 遍历先决条件数组,构建入度数组和邻接表
for(int[] cp : prerequisites) {
indegrees[cp[0]]++; // cp[0] 这门课的入度加1,表示它有一个前置课程
adjacency.get(cp[1]).add(cp[0]); // 在邻接表中,把 cp[1] 指向 cp[0],表示 cp[0] 是 cp[1] 的后继课程
}
// 找到所有入度为0的课程,加入队列
for(int i = 0; i < numCourses; i++)
if(indegrees[i] == 0) queue.add(i);
// BFS拓扑排序
while(!queue.isEmpty()) {
int pre = queue.poll(); // 取出一个入度为0的课程
numCourses--; // 已完成的课程数,减少总的课程数量
// 遍历这门课程的所有后继课程
for(int cur : adjacency.get(pre)) {
indegrees[cur]--; // 当前课程的入度减1,表示前置课程减少
if(indegrees[cur] == 0) queue.add(cur); // 如果入度减为0,加入队列
}
}
// 如果所有课程都被完成(即 numCourses == 0),返回true,否则返回false
return numCourses == 0;
}
}
//DFS 有环图
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> adjacency = new ArrayList<>();// adjacency: 邻接表,用来存储每门课程的后继课程列表(依赖关系)
// 初始化邻接表,每门课程对应一个空的列表
for(int i = 0; i < numCourses; i++)
adjacency.add(new ArrayList<>());
// flags: 标记数组,用来记录每门课程的访问状态
// 0 表示未被访问,1 表示当前路径上正在访问,-1 表示该课程及其后继课程已访问完毕且无环
int[] flags = new int[numCourses];
// 构建邻接表,表示课程之间的依赖关系
// prerequisites[i] = [a, b] 表示必须先完成课程 b 才能学习课程 a
for(int[] cp : prerequisites)
adjacency.get(cp[1]).add(cp[0]);
// 对每门课程进行DFS遍历,检测是否存在环
for(int i = 0; i < numCourses; i++)
if(!dfs(adjacency, flags, i)) return false; // 如果某门课有环,返回 false
return true; // 如果所有课程都能完成,返回 true
}
// dfs: 深度优先搜索检测是否存在环
private boolean dfs(List<List<Integer>> adjacency, int[] flags, int i) {
// 如果该课程正在访问(即已进入当前递归路径),说明存在环,返回 false
if(flags[i] == 1) return false;
// 如果该课程已经访问过且无环,直接返回 true
if(flags[i] == -1) return true;
// 标记该课程正在访问
flags[i] = 1;
// 遍历当前课程的所有后继课程
for(Integer j : adjacency.get(i))
if(!dfs(adjacency, flags, j)) return false; // 如果后继课程存在环,返回 false
// 该课程及其所有后继课程已访问完毕且无环,标记为 -1
flags[i] = -1;
return true;
}
}
208.前缀树
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 次
class Trie {
private Node root; // 根节点,Trie的起始点
public Trie() {
root = new Node(); // 初始化根节点
}
// 插入一个单词到Trie中
public void insert(String word) {
Node node = root; // 从根节点开始构造这个word对应的路径节点
int n = word.length(); // 获取单词的长度
for(int i = 0; i < n; i++){
// 将当前字符添加到当前节点对应的子节点位置,然后递归更新
int id = word.charAt(i) - 'a'; // 计算当前字符相对于'a'的索引
if(node.children[id] == null){ // 如果该字符的子节点不存在
node.children[id] = new Node(); // 创建新的子节点
}
node = node.children[id]; // 移动到当前字符对应的子节点
}
node.isEnd = true; // 最后一个节点的isEnd置为true,表示一个完整的字符串
}
// 查找是否存在完整的单词
public boolean search(String word) {
Node node = searchPrefix(word); // 查找单词的前缀
return node != null && node.isEnd; // 返回不为空且节点标记为尾节点,则包含word这个完整的字符串
}
// 查找是否存在以指定前缀开头的单词
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null; // 返回不为空,则包含了prefix前缀
}
// 查找字典树是否包含word前缀
private Node searchPrefix(String word){
Node node = root; // 从根节点依次开始匹配每个字符
int n = word.length(); // 获取前缀的长度
for(int i = 0; i < n; i++){
// 根据当前字符获取对应的子节点
node = node.children[word.charAt(i) - 'a'];
if(node == null){ // 如果当前节点为空,则不包含这个字符串,直接返回空指针
return null;
}
}
return node; // 否则匹配成功返回当前节点
}
}
//字典树节点
class Node{
Node[] children; // 子节点列表,存储每个字符的子节点
boolean isEnd; // 标记是否为尾节点,表示一个完整单词的结束
public Node(){
children = new Node[26]; // 初始化26个字母的子节点
isEnd = false; // 初始时,节点不是尾节点
}
}