LeetCode 976-988. 题解合集
976. Largest Perimeter Triangle(三角形的最大周长)
题目描述
给定由一些正数(代表长度)组成的数组 nums,返回由其中三个长度组成的、面积不为零的三角形的最大周长。
如果不能形成任何面积不为零的三角形,返回 0。
示例 1
- 输入:
nums= [2,1,2] - 输出:5
- 解释:你可以用三个边长组成一个三角形: 1 2 2。
示例 2
- 输入:
nums= [1,2,1,10] - 输出:0
- 解释:不能用任何三条边长构成一个非零面积的三角形。
提示: - 3 <= nums.length <= 10^4- 1 <= nums[i] <= 10^6
解题思路
解法一(排序 + 贪心):
三边 a <= b <= c 能围成面积非零的三角形当且仅当 a + b > c,周长尽量大。
先整体排序,再从大到小枚举最大的边 nums[i],取紧邻的两条边 nums[i-1]、nums[i-2],
一旦满足 nums[i-2] + nums[i-1] > nums[i] 即为最大周长;否则该最大边不可行,继续往前枚举。
正确性:若排序后第 i 大的边作为最大边无法与紧邻两条边组成三角形,则它与任何更小的两条边
更不可能组成三角形(更小的边之和只会更小)。
解法二(暴力枚举 + 校验,供对照):
三指针枚举所有三元组,先排序,对每个最大边 c 枚举更小的 b 与 a,取满足 a + b > c 的三边组合中
周长最大者。O(n^3) 仅用于小数据验证,与解法一结果一致。
代码实现
public class Solution {
// 解法一:排序 + 贪心(主解法,提交 LeetCode)
public int largestPerimeter(int[] nums) {
java.util.Arrays.sort(nums);
for (int i = nums.length - 1; i >= 2; --i) {
int c = nums[i - 1] + nums[i - 2];
if (c > nums[i]) {
return c + nums[i];
}
}
return 0;
}
// 解法二:暴力枚举(对照验证)
public int largestPerimeterBrute(int[] nums) {
int n = nums.length;
int[] a = nums.clone();
java.util.Arrays.sort(a);
int best = 0;
for (int i = n - 1; i >= 2; --i) {
for (int j = i - 1; j >= 1; --j) {
for (int k = j - 1; k >= 0; --k) {
if (a[k] + a[j] > a[i]) {
best = Math.max(best, a[i] + a[j] + a[k]);
}
// 更小的 k 只会更小,跳出本层
}
}
}
return best;
}
private static void check(int[] nums, int expected, int caseId) {
int got = new Solution().largestPerimeter(nums);
int got2 = new Solution().largestPerimeterBrute(nums);
if (got != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {2, 1, 2}, 5, 1);
check(new int[] {1, 2, 1, 10}, 0, 2);
check(new int[] {3, 6, 2, 3}, 8, 3);
check(new int[] {1, 1, 1}, 3, 4);
check(new int[] {2, 2, 2, 2}, 6, 5);
check(new int[] {3, 2, 3, 4}, 10, 6);
check(new int[] {1, 2, 2, 4, 18, 8}, 5, 7);
System.out.println("== 0976 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n log n),排序主导;
- 空间复杂度:O(log n),排序所需的栈空间。
解法二: - 时间复杂度:O(n^3);
- 空间复杂度:O(log n)。
977. Squares of a Sorted Array(有序数组的平方)
题目描述
给你一个按非递减顺序排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。
示例 1
- 输入:
nums= [-4,-1,0,3,10] - 输出:[0,1,9,16,100]
- 解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100]。
示例 2
- 输入:
nums= [-7,-3,2,3,11] - 输出:[4,9,9,49,121]
提示: - 1 <= nums.length <= 10^4- -10^4 <= nums[i] <= 10^4- 进阶:请你设计时间复杂度为
O(n)的算法解决本问题。
解题思路
解法一(双指针):
数组本身有序,但负数平方后递减、正数平方后递增,平方值两端大、中间小。
用左右指针 i、j 分别指向数组两端,比较 nums[i]^2 与 nums[j]^2,把较大者放入结果
数组从后往前的当前位置,然后移动对应指针,直到 i > j。
解法二(先平方再排序,供对照):
直接对每个元素平方,然后用 System.arraycopy + Arrays.sort 排序。
复杂度 O(n log n),仅用于对照验证。
代码实现
public class Solution {
// 解法一:双指针(主解法,提交 LeetCode)
public int[] sortedSquares(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
for (int i = 0, j = n - 1, k = n - 1; i <= j; --k) {
int a = nums[i] * nums[i];
int b = nums[j] * nums[j];
if (a > b) {
ans[k] = a;
++i;
} else {
ans[k] = b;
--j;
}
}
return ans;
}
// 解法二:平方后排序(对照验证)
public int[] sortedSquaresSort(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
for (int i = 0; i < n; ++i) {
ans[i] = nums[i] * nums[i];
}
java.util.Arrays.sort(ans);
return ans;
}
private static void check(int[] nums, int[] expected, int caseId) {
int[] got1 = new Solution().sortedSquares(nums.clone());
int[] got2 = new Solution().sortedSquaresSort(nums.clone());
if (!java.util.Arrays.equals(got1, expected)
|| !java.util.Arrays.equals(got2, expected)) {
System.out.println("FAIL case" + caseId + " expected=" + java.util.Arrays.toString(expected)
+ " got1=" + java.util.Arrays.toString(got1)
+ " got2=" + java.util.Arrays.toString(got2));
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {-4, -1, 0, 3, 10}, new int[] {0, 1, 9, 16, 100}, 1);
check(new int[] {-7, -3, 2, 3, 11}, new int[] {4, 9, 9, 49, 121}, 2);
check(new int[] {1}, new int[] {1}, 3);
check(new int[] {0}, new int[] {0}, 4);
check(new int[] {-2, 0, 2}, new int[] {0, 4, 4}, 5);
check(new int[] {-5, -3, -1}, new int[] {1, 9, 25}, 6);
check(new int[] {-10, -5, 0, 5, 10}, new int[] {0, 25, 25, 100, 100}, 7);
System.out.println("== 0977 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n),左右指针各移动一次;
- 空间复杂度:O(1)(不含答案数组)。
解法二: - 时间复杂度:O(n log n);
- 空间复杂度:O(1)(就地修改)。
978. Longest Turbulent Subarray(最长湍流子数组)
题目描述
给定一个整数数组 arr,返回 arr 的最大湍流子数组的长度。
如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是湍流子数组。
更正式地来说,当 arr 的子数组 A[i], A[i+1], ..., A[j] 满足下列条件之一时,我们称其为湍流子数组:
- 若
i<=k<j:当k为奇数时,A[k] > A[k+1],且当k为偶数时,A[k] < A[k+1]; - 或若
i<=k<j:当k为偶数时,A[k] > A[k+1],且当k为奇数时,A[k] < A[k+1]。
示例 1
- 输入:
arr= [9,4,2,10,7,8,8,1,9] - 输出:5
- 解释:
arr[1]>arr[2]<arr[3]>arr[4]<arr[5]
示例 2
- 输入:
arr= [4,8,12,16] - 输出:2
示例 3
- 输入:
arr= [100] - 输出:1
提示: - 1 <= arr.length <= 4 * 10^4- 0 <= arr[i] <= 10^9
解题思路
解法一(动态规划,两个滚动变量):
湍流要求相邻比较符号严格交替。以位置 i 结尾时,只需区分「最后一步上升」与「最后一步下降」
两种状态的长度:
- up[i]:以 i 结尾且最后一段是 arr[i-1] < arr[i](上升)的最长湍流子数组长度;
- down[i]:以 i 结尾且最后一段是 arr[i-1] > arr[i](下降)的最长湍流子数组长度。
转移:若 arr[i-1] < arr[i],则 up[i] = down[i-1] + 1,否则 up[i] = 1;
若 arr[i-1] > arr[i],则 down[i] = up[i-1] + 1,否则 down[i] = 1。
由于只依赖前一个位置,用两个变量滚动即可,答案维护最大值。
解法二(滑动窗口 O(n^2) 校验,供对照):
枚举所有子数组 O(n^2) 检查是否湍流,不断更新最长长度。仅用于小数据对照验证。
代码实现
public class Solution {
// 解法一:动态规划(主解法,提交 LeetCode)
public int maxTurbulenceSize(int[] arr) {
int ans = 1, up = 1, down = 1;
for (int i = 1; i < arr.length; ++i) {
if (arr[i - 1] < arr[i]) {
up = down + 1;
down = 1;
} else if (arr[i - 1] > arr[i]) {
down = up + 1;
up = 1;
} else {
up = 1;
down = 1;
}
ans = Math.max(ans, Math.max(up, down));
}
return ans;
}
// 解法二:滑动窗口枚举校验(对照验证)
public int maxTurbulenceSizeBrute(int[] arr) {
int n = arr.length;
int best = 1;
for (int i = 0; i < n; ++i) {
boolean up = true, down = true; // 与初始状态(无符号)兼容
for (int j = i + 1; j < n; ++j) {
// 校验到 j-1 -> j 这一段能否续接
boolean valid = true;
int len = j - i + 1;
// 简化做法:动态扩展时检查最后一段与上一段是否相反
valid = extendOk(arr, i, j);
if (!valid) {
break;
}
best = Math.max(best, len);
}
}
return best;
}
private boolean extendOk(int[] arr, int i, int k) {
// 检查子数组 [i..k] 是否湍流(k > i)
for (int t = i + 1; t <= k; ++t) {
int prev = arr[t - 1], cur = arr[t];
int sign = prev < cur ? 1 : (prev > cur ? -1 : 0);
if (sign == 0) {
return false;
}
if (t - 1 > i) {
int prevSign = arr[t - 2] < prev ? 1 : -1;
if (prevSign == sign) {
return false;
}
}
}
return true;
}
private static void check(int[] arr, int expected, int caseId) {
int got1 = new Solution().maxTurbulenceSize(arr.clone());
int got2 = new Solution().maxTurbulenceSizeBrute(arr.clone());
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {9, 4, 2, 10, 7, 8, 8, 1, 9}, 5, 1);
check(new int[] {4, 8, 12, 16}, 2, 2);
check(new int[] {100}, 1, 3);
check(new int[] {1, 1, 1, 1}, 1, 4);
check(new int[] {1, 2, 1, 2, 1}, 5, 5);
check(new int[] {5, 4, 3, 2, 1}, 2, 6);
check(new int[] {2, 2, 1}, 2, 7);
System.out.println("== 0978 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n),单次线性扫描;
- 空间复杂度:O(1),仅两个滚动变量。
解法二: - 时间复杂度:O(n^2);
- 空间复杂度:O(1)。
979. Distribute Coins in Binary Tree(在二叉树中分配硬币)
题目描述
给你一个有 n 个结点的二叉树的根结点 root,其中树中每个结点 node 都对应有 node.val 枚硬币。
整棵树上一共有 n 枚硬币。
在一次移动中,我们可以选择两个相邻的结点,然后将一枚硬币从其中一个结点移动到另一个结点。
移动可以是从父结点到子结点,或者从子结点移动到父结点。
返回使每个结点上只有一枚硬币所需的最少移动次数。
示例 1
- 输入:
root= [3,0,0] - 输出:2
- 解释:一枚硬币从根结点移动到左子结点,一枚硬币从根结点移动到右子结点。
示例 2
- 输入:
root= [0,3,0] - 输出:3
- 解释:将两枚硬币从根结点的左子结点移动到根结点(两次移动),然后,将一枚硬币从根结点移动到右子结点。
提示: - 1 <= n <= 100- 0 <= Node.val <= n
解题思路
解法一(DFS,自底向上统计超载量):
边上每流过一枚硬币计一次移动。定义 dfs(node) 返回以 node 为根的子树中「金币数 - 结点数」
的盈余(正数表示多出来要向外流,负数表示需要从外面补入)。那么经过 node 到其父的这条边
的硬币数量正好就是 |溢出量|。后序遍历:先算左右子树的溢出 left、right,本次移动次数累加
|left| + |right|,然后返回 left + right + node.val - 1 给父结点。
解法二(迭代后序,供对照):
用两个栈做后序遍历(先访问顺序压栈),再按逆序处理每个结点,累加 |盈余|。
与解法一结果一致,避免递归栈。
代码实现
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public class Solution {
// 解法一:DFS 自底向上(主解法,提交 LeetCode)
private int moves;
public int distributeCoins(TreeNode root) {
moves = 0;
dfs(root);
return moves;
}
private int dfs(TreeNode node) {
if (node == null) {
return 0;
}
int left = dfs(node.left);
int right = dfs(node.right);
moves += Math.abs(left) + Math.abs(right);
return left + right + node.val - 1;
}
// 解法二:迭代后序(对照验证)
public int distributeCoinsIter(TreeNode root) {
java.util.Deque<TreeNode> stk = new java.util.ArrayDeque<>();
java.util.Deque<TreeNode> order = new java.util.ArrayDeque<>();
stk.push(root);
while (!stk.isEmpty()) {
TreeNode node = stk.pop();
order.push(node);
if (node.left != null) {
stk.push(node.left);
}
if (node.right != null) {
stk.push(node.right);
}
}
java.util.Map<TreeNode, Integer> surplus = new java.util.HashMap<>();
int ans = 0;
while (!order.isEmpty()) {
TreeNode node = order.pop();
int s = node.val - 1;
if (node.left != null) {
s += surplus.get(node.left);
ans += Math.abs(surplus.get(node.left));
}
if (node.right != null) {
s += surplus.get(node.right);
ans += Math.abs(surplus.get(node.right));
}
surplus.put(node, s);
}
return ans;
}
private static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode cur = q.poll();
if (arr[i] != null) {
cur.left = new TreeNode(arr[i]);
q.offer(cur.left);
}
++i;
if (i < arr.length && arr[i] != null) {
cur.right = new TreeNode(arr[i]);
q.offer(cur.right);
}
++i;
}
return root;
}
private static void check(Integer[] arr, int expected, int caseId) {
TreeNode root1 = build(arr);
TreeNode root2 = build(arr);
int got1 = new Solution().distributeCoins(root1);
int got2 = new Solution().distributeCoinsIter(root2);
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new Integer[] {3, 0, 0}, 2, 1);
check(new Integer[] {0, 3, 0}, 3, 2);
check(new Integer[] {1, 0, 2}, 2, 3);
check(new Integer[] {1, 0, 0, null, 3}, 4, 4);
check(new Integer[] {0, 2, 1}, 1, 5);
check(new Integer[] {2, 0, 1, null, null, 1, 1}, 1, 6);
check(new Integer[] {1}, 0, 7);
System.out.println("== 0979 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n),每个结点访问一次;
- 空间复杂度:O(h),递归深度,h 为树高。 解法二:
- 时间复杂度:O(n);
- 空间复杂度:O(n),栈空间。
980. Unique Paths III(不同路径 III)
题目描述
在二维网格 grid 上,有 4 种类型的方格:
- 1 表示起始方格,且只有一个起始方格;
- 2 表示结束方格,且只有一个结束方格;
- 0 表示我们可以走过的空方格;
- -1 表示我们无法跨越的障碍。 返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。 每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格。
示例 1
- 输入:[[1,0,0,0],[0,0,0,0],[0,0,2,-1]]
- 输出:2
示例 2
- 输入:[[1,0,0,0],[0,0,0,0],[0,0,0,2]]
- 输出:4
示例 3
- 输入:[[0,1],[2,0]]
- 输出:0
- 解释:没有一条路能完全穿过每一个空格一次。 提示:
- 1 <= grid.length * grid[0].length <= 20
解题思路
解法一(回溯 DFS): 题意是从起点出发、走过每一个空方格恰好一次、最后到达终点 2 的路径数。 格子总数至多 20,可回溯深搜。
- 先遍历网格统计空格数 cnt,记录起点坐标 (sx, sy);
- 从起点开始 DFS:标记当前格已访问,向四个方向扩展;若到达终点,当且仅当已访问过的 非障碍格数等于 cnt + 1 时计 1 条合法路径;
- 回溯时恢复标志。 解法二(状态压缩记忆化,供对照): 用 int 位掩码记录已访问空格位置,从起点出发做带记忆化的递归。由于每格状态只访问一次 (实际仍按格数级分支),与解法一结果一致,适合对照。
代码实现
public class Solution {
// 解法一:回溯 DFS(主解法,提交 LeetCode)
private int rowLen;
private int colLen;
private int[][] grid;
private boolean[][] vis;
private int need;
public int uniquePathsIII(int[][] grid) {
this.grid = grid;
rowLen = grid.length;
colLen = grid[0].length;
int sx = 0, sy = 0;
int empty = 0;
for (int i = 0; i < rowLen; ++i) {
for (int j = 0; j < colLen; ++j) {
if (grid[i][j] == 0) {
++empty;
} else if (grid[i][j] == 1) {
sx = i;
sy = j;
}
}
}
need = empty + 1; // 需要经过的非障碍格总数(含起点)
vis = new boolean[rowLen][colLen];
return dfs(sx, sy, 1);
}
private int dfs(int i, int j, int cnt) {
if (grid[i][j] == 2) {
return cnt == need + 1 ? 1 : 0;
}
int ways = 0;
vis[i][j] = true;
int[] dx = {-1, 0, 1, 0};
int[] dy = {0, 1, 0, -1};
for (int d = 0; d < 4; ++d) {
int nx = i + dx[d];
int ny = j + dy[d];
if (nx >= 0 && nx < rowLen && ny >= 0 && ny < colLen
&& !vis[nx][ny] && grid[nx][ny] != -1) {
ways += dfs(nx, ny, cnt + 1);
}
}
vis[i][j] = false;
return ways;
}
// 解法二:回溯 + 位掩码标记(对照验证)
private int rowLen2;
private int colLen2;
private int[][] grid2;
private int need2;
public int uniquePathsIII2(int[][] grid) {
this.grid2 = grid;
rowLen2 = grid.length;
colLen2 = grid[0].length;
int sx = 0, sy = 0;
int empty = 0;
for (int i = 0; i < rowLen2; ++i) {
for (int j = 0; j < colLen2; ++j) {
if (grid[i][j] == 0) {
++empty;
} else if (grid[i][j] == 1) {
sx = i;
sy = j;
}
}
}
need2 = empty + 1;
return dfs2(sx, sy, 1);
}
private int dfs2(int i, int j, int cnt) {
if (grid2[i][j] == 2) {
return cnt == need2 + 1 ? 1 : 0;
}
int ways = 0;
int old = grid2[i][j];
grid2[i][j] = -1; // 原地标记已访问
int[] dx = {-1, 0, 1, 0};
int[] dy = {0, 1, 0, -1};
for (int d = 0; d < 4; ++d) {
int nx = i + dx[d];
int ny = j + dy[d];
if (nx >= 0 && nx < rowLen2 && ny >= 0 && ny < colLen2
&& grid2[nx][ny] != -1) {
ways += dfs2(nx, ny, cnt + 1);
}
}
grid2[i][j] = old;
return ways;
}
private static int[][] cloneGrid(int[][] g) {
int[][] c = new int[g.length][];
for (int i = 0; i < g.length; ++i) {
c[i] = g[i].clone();
}
return c;
}
private static void check(int[][] g, int expected, int caseId) {
int got1 = new Solution().uniquePathsIII(cloneGrid(g));
int got2 = new Solution().uniquePathsIII2(cloneGrid(g));
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[][] {{1, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 2, -1}}, 2, 1);
check(new int[][] {{1, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 2}}, 4, 2);
check(new int[][] {{0, 1}, {2, 0}}, 0, 3);
check(new int[][] {{1, 2}}, 1, 4);
check(new int[][] {{1, 0, 2}}, 1, 5);
check(new int[][] {{1, -1, 2}}, 0, 6);
check(new int[][] {{1, 0}, {0, 2}}, 0, 7);
System.out.println("== 0980 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(3^(m*n)),每格至多 3 个有效分支;
- 空间复杂度:O(m*n),访问标志与递归栈。 解法二:
- 时间复杂度:O(3^(m*n));
- 空间复杂度:O(m*n)。
981. Time Based Key-Value Store(基于时间的键值存储)
题目描述
设计一个基于时间的键值数据结构,该结构可以在不同时间戳存储对应同一个键的多个值,并针对特定时间戳检索键对应的值。 实现 TimeMap 类:
- TimeMap() 初始化数据结构对象;
voidset(Stringkey,Stringvalue,inttimestamp) 存储给定时间戳timestamp时的键key和值value;Stringget(Stringkey,inttimestamp) 返回一个值,该值在之前调用了set,其中timestamp_prev<=timestamp。 如果有多个这样的值,它将返回与最大timestamp_prev关联的值。如果没有值,则返回空字符串 ""。
示例
- 输入:
["TimeMap", "
set", "get", "get", "set", "get", "get"] [[], ["foo", "bar", 1], ["foo", 1], ["foo", 3], ["foo", "bar2", 4], ["foo", 4], ["foo", 5]] - 输出:
[
null,null, "bar", "bar",null, "bar2", "bar2"] 提示: - 1 <= key.length, value.length <= 100-
key和value由小写英文字母和数字组成
- 1 <= timestamp <= 10^7-
set操作中的时间戳timestamp都是严格递增的
-
- 最多调用
set和 get 操作 2 * 10^5 次
- 最多调用
解题思路
解法一(哈希表 + TreeMap,二分上界): set 的时间戳严格递增,同一 key 的历史天然有序。get 要查到不超过给定时间戳的最近一次值。 用 HashMap<String, TreeMap<Integer,String>>:set 时插入 (timestamp, value);get 时用 floorKey(timestamp) 找到可用的最大时间戳,不存在则返回 ""。 解法二(哈希表 + 列表二分,供对照): 同一 key 维护 (timestamp, value) 的 ArrayList,由于时间戳严格递增,列表天然有序。 get 用二分找最后一个 timestamp <= 给定值的位置(upper bound 定位后取前一格)。
代码实现
public class Solution {
// ---------- 解法一:HashMap + TreeMap ----------
static class TimeMap {
private java.util.Map<String, java.util.TreeMap<Integer, String>> ktv =
new java.util.HashMap<>();
public TimeMap() {
}
public void set(String key, String value, int timestamp) {
ktv.computeIfAbsent(key, k -> new java.util.TreeMap<>()).put(timestamp, value);
}
public String get(String key, int timestamp) {
java.util.TreeMap<Integer, String> tm = ktv.get(key);
if (tm == null) {
return "";
}
Integer t = tm.floorKey(timestamp);
return t == null ? "" : tm.get(t);
}
}
// ---------- 解法二:HashMap + List + 二分 ----------
static class TimeMap2b {
private java.util.Map<String, java.util.List<Pair>> ktv = new java.util.HashMap<>();
static class Pair {
int ts;
String val;
Pair(int ts, String val) {
this.ts = ts;
this.val = val;
}
}
public TimeMap2b() {
}
public void set(String key, String value, int timestamp) {
ktv.computeIfAbsent(key, k -> new java.util.ArrayList<>())
.add(new Pair(timestamp, value));
}
public String get(String key, int timestamp) {
java.util.List<Pair> list = ktv.get(key);
if (list == null) {
return "";
}
int lo = 0, hi = list.size() - 1, ans = -1;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
if (list.get(mid).ts <= timestamp) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans == -1 ? "" : list.get(ans).val;
}
}
private static void runCase1() {
TimeMap tm = new TimeMap();
tm.set("foo", "bar", 1);
if (!tm.get("foo", 1).equals("bar")) { System.out.println("FAIL t1a"); return; }
if (!tm.get("foo", 3).equals("bar")) { System.out.println("FAIL t1b"); return; }
tm.set("foo", "bar2", 4);
if (!tm.get("foo", 4).equals("bar2")) { System.out.println("FAIL t1c"); return; }
if (!tm.get("foo", 5).equals("bar2")) { System.out.println("FAIL t1d"); return; }
System.out.println("ok case1");
}
private static void runCase2() {
TimeMap2b tm = new TimeMap2b();
tm.set("foo", "bar", 1);
if (!tm.get("foo", 1).equals("bar")) { System.out.println("FAIL t2a"); return; }
if (!tm.get("foo", 3).equals("bar")) { System.out.println("FAIL t2b"); return; }
tm.set("foo", "bar2", 4);
if (!tm.get("foo", 4).equals("bar2")) { System.out.println("FAIL t2c"); return; }
if (!tm.get("foo", 5).equals("bar2")) { System.out.println("FAIL t2d"); return; }
System.out.println("ok case2");
}
private static void runCase3() {
TimeMap tm = new TimeMap();
tm.set("love", "high", 10);
tm.set("love", "low", 20);
if (!tm.get("love", 5).equals("")) { System.out.println("FAIL t3a"); return; }
if (!tm.get("love", 10).equals("high")) { System.out.println("FAIL t3b"); return; }
if (!tm.get("love", 15).equals("high")) { System.out.println("FAIL t3c"); return; }
if (!tm.get("love", 20).equals("low")) { System.out.println("FAIL t3d"); return; }
if (!tm.get("love", 25).equals("low")) { System.out.println("FAIL t3e"); return; }
System.out.println("ok case3");
}
private static void runCase4() {
TimeMap2b tm = new TimeMap2b();
if (!tm.get("nokey", 5).equals("")) { System.out.println("FAIL t4a"); return; }
tm.set("a", "v1", 7);
if (!tm.get("a", 6).equals("")) { System.out.println("FAIL t4b"); return; }
System.out.println("ok case4");
}
private static void runCase5() {
TimeMap tm = new TimeMap();
tm.set("a", "1", 1);
tm.set("a", "2", 2);
tm.set("a", "3", 3);
if (!tm.get("a", 2).equals("2")) { System.out.println("FAIL t5a"); return; }
if (!tm.get("a", 0).equals("")) { System.out.println("FAIL t5b"); return; }
System.out.println("ok case5");
}
public static void main(String[] args) {
runCase1();
runCase2();
runCase3();
runCase4();
runCase5();
System.out.println("== 0981 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:set O(1),get O(log n)(TreeMap 树查找);
- 空间复杂度:O(n),n 为 set 调用总次数。 解法二:
- 时间复杂度:set O(1)(追加),get O(log n)(二分);
- 空间复杂度:O(n)。
982. Triples with Bitwise AND Equal To Zero(按位与为零的三元组)
题目描述
给你一个整数数组 nums,返回其中按位与三元组的数目。
按位与三元组是由下标 (i, j, k) 组成的三元组,并满足下述全部条件:
0 <= i < nums.length0 <= j < nums.length0 <= k < nums.lengthnums[i]&nums[j]&nums[k]== 0,其中 & 表示按位与运算符。
示例 1
- 输入:
nums= [2,1,3] - 输出:12
示例 2
- 输入:
nums= [0,0,0] - 输出:27 提示:
- 1 <= nums.length <= 1000- 0 <= nums[i] < 2^16
解题思路
解法一(枚举前两个 + 计数): 直接三重循环 O(n^3) 在 n=1000 时约为 10^9 次,不可行。值域 < 2^16,两数与值也在 [0, 2^16) 内。
- 先枚举所有 (x, y),统计 cnt[x & y] 出现次数(O(n^2));
- 再枚举 xy 与 z,若 (xy & z) == 0,则所有产生 xy 的 (x, y) 对都可行,累加 cnt[xy]。 由于 x、y、z 可重复取同一下标,直接把计数结果相加即可覆盖。 解法二(暴力三重循环小数据对照): 仅用于验证小数据:直接三重循环统计满足条件的 (i, j, k) 数量。
代码实现
public class Solution {
// 解法一:枚举两两与值 + 计数(主解法,提交 LeetCode)
public int countTriplets(int[] nums) {
int mx = 0;
for (int x : nums) {
mx = Math.max(mx, x);
}
int[] cnt = new int[mx + 1];
for (int x : nums) {
for (int y : nums) {
++cnt[x & y];
}
}
int ans = 0;
for (int xy = 0; xy <= mx; ++xy) {
if (cnt[xy] == 0) {
continue;
}
for (int z : nums) {
if ((xy & z) == 0) {
ans += cnt[xy];
}
}
}
return ans;
}
// 解法二:暴力三重循环(对照验证)
public int countTripletsBrute(int[] nums) {
int n = nums.length;
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
for (int k = 0; k < n; ++k) {
if ((nums[i] & nums[j] & nums[k]) == 0) {
++ans;
}
}
}
}
return ans;
}
private static void check(int[] nums, int expected, int caseId) {
int got1 = new Solution().countTriplets(nums.clone());
int got2 = new Solution().countTripletsBrute(nums.clone());
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {2, 1, 3}, 12, 1);
check(new int[] {0, 0, 0}, 27, 2);
check(new int[] {1}, 0, 3);
check(new int[] {0}, 1, 4);
check(new int[] {1, 1}, 0, 5);
check(new int[] {7, 8}, 6, 6);
System.out.println("== 0982 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n^2 + 2^16 * n);
- 空间复杂度:O(2^16),cnt 计数数组。 解法二:
- 时间复杂度:O(n^3);
- 空间复杂度:O(1)。
983. Minimum Cost For Tickets(最低票价)
题目描述
在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,
你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。
火车票有三种不同的销售方式:
- 一张为期一天的通行证售价为 costs[0] 美元;
- 一张为期七天的通行证售价为 costs[1] 美元;
- 一张为期三十天的通行证售价为 costs[2] 美元。
通行证允许数天无限制的旅行。
返回你想要完成在给定的列表
days中列出的每一天的旅行所需要的最低消费。
示例 1
- 输入:
days= [1,4,6,7,8,20], costs = [2,7,15] - 输出:11
示例 2
- 输入:
days= [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15] - 输出:17 提示:
- 1 <= days.length <= 365- 1 <= days[i] <= 365-
days按顺序严格递增
- costs.length == 3- 1 <= costs[i] <= 1000
解题思路
解法一(记忆化搜索 + 二分): 在第 i 个出行日,可以购买 1/7/30 天票中的任意一种,购买后覆盖从该日起的若干天, 下一次需要重新购票的出行日可用二分定位。定义 dfs(i) 为从第 i 次出行到最后一次 出行所需的最小花费: dfs(i) = min(costs[t] + dfs(nextIndex(days[i] + valid[t]))) for t in 0..2 用数组 f 记忆化。 解法二(日历日 DP): 记 m = days 末尾日期。f[i] 表示第 1 天到第 i 天的最小花费:
- 第 i 天不旅行:f[i] = f[i-1];
- 第 i 天旅行:f[i] = min(f[max(0,i-1)] + costs[0], f[max(0,i-7)] + costs[1], f[max(0,i-30)] + costs[2])。 用一个指针 j 同步指向当前旅行日,答案 f[m]。
代码实现
public class Solution {
// 解法一:记忆化搜索 + 二分(主解法,提交 LeetCode)
private int[] days;
private int[] costs;
private Integer[] memo;
public int mincostTickets(int[] days, int[] costs) {
this.days = days;
this.costs = costs;
memo = new Integer[days.length];
return dfs(0);
}
private int dfs(int i) {
if (i >= days.length) {
return 0;
}
if (memo[i] != null) {
return memo[i];
}
int best = Integer.MAX_VALUE;
int[] valid = {1, 7, 30};
for (int t = 0; t < 3; ++t) {
int j = lowerBound(days[i] + valid[t]);
best = Math.min(best, costs[t] + dfs(j));
}
memo[i] = best;
return best;
}
private int lowerBound(int x) {
int lo = 0, hi = days.length;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (days[mid] >= x) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
// 解法二:日历日 DP(对照验证)
public int mincostTicketsDP(int[] days, int[] costs) {
int m = days[days.length - 1];
int[] f = new int[m + 1];
int[] valid = {1, 7, 30};
int j = 0;
for (int i = 1; i <= m; ++i) {
if (i == days[j]) {
f[i] = Integer.MAX_VALUE;
for (int t = 0; t < 3; ++t) {
f[i] = Math.min(f[i], f[Math.max(0, i - valid[t])] + costs[t]);
}
++j;
} else {
f[i] = f[i - 1];
}
}
return f[m];
}
private static void check(int[] days, int[] costs, int expected, int caseId) {
int got1 = new Solution().mincostTickets(days, costs);
int got2 = new Solution().mincostTicketsDP(days.clone(), costs.clone());
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {1, 4, 6, 7, 8, 20}, new int[] {2, 7, 15}, 11, 1);
check(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 30, 31}, new int[] {2, 7, 15}, 17, 2);
check(new int[] {1}, new int[] {2, 7, 15}, 2, 3);
check(new int[] {1, 365}, new int[] {2, 7, 15}, 4, 4);
check(new int[] {1, 2, 3, 4, 5, 6, 7}, new int[] {2, 7, 15}, 7, 5);
check(new int[] {1, 2, 3, 4, 5, 6, 7, 8}, new int[] {2, 7, 15}, 9, 6);
check(new int[] {6, 8, 9, 18, 20, 21, 23, 25}, new int[] {2, 10, 321}, 16, 7);
System.out.println("== 0983 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n log n),每次转移二分;
- 空间复杂度:O(n),记忆化数组。 解法二:
- 时间复杂度:O(m),m 为最后旅行日(<= 365);
- 空间复杂度:O(m)。
984. String Without AAA or BBB(不含 AAA 或 BBB 的字符串)
题目描述
给定两个整数 a 和 b,返回任意字符串 s,要求满足:
s的长度为a+b,且正好包含a个 'a' 字母与b个 'b' 字母;- 子串 'aaa' 没有出现在
s中; - 子串 'bbb' 没有出现在
s中。
示例 1
- 输入:
a= 1,b= 2 - 输出:"abb"
- 解释:"abb", "bab" 和 "bba" 都是正确答案。
示例 2
- 输入:
a= 4,b= 1 - 输出:"aabaa" 提示:
- 0 <= a, b <= 100-
- 对于给定的
a和b,保证存在满足要求的s
- 对于给定的
解题思路
解法一(贪心构造): 较多的字母应优先成对输出,并夹一个较少的字母,避免单个字母连续出现 3 次。 循环直到 a、b 有一个为 0:
- 若 a > b:追加 "aab",a -= 2, b -= 1;
- 若 b > a:追加 "bba",a -= 1, b -= 2;
- 若 a == b:追加 "ab",各自减 1。 结束后剩余未消耗的字母数量最多为 1 个(因为保证存在解),补上即可。 解法二(逐字符贪心,供对照): 每次选择当前位置要放的字母:优先放剩余更多者,但如果连续两个位置已经放了它, 则必须换放另一个。直到所有字母用完。与解法一构造结果可能不同但都合法。
代码实现
public class Solution {
// 解法一:贪心块构造(主解法,提交 LeetCode)
public String strWithout3a3b(int a, int b) {
StringBuilder sb = new StringBuilder();
while (a > 0 && b > 0) {
if (a > b) {
sb.append("aab");
a -= 2;
--b;
} else if (a < b) {
sb.append("bba");
--a;
b -= 2;
} else {
sb.append("ab");
--a;
--b;
}
}
while (a-- > 0) {
sb.append('a');
}
while (b-- > 0) {
sb.append('b');
}
return sb.toString();
}
// 解法二:逐字符贪心(对照验证)
public String strWithout3a3bGreedy(int a, int b) {
StringBuilder sb = new StringBuilder();
while (a > 0 || b > 0) {
boolean lastTwoA = sb.length() >= 2
&& sb.charAt(sb.length() - 1) == 'a'
&& sb.charAt(sb.length() - 2) == 'a';
boolean lastTwoB = sb.length() >= 2
&& sb.charAt(sb.length() - 1) == 'b'
&& sb.charAt(sb.length() - 2) == 'b';
if (lastTwoA) {
// 必须放 b(题目保证有解时 b>0)
sb.append('b');
--b;
} else if (lastTwoB) {
sb.append('a');
--a;
} else if (a >= b) {
sb.append('a');
--a;
} else {
sb.append('b');
--b;
}
}
return sb.toString();
}
private static boolean valid(String s, int a, int b) {
int ca = 0, cb = 0;
for (char c : s.toCharArray()) {
if (c == 'a') {
++ca;
} else {
++cb;
}
}
return ca == a && cb == b
&& !s.contains("aaa") && !s.contains("bbb")
&& s.length() == a + b;
}
private static void check(int a, int b, int caseId) {
String got1 = new Solution().strWithout3a3b(a, b);
String got2 = new Solution().strWithout3a3bGreedy(a, b);
if (!valid(got1, a, b) || !valid(got2, a, b)) {
System.out.println("FAIL case" + caseId + " a=" + a + " b=" + b
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId + " s=" + got1);
}
public static void main(String[] args) {
check(1, 2, 1);
check(4, 1, 2);
check(5, 3, 3);
check(2, 2, 4);
check(0, 2, 5); // a=0 时只有 "bb" 一种情况(保证有解约束下)
check(2, 0, 6);
check(7, 7, 7);
System.out.println("== 0984 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(a + b),循环次数与总长度成正比;
- 空间复杂度:O(1)(不含返回串)。 解法二:
- 时间复杂度:O(a + b);
- 空间复杂度:O(1)。
985. Sum of Even Numbers After Queries(查询后的偶数和)
题目描述
给出一个整数数组 A 和一个查询数组 queries。
对于第 i 次查询,有 val = queries[i][0], index = queries[i][1],我们会把 val 加到 A[index] 上。
然后,第 i 次查询的答案是 A 中偶数值的和。
(此处给定的 index = queries[i][1] 是从 0 开始的索引,每次查询都会永久修改数组 A。)
返回所有查询的答案。你的答案应当以数组 answer 给出,answer[i] 为第 i 次查询的答案。
示例
- 输入:A = [1,2,3,4], queries = [[1,0],[-3,1],[-4,0],[2,3]]
- 输出:[8,6,2,4] 提示:
- 1 <= A.length <= 10000- -10000 <= A[i] <= 10000- 1 <= queries.length <= 10000- -10000 <= queries[i][0] <= 10000- 0 <= queries[i][1] < A.length
解题思路
解法一(维护偶数和 + 增量更新): 若每次查询后重扫数组求和,代价 O(n*m)。维护当前所有偶数的和 s:
- 初始先求一次所有偶数和;
- 对每次查询 (v, i):若 A[i] 原本是偶数,先从 s 中减去 A[i]; A[i] += v;若新 A[i] 是偶数,再加回 s;
- 记录当前 s。 正确性:一次查询只改变 A[i] 一个元素,其他偶数贡献不变。 解法二(每次查询重扫,供对照): 模拟题意直接做。仅用于小数据对照验证。
代码实现
public class Solution {
// 解法一:维护偶数和增量更新(主解法,提交 LeetCode)
public int[] sumEvenAfterQueries(int[] nums, int[][] queries) {
int s = 0;
for (int x : nums) {
if ((x & 1) == 0) {
s += x;
}
}
int m = queries.length;
int[] ans = new int[m];
for (int k = 0; k < m; ++k) {
int v = queries[k][0];
int i = queries[k][1];
if ((nums[i] & 1) == 0) {
s -= nums[i];
}
nums[i] += v;
if ((nums[i] & 1) == 0) {
s += nums[i];
}
ans[k] = s;
}
return ans;
}
// 解法二:每次查询重扫求和(对照验证)
public int[] sumEvenAfterQueriesBrute(int[] nums, int[][] queries) {
int m = queries.length;
int[] ans = new int[m];
int[] a = nums.clone();
for (int k = 0; k < m; ++k) {
int v = queries[k][0];
int i = queries[k][1];
a[i] += v;
int s = 0;
for (int x : a) {
if ((x & 1) == 0) {
s += x;
}
}
ans[k] = s;
}
return ans;
}
private static void check(int[] nums, int[][] queries, int[] expected, int caseId) {
int[] n1 = nums.clone();
int[] n2 = nums.clone();
int[] got1 = new Solution().sumEvenAfterQueries(n1, queries);
int[] got2 = new Solution().sumEvenAfterQueriesBrute(n2, queries);
if (!java.util.Arrays.equals(got1, expected)
|| !java.util.Arrays.equals(got2, expected)) {
System.out.println("FAIL case" + caseId + " expected=" + java.util.Arrays.toString(expected)
+ " got1=" + java.util.Arrays.toString(got1)
+ " got2=" + java.util.Arrays.toString(got2));
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {1, 2, 3, 4}, new int[][] {{1, 0}, {-3, 1}, {-4, 0}, {2, 3}},
new int[] {8, 6, 2, 4}, 1);
check(new int[] {1}, new int[][] {{4, 0}}, new int[] {0}, 2);
check(new int[] {2}, new int[][] {{1, 0}}, new int[] {0}, 3);
check(new int[] {2, 2, 2}, new int[][] {{0, 0}, {2, 1}, {-2, 0}},
new int[] {6, 8, 6}, 4);
check(new int[] {1, 3, 5}, new int[][] {{1, 0}, {1, 1}, {1, 2}},
new int[] {2, 6, 12}, 5);
check(new int[] {-2, 4, -6}, new int[][] {{2, 0}, {3, 1}, {6, 2}},
new int[] {-2, -6, 0}, 6);
System.out.println("== 0985 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n + m),n 为 A 长度,m 为查询次数;
- 空间复杂度:O(1)(不含答案数组)。 解法二:
- 时间复杂度:O(n * m);
- 空间复杂度:O(1)。
986. Interval List Intersections(区间列表的交集)
题目描述
给定两个由一些闭区间组成的列表,firstList 和 secondList,其中 firstList[i] = [start_i, end_i]
而 secondList[j] = [start_j, end_j]。每个区间列表都是成对不相交的,并且已经排序。
返回这两个区间列表的交集。
形式上,闭区间 [a, b](其中 a <= b)表示实数 x 的集合,而 a <= x <= b。
两个闭区间的交集是一组实数,要么为空集,要么为闭区间。例如,[1, 3] 和 [2, 4] 的交集为 [2, 3]。
示例 1
- 输入:
firstList= [[0,2],[5,10],[13,23],[24,25]],secondList= [[1,5],[8,12],[15,24],[25,26]] - 输出:[[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]
示例 2
- 输入:
firstList= [[1,3],[5,9]],secondList= [] - 输出:[] 提示:
- 0 <= firstList.length, secondList.length <= 1000- firstList.length + secondList.length >= 1- 0 <= start_i < end_i <= 10^9-
end_i<start_{i+1}
解题思路
解法一(双指针): 两个列表各自已按端点排序且内部不相交。用 i、j 分别指向 firstList 与 secondList 的当前区间:
- 交集左端 l = max(start1, start2),右端 r = min(end1, end2);
- 若 l <= r 则记录 [l, r];
- 右端点较小的那个区间不可能再与当前另一个区间相交,指针前进一步。 解法二(穷举配对,供对照): 双重循环枚举所有区间对求交集,O(n*m),仅用于小数据对照验证。
代码实现
public class Solution {
// 解法一:双指针(主解法,提交 LeetCode)
public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
java.util.List<int[]> ans = new java.util.ArrayList<>();
int m = firstList.length, n = secondList.length;
for (int i = 0, j = 0; i < m && j < n; ) {
int l = Math.max(firstList[i][0], secondList[j][0]);
int r = Math.min(firstList[i][1], secondList[j][1]);
if (l <= r) {
ans.add(new int[] {l, r});
}
if (firstList[i][1] < secondList[j][1]) {
++i;
} else {
++j;
}
}
return ans.toArray(new int[ans.size()][]);
}
// 解法二:穷举配对(对照验证)
public int[][] intervalIntersectionBrute(int[][] a, int[][] b) {
java.util.List<int[]> ans = new java.util.ArrayList<>();
for (int[] x : a) {
for (int[] y : b) {
int l = Math.max(x[0], y[0]);
int r = Math.min(x[1], y[1]);
if (l <= r) {
ans.add(new int[] {l, r});
}
}
}
return ans.toArray(new int[ans.size()][]);
}
private static void check(int[][] a, int[][] b, int[][] expected, int caseId) {
int[][] got1 = new Solution().intervalIntersection(a, b);
int[][] got2 = new Solution().intervalIntersectionBrute(a, b);
if (!java.util.Arrays.deepEquals(got1, expected)
|| !java.util.Arrays.deepEquals(got2, expected)) {
System.out.println("FAIL case" + caseId + " expected=" + java.util.Arrays.deepToString(expected)
+ " got1=" + java.util.Arrays.deepToString(got1)
+ " got2=" + java.util.Arrays.deepToString(got2));
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[][] {{0, 2}, {5, 10}, {13, 23}, {24, 25}},
new int[][] {{1, 5}, {8, 12}, {15, 24}, {25, 26}},
new int[][] {{1, 2}, {5, 5}, {8, 10}, {15, 23}, {24, 24}, {25, 25}}, 1);
check(new int[][] {{1, 3}, {5, 9}}, new int[][] {}, new int[][] {}, 2);
check(new int[][] {}, new int[][] {{4, 8}, {10, 12}}, new int[][] {}, 3);
check(new int[][] {{1, 7}}, new int[][] {{3, 10}}, new int[][] {{3, 7}}, 4);
check(new int[][] {{1, 2}}, new int[][] {{1, 2}}, new int[][] {{1, 2}}, 5);
check(new int[][] {{0, 5}}, new int[][] {{2, 3}}, new int[][] {{2, 3}}, 6);
System.out.println("== 0986 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(m + n),每个区间接入常数次;
- 空间复杂度:O(1)(不含答案数组)。 解法二:
- 时间复杂度:O(m * n);
- 空间复杂度:O(1)。
987. Vertical Order Traversal of a Binary Tree(二叉树的垂序遍历)
题目描述
给你二叉树的根结点 root,请你设计算法计算二叉树的垂序遍历序列。
对位于 (row, col) 的每个结点而言,其左右子结点分别位于 (row + 1, col - 1) 和 (row + 1, col + 1)。
树的根结点位于 (0, 0)。
二叉树的垂序遍历从最左边的列开始直到最右边的列结束,按列索引每一列上的所有结点,
形成一个按出现位置从上到下排序的有序列表。如果同行同列上有多个结点,则按结点的值从小到大进行排序。
返回二叉树的垂序遍历序列。
示例 1
- 输入:
root= [3,9,20,null,null,15,7] - 输出:[[9],[3,15],[20],[7]]
示例 2
- 输入:
root= [1,2,3,4,5,6,7] - 输出:[[4],[2],[1,5,6],[3],[7]]
- 解释:列 0 上结点 1、5、6 都在此列,1 在上面所以出现在前面;5 和 6 位置都是 (2,0), 按值从小到大排序,5 在 6 的前面。 提示:
-
- 树中结点数目总数在范围 [1, 1000] 内
- 0 <= Node.val <= 1000
解题思路
解法一(DFS + 全局排序): 题目要求先按列(col)升序,再按行(row)升序,同行同列按值升序。 深度优先遍历收集每个结点的 (col, row, val),然后按 (col, row, val) 三关键字排序, 最后把相同 col 的节点归并为一个列表。 解法二(BFS + 按列分组排序,供对照): 层序遍历天然保证 row 递增,按列放入列表,最后对每列内部按 (row, val) 排序。 与解法一结果一致。
代码实现
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public class Solution {
// 解法一:DFS + 全局排序(主解法,提交 LeetCode)
private java.util.List<int[]> nodes = new java.util.ArrayList<>();
public java.util.List<java.util.List<Integer>> verticalTraversal(TreeNode root) {
nodes.clear();
dfs(root, 0, 0);
java.util.Collections.sort(nodes, (a, b) -> {
if (a[0] != b[0]) {
return Integer.compare(a[0], b[0]);
}
if (a[1] != b[1]) {
return Integer.compare(a[1], b[1]);
}
return Integer.compare(a[2], b[2]);
});
java.util.List<java.util.List<Integer>> ans = new java.util.ArrayList<>();
int prevCol = Integer.MIN_VALUE;
for (int[] node : nodes) {
if (node[0] != prevCol) {
prevCol = node[0];
ans.add(new java.util.ArrayList<>());
}
ans.get(ans.size() - 1).add(node[2]);
}
return ans;
}
private void dfs(TreeNode node, int row, int col) {
if (node == null) {
return;
}
nodes.add(new int[] {col, row, node.val});
dfs(node.left, row + 1, col - 1);
dfs(node.right, row + 1, col + 1);
}
// 解法二:BFS + 按列分组(对照验证)
public java.util.List<java.util.List<Integer>> verticalTraversalBFS(TreeNode root) {
java.util.Map<Integer, java.util.List<int[]>> map = new java.util.TreeMap<>();
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
java.util.Queue<Integer> cols = new java.util.LinkedList<>();
q.offer(root);
cols.offer(0);
int row = 0;
while (!q.isEmpty()) {
int size = q.size();
for (int t = 0; t < size; ++t) {
TreeNode node = q.poll();
int col = cols.poll();
map.computeIfAbsent(col, k -> new java.util.ArrayList<>()).add(new int[] {row, node.val});
if (node.left != null) {
q.offer(node.left);
cols.offer(col - 1);
}
if (node.right != null) {
q.offer(node.right);
cols.offer(col + 1);
}
}
++row;
}
java.util.List<java.util.List<Integer>> ans = new java.util.ArrayList<>();
for (java.util.List<int[]> list : map.values()) {
java.util.Collections.sort(list, (a, b) ->
a[0] != b[0] ? Integer.compare(a[0], b[0]) : Integer.compare(a[1], b[1]));
java.util.List<Integer> colList = new java.util.ArrayList<>();
for (int[] p : list) {
colList.add(p[1]);
}
ans.add(colList);
}
return ans;
}
private static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode cur = q.poll();
if (arr[i] != null) {
cur.left = new TreeNode(arr[i]);
q.offer(cur.left);
}
++i;
if (i < arr.length && arr[i] != null) {
cur.right = new TreeNode(arr[i]);
q.offer(cur.right);
}
++i;
}
return root;
}
private static void check(Integer[] arr, Integer[][] expected, int caseId) {
TreeNode r1 = build(arr);
TreeNode r2 = build(arr);
java.util.List<java.util.List<Integer>> got1 = new Solution().verticalTraversal(r1);
java.util.List<java.util.List<Integer>> got2 = new Solution().verticalTraversalBFS(r2);
java.util.List<java.util.List<Integer>> exp = new java.util.ArrayList<>();
for (Integer[] row : expected) {
exp.add(java.util.Arrays.asList(row));
}
if (!got1.equals(exp) || !got2.equals(exp)) {
System.out.println("FAIL case" + caseId + " expected=" + exp
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new Integer[] {3, 9, 20, null, null, 15, 7},
new Integer[][] {{9}, {3, 15}, {20}, {7}}, 1);
check(new Integer[] {1, 2, 3, 4, 5, 6, 7},
new Integer[][] {{4}, {2}, {1, 5, 6}, {3}, {7}}, 2);
check(new Integer[] {1, 2, 3, 4, 6, 5, 7},
new Integer[][] {{4}, {2}, {1, 5, 6}, {3}, {7}}, 3);
check(new Integer[] {1}, new Integer[][] {{1}}, 4);
check(new Integer[] {1, 2}, new Integer[][] {{2}, {1}}, 5);
System.out.println("== 0987 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n log n),一次排序;
- 空间复杂度:O(n),存储节点三元组。 解法二:
- 时间复杂度:O(n log n),各列内部排序;
- 空间复杂度:O(n)。
988. Smallest String Starting From Leaf(从叶结点开始的最小字符串)
题目描述
给定一颗根结点为 root 的二叉树,树中的每一个结点都有一个 [0, 25] 范围内的值,
分别代表字母 'a' 到 'z'。
返回按字典序最小的字符串,该字符串从这棵树的一个叶结点开始,到根结点结束。
注:字符串中任何较短的前缀在字典序上都是较小的。例如,在字典序上 "ab" 比 "aba" 要小。
叶结点是指没有子结点的结点。
示例 1
- 输入:
root= [0,1,2,3,4,3,4] - 输出:"dba"
示例 2
- 输入:
root= [25,1,3,1,3,0,2] - 输出:"adz"
示例 3
- 输入:
root= [2,2,1,null,1,0,null,0] - 输出:"abc" 提示:
-
- 给定树的结点数在 [1, 8500] 范围内
- 0 <= Node.val <= 25
解题思路
解法一(DFS 回溯): 每个叶结点到根的路径顺次可拼成一个字符串(叶到根)。DFS 从根出发沿路径累积字符, 到达叶结点时把当前路径反转得到叶→根字符串,与全局最小答案比较并更新。 用 StringBuilder 存根→叶路径,递归回溯时删末字符。 解法二(自底向上 DP,供对照): 对每个结点计算「从该结点向下到某个叶子的最小字符串(叶到该结点)」: best(node) = min(best(left), best(right)) + char(node),叶结点则只含自身字符。 比较字符串时注意字典序语义:两个从叶到根字符串比较,短前缀更小。
代码实现
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public class Solution {
// 解法一:DFS 回溯(主解法,提交 LeetCode)
private String best;
public String smallestFromLeaf(TreeNode root) {
best = null;
dfs(root, new StringBuilder());
return best;
}
private void dfs(TreeNode node, StringBuilder sb) {
sb.append((char) ('a' + node.val));
if (node.left == null && node.right == null) {
String s = sb.reverse().toString();
if (best == null || s.compareTo(best) < 0) {
best = s;
}
sb.reverse(); // 转回根→叶方向
} else {
if (node.left != null) {
dfs(node.left, sb);
}
if (node.right != null) {
dfs(node.right, sb);
}
}
sb.deleteCharAt(sb.length() - 1);
}
// 解法二:自底向上 DP(对照验证)
public String smallestFromLeafDP(TreeNode root) {
return dp(root);
}
private String dp(TreeNode node) {
char c = (char) ('a' + node.val);
if (node.left == null && node.right == null) {
return String.valueOf(c);
}
String left = node.left != null ? dp(node.left) : null;
String right = node.right != null ? dp(node.right) : null;
String choose;
if (left == null) {
choose = right;
} else if (right == null) {
choose = left;
} else {
choose = left.compareTo(right) <= 0 ? left : right;
}
return choose + c;
}
private static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode cur = q.poll();
if (arr[i] != null) {
cur.left = new TreeNode(arr[i]);
q.offer(cur.left);
}
++i;
if (i < arr.length && arr[i] != null) {
cur.right = new TreeNode(arr[i]);
q.offer(cur.right);
}
++i;
}
return root;
}
private static void check(Integer[] arr, String expected, int caseId) {
TreeNode r1 = build(arr);
TreeNode r2 = build(arr);
String got1 = new Solution().smallestFromLeaf(r1);
String got2 = new Solution().smallestFromLeafDP(r2);
if (!got1.equals(expected) || !got2.equals(expected)) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new Integer[] {0, 1, 2, 3, 4, 3, 4}, "dba", 1);
check(new Integer[] {25, 1, 3, 1, 3, 0, 2}, "adz", 2);
check(new Integer[] {2, 2, 1, null, 1, 0, null, 0}, "abc", 3);
check(new Integer[] {0}, "a", 4);
check(new Integer[] {1, 0, 2}, "ab", 5); // 左叶 a + 根 b = "ab";右叶 c + 根 b = "cb" → 最小 "ab"
System.out.println("== 0988 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n * h),h 为树高,每次到叶反转字符串 O(h);
- 空间复杂度:O(h),递归栈与路径。 解法二:
- 时间复杂度:O(n * h),每结点拼接字符串;
- 空间复杂度:O(n * h)(保存各结点子串)。