LeetCode976~988题目解析

4 阅读8分钟

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,可回溯深搜。

  1. 先遍历网格统计空格数 cnt,记录起点坐标 (sx, sy);
  2. 从起点开始 DFS:标记当前格已访问,向四个方向扩展;若到达终点,当且仅当已访问过的 非障碍格数等于 cnt + 1 时计 1 条合法路径;
  3. 回溯时恢复标志。 解法二(状态压缩记忆化,供对照): 用 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() 初始化数据结构对象;
  • void set(String key, String value, int timestamp) 存储给定时间戳 timestamp 时的键 key 和值 value;
  • String get(String key, int timestamp) 返回一个值,该值在之前调用了 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.length
  • 0 <= j < nums.length
  • 0 <= k < nums.length
  • nums[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) 内。

  1. 先枚举所有 (x, y),统计 cnt[x & y] 出现次数(O(n^2));
  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:

  1. 初始先求一次所有偶数和;
  2. 对每次查询 (v, i):若 A[i] 原本是偶数,先从 s 中减去 A[i]; A[i] += v;若新 A[i] 是偶数,再加回 s;
  3. 记录当前 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)(保存各结点子串)。