刷题总结
数据结构
一、Stack
1.1 Java API
Deque stack = new ArrayDeque<>(); // 或者是LinkedList<>()
// @deprecated
Stack stack = new Stack();
stack.push();
stack.pop();
1.2 经典例题分析
给定一个整数数组 asteroids,表示在同一行的行星。
对于数组中的每一个元素,其绝对值表示行星的大小,正负表示行星的移动方向(正表示向右移动,负表示向左移动)。每一颗行星以相同的速度移动。
找出碰撞后剩下的所有行星。碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。
示例 1:
输入:asteroids = [5,10,-5]
输出:[5,10]
解释:10 和 -5 碰撞后只剩下 10 。 5 和 10 永远不会发生碰撞。
示例 2:
输入:asteroids = [8,-8]
输出:[]
解释:8 和 -8 碰撞后,两者都发生爆炸。
示例 3:
输入:asteroids = [10,2,-5]
输出:[10]
解释:2 和 -5 发生碰撞后剩下 -5 。10 和 -5 发生碰撞后剩下 10 。
示例 4:
输入:asteroids = [-2,-1,1,2]
输出:[-2,-1,1,2]
解释:-2 和 -1 向左移动,而 1 和 2 向右移动。 由于移动方向相同的行星不会发生碰撞,所以最终没有行星发生碰撞。
🔽 通过题目分析,可以遍历asteroids
数组,然后用stack数据结构记录结果
- 入栈情况分析
stack.isEmpty()
当前栈空stack.peek()
是负数,无论当前遍历到的asteroids[i]
为正还是为负,都可以入栈asteroids[i]
是正数,无论stack.peek()
为正还是为负,都可以入栈
- 碰撞场景分析,此场景建立在
!stack.isEmpty()
、stack.peek() >= 0
、asteroides[i] <= 0
基础上- 如果栈顶元素
stack.peek()
大于abs(asteroides[i])
,当前元素被销毁 - 如果栈顶元素
stack.peek()
等于abs(asteroides[i])
,栈顶弹出并和当前元素抵消 - 如果栈顶元素
stack.peek()
小于abs(asteroides[i])
,栈顶弹出,并循环执行碰撞分析场景
- 如果栈顶元素
1.3 练习题目
算法
一、二分查找
1.1 算法模板
1.1.1 基础二分
public int basicBinarySearch(int[] nums, int target) {
int start = 0;
int end = nums.length - 1;
while (start <= end) {
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
start = mid + 1;
} else if (nums[mid] > target) {
end = mid - 1;
}
}
return -1;
}
1.1.2 下边界
第一个等于target
public int lowerbound(int[] nums, int target) {
int start = 0;
int end = nums.length - 1;
while (start <= end) {
if (nums[mid] == target) {
end = mid -1;
} else if (nums[mid] < target) {
start = mid + 1;
} else if (nums[mid] > target) {
end = mid - 1;
}
}
if (start < nums.length && nums[index] == target) {
return start;
}
return -1;
}
第一个大于target
public int lowerbound(int[] nums, int target) {
int start = 0;
int end = nums.length - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
start = mid + 1;
} else if (nums[mid] < target) {
start = mid + 1;
} else if (nums[mid] > target) {
end = mid - 1;
}
}
return start >= nums.length ? -1 : start;
}
第一个大于等于target
public int lowerbound(int[] nums, int target) {
int start = 0;
int end = nums.length - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
end = mid - 1;
} else if (nums[mid] < target) {
start = mid + 1;
} else if (nums[mid] > target) {
end = mid - 1;
}
}
return start >= nums.length ? -1 : start;
}
1.1.3 上边界
最后一个等于target
public int upperbound(int[] nums, int target) {
int start = 0;
int end = nums.length - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
start = mid + 1;
} else if (nums[mid] < target) {
start = mid + 1;
} elset if (nums[mid] > target) {
end = mid - 1;
}
}
if (end >= 0 && nums[end] == target) {
return end;
}
return -1;
}
最后一个小于target
public int upperbound(int[] nums, int target) {
int start = 0;
int end = nums.length - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
end = mid - 1;
} else if (nums[mid] < target) {
start = mid + 1;
} else if (nums[mid] > target) {
end = mid - 1;
}
}
return end < 0 ? -1 : end;
}
最后一个小于等于target
public int upperbound(int[] nums, int target) {
int start = 0;
int end = nums.length - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
end = mid - 1;
} else if (nums[mid] < target) {
start = mid + 1;
} else if (nums[mid] > target) {
end = mid - 1;
}
}
return end < 0 ? -1 : end;
}
1.2 练习题目
二、单调栈
2.1 算法模板 O(n)
基础题目:输入一个数组
nums =[2, 1, 2, 4, 3]
,你需要返回数组[4, 2, 4, -1, -1]
解释:find nextGreaterElement,找下一个比当前数字大的数。
// 倒序遍历
private int[] nextGreaterElement(int[] nums) {
int[] res = new int[nums.length];
Deque<Integer> stack = new ArrayDeque<>();
for (int i = nums.length - 1; i >= 0; i--) {
// step1 维护一个单调栈
// 由于我们求的是nextGreaterElement,因此当前元素大于等于stack.peek()时,需要出栈
while (!stack.isEmpty() && nums[i] >= stack.peek()) {
stack.pop();
}
// step2 保存结果
res[i] = stack.isEmpty() ? -1 : stack.peek();
// step3 当前元素入栈
stack.push(nums[i]);
}
return res;
}
// 正序遍历
private int[] nextGreaterElement(int[] nums) {
int[] res = new int[nums.length];
Arrays.fill(res, -1);
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < nums.length; i++) {
// step1 维护一个单调栈
// 由于我们求的是nextGreaterElement,因此当前元素大于等于stack.peek()时,需要出栈
while (!stack.isEmpty() && nums[i] >= stack.peek()) {
res[i] = stack.peek();
stack.pop();
}
// step3 当前元素入栈
stack.push(i);
}
return res;
}
2.2 练习题目
三、扫描线
3.1 练习题目
四、BFS
BFS问题的本质是:在一幅「图」中找到从起点
start
到终点target
的最近距离,BFS空间复杂度高,所有的BFS都可以转化为DFS
问题变形:
- 走迷宫,有的格子是围墙不能走,从起点到终点的最短距离是多少?
- 两个单词,通过某些替换,把其中一个变成另一个,每次只能替换一个字符,最少要替换几次?
- 连连看游戏,消除的条件不仅仅是图案相同,还得保证两个方块之间的最短连线不能多于两个拐点
4.1 算法模板
队列queue是BFS的核心数据结构;cur.adj() 泛指cur相邻的节点;visited反正走回头路
public int bfs(Node start, Node target) {
Deque<Node> queue = new ArrayDeque<>();
Set<Node> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
int step = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
Node cur = queue.poll();
if (cur.val == target.val) {
return step;
}
for (Node node : cur.adj()) {
if (!visited.contains(node)) {
queue.offer(node);
visited.add(node);
}
}
}
step++;
}
return step;
}
4.2 练习题目
五、递归
递归三要素
- 递归函数的参数和返回值
- 终止条件
- 单层递归的逻辑
5.1 返回值为空的递归
5.2.1 练习题目
5.2 返回值不为空的递归
5.2.2 练习题目
六、回溯
回溯算法是系统地搜索问题的解的纯暴力方法,某个问题的所有可能解的称为问题的解空间,若解空间是有限的,则可将解空间映射成树结构。
6.1 概念讲解
🔽 回溯法是求问题的解,使用的是DFS(深度优先搜索)。
在DFS的过程中发现不是问题的解,那么就开始回溯到上一层或者上一个节点。
DFS是遍历整个搜索空间,而不管是否是问题的解。所以更觉得回溯法是DFS的一种应用,DFS更像是一种工具
任何可以映射成树结构问题的解空间,都可以使用回溯法。很多问题,暴力法没有办法解决,例如
- 排列问题:N个数⾥⾯按⼀定规则找出k个数的集合
- 组合问题:N个数⾥⾯按⼀定规则找出k个数的集合
- 切割问题:⼀个字符串按⼀定规则有⼏种切割⽅式
- 子集问题:⼀个N个数的集合⾥有多少符合条件的⼦集
- 棋盘问题:N皇后,解数独等等
⚠️ 回溯法解决的问题都可以抽象为树形结构!集合的大小为树的宽度、递归的深度为树的深度。
6.2 经典例题分析
6.2.1 组合问题
❓ 组合问题:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。77. 组合
-
分析问题:抽象树形结构
- 通过分析可知,第一步取1后,下一步需要在2、3、4中再依次取1个数字,当取到2个数字时,收集
- 怎么控制上述中,取完1之后,再分别从2、3、4中依次再取数字呢?当取到2个元素时,怎么收集呢?
-
上述分析,感觉可以用递归,下面确定递归函数的返回值和参数
// 全局变量,也可以作为递归函数的参数 // 存放符合条件结果的集合 List<List<Integer>> res = new ArrayList<>(); // 存放符合条件的结果 Deque<Integer> path = new ArrayDeque<>();
递归函数的参数肯定需要n、k,因为时从集合n里面取k个数。此外,还需要一个参数startIndex,为什么?
因为在集合[1,2,3,4]取1之后,下⼀层递归,就要在[2,3,4]中取数了,靠的就是startIndex,如图中红色的部分。
函数的返回值为
void
,因此确定递归函数的形式,如下:private void dfs(int n, int k, int startIndex);
-
怎么判断什么时候收集元素呢?当path数组的大小达到k,说明我们找到了一个满足条件的组合,path就是根节点到叶子节点的路径
// 终止条件代码 if (path.size() == k) { result.add(path); return; }
-
单层搜索逻辑
// 遍历可能的搜索起点 for (int i = begin; i <= n; i++) { // 向路径变量里添加一个数 path.offer(i); // 下一轮搜索,设置的搜索起点要加1,因为组合数理不允许出现重复的元素 dfs(n, k, i + 1); // 深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作 path.pollLast(); }
-
为什么要执行
path.pollLast()
操作?例如在
path.offer()
执行了取1、取2的操作后,结果为[1,2],搜集完成后,后续的操作需要执行取3,只能path.pollLast()
pop出元素2,不然会追加。
private List<List<Integer>> result = new ArrayList<>();
private Deque<Integer> path = new ArrayDeque<>();
public List<List<Integer>> combine(int n, int k) {
if (k <= 0 || n < k) {
return res;
}
Deque<Integer> path = new ArrayDeque<>();
dfs(n, k, 1);
return res;
}
private void dfs(int n, int k, int begin, ) {
// 递归终止条件是:path 的长度等于 k
if (path.size() == k) {
res.add(new ArrayList<>(path));
return;
}
// 遍历可能的搜索起点
for (int i = begin; i <= n; i++) {
// 向路径变量里添加一个数
path.offer(i);
// 下一轮搜索,设置的搜索起点要加 1,因为组合数理不允许出现重复的元素
dfs(n, k, i + 1);
// 深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作
path.pollLast();
}
}
-
剪枝
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){}
❓ 排列问题:给定⼀个 没有重复数字的序列,返回其所有可能的全排列。 46. 全排列
6.3 练习题目
-
组合问题
-
分割问题
-
子集问题
-
排列问题
-
其他
七、DFS
7.1 经典例题分析
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。
该路径 至少包含一个 节点,且不一定经过根节点。路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
🔽 解答此题之前,我们先看另外一道题,从根节点出发,到叶子节点的最大路径和?
private int process(TreeNode root) {
if (root == null) {
return 0;
}
int left = process(root.left);
int right = process(root.right);
return Math.max(left, right) + root.val;
}
上述代码的功能是从根节点到叶子节点的最大路径和,但是此题中,不要求从根节点到叶子节点。因此有两点不同
-
返回给上层节点的信息要保证贡献度(和)最大,如果当前层的贡献度为负,则返回0,代码修改如下:
return Math.max(0, Math.max(maxLeft, maxRigt) + root.val);
-
最大路径和不一定经过根节点,例如示例2中,最大的路径和为
15->20->7
,此时,每遍历一层都要更新全局的最大和结果,整体代码如下:private int globle = Integer.MIN_VALUE; private int process(TreeNode root) { if (root == null) { return 0; } int left = process(root.left); int right = process(root.right); globle = Math.max(globle, left + right + root.val); return Math.max(0, Math.max(left, right) + root.val); }
在给定的二维二进制数组 A 中,存在两座岛。(岛是由四面相连的 1 形成的一个最大组。)
现在,我们可以将 0 变为 1,以使两座岛连接起来,变成一座岛。
返回必须翻转的 0 的最小数目。(可以保证答案至少是 1 。)
示例 1:
输入:A = [[0,1],[1,0]]
输出:1
示例 2:
输入:A = [[0,1,0],[0,0,0],[0,0,1]]
输出:2
示例 3:
输入:A = [[1,1,1,1,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,0,0,1],[1,1,1,1,1]]
输出:1
🔽 解答此题之前,我们先看另外一道题 岛屿数量
给你一个由 '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
🔽 题目分析:显然要用DFS算法,标记访问过的点
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int count = 0;
for (int i = 0; i < nr; i++) {
for (int j = 0; j < nc; j++) {
if (grid[i][j] == '1') {
++count;
process(grid, i, j, nr, nc);
}
}
}
return count;
}
private void process(char[][] grid, int i, int j, int nr, int nc) {
if (i >= nr || i < 0 || j >= nc || j < 0 || grid[i][j] == '0') {
return;
}
// 此处必须标记访问为'0'
grid[i][j] = '0';
process(grid, i - 1, j, nr, nc);
process(grid, i + 1, j, nr, nc);
process(grid, i, j - 1, nr, nc);
process(grid, i ,j + 1, nr, nc);
}
🔽 现在我们回到 934. 最短的桥
public static int shortestBridge(Main main, int[][] A) {
int[][] direction = new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
Deque<int[]> queue = new ArrayDeque<>();
int ans = -1;
boolean[][] visited = new boolean[A.length][A[0].length];
// flag防止2座岛屿都被标记
boolean flag = true;
for (int i = 0; i < A.length && flag; i++) {
for (int j = 0; j < A[0].length; j++) {
if (A[i][j] == 1) {
main.dfs(A, i, j, queue, visited);
flag = false;
break;
}
}
}
while (!queue.isEmpty()) {
int size = queue.size();
ans++;
for (int i = 0; i < size; i++) {
int[] node = queue.poll();
// 上下左右 四个方向BFS,按层BFS,每一个点的所有上下左右四个方向的点作为遍历的第二层
for (int j = 0; j < 4; j++) {
int nx = node[0] + direction[j][0];
int ny = node[1] + direction[j][1];
if (nx < 0 || nx >= A.length || ny < 0 || ny >= A[0].length || visited[nx][ny]) continue;
// 只要有一层中点遍历访问到了另一座岛屿,返回
if (A[nx][ny] == 1) return ans;
// 标记防止重新访问
visited[nx][ny] = true;
// 添加下一层节点到队列
queue.add(new int[]{nx, ny});
}
}
}
return ans;
}
// dfs标记一座岛屿
private void dfs(int[][] A, int i, int j, Deque queue, boolean[][] visited) {
if (i < 0 || i >= A.length || j < 0 || j >= A[0].length || visited[i][j] || A[i][j] != 1) return;
// 标记访问过的点
visited[i][j] = true;
// 比较的同时,讲岛屿坐标放到队列中
queue.add(new int[]{i, j});
dfs(A, i - 1, j, queue, visited);
dfs(A, i + 1, j, queue, visited);
dfs(A, i, j - 1, queue, visited);
dfs(A, i, j + 1, queue, visited);
}
7.2 练习题目
7.2.1 二叉树
八、滑动窗口
8.1 基本题型
- Easy,size fixed
- Midian,size可变,单条件限制
- Median,size可变,双条件限制(模板)
- Hard,size fixed 单条件限制
8.2 模板
// 本质仍然是two pointer, 左边是left, 右边是iterator(i)
public int lengthOfLongestSubstringKDistinct(String s, int k) {
Map<Character, Integer> map = new HashMap<>();
int left = 0;
int res =0;
for (int i = 0; i < s.length(); i++) {
char cur = s.charAt(i);
map.put(cur, map.getOrDefault(cur, 0) + 1); // step1: 进 当前遍历的i进入窗口
while (map.size() > k) { // step2:出 当窗口不符合条件时,left持续退出窗口
char c = s.charAt(left);
map.put(c, map.get(c) - 1);
if (map.get(c) == 0) {
map.remove(c);
}
left++; // 出的时候移动左pointer
}
res = Math.max(res, i - left + 1); // step2:窗口valid,计算结果
}
return res;
}
8.3 经典例题分析
public String minWindow(String s, String t) {
// key: character value: fluent
Map<Character, Integer> window = new HashMap<>();
Map<Character, Integer> need = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
char[] charArrayS = s.toCharArray();
int start = 0;
int end = 0;
// 标记窗口大小是否满足要求
int distance = 0;
// 结果截取范围
int left = 0;
int minLen = Integer.MAX_VALUE;
while (end < s.length()) {
char c = charArrayS[end];
if (!need.containsKey(c)) {
end++;
continue;
}
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c).equals(need.get(c))) {
++distance;
}
end++;
// 左边窗口收缩
while (distance == need.size()) {
if (end - start < minLen) {
minLen = end - start;
left = start;
}
char d = charArrayS[start];
if (!need.containsKey(d)) {
start++;
continue;
}
start++;
if (window.get(d).equals(need.get(d))) {
--distance;
}
window.put(d, window.getOrDefault(d, 0) - 1);
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(left, left + minLen);
}