leetcode刷题笔记java版,持续更新中....20220327
- leetcode热题 HOT 100
- 题目分类
- [3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/)
- [5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/)
- [23. 合并K个升序链表](https://leetcode-cn.com/problems/merge-k-sorted-lists/)
- [42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/)
- 207. 课程表
- 221. 最大正方形
- 226. 翻转二叉树
- 236. 二叉树最近的公共祖先
- 238. 除自身以外数组的乘积
- 239. 滑动窗口最大值
- 240. 搜索二维矩阵II
- 279. 完全平方数
- 283.移动零
- 297. 二叉树的序列化与反序列化
- 300. 最长递增子序列
- 309.买卖股票的最佳时机含冷冻期
- 322.零钱兑换
- 337. 打家劫舍III
- 338. 比特位计数
- [347. 前 K 个高频元素](https://leetcode-cn.com/problems/top-k-frequent-elements/)
- [394. 字符串解码](https://leetcode-cn.com/problems/decode-string/)
- [406. 根据身高重建队列](https://leetcode-cn.com/problems/queue-reconstruction-by-height/)
- [416. 分割等和子集](https://leetcode-cn.com/problems/partition-equal-subset-sum/)
- [437. 路径总和 III](https://leetcode-cn.com/problems/path-sum-iii/)
- [438. 找到字符串中所有字母异位词](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/)
- [538. 把二叉搜索树转换为累加树](https://leetcode-cn.com/problems/convert-bst-to-greater-tree/)
- [647. 回文子串](https://leetcode-cn.com/problems/palindromic-substrings/)
- 高频知识点
- 二叉树专题
- [94. 二叉树的中序遍历](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/)
- [145. 二叉树的后序遍历](https://leetcode-cn.com/problems/binary-tree-postorder-traversal/)
- [100. 相同的树](https://leetcode-cn.com/problems/same-tree/)
- [226. 翻转二叉树](https://leetcode-cn.com/problems/invert-binary-tree/)
- [590. N 叉树的后序遍历](https://leetcode-cn.com/problems/n-ary-tree-postorder-traversal/)
- [103. 二叉树的锯齿形层序遍历](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/)
- [124. 二叉树中的最大路径和](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/)
- 背包问题
- 并查集问题
- 字节后端
leetcode热题 HOT 100
题目分类
分类 | 题号 |
---|---|
深度搜索 | 207、297、437 |
广度搜索 | 207、399 |
拓扑排序 | 210 |
二叉树 | 226、236、538 |
动态规划 | 5、221、279、300、309、322、337、338、416、 647 |
前缀和 | 238、437 |
滑动窗口 | 239、438、3 |
最大最小堆 | 239、347 |
搜索 | 240 |
二分查找 | 240 |
双指针 | 283、42 |
递归 | 23、394 |
Manacher 算法 | 5、647 |
图 | 399 |
并查集 | 399 |
排序 | 406 |
背包问题 | 279、322、416 |
单调栈 | 42 |
3. 无重复字符的最长子串
【方法1滑动窗口】
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) {
return 0;
}
int start = 0;
int max = 1;
Set<Character> set = new HashSet<>();
set.add(s.charAt(0));
for (int i = 1; i < s.length(); i++) {
while (start <= i && set.contains(s.charAt(i))) {
set.remove(s.charAt(start));
start++;
}
set.add(s.charAt(i));
max = Math.max(max, i - start + 1);
}
return max;
}
}
5. 最长回文子串
【方法1】中心扩展法
class Solution {
public String longestPalindrome(String s) {
int max = 0;
int start = 0;
int end = start;
for (int i = 0; i <= 2 * (s.length() - 1); i++) {
int left = i / 2;
int right = (i % 2 == 0) ? left : left + 1;
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
if (right - left + 1 > max) {
start = left;
end = right;
max = right - left + 1;
}
left--;
right++;
}
}
return s.substring(start, end + 1);
}
}
【方法2】manacher算法
class Solution {
public String longestPalindrome(String s) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
stringBuilder.append("#").append(s.charAt(i));
}
s = stringBuilder.append("#").toString();
int start = 0;
int end = 0;
int[] f = new int[s.length()];
// 标记历史最右端力臂所到位置
int right = -1;
// 对称中心 j
int j = -1;
for (int i = 0; i < s.length(); i++) {
// 当前i的力臂长度
int cur_len;
if (right >= i) {
// i关于j的对称点k
int k = j - (i - j);
// 根据历史f[k],得到最小长度
int min_len = Math.min(f[k], right - i);
// 从最小长度往外继续扩展
cur_len = expand(s, i - min_len, i + min_len);
} else {
cur_len = expand(s, i, i);
}
// 记录当前i的力臂信息
f[i] = cur_len;
if (i + cur_len > right) {
j = i;
right = i + cur_len;
}
if (cur_len * 2 + 1 > end - start) {
start = i - cur_len;
end = i + cur_len;
}
}
StringBuilder ans = new StringBuilder();
for (int i = start; i <= end; i++) {
if (s.charAt(i) != '#'){
ans.append(s.charAt(i));
}
}
return ans.toString();
}
private int expand(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return (right - left - 2) / 2;
}
}
23. 合并K个升序链表
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
Queue<ListNode> queue = new LinkedList<>(Arrays.asList(lists));
while (!queue.isEmpty() && queue.size() > 1) {
int size = queue.size();
for (int i = 0; i < size; i += 2) {
queue.offer(mergeList(queue.poll(), queue.poll()));
}
}
System.out.println(queue.size());
return queue.poll();
}
public ListNode mergeList(ListNode pHead, ListNode qHead) {
ListNode head = new ListNode();
ListNode node = head;
while (pHead != null && qHead != null) {
if (pHead.val > qHead.val) {
node.next = qHead;
qHead = qHead.next;
} else {
node.next = pHead;
pHead = pHead.next;
}
node = node.next;
}
if (pHead != null) {
node.next = pHead;
} else {
node.next = qHead;
}
return head.next;
}
}
42. 接雨水
【方法1 双指针】
class Solution {
public int trap(int[] height) {
int left = 0;
int right = height.length - 1;
int leftMax = 0;
int rightMax = 0;
int sum = 0;
while (left <= right) {
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
if (height[left] > height[right]) {
sum += rightMax - height[right];
right--;
} else {
sum += leftMax - height[left];
left++;
}
}
return sum;
}
}
【方法2 单调栈】
class Solution {
public int trap(int[] height) {
Deque<Integer> stack = new LinkedList<>();
int sum = 0;
for (int i = 0; i < height.length; i++) {
while (!stack.isEmpty() && height[stack.peek()] <= height[i]) {
int min = stack.pop();
if (stack.isEmpty()) {
break;
}
int curHeight = Math.min(height[stack.peek()], height[i]) - height[min];
int curWidtd = i - stack.peek() - 1;
sum += curHeight * curWidtd;
}
stack.push(i);
}
return sum;
}
}
207. 课程表
【方法1】拓扑排序、深度搜索
class Solution {
private List<List<Integer>> edges;
// 0-未搜索,1-搜索中,2-已完成
private int[] visited;
// 图中是否存在环,即此题是否有解
private boolean valid = true;
private List<List<Integer>> buildGraph(int nodes, int[][] relationship) {
List<List<Integer>> edges = new ArrayList<>();
for (int i = 0; i < nodes; i++) {
edges.add(new ArrayList<>());
}
for (int[] a : relationship) {
edges.get(a[1]).add(a[0]);
}
return edges;
}
private void dfs(int u) {
// 搜索u时现将u标记为搜索中
visited[u] = 1;
// 遍历所有与u相连的节点
for (int v : edges.get(u)) {
if (visited[v] == 0) {
// v节点未搜索时,则深度搜索v
dfs(v);
// 如果无效则直接返回
if (!valid) {
return;
}
} else if (visited[v] == 1) {
// v节点搜索中时,图中存在环,无效
valid = false;
return;
}
}
// 对节点u完成搜索,标记为已完成
visited[u] = 2;
}
public boolean canFinish(int numCourses, int[][] prerequisites) {
edges = buildGraph(numCourses, prerequisites);
visited = new int[numCourses];
// 遍历节点,如果无效直接退出
for (int i = 0; i < numCourses & valid; i++) {
if (visited[i] == 0) {
// 节点未搜索,则深度搜索此节点
dfs(i);
}
}
return valid;
}
}
【方法2】拓扑排序、广度搜索
class Solution {
private List<List<Integer>> edges;
private int[] inDeg;
private List<List<Integer>> buildGraph(int nodes, int[][] relationship) {
List<List<Integer>> edges = new ArrayList<>();
for (int i = 0; i < nodes; i++) {
edges.add(new ArrayList<>());
}
for (int[] a : relationship) {
edges.get(a[1]).add(a[0]);
++inDeg[a[0]];
}
return edges;
}
public boolean canFinish(int numCourses, int[][] prerequisites) {
inDeg = new int[numCourses];
edges = buildGraph(numCourses, prerequisites);
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (inDeg[i] == 0) {
queue.offer(i);
}
}
List<Integer> stack = new ArrayList<>();
while (!queue.isEmpty()) {
int u = queue.poll();
stack.add(u);
for (int v : edges.get(u)) {
--inDeg[v];
if (inDeg[v] == 0) {
queue.offer(v);
}
}
}
return stack.size() == numCourses;
}
}
221. 最大正方形
【方法1】动态规划
状态转移方程:
定义dp[i][j]为以(i, j) 为有下角的正方形的最大边长,有:
d
p
[
i
]
[
j
]
=
{
m
a
t
r
i
x
[
i
]
[
j
]
i
=
0
或
j
=
0
0
m
a
t
r
i
x
[
i
]
[
j
]
=
0
min
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
,
d
p
[
i
−
1
]
[
j
−
1
]
)
+
1
其
他
dp[i][j] = \begin{cases} matrix[i][j] & i = 0 或 j = 0 \\ 0 & matrix[i][j] = 0 \\ \min(dp[i-1][j], dp[i][j - 1], dp[i-1][j-1]) +1 & 其他 \end{cases}
dp[i][j]=⎩⎪⎨⎪⎧matrix[i][j]0min(dp[i−1][j],dp[i][j−1],dp[i−1][j−1])+1i=0或j=0matrix[i][j]=0其他
class Solution {
public int maximalSquare(char[][] matrix) {
int[][] side = new int[matrix.length][matrix[0].length];
int maxSide = 0;
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
if (i == 0 || j == 0) {
side[i][j] = matrix[i][j] - '0';
} else {
if (matrix[i][j] == '0') {
side[i][j] = 0;
} else {
side[i][j] = Math.min(Math.min(side[i - 1][j], side[i][j - 1]), side[i - 1][j - 1]) + 1;
}
}
maxSide = Math.max(maxSide, side[i][j]);
}
}
return maxSide * maxSide;
}
}
226. 翻转二叉树
【方法1】使用队列按层遍历二叉树
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode node = queue.peek();
TreeNode tmp = node.left;
node.left = node.right;
node.right = tmp;
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
queue.poll();
}
return root;
}
}
【方法2】递归遍历二叉树
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
}
}
236. 二叉树最近的公共祖先
【方法1】递归
递归条件:定义
f
x
f_x
fx表示
x
x
x节点的子树中是否包含 p 节点或 q节点,则公共祖先一定满足
(
f
l
s
o
n
&
&
f
r
s
o
n
)
∣
∣
(
(
x
=
=
p
∣
∣
x
=
=
q
)
&
&
(
f
l
s
o
n
∣
∣
f
r
s
o
n
)
)
(f_{lson} \&\& f_{rson})||((x==p||x==q)\&\&(f_{lson}||f_{rson}))
(flson&&frson)∣∣((x==p∣∣x==q)&&(flson∣∣frson))
- 公共祖先左子树和右子树同时包含p节点或者q节点,则一个子树包含一个节点,另外一个节点必在另一子树;
- 节点本身是p或q的一个节点,且其子树包含另一个节点
- 只要从叶子节点开始搜索,则可以保证深度最大的公共祖先
class Solution {
private TreeNode ans;
public Solution() {
this.ans = null;
}
private boolean dfs(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) return false;
boolean lson = dfs(root.left, p, q);
boolean rson = dfs(root.right, p, q);
if ((lson && rson) || ((root.val == p.val || root.val == q.val) && (lson || rson))) {
ans = root;
}
return lson || rson || (root.val == p.val || root.val == q.val);
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
this.dfs(root, p, q);
return this.ans;
}
}
【方法二】存储前序遍历节点的路径
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
LinkedList<TreeNode> pPath = new LinkedList<>();
LinkedList<TreeNode> qPath = new LinkedList<>();
findPath(root, p, pPath);
findPath(root, p, qPath);
int len = Math.min(pPath.size(), qPath.size());
int i = 0;
for (; i < len; i++) {
if (pPath.get(i) != qPath.get(i)) {
break;
}
}
return pPath.get(i - 1);
}
private boolean findPath(TreeNode root, TreeNode p, LinkedList<TreeNode> path) {
if (root == null) {
return false;
}
path.add(root);
if (root == p) {
return true;
}
if (findPath(root.left, p , path) || findPath(root.right, p, path)) {
return true;
}
path.removeLast();
return false;
}
}
238. 除自身以外数组的乘积
【方法1】利用前缀乘积和后缀乘积数组
class Solution {
public int[] productExceptSelf(int[] nums) {
int[] prefix = new int[nums.length];
int[] suffix = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
if (i == 0) {
prefix[0] = 1;
suffix[nums.length - 1] = 1;
} else {
prefix[i] = prefix[i - 1] * nums[i - 1];
suffix[nums.length - 1 - i] = suffix[nums.length - i] * nums[nums.length - i];
}
}
int[] answer = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
answer[i] = prefix[i] * suffix[i];
}
return answer;
}
}
【方法2】优化,去掉数组prefix和suffix
class Solution {
public int[] productExceptSelf(int[] nums) {
int length = nums.length;
int[] answer = new int[length];
// answer[i] 表示索引 i 左侧所有元素的乘积
// 因为索引为 '0' 的元素左侧没有元素, 所以 answer[0] = 1
answer[0] = 1;
for (int i = 1; i < length; i++) {
answer[i] = nums[i - 1] * answer[i - 1];
}
// R 为右侧所有元素的乘积
// 刚开始右边没有元素,所以 R = 1
int R = 1;
for (int i = length - 1; i >= 0; i--) {
// 对于索引 i,左边的乘积为 answer[i],右边的乘积为 R
answer[i] = answer[i] * R;
// R 需要包含右边所有的乘积,所以计算下一个结果时需要将当前值乘到 R 上
R *= nums[i];
}
return answer;
}
}
239. 滑动窗口最大值
【方法1】优先队列实现最大堆
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (k == 1) {
return nums;
}
List<Integer> result = new LinkedList<>();
Queue<int[]> queue = new PriorityQueue<>((o1, o2) -> o1[0] == o2[0] ? o2[1] - o1[1] : o2[0] - o1[0]);
for (int i = 0; i < k; i++) {
queue.add(new int[]{nums[i], i});
}
result.add(queue.peek()[0]);
for (int i = k; i < nums.length; i++) {
while (queue.peek()[1] <= i - k) {
queue.poll();
}
queue.offer(new int[] {nums[i], i});
result.add(queue.peek()[0]);
}
return result.stream().mapToInt(Integer::intValue).toArray();
}
}
240. 搜索二维矩阵II
【方法1】暴力解法,遍历搜索O(mn)不推荐
【方法2】按行二分查找O(mlogn)
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
for (int i = 0; i < matrix.length; i++) {
int start = 0;
int end = matrix[0].length;
while (start < end) {
int middle = (end - start) / 2 + start;
if (matrix[i][middle] > target) {
end = middle;
} else if (matrix[i][middle] == target) {
return true;
} else {
start = middle + 1;
}
}
}
return false;
}
}
【方法3】Z字型查找
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int x = 0, y = n - 1;
while (x < m && y >= 0) {
if (matrix[x][y] == target) {
return true;
}
if (matrix[x][y] > target) {
--y;
} else {
++x;
}
}
return false;
}
}
279. 完全平方数
【方法1】动态规划
定义dp(n)
为完全平方数的和为n
的个数,则
n
=
i
2
+
n
−
i
2
,
其
中
i
=
[
1
,
n
]
,
且
为
整
数
n = i^2 + n - i^2, 其中i = [1,\sqrt{n}], 且为整数
n=i2+n−i2,其中i=[1,n],且为整数
则
KaTeX parse error: Got function '\sqrt' with no arguments as superscript at position 14: dp(n) = min^\̲s̲q̲r̲t̲{n}_idp(n-i*i) …
class Solution {
public int numSquares(int n) {
int[] f = new int[n + 1];
for (int i = 1; i <= n; i++) {
int min = Integer.MAX_VALUE;
for (int j = 1; j * j <= i; j++) {
min = Math.min(min, f[i - j * j]);
}
f[i] = min + 1;
}
return f[n];
}
}
283.移动零
【方法1】双指针
class Solution {
public void moveZeroes(int[] nums) {
if (nums == null || nums.length == 0) {
return;
}
int end = nums.length;
for (int i = nums.length - 1; i >= 0; i--) {
if (nums[i] == 0) {
end--;
int j = i;
while (j < end) {
nums[j] = nums[j + 1];
j++;
}
nums[end] = 0;
}
}
}
}
297. 二叉树的序列化与反序列化
【方法1】深度搜索、二叉树中序遍历
public class Codec {
public String serialize(TreeNode root) {
return rserialize(root, "");
}
public TreeNode deserialize(String data) {
String[] dataArray = data.split(",");
List<String> dataList = new LinkedList<String>(Arrays.asList(dataArray));
return rdeserialize(dataList);
}
public String rserialize(TreeNode root, String str) {
if (root == null) {
str += "None,";
} else {
str += str.valueOf(root.val) + ",";
str = rserialize(root.left, str);
str = rserialize(root.right, str);
}
return str;
}
public TreeNode rdeserialize(List<String> dataList) {
if (dataList.get(0).equals("None")) {
dataList.remove(0);
return null;
}
TreeNode root = new TreeNode(Integer.valueOf(dataList.get(0)));
dataList.remove(0);
root.left = rdeserialize(dataList);
root.right = rdeserialize(dataList);
return root;
}
}
300. 最长递增子序列
【方法1】动态规划
定义dp(i)
为下标为i结尾的子数组的最长递增数组长度,则
d
p
(
i
)
=
m
a
x
(
d
p
(
j
)
)
+
1
,
其
中
0
=
<
j
<
i
且
n
u
m
s
[
j
]
<
n
u
m
s
[
i
]
dp(i) = max(dp(j)) + 1, 其中 0=<j<i 且 nums[j] < nums[i]
dp(i)=max(dp(j))+1,其中0=<j<i且nums[j]<nums[i]
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
dp[0] = 1;
int max = 1;
for (int i = 0; i < nums.length; i++) {
// 只包括当前数本身,即前面都是递减数列时
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
max = Math.max(max, dp[i]);
}
return max;
}
}
309.买卖股票的最佳时机含冷冻期
【方法1】动态规划
买入=负收益,卖出=正收益,开始时必须先买入。
我们用 dp[i] 表示第 i天结束之后的「累计最大收益」。根据题目描述,由于我们最多只能同时买入(持有)一支股票,并且卖出股票后有冷冻期的限制,因此我们会有三种不同的状态:
-
①我们目前持有一支股票,对应的「累计最大收益」记为 d[i][0];
-
②我们目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」记为 d[i][1];
-
③我们目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」记为 d[i][2];
d p [ i ] = { d p [ i ] [ 0 ] 持 有 股 票 d p [ i ] [ 1 ] 不 持 有 股 票 , 处 于 冷 冻 期 d p [ i ] [ 2 ] 不 持 有 股 票 , 不 处 于 冷 冻 期 dp[i] = \begin{cases} dp[i][0] & 持有股票 \\ dp[i][1] & 不持有股票,处于冷冻期 \\ dp[i][2] & 不持有股票,不处于冷冻期 \\ \end{cases} dp[i]=⎩⎪⎨⎪⎧dp[i][0]dp[i][1]dp[i][2]持有股票不持有股票,处于冷冻期不持有股票,不处于冷冻期
- 第①种,第i天持有股票有两种情况,1)第i天之前买入的,此时dp[i][0] = dp[i-1][0]。2)第i天买入的,即dp[i-1][2] - prices[i]。则dp[i][0]应为二者较大值
- 第②种,第i天不持有股票且处于冷冻期,即第i-1天持有股票,第i天卖出。dp[i][1] = dp[i-1][0] + prices[i]。
- 第③种,第i天不持有股票且不处于冷冻期,即第i-1天处于冷冻期或不处于冷冻期的最大值。
因此,状态转移方程:
{
d
p
[
i
]
[
0
]
=
m
a
x
(
d
p
[
i
−
1
]
[
0
]
,
d
p
[
i
−
1
]
[
2
]
−
p
r
i
c
e
s
[
i
]
)
d
p
[
i
]
[
1
]
=
d
p
[
i
−
1
]
[
0
]
+
p
r
i
c
e
s
[
i
]
d
p
[
i
]
[
2
]
=
m
a
x
(
d
p
[
i
−
1
]
[
1
]
,
d
p
[
i
−
1
]
[
2
]
)
\begin{cases} dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i])\\ dp[i][1] = dp[i-1][0] + prices[i]\\ dp[i][2] = max(dp[i-1][1], dp[i-1][2]) \end{cases}
⎩⎪⎨⎪⎧dp[i][0]=max(dp[i−1][0],dp[i−1][2]−prices[i])dp[i][1]=dp[i−1][0]+prices[i]dp[i][2]=max(dp[i−1][1],dp[i−1][2])
则最大利润为:
p
r
o
f
i
t
=
m
a
x
(
d
p
[
i
]
[
0
]
,
d
p
[
i
]
[
1
]
,
d
p
[
i
]
[
2
]
)
profit = max(dp[i][0], dp[i][1], dp[i][2])
profit=max(dp[i][0],dp[i][1],dp[i][2])
因为持有股票必须要先买入股票,所以边界条件为:
{
d
p
[
0
]
[
0
]
=
−
p
r
i
c
e
s
[
0
]
d
p
[
0
]
[
1
]
=
0
d
p
[
0
]
[
2
]
=
0
\begin{cases} dp[0][0] = -prices[0]\\ dp[0][1] = 0\\ dp[0][2] = 0\\ \end{cases}
⎩⎪⎨⎪⎧dp[0][0]=−prices[0]dp[0][1]=0dp[0][2]=0
class Solution {
public int maxProfit(int[] prices) {
// 分别对应dp[i-1][0]、dp[i-1][1]、dp[i-1][2]
int last0 = -prices[0];
int last1 = 0;
int last2 = 0;
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
int current0 = Math.max(last0, last2 - prices[i]);
int current1 = last0 + prices[i];
int current2 = Math.max(last1, last2);
maxProfit = Math.max(Math.max(current0, current1), current2);
last0 = current0;
last1 = current1;
last2 = current2;
}
return maxProfit;
}
}
322.零钱兑换
【方法1】动态规划
定义dp[i]
为总金额为i的最小零钱数量,则有:
d
p
[
i
]
=
∑
j
=
0
n
m
i
n
(
d
p
[
i
−
c
o
i
n
s
[
j
]
]
)
+
1
dp[i] = \sum_{j=0}^n min(dp[i - coins[j]]) + 1
dp[i]=j=0∑nmin(dp[i−coins[j]])+1
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (i - coins[j] >= 0) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
337. 打家劫舍III
【方法一】动态规划
对于一个根节点root,可以分为选择此节点或者不选择则:
{
d
p
[
r
o
o
t
]
[
0
]
=
m
a
x
(
d
p
[
l
e
f
t
]
[
0
]
,
d
p
[
l
e
f
t
]
[
1
]
)
+
m
a
x
(
d
p
[
r
i
g
h
t
]
[
0
]
,
d
p
[
r
i
g
h
t
]
[
1
]
)
d
p
[
r
o
o
t
]
[
1
]
=
d
p
[
l
e
f
t
]
[
0
]
+
d
p
[
r
i
g
h
t
]
[
0
]
+
r
o
o
t
.
v
a
l
\begin{cases} dp[root][0] = max(dp[left][0], dp[left][1]) + max(dp[right][0], dp[right][1])\\ dp[root][1] = dp[left][0] + dp[right][0] + root.val \end{cases}
{dp[root][0]=max(dp[left][0],dp[left][1])+max(dp[right][0],dp[right][1])dp[root][1]=dp[left][0]+dp[right][0]+root.val
class Solution {
public int rob(TreeNode root) {
int[] sum = search(root);
return Math.max(sum[0], sum[1]);
}
private int[] search(TreeNode root) {
if (root == null) {
return new int[] {0, 0};
}
int[] left = search(root.left);
int[] right = search(root.right);
int sum0 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
int sum1 = left[0] + right[0] + root.val;
return new int[] {sum0, sum1};
}
}
338. 比特位计数
【方法1】动态规划
b i t [ i ] = { 1 , i 是 2 k b i t [ i / 2 ] , i 是 偶 数 b i t [ i − 1 ] + 1 , i 是 奇 数 bit[i] = \begin{cases} 1, & i 是2^k\\ bit[i/2], &i是偶数\\ bit[i-1] + 1, &i是奇数 \end{cases} bit[i]=⎩⎪⎨⎪⎧1,bit[i/2],bit[i−1]+1,i是2ki是偶数i是奇数
class Solution {
public int[] countBits(int n) {
int[] bits = new int[n + 1];
int k = 1;
bits[0] = 0;
for (int i = 1; i <= n; i++) {
if (i == 2 * k) {
bits[i] = 1;
k = 2 * k;
} else {
if (i % 2 == 0) {
bits[i] = bits[i/2];
} else {
bits[i] = bits[i - 1] + 1;
}
}
}
return bits;
}
}
347. 前 K 个高频元素
【方法1】最大堆
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
for (int num : nums) {
occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
}
// int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] m, int[] n) {
return m[1] - n[1];
}
});
for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
int num = entry.getKey(), count = entry.getValue();
if (queue.size() == k) {
if (queue.peek()[1] < count) {
queue.poll();
queue.offer(new int[]{num, count});
}
} else {
queue.offer(new int[]{num, count});
}
}
int[] ret = new int[k];
for (int i = 0; i < k; ++i) {
ret[i] = queue.poll()[0];
}
return ret;
}
}
394. 字符串解码
【方法1】递归
class Solution {
public String decodeString(String s) {
StringBuilder result = new StringBuilder(s);
Deque<Integer> stack = new LinkedList<>();
for (int i = 0; i < result.length(); i++) {
if (result.charAt(i) == '[') {
stack.push(i);
} else if (result.charAt(i) == ']') {
int index = stack.pop();
int start = index - 1;
while (start >= 0 && result.charAt(start) >= '0' && result.charAt(start) <= '9') {
start--;
}
int num = Integer.parseInt(result.substring(start + 1, index));
StringBuilder head = new StringBuilder(result.substring(0, start + 1));
String tail = result.substring(i + 1);
for (int j = 0; j < num; j++) {
head.append(result, index + 1, i);
}
head.append(tail);
result = head;
i = start + num * (i - index - 1) - 1;
}
}
return result.toString();
}
}
406. 根据身高重建队列
【方法1】排序
class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people, (o1, o2) -> o1[0] == o2[0] ? o2[1] - o1[1] : o1[0] - o2[0]);
int[][] ans = new int[people.length][];
for (int i = 0; i < people.length; i++) {
int space = people[i][1] + 1;
for (int j = 0; j < ans.length ; j++) {
if (ans[j] == null) {
space--;
}
if (space == 0) {
ans[j] = people[i];
break;
}
}
}
return ans;
}
}
416. 分割等和子集
【方法1】动态规划,01背包问题
此问题对于每一个数字都由选取和不选取的情况,是一个01背包问题。即在数组中选取一定的数字,它们的和为数组和的一半(target = sum/2)。所以要先求出数组和,在求解数组和sum的时候,可以快速做出一些判断
- 如果数组和为奇数,则和的一半是小数,正整数数组无法分割,返回FALSE。
- 如果数组最大值maxNum>sum/2,则除maxNum之外的数无法等于sum/2,返回FALSE。
构建n行target+1列的二维数组dp,其中dp[i][j]表示在数组nums的[0, i]范围内选取一定的数(可以一个都不选),其和是否能等于j。由此可见
-
对于dp[i][0]=true,对于0<=i<n。因为可以一个都不选;
-
对于dp[0][num[0]]=true,对于i = 0且j = num[0]。只选择num[0]的时候dp[0][num[0]]为true.
-
对于其他情形有
-
当j>=num[i]时,因为和大于num[i],可以选择num[i]也可以不选择。
不选择num[i]时,
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] , 当 j > = n u m [ i ] 且 不 选 择 n u m [ i ] 时 dp[i][j] = dp[i-1][j], 当j >= num[i]且不选择num[i]时 dp[i][j]=dp[i−1][j],当j>=num[i]且不选择num[i]时
选择num[i]时,
d p [ i ] [ j ] = d [ i − 1 ] [ j − n u m [ i ] ] , 当 j > = n u m [ i ] 且 选 择 n u m [ i ] 时 dp[i][j] = d[i-1][j-num[i]], 当j >= num[i]且选择num[i]时 dp[i][j]=d[i−1][j−num[i]],当j>=num[i]且选择num[i]时
-
-
当j<nums[i]时,此时不能选择num[i],
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] , 当 j < n u m [ i ] 时 dp[i][j] = dp[i-1][j], 当j<num[i]时 dp[i][j]=dp[i−1][j],当j<num[i]时
综上有:
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
]
∣
∣
d
p
[
i
−
1
]
[
j
−
n
u
m
[
i
]
]
,
j
>
=
n
u
m
[
i
]
d
p
[
i
−
1
]
[
j
]
,
j
<
n
u
m
[
i
]
dp[i][j] = \begin{cases} dp[i-1][j] || dp[i-1][j-num[i]], && j>=num[i]\\ dp[i-1][j], && j<num[i] \end{cases}
dp[i][j]={dp[i−1][j]∣∣dp[i−1][j−num[i]],dp[i−1][j],j>=num[i]j<num[i]
则dp[n][target]的结果即为所求值
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
int max = nums[0];
for (int i : nums) {
sum += i;
max = Math.max(max, i);
}
if (sum % 2 == 1 || max > sum / 2) {
return false;
}
int target = sum / 2;
boolean[][] dp = new boolean[nums.length][target + 1];
for (int i = 0; i < dp.length; i++) {
for (int j = 0; j < target + 1; j++) {
if (j == 0) {
dp[i][j] = true;
continue;
}
if (i == 0) {
if (j == nums[0]) {
dp[i][j] = true;
}
continue;
}
if (j >= nums[i]) {
dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i]];
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[nums.length - 1][target];
}
}
优化空间复杂度
上述过程可以看出状态转移过程中,只与上一行有关系,故只需维护一维数组即可。
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
]
∣
∣
d
p
[
i
−
1
]
[
j
−
n
u
m
[
i
]
]
,
j
>
=
n
u
m
[
i
]
d
p
[
i
−
1
]
[
j
]
,
j
<
n
u
m
[
i
]
dp[i][j] = \begin{cases}dp[i-1][j] || dp[i-1][j-num[i]], && j>=num[i]\\dp[i-1][j], && j<num[i]\end{cases}
dp[i][j]={dp[i−1][j]∣∣dp[i−1][j−num[i]],dp[i−1][j],j>=num[i]j<num[i]
且需要注意的是第二层的循环我们需要从大到小计算,因为如果我们从小到大更新dp 值,那么在计算dp[j] 值的时候,dp[j−nums[i]] 已经是被更新过的状态,不再是上一行的dp 值。
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
int max = nums[0];
for (int i : nums) {
sum += i;
max = Math.max(max, i);
}
if (sum % 2 == 1 || max > sum / 2) {
return false;
}
int target = sum / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int i = 0; i < nums.length; i++) {
int num = nums[i];
for (int j = target; j >= num; j--) {
dp[j] = dp[j] || dp[j - num];
}
}
return dp[target];
}
}
437. 路径总和 III
【方法1】深度搜索dfs
class Solution {
public int pathSum(TreeNode root, int targetSum) {
if (root == null) {
return 0;
}
int ans = dfs(root, targetSum);
ans += pathSum(root.left, targetSum);
ans += pathSum(root.right, targetSum);
return ans;
}
public int dfs(TreeNode root, int sum) {
if (root == null) {
return 0;
}
int path = 0;
if (sum == root.val) {
path++;
}
path += dfs(root.left, sum - root.val);
path += dfs(root.right, sum - root.val);
return path;
}
}
【方法2】前缀和
可以记录以某一节点为结尾的路径之和,则两个前缀和的差值为target时,也就存在一条路径和为target。
如10->5->2->1,target为8时,节点5的前缀和为15,节点1的前缀和为23,因为23-15=8,所以存在一条和为8的路径。为此需要记录前缀和为x时的路径个数y,可以使用hashmap。这里注意两点:
- 初始时hashmap.put(0,1),保证找到两个前缀和之差为0的必有一条路径(它本身)
- 当我们退出当前节点时,我们需要及时更新已经保存的前缀和
class Solution {
public int pathSum(TreeNode root, int targetSum) {
Map<Integer, Integer> prefix = new HashMap<>();
prefix.put(0, 1);
return prefixSum(root, prefix, 0, targetSum);
}
public int prefixSum(TreeNode root, Map<Integer, Integer> prefix, int sum, int target) {
if (root == null) {
return 0;
}
sum += root.val;
// 历史路径中是否存在前缀和为sum - target的路径数
int path = prefix.getOrDefault(sum - target, 0);
prefix.put(sum, prefix.getOrDefault(sum, 0) + 1);
path += prefixSum(root.left, prefix, sum, target);
path += prefixSum(root.right, prefix, sum, target);
prefix.put(sum, prefix.getOrDefault(sum, 0) - 1);
return path;
}
}
438. 找到字符串中所有字母异位词
【方法1】暴力解法
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> ans = new LinkedList<>();
int[] hash = new int[26];
for (int i = 0; i < p.length(); i++) {
hash[p.charAt(i) - 'a']++;
}
int[] map = new int[26];
for (int i = 0; i < s.length(); i++) {
if (s.length() - i < p.length()) {
break;
}
for (int j = i; j < i + p.length(); j++) {
map[s.charAt(j) - 'a']++;
}
boolean equal = true;
for (int j = 0; j < hash.length; j++) {
if (map[j] != hash[j]) {
equal = false;
break;
}
}
if (equal) {
ans.add(i);
}
Arrays.fill(map, 0);
}
return ans;
}
}
【方法2】滑动窗口
可以考虑利用p.length()
长度作为一个窗口,在字符串s上进行滑动,每次将窗口的头部滑出一个元素,在尾部划入一个元素。利用数组sHash的字符串编码值作为窗口内子串的异位词编码,数组pHash为字符串p的异位词编码,当二者相等时即为一个子串。
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> ans = new LinkedList<>();
if (s.length() < p.length()) {
return ans;
}
int[] pHash = new int[26];
for (int i = 0; i < p.length(); i++) {
pHash[p.charAt(i) - 'a']++;
}
int[] sHash = new int[26];
for (int i = 0; i < p.length(); i++) {
sHash[s.charAt(i) - 'a']++;
}
if (Arrays.equals(sHash, pHash)) {
ans.add(0);
}
for (int i = 0; i < s.length() - p.length(); i++) {
sHash[s.charAt(i) - 'a']--;
sHash[s.charAt(i + p.length()) - 'a']++;
if (Arrays.equals(sHash, pHash)) {
ans.add(i + 1);
}
}
return ans;
}
}
538. 把二叉搜索树转换为累加树
【方法1】反序中序遍历
这里比较重要的是要用一个全局变量记录当前的和,用当前和+当前节点的值即为累加值。
class Solution {
int sum = 0; // 记录当前和
public TreeNode convertBST(TreeNode root) {
dfs(root);
return root;
}
private void dfs(TreeNode root) {
if (root == null) {
return;
}
dfs(root.right);
root.val += sum;
sum = root.val;
dfs(root.left);
}
}
647. 回文子串
【方法1】中心扩展
对于一个回文字符串来说,如果长度为奇数则其回文中心有两个,长度为偶则其回文中心有一个,假如字符串长度为4,可以分析出:
回文中心1 | 回文中心2 | 编号 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 1 | 2 |
1 | 2 | 3 |
2 | 2 | 4 |
2 | 3 | 5 |
3 | 3 | 6 |
可以看出:
对于长度为n的字符串共有2(n-1)个回文中心组和,可以直接遍历0-2(n-1)即可得出所有回文中心,然后分别对回文中心向左、向右扩展即可得出所有回文子串。
class Solution {
public int countSubstrings(String s) {
int num = 0;
for (int i = 0; i <= 2 * (s.length() - 1); i++) {
int left = i / 2;
int right = (i % 2 == 0) ? left : left + 1;
while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
num++;
}
}
return num;
}
}
【方法2】Manacher 算法
Manacher 算法应用的一个前提是回文字符串长度为奇数,即只能有一个回文中心。可以在每个字符中间和开头与结尾都插入一个特殊字符,使得回文字符串长度永远为奇数。如回文串abba
,回文中心为(a,b)
。插入特殊字符#
(任意特殊字符即可)后变为#a#b#b#a#
仍然为回文字符串,但回文中心变为(#)
,且字符串长度为奇数。
Manacher 算法的核心在于提出了臂长概念:
对于字符串ebabababe
|e|b|a|b|a|b|a|b|e|
-------------------
|0|1|2|3|4|5|6|7|8|
-------------------
|--len--|j|--len--|
当回文中心j = 4
时,以i为中心最长回文子串的半径为臂长len, 记为f[j]。则当遍历到j时,求解f[i]时,可以找出与i关于对称的点k
-------------------
|e|b|a|b|a|b|a|b|e|
-------------------
|0|1|2|3|4|5|6|7|8|
-------------------
|---|k|-|j|-|i|---|
-------------------
由于对称性关系,f[i]至少为min(f[k], j + len - i)。下面分析这个结论的原因,当我们把k点对称过去后,可以直到k的臂长为f[k],而以j为回文中心的臂长为len,在len长度内,以i为回文中心的字符串长度最大为 j + len - i,所以有
f
[
i
]
=
m
i
n
(
f
(
k
)
,
j
+
l
e
n
−
i
)
,
其
中
k
=
j
−
(
i
−
j
)
;
f[i] = min(f(k), j+len-i), 其中k = j - (i - j);
f[i]=min(f(k),j+len−i),其中k=j−(i−j);
然后以右臂最长的回文中心作为j,即i的对称中心。这样我们利用历史信息f,得到了i的最小的力臂长度,再进行中心扩展时,可以直接从最小力臂长度开始往外扩展,而不用每次从i开始扩展。
对于力臂长度为f[i]的正常回文串,其回文子串有f[i]个,又因为**回文串要求都为奇数,会对偶数回文串插入#变为奇数,所以子串为f[i]/2。**比如#a#a#,力臂长度为2,回文子串个数为1,即aa。为了不让下标越界,一个很简单的办法,就是在开头加一个 $,并在结尾加一个 !。
class Solution {
public int countSubstrings(String s) {
StringBuilder stringBuilder = new StringBuilder("$#");
for(int i = 0; i < s.length(); i++) {
stringBuilder.append(s.charAt(i));
stringBuilder.append('#');
}
s = stringBuilder.append("!").toString();
int count = 0;
int[] dp = new int[s.length()];
int right = -1;
int j = -1;
for (int i = 1; i < s.length(); i++) {
if (right >= i) {
int k = j - (i - j);
int minLen = Math.min(dp[k], right - i);
dp[i] = expand(s, i - minLen, i + minLen) + 1;
} else {
dp[i] = expand(s, i, i) + 1;
}
if (i + dp[i] > right) {
right = i + dp[i];
j = i;
}
count += dp[i] / 2;
}
return count;
}
private int expand (String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return (right - left - 2) / 2;
}
}
高频知识点
分类 | 题号 |
---|---|
二叉树 | 94、145、226、236、113、100、226、590、103 |
背包问题 | 474、494、416、518、322、139、377、1049、1449、279 |
并查集 | 547、684、1319、1631、959、1202、947、721、803、1579、778 |
单调栈 | |
动态规划 | 221、1277、121、122、123 |
dfs | 113 |
图路径算法Dijkstra | 399、1631 |
二叉树专题
94. 二叉树的中序遍历
【二叉树中序遍历——递归】
class Solution {
List<Integer> list = new LinkedList<>();
public List<Integer> inorderTraversal(TreeNode root) {
dfs(root);
return list;
}
private void dfs(TreeNode root) {
if (root == null) {
return;
}
dfs(root.left);
list.add(root.val);
dfs(root.right);
}
}
【二叉树中序遍历——栈】
class Solution{
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new LinkedList<>();
Deque<TreeNode> stack = new LinkedList<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
ans.add(root.val);
root = root.right;
}
return ans;
}
}
【二叉树中序遍历——Morris算法】
O(1)空间复杂度算法的Morris算法;
- 节点x无左孩子
- x加入结果
- x = x.right
- 节点x有左孩子,找到predecessor(当前节点左子树最右节点)
- predecessor无右孩子,predecessor.right = x, x = x.left
- predecessor有右孩子,x加入结果,x = x.right
class Solution {
List<Integer> list = new LinkedList<>();
public List<Integer> inorderTraversal(TreeNode root) {
morris(root);
return list;
}
private void morris(TreeNode root) {
while (root != null) {
if (root.left == null) {
list.add(root.val);
root = root.right;
} else {
TreeNode predecessor = getPredecessor(root);
if (predecessor.right == null) {
predecessor.right = root;
root = root.left;
} else {
list.add(root.val);
root = root.right;
}
}
}
}
// predecessor为当前节点向左走一步,然后向右走到无法再走为止
private TreeNode getPredecessor(TreeNode node) {
TreeNode predecessor = node.left;
while (predecessor.right != null && predecessor.right != node) {
predecessor = predecessor.right;
}
return predecessor;
}
}
145. 二叉树的后序遍历
【二叉树后序遍历——递归】
class Solution{
List<Integer> list = new LinkedList<>();
public List<Integer> postorderTraversal1(TreeNode root) {
dfs(root);
return list;
}
private void dfs(TreeNode root) {
if (root == null) {
return;
}
dfs(root.left);
dfs(root.right);
list.add(root.val);
}
}
【二叉树后序遍历——栈】
class Solution {
List<Integer> list = new LinkedList<>();
Deque<TreeNode> stack = new LinkedList<>();
public List<Integer> postorderTraversal(TreeNode root) {
TreeNode prev = null;
while(root != null || !stack.isEmpty()) {
while(root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
if (root.right == null || root.right == prev) {
list.add(root.val);
prev = root;
root = null;
} else {
stack.push(root);
root = root.right;
}
}
return list;
}
}
100. 相同的树
【两个树的遍历】
class Solution {
boolean isSame = true;
public boolean isSameTree(TreeNode p, TreeNode q) {
dfs(p, q);
return isSame;
}
private void dfs(TreeNode root1, TreeNode root2) {
if (root1 == null && root2 == null) {
return;
}
if (root1 == null || root2 == null) {
isSame = false;
return;
}
dfs(root1.left, root2.left);
if (root1.val != root2.val) {
isSame = false;
}
dfs(root1.right, root2.right);
}
}
226. 翻转二叉树
【后序遍历】
class Solution {
public TreeNode invertTree(TreeNode root) {
dfs(root);
return root;
}
private void dfs(TreeNode root) {
if (root == null) {
return;
}
dfs(root.left);
dfs(root.right);
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
}
}
590. N 叉树的后序遍历
【后序遍历】
public class Solution590 {
List<Integer> ans = new LinkedList<>();
public List<Integer> postorder(Node root) {
dfs(root);
return ans;
}
private void dfs(Node root) {
if (root == null) {
return;
}
if (root.children != null && root.children.size() > 0) {
root.children.forEach(this::dfs);
}
ans.add(root.val);
}
}
103. 二叉树的锯齿形层序遍历
【二叉树的按层次遍历——队列】
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
if (root == null) {
return new LinkedList<>();
}
List<List<Integer>> ans = new LinkedList<>();
Queue<TreeNode> queue = new LinkedList<>();
boolean isOrderLeft = false;
queue.offer(root);
while (!queue.isEmpty()) {
LinkedList<Integer> cur = new LinkedList<>();
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode top = queue.poll();
if (top.left != null) {
queue.offer(top.left);
}
if (top.right != null) {
queue.offer(top.right);
}
if (isOrderLeft) {
cur.addFirst(top.val);
} else {
cur.addLast(top.val);
}
}
ans.add(cur);
isOrderLeft = !isOrderLeft;
}
return ans;
}
}
124. 二叉树中的最大路径和
【节点的最大贡献】
class Solution {
int max = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return max;
}
private int dfs(TreeNode root) {
if (root == null) {
return 0;
}
int left = Math.max(dfs(root.left), 0);
int right = Math.max(dfs(root.right), 0);
int path = left + right + root.val;
max = Math.max(max, path);
return root.val + Math.max(left, right);
}
}
背包问题
474. 一和零
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i < strs.length; i++) {
int one = getSum(strs[i]);
int zero = strs[i].length() - one;
for (int j = m; j >= zero; j--) {
for (int k = n; k >= one; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - zero][k - one] + 1);
}
}
}
return dp[m][n];
}
public int getSum(String str) {
int sum = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == '1') {
sum++;
}
}
return sum;
}
}
1049. 最后一块石头的重量 II
【方法1 0-1背包】
对于stones[]数组来说,假设最后剩余石头重量为left,石头开始总重量为sum,则sum-left后所有的石头可以完全粉碎,即将石头分为两部分重量相等为neg,即sum - left = 2*neg。则neg = (sum - left)/2,要想left尽可能小,则neg需要尽可能接近sum/2。因此问题转化为,在stone中挑选石头使其总重量尽可能接近sum/2的背包问题。
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int i = 0; i < stones.length; i++) {
sum += stones[i];
}
int n = sum / 2;
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for (int i = 0; i < stones.length; i++) {
for (int j = n; j >= stones[i]; j--) {
dp[j] = dp[j - stones[i]] || dp[j];
}
}
int neg = 0;
for (int i = n; i >= 0; i--) {
if (dp[i]) {
neg = i;
break;
}
}
System.out.println(neg);
return sum - 2 * neg;
}
}
并查集问题
并查集算法
路径压缩
在查询的时候将节点的父节点直接指向根节点:
路径压缩后
public int find(int x) {
if (parent[x] != x) {
int origin = parent[x];
// 找到x父节点的根节点,并将x父节点指向根节点
parent[x] = find(parent[x]);
weight[x] *= weight[origin];
}
return parent[x];
}
路径压缩后,并查集的高度为2.
合并路径
weight * weight1 = weight2 * input ==> weight = weight2 * input / weight1
public class UnionFind {
private int[] parent;
private double[] weight;
public UnionFind(int n) {
parent = new int[n];
weight = new double[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
weight[i] = 1.0;
}
}
public int find(int x) {
if (parent[x] != x) {
int origin = parent[x];
parent[x] = find(parent[x]);
weight[x] *= weight[origin];
}
return parent[x];
}
public void union(int x, int y, double value) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
parent[rootX] = rootY;
weight[rootX] = weight[y] * value / weight[x];
}
}
399. 除法求值
【方法1 带权重的并查集】
class Solution {
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
int size = equations.size();
UnionFind unionFind = new UnionFind(2 * size);
HashMap<String, Integer> map = new HashMap<>();
int index = 0;
for (int i = 0; i < equations.size(); i++) {
String var1 = equations.get(i).get(0);
String var2 = equations.get(i).get(1);
if (map.get(var1) == null) {
map.put(var1, index);
index++;
}
if (map.get(var2) == null) {
map.put(var2, index);
index++;
}
unionFind.union(map.get(var1), map.get(var2), values[i]);
}
double[] ans = new double[queries.size()];
for (int i = 0; i < ans.length; i++) {
String var1 = queries.get(i).get(0);
String var2 = queries.get(i).get(1);
if (map.get(var1) != null && map.get(var2) != null) {
ans[i] = unionFind.getWeight(map.get(var1), map.get(var2));
} else {
ans[i] = -1.0;
}
}
return ans;
}
class UnionFind {
private int[] parent;
private double[] weight;
public UnionFind(int n) {
parent = new int[n];
weight = new double[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
weight[i] = 1.0;
}
}
public int find(int x) {
if (parent[x] != x) {
int origin = parent[x];
parent[x] = find(parent[x]);
weight[x] *= weight[origin];
}
return parent[x];
}
public void union(int x, int y, double value) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
parent[rootX] = rootY;
weight[rootX] = weight[y] * value / weight[x];
}
public double getWeight(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return weight[x] / weight[y];
} else {
return -1.0;
}
}
}
}
1319. 连通网络的操作次数
【方法1并查集, 使用count表示集合个数】
class Solution {
public int makeConnected(int n, int[][] connections) {
UnionFind unionFind = new UnionFind(n);
for (int i = 0; i < connections.length; i++) {
unionFind.union(connections[i][0], connections[i][1]);
}
System.out.println(n);
System.out.println(unionFind.count);
System.out.println(unionFind.left);
if (unionFind.left >= unionFind.count - 1) {
return unionFind.count - 1;
} else {
return -1;
}
}
class UnionFind {
private int[] parent;
private int count;
private int left;
public UnionFind(int n) {
parent = new int[n];
count = n;
left = 0;
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
left++;
return;
}
parent[rootX] = rootY;
count--;
}
}
959. 由斜杠划分区域
按区域的最小粒度划分
class Solution {
public int regionsBySlashes(String[] grid) {
int N = grid.length;
int size = 4 * N * N;
UnionFind unionFind = new UnionFind(size);
for (int i = 0; i < N; i++) {
char[] row = grid[i].toCharArray();
for (int j = 0; j < N; j++) {
// 二维网格转换为一维表格,index 表示将单元格拆分成 4 个小三角形以后,编号为 0 的小三角形的在并查集中的下标
int index = 4 * (i * N + j);
char c = row[j];
// 单元格内合并
if (c == '/') {
// 合并 0、3,合并 1、2
unionFind.union(index, index + 3);
unionFind.union(index + 1, index + 2);
} else if (c == '\\') {
// 合并 0、1,合并 2、3
unionFind.union(index, index + 1);
unionFind.union(index + 2, index + 3);
} else {
unionFind.union(index, index + 1);
unionFind.union(index + 1, index + 2);
unionFind.union(index + 2, index + 3);
}
// 单元格间合并
// 向右合并:1(当前)、3(右一列)
if (j + 1 < N) {
unionFind.union(index + 1, 4 * (i * N + j + 1) + 3);
}
// 向下合并:2(当前)、0(下一行)
if (i + 1 < N) {
unionFind.union(index + 2, 4 * ((i + 1) * N + j));
}
}
}
return unionFind.count;
}
class UnionFind {
private int[] parent;
private int count;
public UnionFind(int n) {
parent = new int[n];
count = n;
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
parent[rootX] = rootY;
count--;
}
}
}
字节后端
25. K 个一组翻转链表
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode slow = head;
ListNode fast = head;
ListNode newHead = new ListNode();
ListNode prev = newHead;
while (fast != null) {
int i = 0;
for (; i < k && fast != null; i++) {
fast = fast.next;
}
if (i < k) {
break;
}
prev.next = reverse(slow, fast);
slow.next = fast;
prev = slow;
slow = fast;
}
return newHead.next;
}
private ListNode reverse(ListNode head, ListNode tail) {
ListNode newHead = new ListNode();
while (head != tail) {
ListNode tmp = newHead.next;
newHead.next = head;
head = head.next;
newHead.next.next = tmp;
}
return newHead.next;
}
}
113. 路径总和 II
【方法1】深度搜索
class Solution {
List<List<Integer>> paths = new LinkedList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
dfs(root, targetSum);
System.out.println(paths);
System.out.println(path);
return paths;
}
public void dfs(TreeNode root, int targetSum) {
if (root == null) {
return;
}
path.offerLast(root.val);
targetSum -= root.val;
if (root.left == null && root.right == null && targetSum == 0) {
paths.add(new LinkedList<>(path));
}
dfs(root.left, targetSum);
dfs(root.right, targetSum);
//用完当前节点要进行清理!!
path.pollLast();
}
}
【】
121. 买卖股票的最佳时机
状态转移方程:
{
d
p
[
i
]
=
d
p
[
i
−
1
]
−
p
r
i
c
e
s
[
i
−
1
]
+
p
r
i
c
e
s
[
i
]
第
i
天
卖
出
时
收
益
\begin{cases} dp[i] = dp[i-1] - prices[i-1] + prices[i] & 第i天卖出时收益 \end{cases}
{dp[i]=dp[i−1]−prices[i−1]+prices[i]第i天卖出时收益
最大利润
m
a
x
(
d
p
[
i
]
)
max(dp[i])
max(dp[i])
class Solution {
public int maxProfit(int[] prices) {
int last0 = 0;
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
int current0 = Math.max(last0 - prices[i - 1] + prices[i], 0);
maxProfit = Math.max(current0, maxProfit);
last0 = current0;
}
return maxProfit;
}
}
122. 买卖股票的最佳时机II
状态转移方程:
{
d
p
[
i
]
[
0
]
=
m
a
x
(
d
p
[
i
−
1
]
[
0
]
,
d
p
[
i
−
1
]
[
1
]
−
p
r
i
c
e
s
[
i
]
)
第
i
天
持
有
股
票
的
最
大
收
益
d
p
[
i
]
[
1
]
=
m
a
x
(
d
p
[
i
−
1
]
[
1
]
,
d
p
[
i
−
1
]
[
0
]
+
p
r
i
c
e
s
[
i
]
)
第
i
天
不
持
有
股
票
的
最
大
收
益
\begin{cases} dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]) & 第i天持有股票的最大收益\\ dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) & 第i天不持有股票的最大收益 \end{cases}
{dp[i][0]=max(dp[i−1][0],dp[i−1][1]−prices[i])dp[i][1]=max(dp[i−1][1],dp[i−1][0]+prices[i])第i天持有股票的最大收益第i天不持有股票的最大收益
边界条件:
{
d
p
[
0
]
[
0
]
=
−
p
r
i
c
e
s
[
0
]
第
1
天
只
能
买
入
股
票
,
收
益
为
负
d
p
[
0
]
[
1
]
=
0
\begin{cases} dp[0][0] = -prices[0] & 第1天只能买入股票,收益为负\\ dp[0][1] = 0 \end{cases}
{dp[0][0]=−prices[0]dp[0][1]=0第1天只能买入股票,收益为负
最大利润
m
a
x
(
d
p
[
i
]
[
0
]
,
d
p
[
i
]
[
1
]
)
max(dp[i][0], dp[i][1])
max(dp[i][0],dp[i][1])
class Solution {
public int maxProfit(int[] prices) {
int last0 = -prices[0];
int last1 = 0;
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
int current0 = Math.max(last0, last1 - prices[i]);
int current1 = Math.max(last1, last0 + prices[i]);
maxProfit = Math.max(current0, current1);
last0 = current0;
last1 = current1;
}
return maxProfit;
}
}
123. 买卖股票的最佳时机III
状态转移方程:
{
d
p
[
i
]
[
0
]
=
m
a
x
(
d
p
[
i
−
1
]
[
0
]
,
−
p
r
i
c
e
[
i
]
)
第
i
天
完
成
0
笔
交
易
且
持
有
股
票
的
最
大
收
益
d
p
[
i
]
[
1
]
=
m
a
x
(
d
p
[
i
−
1
]
[
0
]
+
p
r
i
c
e
[
i
]
,
d
p
[
i
−
1
]
[
1
]
)
第
i
天
完
成
1
笔
交
易
且
不
持
有
股
票
的
最
大
收
益
d
p
[
i
]
[
2
]
=
m
a
x
(
d
p
[
i
−
1
]
[
1
]
−
p
r
i
c
e
[
i
]
,
d
p
[
i
−
1
]
[
2
]
)
第
i
天
完
成
1
笔
交
易
且
持
有
股
票
的
最
大
收
益
d
p
[
i
]
[
3
]
=
m
a
x
(
d
p
[
i
−
1
]
[
2
]
+
p
r
i
c
e
[
i
]
,
d
p
[
i
−
1
]
[
3
]
)
第
i
天
完
成
2
笔
交
易
的
最
大
收
益
\begin{cases} dp[i][0] = max(dp[i-1][0], -price[i]) & 第i天完成0笔交易且持有股票的最大收益\\ dp[i][1] = max(dp[i-1][0] + price[i], dp[i-1][1]) & 第i天完成1笔交易且不持有股票的最大收益\\ dp[i][2] = max(dp[i-1][1] - price[i], dp[i-1][2]) & 第i天完成1笔交易且持有股票的最大收益\\ dp[i][3] = max(dp[i-1][2] + price[i], dp[i-1][3]) & 第i天完成2笔交易的最大收益\\ \end{cases}
⎩⎪⎪⎪⎨⎪⎪⎪⎧dp[i][0]=max(dp[i−1][0],−price[i])dp[i][1]=max(dp[i−1][0]+price[i],dp[i−1][1])dp[i][2]=max(dp[i−1][1]−price[i],dp[i−1][2])dp[i][3]=max(dp[i−1][2]+price[i],dp[i−1][3])第i天完成0笔交易且持有股票的最大收益第i天完成1笔交易且不持有股票的最大收益第i天完成1笔交易且持有股票的最大收益第i天完成2笔交易的最大收益
边界条件:
{
d
p
[
i
]
[
0
]
=
−
p
r
i
c
e
[
0
]
第
1
天
完
成
0
笔
交
易
且
持
有
股
票
的
最
大
收
益
d
p
[
i
]
[
1
]
=
0
第
1
天
完
成
1
笔
交
易
且
不
持
有
股
票
的
最
大
收
益
d
p
[
i
]
[
2
]
=
−
p
r
i
c
e
[
0
]
第
1
天
完
成
1
笔
交
易
且
持
有
股
票
的
最
大
收
益
d
p
[
i
]
[
3
]
=
0
第
1
天
完
成
2
笔
交
易
的
最
大
收
益
\begin{cases} dp[i][0] = -price[0] & 第1天完成0笔交易且持有股票的最大收益\\ dp[i][1] = 0 & 第1天完成1笔交易且不持有股票的最大收益\\ dp[i][2] = - price[0] & 第1天完成1笔交易且持有股票的最大收益\\ dp[i][3] = 0 & 第1天完成2笔交易的最大收益\\ \end{cases}
⎩⎪⎪⎪⎨⎪⎪⎪⎧dp[i][0]=−price[0]dp[i][1]=0dp[i][2]=−price[0]dp[i][3]=0第1天完成0笔交易且持有股票的最大收益第1天完成1笔交易且不持有股票的最大收益第1天完成1笔交易且持有股票的最大收益第1天完成2笔交易的最大收益
最大利润:
m
a
x
(
d
p
[
i
]
[
0
]
,
d
p
[
i
]
[
1
]
,
d
p
[
i
]
[
2
]
,
d
p
[
i
]
[
3
]
)
max(dp[i][0], dp[i][1], dp[i][2], dp[i][3])
max(dp[i][0],dp[i][1],dp[i][2],dp[i][3])
class Solution {
public int maxProfit(int[] prices) {
int last0 = -prices[0];
int last1 = 0;
int last2 = -prices[0];
int last3 = 0;
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
int current0 = Math.max(last0, -prices[i]);
int current1 = Math.max(last0 + prices[i], last1);
int current2 = Math.max(last1 - prices[i], last2);
int current3 = Math.max(last3, last2 + prices[i]);
maxProfit = Math.max(Math.max(Math.max(current0, current1), current2), current3);
last0 = current0;
last1 = current1;
last2 = current2;
last3 = current3;
}
return maxProfit;
}
}