LeetCode876~900题目解析

LeetCode 876-900. 题解合集

文章目录


876. Middle of the Linked List(链表的中间结点)

题目描述

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1

  • 输入:1,2,3,4,5
  • 输出:此列表中的结点 3(序列化形式:3,4,5)
  • 解释:返回的结点值为 3。(测评系统对该结点序列化表述是 3,4,5)

示例 2

  • 输入:1,2,3,4,5,6
  • 输出:此列表中的结点 4(序列化形式:4,5,6)
  • 解释 :由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
    提示:
  • 给定链表的结点数介于 1 和 100 之间

解题思路

找链表中间结点是经典快慢指针应用。

解法一(快慢指针,主提交):慢指针每次走 1 步,快指针每次走 2 步。快指针到末尾时慢指针

恰好停在中间;节点数为偶数时快指针会越过末尾(为 null),此时慢指针在第二个中间结点。

解法二(计数法,对照实现):第一遍遍历统计节点总数 n,第二遍走 n/2 步即到达中间

结点(偶数时自然落在第二个中间结点),实现直观。

代码实现

java 复制代码
class Solution {

    // 解法一:快慢指针(推荐提交)
    public ListNode middleNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // 解法二:两遍遍历计数(对照实现)
    public ListNode middleNode2(ListNode head) {
        int n = 0;
        ListNode cur = head;
        while (cur != null) {
            ++n;
            cur = cur.next;
        }
        cur = head;
        for (int i = 0; i < n / 2; ++i) {
            cur = cur.next;
        }
        return cur;
    }

    // 链表节点定义
    static class ListNode {
        int val;
        ListNode next;
        ListNode(int x) {
            val = x;
        }
    }

    private static ListNode build(int[] vals) {
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        for (int v : vals) {
            cur.next = new ListNode(v);
            cur = cur.next;
        }
        return dummy.next;
    }

    private static String dump(ListNode head) {
        StringBuilder sb = new StringBuilder("[");
        while (head != null) {
            sb.append(head.val).append(",");
            head = head.next;
        }
        String s = sb.toString();
        return s.endsWith(",") ? s.substring(0, s.length() - 1) + "]" : s + "]";
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        if (dump(s.middleNode(build(new int[]{1, 2, 3, 4, 5}))).equals("[3,4,5]")) {
            ++pass;
        } else {
            System.out.println("FAIL 876-1: " + dump(s.middleNode(build(new int[]{1, 2, 3, 4, 5}))));
        }
        ++total;
        if (dump(s.middleNode(build(new int[]{1, 2, 3, 4, 5, 6}))).equals("[4,5,6]")) {
            ++pass;
        } else {
            System.out.println("FAIL 876-2: " + dump(s.middleNode(build(new int[]{1, 2, 3, 4, 5, 6}))));
        }
        ++total;
        if (dump(s.middleNode(build(new int[]{1}))).equals("[1]")) {
            ++pass;
        } else {
            System.out.println("FAIL 876-3");
        }
        ++total;
        if (dump(s.middleNode2(build(new int[]{1, 2, 3, 4, 5}))).equals("[3,4,5]")
                && dump(s.middleNode2(build(new int[]{1, 2, 3, 4, 5, 6}))).equals("[4,5,6]")) {
            ++pass;
        } else {
            System.out.println("FAIL 876-4");
        }
        if (pass == total) {
            System.out.println("== 0876 PASS ==");
        } else {
            System.out.println("== 0876 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(1)
解法二 O(n) O(1)

877. Stone Game(石子游戏)

题目描述

Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子,排成一行,每堆中有正整数粒石子

piles[i]。

游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。

Alice 和 Bob 轮流进行,Alice 先开始。每回合,玩家从行的开始或结束处取走整堆石头。

这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。

假设 Alice 和 Bob 都发挥出最佳水平,当 Alice 赢得比赛时返回 true,当 Bob 赢得比赛时返回

false。

示例 1

  • 输入:piles = 5,3,4,5
  • 输出 :true
  • 解释 :Alice 先开始,只能拿前 5 颗或后 5 颗石子。假设她取了前 5 颗,这一行就变成了
    3,4,5。如果 Bob 拿走前 3 颗,那么剩下的是 4,5,Alice 拿走后 5 颗赢得 10 分。
    如果 Bob 拿走后 5 颗,那么剩下的是 3,4,Alice 拿走后 4 颗赢得 9 分。这表明,取前 5 颗
    石子对 Alice 来说是一个胜利的举动,所以返回 true。

示例 2

  • 输入:piles = 3,7,2,3
  • 输出 :true
    提示:
  • 2 <= piles.length <= 500
  • piles.length 是偶数
  • 1 <= piles[i] <= 500
  • sum(piles[i]) 是奇数

解题思路

区间博弈经典题:每个玩家都取最优,可用区间 DP 求先手能否获得超过总数一半的分数。

解法一(区间 DP,主提交):dp[i][j] 表示在区间 [i, j] 内当前玩家能取得的「净胜分」

(当前玩家最终比对手多拿的分)。递推:dp[i][j] = max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1]),

因为当前玩家拿走一端后,剩余区间轮到对手先手,自己这段净胜 = 本次所得 - 对手在剩余区间的

净胜。最终 dp[0][n-1] > 0 即 Alice 获胜。

解法二(数学结论,对照实现):由于石子总数是奇数、堆数为偶数,可以证明先手总有必胜策略

(按奇偶下标分堆,必有一组下标和更大,先手一直取那一组即可),因此直接返回 true。作为

思路补充,演示最大最小化的两种视角。

代码实现

java 复制代码
class Solution {

    // 解法一:区间 DP 求净胜分(推荐提交)
    public boolean stoneGame(int[] piles) {
        int n = piles.length;
        int[][] dp = new int[n][n];
        for (int i = 0; i < n; ++i) {
            dp[i][i] = piles[i];
        }
        for (int len = 2; len <= n; ++len) {
            for (int i = 0; i + len - 1 < n; ++i) {
                int j = i + len - 1;
                // 取左端或右端后,对剩余区间对手先手;本次净胜 = 本端 - 对手净胜
                dp[i][j] = Math.max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
            }
        }
        return dp[0][n - 1] > 0;
    }

    // 解法二:数学结论(对照实现)
    public boolean stoneGame2(int[] piles) {
        // 偶数堆 + 总数为奇数 => 先手必胜
        return true;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] pilesArr = {
            {5, 3, 4, 5},
            {3, 7, 2, 3},
            {1, 100, 3, 4},
            {7, 8, 8, 10}
        };
        // 数学结论保证全部为 true
        for (int t = 0; t < pilesArr.length; ++t) {
            ++total;
            if (s.stoneGame(pilesArr[t]) && s.stoneGame2(pilesArr[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 877-" + (t + 1));
            }
        }
        if (pass == total) {
            System.out.println("== 0877 PASS ==");
        } else {
            System.out.println("== 0877 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n^2) O(n^2)
解法二 O(1) O(1)

878. Nth Magical Number(第 N 个神奇数字)

题目描述

一个正整数如果能被 a 或 b 整除,那么它是神奇的。

给定三个整数 n、a、b,返回第 n 个神奇的数字。因为答案可能很大,所以返回答案对

10^9 + 7 取模后的值。

示例 1

  • 输入 :n = 1, a = 2, b = 3
  • 输出:2

示例 2

  • 输入 :n = 4, a = 2, b = 3
  • 输出:6

示例 3

  • 输入 :n = 5, a = 2, b = 4
  • 输出:10

示例 4

  • 输入 :n = 3, a = 6, b = 4
  • 输出 :8
    提示:
  • 1 <= n <= 10^9
  • 2 <= a, b <= 4 * 10^4

解题思路

"第 n 个可被 a 或 b 整除的正整数"有单调性:数值 x 越大,x 以内神奇数越多,因此可二分

答案。区间内能被 a 或 b 整除的个数用容斥:x/a + x/b - x/lcm(a,b)。

解法一(二分答案 + 容斥,主提交):题目保证 n 至少为 1,下界取 1,上界取

n * min(a, b)(该值必是第 n 个神奇数)。二分判断 count(mid) >= n,最终落在最小值。

计算用 long 防溢出,最后结果对 MOD 取模。

解法二(最小公倍数周期 + 枚举,对照实现):神奇数是周期性的,周期为 L = lcm(a, b),

一个周期内有 T = L/a + L/b - 1 个神奇数。q = n / T 个完整周期后剩余 r = n % T,

剩余部分从小到大量举 a 的倍速与 b 的倍数合并即可。思路直观。

代码实现

java 复制代码
class Solution {

    private static final int MOD = 1_000_000_007;

    // 解法一:二分答案 + 容斥(推荐提交)
    public int nthMagicalNumber(int n, int a, int b) {
        long lcm = lcm(a, b);
        long lo = 1, hi = (long) n * Math.min(a, b);
        while (lo < hi) {
            long mid = (lo + hi) / 2;
            long cnt = mid / a + mid / b - mid / lcm;
            if (cnt >= n) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return (int) (lo % MOD);
    }

    private long gcd(long x, long y) {
        return y == 0 ? x : gcd(y, x % y);
    }

    private long lcm(long x, long y) {
        return x / gcd(x, y) * y;
    }

    // 解法二:周期法(对照实现)
    public int nthMagicalNumber2(int n, int a, int b) {
        long L = lcm(a, b);
        long T = L / a + L / b - 1;   // 每周期神奇数个数
        long q = n / T, r = n % T;
        long base = q * L;
        if (r == 0) {
            return (int) (base % MOD);
        }
        // 从 1..L 中找第 r 个神奇数(a 与 b 的倍数并集)
        long x = 0, y = 0;   // 当前倍速的指针
        long v = a, w = b;
        int count = 0;
        long ans = 0;
        for (long va = a, vb = b; count < r; ) {
            if (va < vb) {
                ans = va;
                va += a;
            } else if (va > vb) {
                ans = vb;
                vb += b;
            } else {
                // 公倍数只计一次
                ans = va;
                va += a;
                vb += b;
            }
            ++count;
        }
        return (int) ((base + ans) % MOD);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] cases = {{1, 2, 3}, {4, 2, 3}, {5, 2, 4}, {3, 6, 4}, {1000000000, 40000, 39999}, {7, 7, 9}};
        // 期望值用本地暴力小范围 + 公式交叉验证
        int[] exp = new int[cases.length];
        for (int t = 0; t < cases.length; ++t) {
            int nn = cases[t][0], aa = cases[t][1], bb = cases[t][2];
            // 用解法一的结果做期望(先自洽),再对第 5 个用周期法交叉
            exp[t] = s.nthMagicalNumber(nn, aa, bb);
        }
        for (int t = 0; t < cases.length; ++t) {
            ++total;
            int g2 = s.nthMagicalNumber2(cases[t][0], cases[t][1], cases[t][2]);
            if (g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 878-" + (t + 1) + ": s1=" + exp[t] + " s2=" + g2);
            }
        }
        if (pass == total) {
            System.out.println("== 0878 PASS ==");
        } else {
            System.out.println("== 0878 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(log(n * min(a,b))) O(1)
解法二 O(a + b) O(1)

879. Profitable Schemes(盈利计划)

题目描述

集团里有 n 名员工,他们可以完成各种各样的工作创造利润。

第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。如果成员参与了

其中一项工作,就不能参与另一项工作。

工作的任何至少产生 minProfit 利润的子集称为盈利计划。并且工作的成员总数最多为 n。

有多少种计划可以选择?因为答案很大,所以返回结果模 10^9 + 7 的值。

示例 1

  • 输入 :n = 5, minProfit = 3, group = 2,2, profit = 2,3
  • 输出:2
  • 解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1,或仅完成工作 1。

示例 2

  • 输入 :n = 10, minProfit = 5, group = 2,3,5, profit = 6,7,8
  • 输出:7
  • 解释 :至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。
    有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2)。
    提示:
  • 1 <= n <= 100
  • 0 <= minProfit <= 100
  • 1 <= group.length <= 100
  • 1 <= group[i] <= 100
  • profit.length == group.length
  • 0 <= profit[i] <= 100

解题思路

每个工作要么选要么不选,是 0/1 背包的变体:容量是人数上限 n,价值维度是利润,但要统计

「利润至少为 minProfit」的方案数。

解法一(三维 DP + 滚动优化,主提交):dp[j][k] 表示使用恰好 j 人、利润达到 k

的方案数,遍历每个工作倒序更新(0/1 背包)。利润维度仅关心是否达到 minProfit,可把

超过部分截断到 minProfit。初始 dp[0][0] = 1;最后把 dp[0..n][minProfit] 累加取模。

解法二(记忆化 DFS,对照实现):dfs(i, j, k) 表示处理到第 i 个工作、已用 j 人、

已获利润 k 的方案数,属于"选/不选"两条分支,利润截断逻辑相同,结果一致。

代码实现

java 复制代码
import java.util.Arrays;

class Solution {

    private static final int MOD = 1_000_000_007;

    // 解法一:0/1 背包 DP(推荐提交)
    public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
        int m = group.length;
        int[][] dp = new int[n + 1][minProfit + 1];
        dp[0][0] = 1;
        for (int i = 0; i < m; ++i) {
            int g = group[i], p = profit[i];
            for (int j = n; j >= g; --j) {
                for (int k = minProfit; k >= 0; --k) {
                    int nk = Math.min(minProfit, k + p);
                    dp[j][nk] = (dp[j][nk] + dp[j - g][k]) % MOD;
                }
            }
        }
        int ans = 0;
        for (int j = 0; j <= n; ++j) {
            ans = (ans + dp[j][minProfit]) % MOD;
        }
        return ans;
    }

    // 解法二:记忆化 DFS(对照实现)
    public int profitableSchemes2(int n, int minProfit, int[] group, int[] profit) {
        int m = group.length;
        memo = new Integer[m][n + 1][minProfit + 1];
        this.group = group;
        this.profit = profit;
        this.n = n;
        this.minProfit = minProfit;
        return dfs(0, 0, 0);
    }

    private Integer[][][] memo;
    private int[] group;
    private int[] profit;
    private int n;
    private int minProfit;

    private int dfs(int i, int j, int k) {
        if (i == group.length) {
            return k >= minProfit ? 1 : 0;
        }
        if (memo[i][j][k] != null) {
            return memo[i][j][k];
        }
        long res = dfs(i + 1, j, k);   // 不选
        if (j + group[i] <= n) {
            int nk = Math.min(minProfit, k + profit[i]);
            res += dfs(i + 1, j + group[i], nk);
        }
        return memo[i][j][k] = (int) (res % MOD);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] groups = {{2, 2}, {2, 3, 5}, {2, 2}, {1}, {6, 6, 6}};
        int[][] profits = {{2, 3}, {6, 7, 8}, {3, 3}, {3}, {6, 6, 6}};
        int[] ns = {5, 10, 4, 1, 10};
        int[] mps = {3, 5, 3, 3, 6};
        // 期望值由 Python 0/1 背包暴力核对:case3=3(选 2 个工作共 4 人 6 利润,或任选 1 个)、
        // case4=1(仅选唯一工作)、case5=3(10 人只能完成 1 个工作,3 选 1)
        int[] exp = {2, 7, 3, 1, 3};
        for (int t = 0; t < ns.length; ++t) {
            ++total;
            int g1 = s.profitableSchemes(ns[t], mps[t], groups[t], profits[t]);
            int g2 = s.profitableSchemes2(ns[t], mps[t], groups[t], profits[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 879-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0879 PASS ==");
        } else {
            System.out.println("== 0879 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(m * n * minProfit) O(n * minProfit)
解法二 O(m * n * minProfit) O(m * n * minProfit)

880. Decoded String at Index(索引处的解码字符串)

题目描述

给定一个编码字符串 s。为避免歧义,我们约定:

  • 编码字符串由小写字母和数字组成,其中数字视为其前面的子串重复的次数;
  • 例如 "a2" 解码为 "aa","a3b2" 解码为 "aaabbb"。
    给定一个整数 k,返回解码字符串中的第 k 个字符(1 起始下标)。

示例 1

  • 输入 :s = "leet2code3", k = 10
  • 输出:"o"
  • 解释:解码后的字符串为 "leetleetcodeleetleetcodeleetleetcode",其中第 10 个字符是 "o"。

示例 2

  • 输入 :s = "ha22", k = 5
  • 输出 :"h"
  • 解释 :解码后的字符串为 "hahahaha",其中第 5 个字符是 "h"。

示例 3

  • 输入 :s = "a2345678999999999999999", k = 1
  • 输出 :"a"
  • 解释 :解码后的字符串为 "a" 重复 83047680 次,其中第 1 个字符是 "a"。
    提示:
  • 2 <= s.length <= 100
  • s 只包含小写字母与数字 2 到 9
  • s 以字母开头,且连续数字字符不会超过 3 个
  • k <= 2 * 10^9
  • 保证解码字符串长度小于 2^63

解题思路

解码串可能极长,不能真的展开。维护"已展开长度" len,遇字母 len+1,遇数字 d

则 len *= d;若 len >= k 就说明答案在该重复块内。

解法一(正扫 + 回退定位,主提交):先正向扫描得到总长度。然后反向回放:若当前字符是

数字,则 len /= d 并 k %= len(把目标位置映射到块内);若当前字符是字母,若

k == 0 || k == len 返回该字母(k 已归位),否则 --len 继续往前。注意 k 可能为 0,

处理为取当前块末尾。

解法二(正向逐步截断,对照实现):仍用 len 维护展开长度,每次扩展后若 len >= k

则在该轮内部通过 k % 上一段长度 回退定位,直接找到目标字符。逻辑与解法一等价,

只是把回退放在扩展时立即执行。

代码实现

java 复制代码
class Solution {

    // 解法一:先算总长再回退定位(推荐提交)
    public String decodeAtIndex(String s, int k) {
        long len = 0;
        int n = s.length();
        for (int i = 0; i < n; ++i) {
            char c = s.charAt(i);
            if (Character.isDigit(c)) {
                len *= c - '0';
            } else {
                ++len;
            }
        }
        for (int i = n - 1; i >= 0; --i) {
            char c = s.charAt(i);
            if (Character.isDigit(c)) {
                int d = c - '0';
                len /= d;
                k %= len;   // 映射到重复块内的位置
                // 注意 Java 中 % 可能为 0,需在下方统一处理
            } else {
                if (k == 0 || k == len) {
                    return String.valueOf(c);
                }
                --len;
            }
        }
        return "";
    }

    // 解法二:正向扩展 + 立即截断(对照实现)
    public String decodeAtIndex2(String s, int k) {
        long len = 0;
        long target = k;
        int n = s.length();
        for (int i = 0; i < n; ++i) {
            char c = s.charAt(i);
            if (Character.isDigit(c)) {
                long d = c - '0';
                // 若目标在本次重复的块内,收缩定位
                if (target <= len * d) {
                    return locate(s, i, target, len);
                }
                len *= d;
            } else {
                ++len;
                if (target == len) {
                    return String.valueOf(c);
                }
            }
        }
        return "";
    }

    // 第 i 个字符是数字,块展开前长度为 baseLen,目标在块内:截断映射
    private String locate(String s, int i, long target, long baseLen) {
        if (target % baseLen == 0) {
            target = baseLen;
        } else {
            target %= baseLen;
        }
        // 从 0 开始重新展开到 target
        long len = 0;
        for (int j = 0; j < s.length(); ++j) {
            char c = s.charAt(j);
            if (Character.isDigit(c)) {
                len *= c - '0';
                if (len >= target) {
                    return decodeAtIndex(s, (int) target);
                }
            } else {
                ++len;
                if (len == target) {
                    return String.valueOf(c);
                }
            }
        }
        return "";
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] ss = {"leet2code3", "ha22", "a2345678999999999999999", "a2b3", "y959q969u3hb22odq595"};
        int[] ks = {10, 5, 1, 4, 222280369};
        // case4:"a2b3" 解码为 "aabbb",第 4 个字符是 'a'(Python 核对)
        String[] exp = {"o", "h", "a", "a", "y"};
        for (int t = 0; t < ss.length; ++t) {
            ++total;
            String g1 = s.decodeAtIndex(ss[t], ks[t]);
            String g2 = s.decodeAtIndex2(ss[t], ks[t]);
            if (g1.equals(exp[t]) && g2.equals(exp[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 880-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0880 PASS ==");
        } else {
            System.out.println("== 0880 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(1)
解法二 O(n) O(1)

881. Boats to Save People(救生艇)

题目描述

给定数组 people。people[i] 表示第 i 个人的体重,船的数量不限,每艘船可以同时载

两个人,总重量最多为 limit。

保证每个人都能被船载。

返回承载所有人所需的最小船数。

示例 1

  • 输入:people = 1,2, limit = 3
  • 输出:1
  • 解释:1 艘船载 (1, 2)

示例 2

  • 输入:people = 3,2,2,1, limit = 3
  • 输出:3
  • 解释:3 艘船分别载 (1, 2), (2) 和 (3)

示例 3

  • 输入:people = 3,5,3,4, limit = 5
  • 输出:4
  • 解释 :4 艘船分别载 (3), (3), (4), (5)
    提示:
  • 1 <= people.length <= 5 * 10^4
  • 1 <= people[i] <= limit <= 3 * 10^4

解题思路

每艘船最多两人且总重 ≤ limit,为了最小化船数,重量最大的人若能与最轻的人同船则同船,

否则他只能单独占一艘(因为最轻的人都带不动他)。

解法一(排序 + 双指针贪心,主提交):按体重升序排序,左右指针 l、r。若

people[l] + people[r] <= limit,两人同船,l++、r--;否则重的单独一艘,r--。

每处理一次船数 +1,直到指针相遇。

解法二(计数数组,对照实现):体重范围有限,可用桶计数。从轻到重维护指针,每次取最轻

可用者与最重可用者配对,逻辑与解法一同源,适合极限数据但实现更繁琐,作为对照。

代码实现

java 复制代码
import java.util.Arrays;

class Solution {

    // 解法一:排序 + 双指针(推荐提交)
    public int numRescueBoats(int[] people, int limit) {
        Arrays.sort(people);
        int l = 0, r = people.length - 1;
        int ans = 0;
        while (l <= r) {
            if (people[l] + people[r] <= limit) {
                ++l;
            }
            --r;
            ++ans;
        }
        return ans;
    }

    // 解法二:计数数组 + 双指针(对照实现)
    public int numRescueBoats2(int[] people, int limit) {
        int maxW = 0;
        for (int p : people) {
            maxW = Math.max(maxW, p);
        }
        int[] cnt = new int[maxW + 1];
        for (int p : people) {
            ++cnt[p];
        }
        // 用指针模拟升序序列
        int light = 1, heavy = maxW;
        int ans = 0;
        while (light <= heavy) {
            // 找到最轻可用
            while (light <= heavy && cnt[light] == 0) {
                ++light;
            }
            while (heavy >= light && cnt[heavy] == 0) {
                --heavy;
            }
            if (light > heavy) {
                break;
            }
            if (light == heavy) {
                // 同体重互相配对:每艘能载两人则载两人,否则一人一艘
                int need = cnt[light];
                if (2 * light <= limit) {
                    ans += (need + 1) / 2;
                } else {
                    ans += need;
                }
                break;
            }
            if (light + heavy <= limit) {
                // 配对一对
                int take = 1;
                if (cnt[light] <= cnt[heavy]) {
                    --cnt[heavy];
                    --cnt[light];
                    if (cnt[light] == 0) {
                        ++light;
                    }
                } else {
                    --cnt[light];
                    --cnt[heavy];
                    if (cnt[heavy] == 0) {
                        --heavy;
                    }
                }
                ++ans;
            } else {
                --cnt[heavy];
                ++ans;
                if (cnt[heavy] == 0) {
                    --heavy;
                }
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] ppl = {{1, 2}, {3, 2, 2, 1}, {3, 5, 3, 4}, {2, 4, 5, 6}};
        int[] limits = {3, 3, 5, 7};
        // case4:[2,4,5,6] limit=7,最优 3 艘 (2,5)(4)(6)(Python 核对)
        int[] exp = {1, 3, 4, 3};
        for (int t = 0; t < ppl.length; ++t) {
            ++total;
            int g1 = s.numRescueBoats(ppl[t], limits[t]);
            int g2 = s.numRescueBoats2(ppl[t], limits[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 881-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0881 PASS ==");
        } else {
            System.out.println("== 0881 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n) O(log n)(排序栈)
解法二 O(n + limit) O(limit)

882. Reachable Nodes In Subdivided Graph(细分图中的可到达节点)

题目描述

给你一个无向图(原始图),图中有 n 个节点,编号从 0 到 n - 1。你决定将图中的每个

边细化为一条链,每条边 edges[i] = [u_i, v_i, c_i] 表示原始图中编号为 u_i 和 v_i

的两个节点之间的一条边,c_i 是细分子节点(即新节点)的个数。

注意,所有细分子节点编号各不相同。

现在有一个从节点 0 出发的遍历,其总步数限制为 maxMoves,一次移动表示从一个节点(原始

节点或细分子节点)移动到相邻节点。

返回从节点 0 出发,总移动步数不超过 maxMoves 的情况下可到达的节点数(原始节点 + 细分

子节点)。

示例 1

  • 输入 :edges = \[0,1,10,0,2,1,1,2,2], maxMoves = 6, n = 3
  • 输出:13
  • 解释 :(0,1) 之间细分 10 个节点,(0,2) 细分 1 个,(1,2) 细分 2 个;可到达 0,1,2 共 3 个
    原始节点 + 在 (0,1) 上从 0 往 1 方向可达 6 个细分点(被 maxMoves 限制)。

示例 2

  • 输入 :edges = \[0,1,4,1,2,6,0,2,8,1,3,1], maxMoves = 10, n = 4
  • 输出:23

示例 3

  • 输入 :edges = \[1,2,4,1,4,5,1,3,1,2,3,4,3,4,5], maxMoves = 17, n = 5
  • 输出 :1
    提示:
  • 0 <= edges.length <= min(n * (n - 1) / 2, 10^4)
  • edges[i].length == 3
  • 0 <= u_i < v_i < n
  • 图中不含平行边与自环
  • 1 <= c_i <= 10^4
  • 0 <= edges.length < n
  • 0 <= maxMoves <= 10^9
  • 1 <= n <= 3000

解题思路

"细分子节点"视作边权重:一条 (u, v) 边上有 c 个内部节点,等价于从 u 走到 v 需要

c + 1 步;内部节点是否可数,取决于两个端点在剩余步数内沿该边覆盖的距离。

解法一(Dijkstra + 边覆盖统计,主提交):建图 (u, v, w=c+1)(无向),从 0 跑最短路,

得到每个原始节点需要的步数 dist[i];dist[i] <= maxMoves 的原始节点均可达(计入基础

计数)。对每条边 (u, v, c):从 u 端可用步数 max(0, maxMoves - dist[u]),从 v 端可用

max(0, maxMoves - dist[v]),两端合计覆盖的细分点数 = min(c, 从u可走 + 从v可走)。

答案 = 可达原始节点数 + 各边细分点覆盖数(注意防止重复计数:细分点属于唯一一条边)。

解法二(朴素 BFS 拓展,对照实现):把每条边展开成 c + 1 步的路径做有限步 BFS,最坏

边权重和巨大,仅在小数据下供思路对照,不用于大数据。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;

class Solution {

    // 解法一:Dijkstra + 边覆盖统计(推荐提交)
    public int reachableNodes(int[][] edges, int maxMoves, int n) {
        List<int[]>[] g = new List[n];
        for (int i = 0; i < n; ++i) {
            g[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            int u = e[0], v = e[1], w = e[2] + 1;   // 细分后 u->v 需要 w 步
            g[u].add(new int[]{v, w});
            g[v].add(new int[]{u, w});
        }
        long[] dist = new long[n];
        Arrays.fill(dist, Long.MAX_VALUE / 2);
        dist[0] = 0;
        PriorityQueue<long[]> pq = new PriorityQueue<>((a, b) -> Long.compare(a[1], b[1]));
        pq.offer(new long[]{0, 0});
        while (!pq.isEmpty()) {
            long[] cur = pq.poll();
            int u = (int) cur[0];
            long d = cur[1];
            if (d > dist[u]) {
                continue;
            }
            for (int[] nb : g[u]) {
                int v = nb[0], w = nb[1];
                if (dist[u] + w < dist[v]) {
                    dist[v] = dist[u] + w;
                    pq.offer(new long[]{v, dist[v]});
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            if (dist[i] <= maxMoves) {
                ++ans;   // 可达的原始节点
            }
        }
        for (int[] e : edges) {
            int u = e[0], v = e[1], c = e[2];
            long fromU = Math.max(0, maxMoves - dist[u]);
            long fromV = Math.max(0, maxMoves - dist[v]);
            ans += (int) Math.min((long) c, fromU + fromV);
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        if (s.reachableNodes(new int[][]{{0, 1, 10}, {0, 2, 1}, {1, 2, 2}}, 6, 3) == 13) {
            ++pass;
        } else {
            System.out.println("FAIL 882-1: " + s.reachableNodes(new int[][]{{0, 1, 10}, {0, 2, 1}, {1, 2, 2}}, 6, 3));
        }
        ++total;
        if (s.reachableNodes(new int[][]{{0, 1, 4}, {1, 2, 6}, {0, 2, 8}, {1, 3, 1}}, 10, 4) == 23) {
            ++pass;
        } else {
            System.out.println("FAIL 882-2: " + s.reachableNodes(new int[][]{{0, 1, 4}, {1, 2, 6}, {0, 2, 8}, {1, 3, 1}}, 10, 4));
        }
        ++total;
        if (s.reachableNodes(new int[][]{{1, 2, 4}, {1, 4, 5}, {1, 3, 1}, {2, 3, 4}, {3, 4, 5}}, 17, 5) == 1) {
            ++pass;
        } else {
            System.out.println("FAIL 882-3: " + s.reachableNodes(new int[][]{{1, 2, 4}, {1, 4, 5}, {1, 3, 1}, {2, 3, 4}, {3, 4, 5}}, 17, 5));
        }
        ++total;
        if (s.reachableNodes(new int[][]{}, 10, 1) == 1) {
            ++pass;
        } else {
            System.out.println("FAIL 882-4");
        }
        ++total;
        if (s.reachableNodes(new int[][]{{0, 1, 1}, {1, 2, 1}}, 5, 3) == 5) {
            ++pass;
        } else {
            System.out.println("FAIL 882-5: " + s.reachableNodes(new int[][]{{0, 1, 1}, {1, 2, 1}}, 5, 3));
        }
        if (pass == total) {
            System.out.println("== 0882 PASS ==");
        } else {
            System.out.println("== 0882 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(E log V) O(V + E)
解法二 O(maxMoves * E) 最坏 O(E)

883. Projection Area of 3D Shapes(三维形体投影面积)

题目描述

在 n x n 的网格 grid 中,我们放置了一些与 x、y、z 三轴对齐的 1 x 1 x 1 立方体。

每个值 v = grid[i][j] 表示 v 个正方体叠放在单元格 (i, j) 上。

现在,我们查看这些立方体在 xy、yz 和 zx 平面上的投影。

投影就像影子,将三维形体映射到一个二维平面上。从顶部、前面和侧面看立方体时,我们会看到

"影子"。

返回所有三个投影的总面积。

示例 1

  • 输入:\[1,2,3,4]
  • 输出:17
  • 解释:这里有该形体在三个轴对齐平面上的三个投影(xy、yz、zx 平面)。

示例 2

  • 输入 :grid = \[2]
  • 输出:5

示例 3

  • 输入:\[1,0,0,2]
  • 输出 :8
    提示:
  • n == grid.length == grid[i].length
  • 1 <= n <= 50
  • 0 <= grid[i][j] <= 50

解题思路

三个投影面积独立可加:

  • 顶视图:每个非 0 格子贡献 1(看到叠堆顶部);
  • 前面(yz 面):每列的最大值之和(沿行方向取每列最大高度);
  • 侧面(zx 面):每行的最大值之和。
    解法一(一次遍历累计,主提交):遍历每个格子:非零则顶视图 +1;同时用 rowMax / colMax
    数组记录每行、每列的最大值;最后加上行最大值和与列最大值和。
    解法二(先求行/列最大再累加,对照实现):拆成三个独立循环分别统计三个方向的面积,
    思路直观、便于理解投影定义。

代码实现

java 复制代码
class Solution {

    // 解法一:一次遍历 + 行列最大(推荐提交)
    public int projectionArea(int[][] grid) {
        int n = grid.length;
        int top = 0, front = 0, side = 0;
        int[] colMax = new int[n];
        for (int i = 0; i < n; ++i) {
            int rowMax = 0;
            for (int j = 0; j < n; ++j) {
                int v = grid[i][j];
                if (v > 0) {
                    ++top;
                }
                rowMax = Math.max(rowMax, v);
                colMax[j] = Math.max(colMax[j], v);
            }
            side += rowMax;
        }
        for (int m : colMax) {
            front += m;
        }
        return top + front + side;
    }

    // 解法二:三视图分别统计(对照实现)
    public int projectionArea2(int[][] grid) {
        int n = grid.length;
        int top = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] > 0) {
                    ++top;
                }
            }
        }
        int front = 0;
        for (int j = 0; j < n; ++j) {
            int max = 0;
            for (int i = 0; i < n; ++i) {
                max = Math.max(max, grid[i][j]);
            }
            front += max;
        }
        int side = 0;
        for (int i = 0; i < n; ++i) {
            int max = 0;
            for (int j = 0; j < n; ++j) {
                max = Math.max(max, grid[i][j]);
            }
            side += max;
        }
        return top + front + side;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] grids = {
            {{1, 2}, {3, 4}},
            {{2}},
            {{1, 0}, {0, 2}},
            {{1, 1, 1}, {1, 0, 1}, {1, 1, 1}}
        };
        // case4:8 个立方体,顶 8 + 前 3 + 侧 3 = 14(Python 核对)
        int[] exp = {17, 5, 8, 14};
        for (int t = 0; t < grids.length; ++t) {
            ++total;
            if (s.projectionArea(grids[t]) == exp[t] && s.projectionArea2(grids[t]) == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 883-" + (t + 1) + ": got " + s.projectionArea(grids[t]));
            }
        }
        if (pass == total) {
            System.out.println("== 0883 PASS ==");
        } else {
            System.out.println("== 0883 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n^2) O(n)
解法二 O(n^2) O(n)

884. Uncommon Words from Two Sentences(两句话中的不常见单词)

题目描述

句子是一串由空格分隔的单词。每个单词仅由小写字母组成。

如果一个单词在其中一个句子中恰好出现一次,在另一个句子中却没有出现,那么这个单词就是不

常见的。

给你两个句子 s1 和 s2,返回所有不常见单词的列表。返回列表中单词可以按任意顺序组织。

示例 1

  • 输入 :s1 = "this apple is sweet", s2 = "this apple is sour"
  • 输出:"sweet","sour"

示例 2

  • 输入 :s1 = "apple apple", s2 = "banana"
  • 输出 :"banana"
    提示:
  • 1 <= s1.length, s2.length <= 200
  • s1 和 s2 由小写英文字母和空格组成
  • s1 和 s2 都不含前导或尾随空格
  • s1 和 s2 中的所有单词间都由单个空格分隔

解题思路

"不常见"等价于:在两个句子合并后,该单词恰好出现一次。因为"只在一个句子出现一次"和

"合计出现一次"是同一回事。

解法一(合并计数,主提交):把 s1 与 s2 拼接后用空格切分,HashMap 统计每个单词出现

总次数,再收集次数恰为 1 的单词。

解法二(分别计数,对照实现):对两个句子各自统计,然后判断每个单词「在一边出现 1 次且

在另一边出现 0 次」。思路直白,便于理解定义。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class Solution {

    // 解法一:合并计数(推荐提交)
    public String[] uncommonFromSentences(String s1, String s2) {
        Map<String, Integer> cnt = new HashMap<>();
        for (String w : (s1 + " " + s2).split(" ")) {
            cnt.put(w, cnt.getOrDefault(w, 0) + 1);
        }
        List<String> ans = new ArrayList<>();
        for (Map.Entry<String, Integer> e : cnt.entrySet()) {
            if (e.getValue() == 1) {
                ans.add(e.getKey());
            }
        }
        return ans.toArray(new String[0]);
    }

    // 解法二:分别计数再比对(对照实现)
    public String[] uncommonFromSentences2(String s1, String s2) {
        Map<String, Integer> a = new HashMap<>();
        Map<String, Integer> b = new HashMap<>();
        for (String w : s1.split(" ")) {
            a.put(w, a.getOrDefault(w, 0) + 1);
        }
        for (String w : s2.split(" ")) {
            b.put(w, b.getOrDefault(w, 0) + 1);
        }
        List<String> ans = new ArrayList<>();
        for (Map.Entry<String, Integer> e : a.entrySet()) {
            if (e.getValue() == 1 && !b.containsKey(e.getKey())) {
                ans.add(e.getKey());
            }
        }
        for (Map.Entry<String, Integer> e : b.entrySet()) {
            if (e.getValue() == 1 && !a.containsKey(e.getKey())) {
                ans.add(e.getKey());
            }
        }
        return ans.toArray(new String[0]);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] cases = {
            {"this apple is sweet", "this apple is sour"},
            {"apple apple", "banana"},
            {"s z z z s", "s z ejt"}
        };
        String[][] exps = {
            {"sweet", "sour"},
            {"banana"},
            {"ejt"}
        };
        for (int t = 0; t < cases.length; ++t) {
            ++total;
            java.util.Set<String> g1 = new java.util.HashSet<>(java.util.Arrays.asList(s.uncommonFromSentences(cases[t][0], cases[t][1])));
            java.util.Set<String> g2 = new java.util.HashSet<>(java.util.Arrays.asList(s.uncommonFromSentences2(cases[t][0], cases[t][1])));
            java.util.Set<String> e = new java.util.HashSet<>(java.util.Arrays.asList(exps[t]));
            if (g1.equals(e) && g2.equals(e)) {
                ++pass;
            } else {
                System.out.println("FAIL 884-" + (t + 1) + ": got " + g1);
            }
        }
        if (pass == total) {
            System.out.println("== 0884 PASS ==");
        } else {
            System.out.println("== 0884 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n1 + n2) O(n1 + n2)
解法二 O(n1 + n2) O(n1 + n2)

885. Spiral Matrix III(螺旋矩阵 III)

题目描述

在 rows x cols 的网格上,你从单元格 (rStart, cStart) 面朝东面开始。网格的西北角位于

第一行第一列,网格的东南角位于最后一行最后一列。

你需要以顺时针按访问顺序返回网格中的所有单元格的坐标。

从起始单元格开始:首先,向上(北)移动一行,然后向右(东)移动一列,再向下(南)移动

两行,再向左(西)移动两列,再向上(北)移动三行......依次类推(步长每两次转向 +1)。

如果移动的次数与步长冲突,步长仍然适用,但不在网格中的单元格直接被跳过。

最后按访问顺序返回所有单元格的位置。

示例 1

  • 输入 :rows = 1, cols = 4, rStart = 0, cStart = 0
  • 输出:\[0,0,0,1,0,2,0,3]

示例 2

  • 输入 :rows = 5, cols = 6, rStart = 1, cStart = 4
  • 输出 :\[1,4,1,5,2,5,2,4,2,3,1,3,0,3,0,4,0,5,3,5,3,4,3,3,3,2,2,2,1,2,0,2,4,5,4,4,4,3,4,2,4,1,3,1,2,1,1,1,0,1,4,0,3,0,2,0,1,0,0,0]
    提示:
  • 1 <= rows <= 100
  • 1 <= cols <= 100
  • 0 <= rStart < rows
  • 0 <= cStart < cols

解题思路

螺旋扩展的步长模式:1、1、2、2、3、3、4、4......每走满两段长度一致的方向后步长 +1。

方向按 东、南、西、北 循环(顺时针)。

解法一(步长模拟,主提交):维护步长 step,每段走 step 步后转向,走完两段后 step+1。

在步上累计实际移动的距离,只有落在网格内的单元格才加入答案;当答案已收集满

rows * cols 个时提前结束。

解法二(转向计数法,对照实现):用变量记录当前已走的步数与段内剩余,通过 (方向段数-1)/2+1

计算步长;逻辑与解法一同构,仅换一种计数器组织方式。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.List;

class Solution {

    private static final int[] DR = {0, 1, 0, -1};   // 东、南、西、北
    private static final int[] DC = {1, 0, -1, 0};

    // 解法一:步长模拟(推荐提交)
    public int[][] spiralMatrixIII(int rows, int cols, int rStart, int cStart) {
        int total = rows * cols;
        int[][] ans = new int[total][2];
        int idx = 0;
        ans[idx][0] = rStart;
        ans[idx][1] = cStart;
        ++idx;
        int r = rStart, c = cStart;
        int step = 1;
        int dir = 0;
        while (idx < total) {
            for (int seg = 0; seg < 2; ++seg) {
                for (int k = 0; k < step; ++k) {
                    r += DR[dir];
                    c += DC[dir];
                    if (r >= 0 && r < rows && c >= 0 && c < cols) {
                        ans[idx][0] = r;
                        ans[idx][1] = c;
                        ++idx;
                        if (idx == total) {
                            return ans;
                        }
                    }
                }
                dir = (dir + 1) % 4;
            }
            ++step;
        }
        return ans;
    }

    // 解法二:段计数法(对照实现)
    public int[][] spiralMatrixIII2(int rows, int cols, int rStart, int cStart) {
        int total = rows * cols;
        int[][] ans = new int[total][2];
        int idx = 0;
        ans[idx][0] = rStart;
        ans[idx][1] = cStart;
        ++idx;
        int r = rStart, c = cStart;
        int dir = 0;
        int stepsTaken = 0;   // 已走的段数
        while (idx < total) {
            int curStep = stepsTaken / 2 + 1;   // 段 0、1 -> 1;段 2、3 -> 2 ...
            for (int k = 0; k < curStep; ++k) {
                r += DR[dir];
                c += DC[dir];
                if (r >= 0 && r < rows && c >= 0 && c < cols) {
                    ans[idx][0] = r;
                    ans[idx][1] = c;
                    ++idx;
                    if (idx == total) {
                        return ans;
                    }
                }
            }
            dir = (dir + 1) % 4;
            ++stepsTaken;
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        {
            int[][] got = s.spiralMatrixIII(1, 4, 0, 0);
            int[][] exp = {{0, 0}, {0, 1}, {0, 2}, {0, 3}};
            if (java.util.Arrays.deepEquals(got, exp)) {
                ++pass;
            } else {
                System.out.println("FAIL 885-1");
            }
        }
        ++total;
        {
            int[][] got = s.spiralMatrixIII(5, 6, 1, 4);
            int[][] exp = {
                {1, 4}, {1, 5}, {2, 5}, {2, 4}, {2, 3}, {1, 3}, {0, 3}, {0, 4}, {0, 5},
                {3, 5}, {3, 4}, {3, 3}, {3, 2}, {2, 2}, {1, 2}, {0, 2}, {4, 5}, {4, 4},
                {4, 3}, {4, 2}, {4, 1}, {3, 1}, {2, 1}, {1, 1}, {0, 1}, {4, 0}, {3, 0},
                {2, 0}, {1, 0}, {0, 0}
            };
            if (java.util.Arrays.deepEquals(got, exp)) {
                ++pass;
            } else {
                System.out.println("FAIL 885-2");
            }
        }
        ++total;
        {
            int[][] got = s.spiralMatrixIII2(1, 4, 0, 0);
            if (java.util.Arrays.deepEquals(got, new int[][]{{0, 0}, {0, 1}, {0, 2}, {0, 3}})) {
                ++pass;
            } else {
                System.out.println("FAIL 885-3");
            }
        }
        ++total;
        {
            int[][] got = s.spiralMatrixIII(3, 3, 0, 0);
            if (got.length == 9) {
                ++pass;
            } else {
                System.out.println("FAIL 885-4");
            }
        }
        if (pass == total) {
            System.out.println("== 0885 PASS ==");
        } else {
            System.out.println("== 0885 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O((max(rows, cols))^2) O(rows * cols)
解法二 O((max(rows, cols))^2) O(rows * cols)

886. Possible Bipartition(可能的二分法)

题目描述

给定一组 n 人(编号为 1, 2, ..., n),我们想把所有人分进任意大小的两组。每个人都可能

不喜欢其他人,那么他们不应该属于同一组。

给定整数 n 和数组 dislikes,其中 dislikes[i] = [a_i, b_i],表示不允许将编号为 a_i

和 b_i 的人归入同一组。

当可以用这种方法将所有人分进两组时,返回 true;否则返回 false。

示例 1

  • 输入 :n = 4, dislikes = \[1,2,1,3,2,4]
  • 输出 :true

示例 2

  • 输入 :n = 3, dislikes = \[1,2,1,3,2,3]
  • 输出 :false

示例 3

  • 输入 :n = 5, dislikes = \[1,2,2,3,3,4,4,5,1,5]
  • 输出 :false
    提示:
  • 1 <= n <= 2000
  • 0 <= dislikes.length <= 10^4
  • dislikes[i].length == 2
  • 1 <= dislikes[i][j] <= n
  • a_i < b_i
  • dislikes 中每一组都不同

解题思路

"互不喜欢的人不能同组"等价于把每个人看成节点、dislike 看成边,判断该图是否二分图。

解法一(染色法 BFS,主提交):建邻接表,用 color 数组记录 0/1 两组。遍历每个未染色

节点做 BFS 染色:相邻节点必须染不同色,若冲突返回 false。所有连通分量都成功即 true。

解法二(染色法 DFS,对照实现):同样 0/1 染色,用递归深度优先遍历;实现更简洁。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

class Solution {

    private List<Integer>[] g;
    private int[] color;

    // 解法一:BFS 染色(推荐提交)
    public boolean possibleBipartition(int n, int[][] dislikes) {
        g = new List[n + 1];
        for (int i = 1; i <= n; ++i) {
            g[i] = new ArrayList<>();
        }
        for (int[] d : dislikes) {
            g[d[0]].add(d[1]);
            g[d[1]].add(d[0]);
        }
        color = new int[n + 1];   // 0 未染色,1/2 两种颜色
        for (int i = 1; i <= n; ++i) {
            if (color[i] == 0) {
                if (!bfs(i)) {
                    return false;
                }
            }
        }
        return true;
    }

    private boolean bfs(int start) {
        Queue<Integer> q = new ArrayDeque<>();
        q.offer(start);
        color[start] = 1;
        while (!q.isEmpty()) {
            int u = q.poll();
            for (int v : g[u]) {
                if (color[v] == 0) {
                    color[v] = 3 - color[u];
                    q.offer(v);
                } else if (color[v] == color[u]) {
                    return false;
                }
            }
        }
        return true;
    }

    // 解法二:DFS 染色(对照实现)
    public boolean possibleBipartition2(int n, int[][] dislikes) {
        g = new List[n + 1];
        for (int i = 1; i <= n; ++i) {
            g[i] = new ArrayList<>();
        }
        for (int[] d : dislikes) {
            g[d[0]].add(d[1]);
            g[d[1]].add(d[0]);
        }
        color = new int[n + 1];
        for (int i = 1; i <= n; ++i) {
            if (color[i] == 0) {
                color[i] = 1;
                if (!dfs2(i)) {
                    return false;
                }
            }
        }
        return true;
    }

    private boolean dfs2(int u) {
        for (int v : g[u]) {
            if (color[v] == 0) {
                color[v] = 3 - color[u];
                if (!dfs2(v)) {
                    return false;
                }
            } else if (color[v] == color[u]) {
                return false;
            }
        }
        return true;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] d1 = {{1, 2}, {1, 3}, {2, 4}};
        int[][] d2 = {{1, 2}, {1, 3}, {2, 3}};
        int[][] d3 = {{1, 2}, {2, 3}, {3, 4}, {4, 5}, {1, 5}};
        ++total;
        if (s.possibleBipartition(4, d1) && s.possibleBipartition2(4, d1)) {
            ++pass;
        } else {
            System.out.println("FAIL 886-1");
        }
        ++total;
        if (!s.possibleBipartition(3, d2) && !s.possibleBipartition2(3, d2)) {
            ++pass;
        } else {
            System.out.println("FAIL 886-2");
        }
        ++total;
        if (!s.possibleBipartition(5, d3) && !s.possibleBipartition2(5, d3)) {
            ++pass;
        } else {
            System.out.println("FAIL 886-3");
        }
        ++total;
        if (s.possibleBipartition(5, new int[][]{}) && s.possibleBipartition2(5, new int[][]{})) {
            ++pass;
        } else {
            System.out.println("FAIL 886-4");
        }
        if (pass == total) {
            System.out.println("== 0886 PASS ==");
        } else {
            System.out.println("== 0886 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n + m) O(n + m)
解法二 O(n + m) O(n + m)(含递归栈)

887. Super Egg Drop(鸡蛋掉落)

题目描述

给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。

已知存在楼层 f,满足 0 <= f <= n,任何从高于 f 的楼层落下的鸡蛋都会碎,从 f 层或

比它低的楼层落下的鸡蛋都不会破。

每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果

鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中重复使用

这枚鸡蛋。

请你计算并返回要确定 f 确切的值的最小操作次数是多少?

示例 1

  • 输入 :k = 1, n = 2
  • 输出:2
  • 解释 :
    鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0。
    否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1。
    如果它没碎,那么肯定能得出 f = 2。
    因此,在最坏的情况下,我们需要移动 2 次以确定 f 是多少。

示例 2

  • 输入 :k = 2, n = 6
  • 输出:3

示例 3

  • 输入 :k = 3, n = 14
  • 输出 :4
    提示:
  • 1 <= k <= 100
  • 1 <= n <= 10^4

解题思路

经典"鸡蛋掉落"问题:dpkn 表示 k 个鸡蛋测 n 层所需最少次数。若在某层扔:

  • 碎了:还要测下方 (k-1, 层-1);
  • 没碎:还要测上方 (k, n-层)。
    直接做三维会超时,需利用单调性优化或换定义。
    解法一(dpmk 反向定义,主提交):dp[m][k] = 允许 m 次操作、k 个鸡蛋最多能确定的
    楼层数。递推 dp[m][k] = dp[m-1][k-1] + dp[m-1][k] + 1(一次测试后,碎了用 (k-1,m-1)、
    没碎用 (k,m-1))。不断增大 m,直到 dp[m][k] >= n,此时 m 即答案。时间约为
    O(k * ans)。
    解法二(记忆化搜索 + 二分,对照实现):dfs(k, n) 用经典公式
    1 + min over x of max(dfs(k-1, x-1), dfs(k, n-x)),配合记忆化与对 x 二分(单调性),
    复杂度 O(k n log n) 量级,直观但较慢。

代码实现

java 复制代码
class Solution {

    // 解法一:dp[m][k] = m 次操作 k 个鸡蛋最多测的楼层数(推荐提交)
    public int superEggDrop(int k, int n) {
        int[] dp = new int[k + 1];   // dp[j] = 当前操作次数下,j 个鸡蛋最多层数
        int m = 0;
        while (dp[k] < n) {
            ++m;
            for (int j = k; j >= 1; --j) {
                dp[j] = dp[j] + dp[j - 1] + 1;
            }
        }
        return m;
    }

    // 解法二:记忆化 DFS + 二分优化(对照实现)
    public int superEggDrop2(int k, int n) {
        memo2 = new int[k + 1][n + 1];
        return dfs2(k, n);
    }

    private int[][] memo2;

    private int dfs2(int k, int n) {
        if (k == 1 || n == 0) {
            return n == 0 ? 0 : n;
        }
        if (n == 1) {
            return 1;
        }
        if (memo2[k][n] != 0) {
            return memo2[k][n];
        }
        // 求最优扔蛋楼层 x:dfs(k-1, x-1) 递增,dfs(k, n-x) 递减,两者交点附近最优
        int lo = 1, hi = n;
        while (lo + 1 < hi) {
            int mid = (lo + hi) / 2;
            int broken = dfs2(k - 1, mid - 1);
            int safe = dfs2(k, n - mid);
            if (broken <= safe) {
                lo = mid;
            } else {
                hi = mid;
            }
        }
        int best = Math.min(
                Math.max(dfs2(k - 1, lo - 1), dfs2(k, n - lo)),
                Math.max(dfs2(k - 1, hi - 1), dfs2(k, n - hi)));
        return memo2[k][n] = best + 1;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] cases = {{1, 2}, {2, 6}, {3, 14}, {2, 10}, {100, 10000}, {4, 5000}};
        // 期望值由 Python 同款 dp[m][k] 验证:k=4,n=5000 需要 19 次
        int[] exp = {2, 3, 4, 4, 14, 19};
        for (int t = 0; t < cases.length; ++t) {
            ++total;
            int g1 = s.superEggDrop(cases[t][0], cases[t][1]);
            if (g1 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 887-" + (t + 1) + ": got " + g1 + " exp " + exp[t]);
            }
        }
        ++total;
        if (s.superEggDrop2(2, 6) == 3 && s.superEggDrop2(3, 14) == 4 && s.superEggDrop2(1, 5) == 5) {
            ++pass;
        } else {
            System.out.println("FAIL 887-m2");
        }
        if (pass == total) {
            System.out.println("== 0887 PASS ==");
        } else {
            System.out.println("== 0887 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(k * ans) O(k)(滚动数组)
解法二 O(k * n * log n) O(k * n)

888. Fair Candy Swap(公平的糖果交换)

题目描述

爱丽丝和鲍勃拥有不同总数量的糖果。给你两个数组 aliceSizes 和 bobSizes,

aliceSizes[i] 是爱丽丝拥有的第 i 盒糖果中的糖果数量,bobSizes[j] 是鲍勃拥有的第 j 盒

糖果中的糖果数量。

两人想要互相交换一盒糖果,这样在交换之后,他们就可以拥有相同总数量的糖果。一个人拥有的

糖果总数量是他们每盒糖果数量的总和。

返回一个整数数组 answer,其中 answer[0] 是爱丽丝必须交换的糖果盒中的糖果的个数,

answer[1] 是鲍勃必须交换的糖果盒中的糖果的个数。如果存在多个答案,你可以返回其中任何

一个。题目测试用例保证存在与输入对应的答案。

示例 1

  • 输入 :aliceSizes = 1,1, bobSizes = 2,2
  • 输出:1,2

示例 2

  • 输入 :aliceSizes = 1,2, bobSizes = 2,3
  • 输出:1,2

示例 3

  • 输入 :aliceSizes = 2, bobSizes = 1,3
  • 输出:2,3

示例 4

  • 输入 :aliceSizes = 1,2,5, bobSizes = 2,4
  • 输出 :5,4
    提示:
  • 1 <= aliceSizes.length, bobSizes.length <= 10^4
  • 1 <= aliceSizes[i], bobSizes[j] <= 10^5
  • 爱丽丝和鲍勃的糖果总数量不同
  • 题目数据保证对于给定的输入至少存在一个有效答案

解题思路

设爱丽丝总数 sa、鲍勃总数 sb。交换 a 与 b 后相等,则

sa - a + b == sb - b + a,即 b == a + (sb - sa)/2。令 delta = (sb - sa) / 2,

只需在爱丽丝的每一盒 a 中找是否存在 a + delta 属于鲍勃的糖果盒。

解法一(哈希集,主提交):把 bobSizes 放入 HashSet,遍历 aliceSizes,若

a + delta 在集合中,返回 [a, a + delta]。

解法二(排序双指针,对照实现):两个数组排序后,用差值 delta 双指针同时推进找配对。

复杂度 O(n log n),仅作对照。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

class Solution {

    // 解法一:哈希集合(推荐提交)
    public int[] fairCandySwap(int[] aliceSizes, int[] bobSizes) {
        int sa = 0, sb = 0;
        for (int x : aliceSizes) {
            sa += x;
        }
        Set<Integer> bobSet = new HashSet<>();
        for (int x : bobSizes) {
            sb += x;
            bobSet.add(x);
        }
        int delta = (sb - sa) / 2;   // b - a == delta
        for (int a : aliceSizes) {
            int b = a + delta;
            if (bobSet.contains(b)) {
                return new int[]{a, b};
            }
        }
        return new int[]{0, 0};   // 题目保证有解
    }

    // 解法二:排序双指针(对照实现)
    public int[] fairCandySwap2(int[] aliceSizes, int[] bobSizes) {
        int sa = 0, sb = 0;
        for (int x : aliceSizes) {
            sa += x;
        }
        for (int x : bobSizes) {
            sb += x;
        }
        int delta = (sb - sa) / 2;
        java.util.Arrays.sort(aliceSizes);
        java.util.Arrays.sort(bobSizes);
        int i = 0, j = 0;
        while (i < aliceSizes.length && j < bobSizes.length) {
            int diff = bobSizes[j] - aliceSizes[i];
            if (diff == delta) {
                return new int[]{aliceSizes[i], bobSizes[j]};
            } else if (diff < delta) {
                ++j;
            } else {
                ++i;
            }
        }
        return new int[]{0, 0};
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] as = {{1, 1}, {1, 2}, {2}, {1, 2, 5}, {2, 4}};
        int[][] bs = {{2, 2}, {2, 3}, {1, 3}, {2, 4}, {1, 3}};
        // 校验条件:交换后相等
        for (int t = 0; t < as.length; ++t) {
            ++total;
            int[] r1 = s.fairCandySwap(as[t], bs[t]);
            int[] r2 = s.fairCandySwap2(as[t], bs[t]);
            if (valid(as[t], bs[t], r1) && valid(as[t], bs[t], r2)) {
                ++pass;
            } else {
                System.out.println("FAIL 888-" + (t + 1) + ": " + java.util.Arrays.toString(r1));
            }
        }
        if (pass == total) {
            System.out.println("== 0888 PASS ==");
        } else {
            System.out.println("== 0888 FAIL ==");
            System.exit(1);
        }
    }

    private static boolean valid(int[] a, int[] b, int[] r) {
        int sa = 0, sb = 0;
        for (int x : a) {
            sa += x;
        }
        for (int x : b) {
            sb += x;
        }
        return sa - r[0] + r[1] == sb - r[1] + r[0];
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n + m) O(m)
解法二 O(n log n + m log m) O(1)

889. Construct Binary Tree from Preorder and Postorder Traversal

题目描述

给定两个整数数组,preorder 和 postorder,其中 preorder 是一个具有无重复值的二叉树

的前序遍历,postorder 是同一棵树的后序遍历,重构并返回二叉树。

如果存在多个答案,您可以返回其中任何一个。

示例 1

  • 输入 :preorder = 1,2,4,5,3,6,7, postorder = 4,5,2,6,7,3,1
  • 输出:1,2,3,4,5,6,7

示例 2

  • 输入 :preorder = 1, postorder = 1
  • 输出 :1
    提示:
  • 1 <= preorder.length <= 30
  • 1 <= preorder[i] <= preorder.length
  • preorder 中所有值都不同
  • postorder.length == preorder.length
  • 1 <= postorder[i] <= postorder.length
  • postorder 中所有值都不同
  • 保证 preorder 和 postorder 是同一棵二叉树的前序遍历和后序遍历

解题思路

前序 [根, 左子树..., 右子树...],后序 [左子树..., 右子树..., 根]。已知根后,左子树的

根 preorderl+1 必为左子树首个节点(若左子树存在),其在后序中的位置可确定左子树规模;

之后左右子树递归即可,先序与后序都能唯一划分。

解法一(递归 + 哈希定位,主提交):用 Map 记录后序每个值的下标。递归函数

dfs(pl, pr, ol, or) 对应先序区间与后序区间:root = preorder[pl];若区间只剩根则直接

返回;否则左子节点为 preorder[pl+1],查它在后序中的位置 idx,左子树长度为

idx - ol + 1,据此切分并递归。

解法二(公共祖先递归 + 指针,对照实现):设根为 root,先序第一个即根;若子区间长度

为 1 直接返回;否则 root.left = 以 preorder[pl+1] 为根的子树,其大小由 postorder 中

该值位置决定。套路与解法一相同,仅把区间递归改为"值驱动"写法,便于对照。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;

class Solution {

    // 解法一:递归 + 哈希定位左子树边界(推荐提交)
    public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
        Map<Integer, Integer> pos = new HashMap<>();
        for (int i = 0; i < postorder.length; ++i) {
            pos.put(postorder[i], i);
        }
        this.pre = preorder;
        this.pos = pos;
        return build(0, preorder.length - 1, 0, postorder.length - 1);
    }

    private int[] pre;
    private Map<Integer, Integer> pos;

    private TreeNode build(int pl, int pr, int ol, int or) {
        if (pl > pr) {
            return null;
        }
        TreeNode root = new TreeNode(pre[pl]);
        if (pl == pr) {
            return root;
        }
        // 左子树根为先序下一个元素,其位置决定左子树大小
        int leftVal = pre[pl + 1];
        int idx = pos.get(leftVal);
        int leftLen = idx - ol + 1;
        root.left = build(pl + 1, pl + leftLen, ol, idx);
        root.right = build(pl + leftLen + 1, pr, idx + 1, or - 1);
        return root;
    }

    // 解法二:值驱动递归(对照实现)
    public TreeNode constructFromPrePost2(int[] preorder, int[] postorder) {
        Map<Integer, Integer> pos2 = new HashMap<>();
        for (int i = 0; i < postorder.length; ++i) {
            pos2.put(postorder[i], i);
        }
        return build2(preorder, 0, preorder.length - 1, postorder, 0, postorder.length - 1, pos2);
    }

    private TreeNode build2(int[] pre, int pl, int pr, int[] post, int ol, int or, Map<Integer, Integer> pos2) {
        if (pl > pr) {
            return null;
        }
        TreeNode root = new TreeNode(pre[pl]);
        if (pl == pr) {
            return root;
        }
        int leftVal = pre[pl + 1];
        int idx = pos2.get(leftVal);
        int leftLen = idx - ol + 1;
        root.left = build2(pre, pl + 1, pl + leftLen, post, ol, idx, pos2);
        root.right = build2(pre, pl + leftLen + 1, pr, post, idx + 1, or - 1, pos2);
        return root;
    }

    // 二叉树节点定义
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) {
            val = x;
        }
    }

    // 层序转回数组(用于对比)
    private static String dump(TreeNode root) {
        StringBuilder sb = new StringBuilder("[");
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        while (!q.isEmpty()) {
            TreeNode cur = q.poll();
            if (cur == null) {
                continue;
            }
            sb.append(cur.val).append(",");
            q.offer(cur.left);
            q.offer(cur.right);
        }
        String s = sb.toString();
        return "[" + s.substring(0, s.length() - 1) + "]";
    }

    private static String dump2(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        preOrder(root, sb);
        return sb.toString();
    }

    private static void preOrder(TreeNode node, StringBuilder sb) {
        if (node == null) {
            return;
        }
        sb.append(node.val).append(",");
        preOrder(node.left, sb);
        preOrder(node.right, sb);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] pre1 = {1, 2, 4, 5, 3, 6, 7};
        int[] post1 = {4, 5, 2, 6, 7, 3, 1};
        ++total;
        {
            TreeNode t1 = s.constructFromPrePost(pre1, post1);
            TreeNode t2 = s.constructFromPrePost2(pre1, post1);
            if (dump2(t1).equals("1,2,4,5,3,6,7,") && dump2(t2).equals("1,2,4,5,3,6,7,")) {
                ++pass;
            } else {
                System.out.println("FAIL 889-1: " + dump2(t1));
            }
        }
        ++total;
        {
            int[] pre2 = {1}, post2 = {1};
            TreeNode t1 = s.constructFromPrePost(pre2, post2);
            if (t1 != null && t1.val == 1 && t1.left == null && t1.right == null) {
                ++pass;
            } else {
                System.out.println("FAIL 889-2");
            }
        }
        ++total;
        {
            int[] pre3 = {2, 1, 3}, post3 = {3, 1, 2};
            TreeNode t1 = s.constructFromPrePost(pre3, post3);
            if (dump2(t1).equals("2,1,3,")) {
                ++pass;
            } else {
                System.out.println("FAIL 889-3: " + dump2(t1));
            }
        }
        ++total;
        {
            int[] pre4 = {6, 4, 3, 5, 8, 7}, post4 = {3, 5, 4, 7, 8, 6};
            TreeNode t1 = s.constructFromPrePost(pre4, post4);
            if (dump2(t1).equals("6,4,3,5,8,7,")) {
                ++pass;
            } else {
                System.out.println("FAIL 889-4: " + dump2(t1));
            }
        }
        if (pass == total) {
            System.out.println("== 0889 PASS ==");
        } else {
            System.out.println("== 0889 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(n)(递归栈 + 哈希)
解法二 O(n) O(n)

890. Find and Replace Pattern(查找和替换模式)

题目描述

你有一个单词列表 words 和一个模式 pattern,你想知道 words 中的哪些单词与模式匹配。

如果存在字母的排列 p,使得将模式中的每个字母 x 替换为 p(x) 之后,我们就得到了所需

的单词,那么单词与模式是匹配的。(回想一下,字母的排列是从字母到字母的双射:每个字母映射

到另一个字母,没有两个字母映射到同一个字母。)

返回 words 中与给定模式匹配的单词列表。

你可以按任何顺序返回答案。

示例

  • 输入 :words = "abc","deq","mee","aqq","dkd","ccc", pattern = "abb"
  • 输出:"mee","aqq"
  • 解释 :
    "mee" 与模式匹配,因为排列 {a -> m, b -> e, ...}。
    "ccc" 与模式匹配,因为 {a -> c, b -> c, ...}。但映射不是双射,c 同时映射给 a 和 b。
    "abc" 与模式匹配,因为 {a -> b, b -> c, ...}。
    提示:
  • 1 <= words.length <= 50
  • 1 <= pattern.length = words[i].length <= 20

解题思路

双射映射需要"单词 → 模式"和"模式 → 单词"两个方向都一致。等价于:单词与模式做一次

规范化(把每个字母替换成其首次出现位置),得到的"形状"相同。

解法一(双映射校验,主提交):对每个单词,用两个数组/哈希维护 word -> pattern 与

pattern -> word 的映射;遍历每个位置,若任一方向出现不一致映射即不匹配。实现直接,语义

清晰。

解法二(归一化形状比较,对照实现):把字符串转换成由"首见下标"组成的模式串,如

"abb" -> "011",单词同样转换后比较。用较少的状态变量即可。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

class Solution {

    // 解法一:双向映射校验(推荐提交)
    public List<String> findAndReplacePattern(String[] words, String pattern) {
        List<String> ans = new ArrayList<>();
        char[] p = pattern.toCharArray();
        for (String w : words) {
            if (isMatch(w, pattern)) {
                ans.add(w);
            }
        }
        return ans;
    }

    private boolean isMatch(String word, String pattern) {
        Map<Character, Character> w2p = new HashMap<>();
        Map<Character, Character> p2w = new HashMap<>();
        for (int i = 0; i < word.length(); ++i) {
            char wc = word.charAt(i);
            char pc = pattern.charAt(i);
            if (w2p.containsKey(wc) && w2p.get(wc) != pc) {
                return false;
            }
            if (p2w.containsKey(pc) && p2w.get(pc) != wc) {
                return false;
            }
            w2p.put(wc, pc);
            p2w.put(pc, wc);
        }
        return true;
    }

    // 解法二:归一化形状比较(对照实现)
    public List<String> findAndReplacePattern2(String[] words, String pattern) {
        List<String> ans = new ArrayList<>();
        String target = shape(pattern);
        for (String w : words) {
            if (shape(w).equals(target)) {
                ans.add(w);
            }
        }
        return ans;
    }

    private String shape(String s) {
        StringBuilder sb = new StringBuilder();
        Map<Character, Integer> first = new HashMap<>();
        for (int i = 0; i < s.length(); ++i) {
            char c = s.charAt(i);
            Integer idx = first.get(c);
            if (idx == null) {
                first.put(c, i);
                idx = i;
            }
            sb.append(idx).append(',');
        }
        return sb.toString();
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] words = {"abc", "deq", "mee", "aqq", "dkd", "ccc"};
        ++total;
        {
            List<String> g1 = s.findAndReplacePattern(words, "abb");
            List<String> g2 = s.findAndReplacePattern2(words, "abb");
            Set<String> exp = new HashSet<>(java.util.Arrays.asList("mee", "aqq"));
            if (new HashSet<>(g1).equals(exp) && new HashSet<>(g2).equals(exp)) {
                ++pass;
            } else {
                System.out.println("FAIL 890-1: " + g1);
            }
        }
        ++total;
        {
            String[] w2 = {"a", "b", "c"};
            List<String> g1 = s.findAndReplacePattern(w2, "p");
            if (g1.size() == 3) {
                ++pass;
            } else {
                System.out.println("FAIL 890-2");
            }
        }
        ++total;
        {
            String[] w3 = {"ba", "ab", "aa"};
            List<String> g1 = s.findAndReplacePattern(w3, "ab");
            Set<String> exp = new HashSet<>(java.util.Arrays.asList("ba", "ab"));
            if (new HashSet<>(g1).equals(exp)) {
                ++pass;
            } else {
                System.out.println("FAIL 890-3: " + g1);
            }
        }
        if (pass == total) {
            System.out.println("== 0890 PASS ==");
        } else {
            System.out.println("== 0890 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n * L) O(L)
解法二 O(n * L) O(L)

891. Sum of Subsequence Widths(子序列宽度之和)

题目描述

一个序列的宽度定义为该序列中最大元素和最小元素的差值。

给你一个整数数组 nums,返回 nums 的所有非空子序列的宽度之和。由于答案可能非常大,

请返回对 10^9 + 7 取余后的结果。

子序列定义为从一个数组里删除一些(或者不删除)元素,但不改变剩下元素的顺序得到的数组。

示例 1

  • 输入 :nums = 2,1,3
  • 输出:6
  • 解释 :子序列为 1, 2, 3, 2,1, 2,3, 1,3, 2,1,3,宽度分别是 0, 0, 0, 1, 1, 2, 2,
    宽度之和是 6。

示例 2

  • 输入 :nums = 2
  • 输出 :0
    提示:
  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^5

解题思路

宽度只由子序列的最大值与最小值决定,与中间元素无关。排序后,对每个元素 nums[i](升序

排列后下标 i),它作为"最大值"的子序列有 2^i 个(其余 i 个更小元素任选),作为"最小值"

的子序列有 2^(n-1-i) 个(其余更小元素任选)。因此总宽度和 =

Σ nums[i] * (2^i - 2^(n-1-i))。

解法一(排序 + 幂次贡献,主提交):排序后预处理 2 的幂数组,一次遍历累加贡献并取模。

时间复杂度 O(n log n),空间 O(n)。

解法二(双指针首尾贡献,对照实现):排序后从两端同时向中间聚合,利用对称性

nums[i] * 2^i - nums[n-1-i] * 2^(n-1-i) 逐对相加,结果等价,实现更紧凑。

代码实现

java 复制代码
import java.util.Arrays;

class Solution {

    private static final int MOD = 1_000_000_007;

    // 解法一:排序 + 幂次贡献(推荐提交)
    public int sumSubseqWidths(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        long[] pow2 = new long[n];
        pow2[0] = 1;
        for (int i = 1; i < n; ++i) {
            pow2[i] = pow2[i - 1] * 2 % MOD;
        }
        long ans = 0;
        for (int i = 0; i < n; ++i) {
            ans = (ans + nums[i] * (pow2[i] - pow2[n - 1 - i]) % MOD + MOD) % MOD;
        }
        return (int) ans;
    }

    // 解法二:两端对称聚合(对照实现)
    public int sumSubseqWidths2(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        long[] pow2 = new long[n];
        pow2[0] = 1;
        for (int i = 1; i < n; ++i) {
            pow2[i] = pow2[i - 1] * 2 % MOD;
        }
        long ans = 0;
        // 首尾配对:i 与 j=n-1-i 的贡献合并为 (nums[j]-nums[i]) * (2^j - 2^i)
        for (int i = 0; i < n / 2; ++i) {
            int j = n - 1 - i;
            long diff = nums[j] - nums[i];
            long eDiff = (pow2[j] - pow2[i] + MOD) % MOD;
            ans = (ans + diff % MOD * eDiff % MOD) % MOD;
        }
        return (int) ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {{2, 1, 3}, {2}, {5}, {3, 7, 2, 9}, {1, 2, 3, 4}};
        // 期望值由 Python 暴力核对过:{2,1,3}=6, {2}=0, {5}=0, {3,7,2,9}=57, {1,2,3,4}=23
        int[] exp = {6, 0, 0, 57, 23};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            int g1 = s.sumSubseqWidths(arrs[t]);
            int g2 = s.sumSubseqWidths2(arrs[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 891-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0891 PASS ==");
        } else {
            System.out.println("== 0891 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n) O(n)
解法二 O(n log n) O(n)

892. Surface Area of 3D Shapes(三维形体的表面积)

题目描述

给你一个 n * n 的网格 grid,上面放置着一些 1 x 1 x 1 的正方体。每个值

v = grid[i][j] 表示 v 个正方体叠放在对应单元格 (i, j) 上。

放置好正方体后,任何直接相邻的正方体都会互相粘在一起,形成一些不规则的三维形体。

请你返回最终这些形体的总表面积。

注意:每个形体的底面也需要计入表面积中。

示例 1

  • 输入 :grid = \[1,2,3,4]
  • 输出:34

示例 2

  • 输入 :grid = \[1,1,1,1,0,1,1,1,1]
  • 输出:32

示例 3

  • 输入 :grid = \[2,2,2,2,1,2,2,2,2]
  • 输出 :46
    提示:
  • n == grid.length
  • n == grid[i].length
  • 1 <= n <= 50
  • 0 <= grid[i][j] <= 50

解题思路

每个格子叠 v 个方块:上下两个面始终贡献 2 * (v > 0 ? 1 : 0);与四个方向相邻格子

接触的面互相抵消,只统计暴露在外面部分的面积。

解法一(逐格统计,主提交):对每个格子,若 v > 0 贡献上下 2 面;再对每个方向(右、

下两个方向即可,避免重复)统计 max(0, v - 邻居) 两倍作为侧面,边界视为 0。

解法二(总贡献减重叠,对照实现):所有方块的 6v 总和减去相邻两叠之间接触面积的两倍

(每对相邻接触面贡献 -2)。两种做法结果一致。

代码实现

java 复制代码
class Solution {

    private static final int[] DR = {0, 1, 0, -1};
    private static final int[] DC = {1, 0, -1, 0};

    // 解法一:逐格统计(推荐提交)
    public int surfaceArea(int[][] grid) {
        int n = grid.length;
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                int v = grid[i][j];
                if (v > 0) {
                    ans += 2;   // 上下两面
                    for (int d = 0; d < 4; ++d) {
                        int ni = i + DR[d], nj = j + DC[d];
                        int neighbor = 0;
                        if (ni >= 0 && ni < n && nj >= 0 && nj < n) {
                            neighbor = grid[ni][nj];
                        }
                        if (v > neighbor) {
                            ans += v - neighbor;
                        }
                    }
                }
            }
        }
        return ans;
    }

    // 解法二:先算总暴露再消除连接(对照实现,只查右、下邻)
    public int surfaceArea2(int[][] grid) {
        int n = grid.length;
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] > 0) {
                    ans += 4 * grid[i][j] + 2;
                }
            }
        }
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (j + 1 < n) {
                    ans -= 2 * Math.min(grid[i][j], grid[i][j + 1]);
                }
                if (i + 1 < n) {
                    ans -= 2 * Math.min(grid[i][j], grid[i + 1][j]);
                }
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] grids = {
            {{1, 2}, {3, 4}},
            {{1, 1, 1}, {1, 0, 1}, {1, 1, 1}},
            {{2, 2, 2}, {2, 1, 2}, {2, 2, 2}},
            {{1}},
            {{1, 0}, {0, 2}}
        };
        int[] exp = {34, 32, 46, 6, 16};
        for (int t = 0; t < grids.length; ++t) {
            ++total;
            int g1 = s.surfaceArea(grids[t]);
            int g2 = s.surfaceArea2(grids[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 892-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0892 PASS ==");
        } else {
            System.out.println("== 0892 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n^2) O(1)
解法二 O(n^2) O(1)

893. Groups of Special-Equivalent Strings(特殊等价字符串组)

题目描述

给你一个字符串数组 words。

一步操作中,你可以交换字符串 words[i] 的任意两个偶数下标对应的字符或任意两个奇数下标

对应的字符。

对两个字符串 words[i] 和 words[j] 而言,如果经过任意次数的操作,words[i] == wordsj``,那么这两个字符串是特殊等价的。

现在规定:words 的一组特殊等价字符串就是 words 的一个同时满足下述条件的非空子集:

  • 该组中的每一对字符串都是特殊等价的;
  • 该组字符串已经涵盖了该类别中的所有特殊等价字符串,容量达到理论上的最大值。
    返回 words 中特殊等价字符串组的数量。

示例 1

  • 输入 :words = "abcd","cdab","cbad","xyzz","zzxy","zzyx"
  • 输出:3
  • 解释:其中一组为 "abcd", "cdab", "cbad",另外两组分别是 "xyzz", "zzxy" 和 "zzyx"。

示例 2

  • 输入 :words = "abc","acb","bac","bca","cab","cba"
  • 输出 :3
    提示:
  • 1 <= words.length <= 1000
  • 1 <= words[i].length <= 20
  • 所有 words[i] 都只由小写字母组成

解题思路

只能交换偶数位之间、奇数位之间的字符,因此一个字符串的"偶数下标字符多重集"与"奇数下标

字符多重集"就是它的不变量:两个字符串特殊等价当且仅当这两个多重集完全相同。

解法一(频次向量 + Set 去重,主提交):对每个单词构造长度为 26 的偶数位频次数组与

奇数位频次数组,拼接成一个可哈希的键(如 52 长度的字符串),放入 HashSet,返回集合大小。

解法二(排序拼接,对照实现):把每个单词的偶数位字符排序、奇数位字符排序后拼接成

sortedEven + sortedOdd 作为键,同样用 Set 去重。

代码实现

java 复制代码
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

class Solution {

    // 解法一:频次向量键(推荐提交)
    public int numSpecialEquivGroups(String[] words) {
        Set<String> seen = new HashSet<>();
        for (String w : words) {
            int[] even = new int[26];
            int[] odd = new int[26];
            for (int i = 0; i < w.length(); ++i) {
                if (i % 2 == 0) {
                    ++even[w.charAt(i) - 'a'];
                } else {
                    ++odd[w.charAt(i) - 'a'];
                }
            }
            StringBuilder key = new StringBuilder();
            for (int x : even) {
                key.append(x).append(',');
            }
            key.append('|');
            for (int x : odd) {
                key.append(x).append(',');
            }
            seen.add(key.toString());
        }
        return seen.size();
    }

    // 解法二:排序拼接键(对照实现)
    public int numSpecialEquivGroups2(String[] words) {
        Set<String> seen = new HashSet<>();
        for (String w : words) {
            StringBuilder e = new StringBuilder();
            StringBuilder o = new StringBuilder();
            for (int i = 0; i < w.length(); ++i) {
                char c = w.charAt(i);
                if (i % 2 == 0) {
                    e.append(c);
                } else {
                    o.append(c);
                }
            }
            char[] ec = e.toString().toCharArray();
            char[] oc = o.toString().toCharArray();
            Arrays.sort(ec);
            Arrays.sort(oc);
            seen.add(new String(ec) + "|" + new String(oc));
        }
        return seen.size();
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] cases = {
            {"abcd", "cdab", "cbad", "xyzz", "zzxy", "zzyx"},
            {"abc", "acb", "bac", "bca", "cab", "cba"},
            {"aa", "bb"},
            {"a"},
            {"ab", "ba", "abba", "baab"}
        };
        // case5:"ab"(a,b) 与 "ba"(b,a) 偶奇位频次同,同组;"abba"(偶a,b 奇b,a) 与 "baab" 同组 → 2 组
        // 但 Python 核对:abba 偶位是 a,b;奇位是 b,a ------ 与 "ab"/"ba" 也同键 → 实际 3 组
        int[] exp = {3, 3, 2, 1, 3};
        for (int t = 0; t < cases.length; ++t) {
            ++total;
            int g1 = s.numSpecialEquivGroups(cases[t]);
            int g2 = s.numSpecialEquivGroups2(cases[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 893-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0893 PASS ==");
        } else {
            System.out.println("== 0893 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n * L) O(n * L)
解法二 O(n * L log L) O(n * L)

894. All Possible Full Binary Trees(所有可能的真二叉树)

题目描述

给你一个整数 n,请你找出所有可能含 n 个节点的真二叉树,并以列表形式返回。答案中

每棵树的每个节点都必须符合 Node.val == 0。

答案的每个元素都是一棵真二叉树的根节点。你可以按任意顺序返回最终的真二叉树列表。

真二叉树是一类二叉树,树中每个节点恰好有 0 或 2 个子节点。

示例 1

  • 输入 :n = 7
  • 输出 :\[0,0,0,`null`,`null`,0,0,`null`,`null`,0,0,0,0,0,`null`,`null`,0,0,0,0,0,0,0,0,0,0,0,

    0,0,0,0,0,`null`,`null`,`null`,`null`,0,0\],\[0,0,0,0,0,`null`,`null`,0,0\]

示例 2

  • 输入 :n = 3
  • 输出 :\[0,0,0]
    提示:
  • 1 <= n <= 20

解题思路

真二叉树节点数必为奇数,否则无解。根节点固定占 1 个,剩余 n-1 个节点分配给左右两棵

真二叉树,两边也必须是奇数,递归构造即可。

解法一(记忆化 DFS,主提交):dfs(n) 返回 n 个节点能构造的所有真二叉树根节点列表;

枚举左子树节点数 i = 1, 3, 5, ...,右子树为 n-1-i,两两组合。用 Map 做记忆化。

解法二(递推 DP,对照实现):按节点数从小到大的顺序用数组保存结果:dp[i] 由

dp[l] x dp[r] 组合(l + r = i - 1)。避免递归栈,便于理解规模递增关系。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class Solution {

    private Map<Integer, List<TreeNode>> memo = new HashMap<>();

    // 解法一:记忆化 DFS(推荐提交)
    public List<TreeNode> allPossibleFBT(int n) {
        return dfs(n);
    }

    private List<TreeNode> dfs(int n) {
        if (n % 2 == 0) {
            return new ArrayList<>();
        }
        if (memo.containsKey(n)) {
            return memo.get(n);
        }
        List<TreeNode> res = new ArrayList<>();
        if (n == 1) {
            res.add(new TreeNode(0));
            memo.put(n, res);
            return res;
        }
        // 左子树要满足奇数节点,右子树 n-1-i 也要是奇数
        for (int i = 1; i < n; i += 2) {
            int right = n - 1 - i;
            if (right % 2 == 0) {
                continue;
            }
            for (TreeNode l : dfs(i)) {
                for (TreeNode r : dfs(right)) {
                    TreeNode root = new TreeNode(0);
                    root.left = l;
                    root.right = r;
                    res.add(root);
                }
            }
        }
        memo.put(n, res);
        return res;
    }

    // 解法二:递推 DP(对照实现)
    public List<TreeNode> allPossibleFBT2(int n) {
        List<List<TreeNode>> dp = new ArrayList<>();
        for (int i = 0; i <= n; ++i) {
            dp.add(new ArrayList<>());
        }
        if (n % 2 == 0) {
            return dp.get(n);
        }
        dp.get(1).add(new TreeNode(0));
        for (int size = 3; size <= n; size += 2) {
            for (int left = 1; left < size; left += 2) {
                int right = size - 1 - left;
                if (right % 2 == 0) {
                    continue;
                }
                for (TreeNode l : dp.get(left)) {
                    for (TreeNode r : dp.get(right)) {
                        TreeNode root = new TreeNode(0);
                        root.left = l;
                        root.right = r;
                        dp.get(size).add(root);
                    }
                }
            }
        }
        return dp.get(n);
    }

    // 真二叉树节点定义
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) {
            val = x;
        }
    }

    private static int countNodes(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return 1 + countNodes(root.left) + countNodes(root.right);
    }

    private static boolean isFull(TreeNode root) {
        if (root == null) {
            return true;
        }
        if (root.left == null && root.right == null) {
            return true;
        }
        if (root.left == null || root.right == null) {
            return false;
        }
        return isFull(root.left) && isFull(root.right);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        {
            List<TreeNode> r = s.allPossibleFBT(7);
            boolean allFull = true;
            for (TreeNode t : r) {
                allFull &= isFull(t) && countNodes(t) == 7;
            }
            if (r.size() == 5 && allFull) {
                ++pass;
            } else {
                System.out.println("FAIL 894-1: size=" + r.size());
            }
        }
        ++total;
        {
            List<TreeNode> r = s.allPossibleFBT(3);
            if (r.size() == 1 && countNodes(r.get(0)) == 3) {
                ++pass;
            } else {
                System.out.println("FAIL 894-2");
            }
        }
        ++total;
        {
            List<TreeNode> r = s.allPossibleFBT(2);
            if (r.isEmpty()) {
                ++pass;
            } else {
                System.out.println("FAIL 894-3");
            }
        }
        ++total;
        {
            List<TreeNode> r = s.allPossibleFBT2(7);
            if (r.size() == 5) {
                ++pass;
            } else {
                System.out.println("FAIL 894-4: size=" + r.size());
            }
        }
        ++total;
        {
            // 卡特兰数验证:n=9 应有 14 棵
            List<TreeNode> r = s.allPossibleFBT(9);
            if (r.size() == 14) {
                ++pass;
            } else {
                System.out.println("FAIL 894-5: size=" + r.size());
            }
        }
        if (pass == total) {
            System.out.println("== 0894 PASS ==");
        } else {
            System.out.println("== 0894 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n * C(n)) O(n * C(n))
解法二 O(n * C(n)) O(n * C(n))

895. Maximum Frequency Stack(最大频率栈)

题目描述

设计一个类似堆栈的数据结构,将元素推入堆栈,并从堆栈中弹出出现频率最高的元素。

实现 FreqStack 类:

  • FreqStack() 构造一个空的堆栈。
  • void push(int val) 将一个整数 val 压入栈顶。
  • int pop() 删除并返回堆栈中出现频率最高的元素。如果出现频率最高的元素不只一个,则
    移除并返回最接近栈顶的元素。

示例 1

  • 输入 :
    "FreqStack","`push`","`push`","`push`","`push`","`push`","`push`","`pop`","`pop`","`pop`","`pop`",

    \[\],\[5\],\[7\],\[5\],\[7\],\[4\],\[5\],\[\],\[\],\[\],\[\]

  • 输出 :`null`,`null`,`null`,`null`,`null`,`null`,`null`,5,7,5,4
  • 解释 :
    freqStack.push(5); // 堆栈为 5
    freqStack.push(7); // 堆栈是 5,7
    freqStack.push(5); // 堆栈是 5,7,5
    freqStack.push(7); // 堆栈是 5,7,5,7
    freqStack.push(4); // 堆栈是 5,7,5,7,4
    freqStack.push(5); // 堆栈是 5,7,5,7,4,5
    freqStack.pop(); // 返回 5
    freqStack.pop(); // 返回 7
    freqStack.pop(); // 返回 5
    freqStack.pop(); // 返回 4
    提示:
  • 0 <= val <= 10^9
  • push 和 pop 最多调用 2 * 10^4 次
  • 输入保证在调用 pop 之前栈中至少有一个元素

解题思路

题目要求"频率最高,同频时最靠近栈顶",经典做法是按频率分组维护栈。

解法一(频率分组栈,主提交):用 freq 记录每个 val 的出现次数,用 Map<Integer, Deque<Integer>> 记录各频率对应的栈(存放入栈顺序),再维护全局最大频率 maxFreq。

push 时 val 频率 +1,放入对应频次栈并更新 maxFreq;pop 时从 maxFreq 频次栈弹出,

该 val 频率 -1,若该栈空了 maxFreq--。

解法二(优先队列,对照实现):堆化元素 (freq, 时间戳, val),push 更新计数与时间戳,

pop 取堆顶。语义与解法一一对应,用优先队列组织。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;

class FreqStack {

    private final Map<Integer, Integer> freq = new HashMap<>();       // val -> 出现次数
    private final Map<Integer, Deque<Integer>> buckets = new HashMap<>(); // freq -> 栈
    private int maxFreq;

    public FreqStack() {
    }

    public void push(int val) {
        int f = freq.getOrDefault(val, 0) + 1;
        freq.put(val, f);
        maxFreq = Math.max(maxFreq, f);
        buckets.computeIfAbsent(f, k -> new ArrayDeque<>()).push(val);
    }

    public int pop() {
        Deque<Integer> stack = buckets.get(maxFreq);
        int val = stack.pop();
        freq.put(val, freq.get(val) - 1);
        if (stack.isEmpty()) {
            --maxFreq;
        }
        return val;
    }

    // 解法二:优先队列实现(对照)
    static class FreqStack2 {

        private static class Node {
            int freq;
            int time;
            int val;

            Node(int freq, int time, int val) {
                this.freq = freq;
                this.time = time;
                this.val = val;
            }
        }

        private final Map<Integer, Integer> freq = new HashMap<>();
        private final java.util.PriorityQueue<Node> pq =
                new java.util.PriorityQueue<>((a, b) ->
                        a.freq != b.freq ? b.freq - a.freq : b.time - a.time);
        private int ts;

        public void push(int val) {
            int f = freq.getOrDefault(val, 0) + 1;
            freq.put(val, f);
            pq.offer(new Node(f, ++ts, val));
        }

        public int pop() {
            // 懒删除:跳过频率已过期的残留节点
            while (true) {
                Node node = pq.poll();
                if (node.freq == freq.get(node.val)) {
                    freq.put(node.val, node.freq - 1);
                    return node.val;
                }
            }
        }
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        int pass = 0, total = 0;
        {
            FreqStack fs = new FreqStack();
            int[] seq = new int[4];
            fs.push(5);
            fs.push(7);
            fs.push(5);
            fs.push(7);
            fs.push(4);
            fs.push(5);
            seq[0] = fs.pop();
            seq[1] = fs.pop();
            seq[2] = fs.pop();
            seq[3] = fs.pop();
            ++total;
            if (seq[0] == 5 && seq[1] == 7 && seq[2] == 5 && seq[3] == 4) {
                ++pass;
            } else {
                System.out.println("FAIL 895-1: " + java.util.Arrays.toString(seq));
            }
        }
        {
            FreqStack2 fs = new FreqStack2();
            fs.push(1);
            fs.push(1);
            fs.push(2);
            ++total;
            if (fs.pop() == 1) {
                ++pass;
            } else {
                System.out.println("FAIL 895-2a");
            }
            fs.push(2);
            ++total;
            // 此刻 1 频 1、2 频 2:先弹 2;随后 2 又频 1 且最接近栈顶(t3 > t1)→ 再弹 2
            if (fs.pop() == 2 && fs.pop() == 2) {
                ++pass;
            } else {
                System.out.println("FAIL 895-2b");
            }
        }
        if (pass == total) {
            System.out.println("== 0895 PASS ==");
        } else {
            System.out.println("== 0895 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(1)(push/pop) O(n)
解法二 O(log n)(push/pop) O(n)

896. Monotonic Array(单调数列)

题目描述

如果数组是单调递增或单调递减的,那么它是单调的。

如果对于所有 i <= j,nums[i] <= nums[j],那么数组 nums 是单调递增的。如果对于所有

i <= j,nums[i] >= nums[j],那么数组 nums 是单调递减的。

当给定的数组 nums 是单调数组时返回 true,否则返回 false。

示例 1

  • 输入 :nums = 1,2,2,3
  • 输出 :true

示例 2

  • 输入 :nums = 6,5,4,4
  • 输出 :true

示例 3

  • 输入 :nums = 1,3,2
  • 输出 :false
    提示:
  • 1 <= nums.length <= 10^5
  • -10^5 <= nums[i] <= 10^5

解题思路

单调数组不允许"先升后降"或"先降后升",即相邻差符号不能改变。

解法一(一次遍历定趋势,主提交):记录初次出现的非零相邻差符号;之后若出现相反符号的

相邻差,返回 false。时间 O(n),空间 O(1)。

解法二(两次检查,对照实现):分别检查是否单调递增(所有相邻差 <= 0 且 >= 0)与单调

递减,两者任一成立即 true。思路直白,便于理解。

代码实现

java 复制代码
class Solution {

    // 解法一:一次遍历定趋势(推荐提交)
    public boolean isMonotonic(int[] nums) {
        int sign = 0;   // 0 未知,1 递增,-1 递减
        for (int i = 1; i < nums.length; ++i) {
            int diff = nums[i] - nums[i - 1];
            if (diff == 0) {
                continue;
            }
            if (sign == 0) {
                sign = diff > 0 ? 1 : -1;
            } else if ((diff > 0 && sign < 0) || (diff < 0 && sign > 0)) {
                return false;
            }
        }
        return true;
    }

    // 解法二:递增/递减分别检查(对照实现)
    public boolean isMonotonic2(int[] nums) {
        boolean inc = true;
        boolean dec = true;
        for (int i = 1; i < nums.length; ++i) {
            if (nums[i] < nums[i - 1]) {
                inc = false;
            }
            if (nums[i] > nums[i - 1]) {
                dec = false;
            }
        }
        return inc || dec;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {1, 2, 2, 3}, {6, 5, 4, 4}, {1, 3, 2}, {1}, {1, 1, 1},
            {3, 2, 1, 0, -1}, {-1, -5, -10}, {1, 2, 3, 2}
        };
        boolean[] exp = {true, true, false, true, true, true, true, false};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            if (s.isMonotonic(arrs[t]) == exp[t] && s.isMonotonic2(arrs[t]) == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 896-" + (t + 1) + ": got " + s.isMonotonic(arrs[t]) + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0896 PASS ==");
        } else {
            System.out.println("== 0896 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(1)
解法二 O(n) O(1)

题目描述

给你一棵二叉搜索树的 root,请你按中序遍历将其重新排列为一棵递增顺序搜索树,使树中最

左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。

示例 1

  • 输入 :root = 5,3,6,2,4,`null`,8,1,`null`,`null`,`null`,7,9
  • 输出 :1,`null`,2,`null`,3,`null`,4,`null`,5,`null`,6,`null`,7,`null`,8,`null`,9

示例 2

  • 输入 :root = 5,1,7
  • 输出 :1,`null`,5,`null`,7
    提示:
  • 树中节点数的取值范围是 [1, 100]
  • 0 <= Node.val <= 1000

解题思路

中序遍历二叉搜索树得到升序序列,把序列串成只有右孩子的链即可。

解法一(中序遍历 + 重建,主提交):递归中序收集节点值(或直接用节点),再从头到尾用

right 指针串起来,每个节点的 left 置空。返回新链头。

解法二(遍历中动态改链,对照实现):维护虚拟头节点 dummy 与当前尾指针 cur,中序

递归访问时把访问到的节点立即接到 cur.right,并把其 left 置空,cur 后移。只遍历一遍、

不需要额外收集。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Queue;

class Solution {

    private TreeNode cur;

    // 解法一:中序收集 + 重建链(推荐提交)
    public TreeNode increasingBST(TreeNode root) {
        Deque<TreeNode> list = new ArrayDeque<>();
        collect(root, list);
        TreeNode dummy = new TreeNode(-1);
        TreeNode tail = dummy;
        while (!list.isEmpty()) {
            TreeNode node = list.pollFirst();
            node.left = null;
            node.right = null;
            tail.right = node;
            tail = node;
        }
        return dummy.right;
    }

    private void collect(TreeNode node, Deque<TreeNode> out) {
        if (node == null) {
            return;
        }
        collect(node.left, out);
        out.addLast(node);
        collect(node.right, out);
    }

    // 解法二:遍历中动态串链(对照实现)
    public TreeNode increasingBST2(TreeNode root) {
        TreeNode dummy = new TreeNode(-1);
        cur = dummy;
        inorder(root);
        return dummy.right;
    }

    private void inorder(TreeNode node) {
        if (node == null) {
            return;
        }
        inorder(node.left);
        node.left = null;
        cur.right = node;
        cur = node;
        inorder(node.right);
    }

    // 二叉树节点定义
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) {
            val = x;
        }
    }

    // 层序建树
    private static TreeNode build(Integer[] vals) {
        if (vals == null || vals.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(vals[0]);
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < vals.length) {
            TreeNode cur = q.poll();
            if (vals[i] != null) {
                cur.left = new TreeNode(vals[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < vals.length && vals[i] != null) {
                cur.right = new TreeNode(vals[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    private static String toChain(TreeNode root) {
        StringBuilder sb = new StringBuilder("[");
        while (root != null) {
            sb.append(root.val).append(",");
            root = root.right;
        }
        String s = sb.toString();
        return s.endsWith(",") ? s.substring(0, s.length() - 1) + "]" : s + "]";
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        {
            // 两解法会原地改写树结构,必须分别建树
            TreeNode root1 = build(new Integer[]{5, 3, 6, 2, 4, null, 8, 1, null, null, null, 7, 9});
            TreeNode r1 = s.increasingBST(root1);
            TreeNode root2 = build(new Integer[]{5, 3, 6, 2, 4, null, 8, 1, null, null, null, 7, 9});
            TreeNode r2 = s.increasingBST2(root2);
            String exp = "[1,2,3,4,5,6,7,8,9]";
            if (toChain(r1).equals(exp) && toChain(r2).equals(exp)) {
                ++pass;
            } else {
                System.out.println("FAIL 897-1: " + toChain(r1));
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{5, 1, 7});
            TreeNode r1 = s.increasingBST(root);
            if (toChain(r1).equals("[1,5,7]")) {
                ++pass;
            } else {
                System.out.println("FAIL 897-2: " + toChain(r1));
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{2, 1, 3});
            TreeNode r1 = s.increasingBST(root);
            if (toChain(r1).equals("[1,2,3]")) {
                ++pass;
            } else {
                System.out.println("FAIL 897-3");
            }
        }
        ++total;
        {
            TreeNode root1 = build(new Integer[]{1});
            TreeNode r1 = s.increasingBST(root1);
            TreeNode root2 = build(new Integer[]{1});
            TreeNode r2 = s.increasingBST2(root2);
            if (toChain(r1).equals("[1]") && toChain(r2).equals("[1]")) {
                ++pass;
            } else {
                System.out.println("FAIL 897-4");
            }
        }
        if (pass == total) {
            System.out.println("== 0897 PASS ==");
        } else {
            System.out.println("== 0897 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

| 解法 | 时间复杂度 | 空间复杂度 |

| --- | | --- |


898. Bitwise ORs of Subarrays(子数组按位或操作)

题目描述

给定一个整数数组 arr,返回所有 arr 的非空子数组的不同按位或的数量。

子数组的按位或是子数组中每个整数的按位或。含有一个整数的子数组的按位或就是该整数。

子数组是数组内连续的非空元素序列。

示例 1

  • 输入 :arr = 0
  • 输出:1
  • 解释:只有一个可能的结果 0。

示例 2

  • 输入 :arr = 1,1,2
  • 输出:3
  • 解释:可能的结果为 1、2、3。

示例 3

  • 输入 :arr = 1,2,4
  • 输出:6
  • 解释 :可能的结果是 1、2、3、4、6 以及 7。
    提示:
  • 1 <= arr.length <= 5 * 10^4
  • 0 <= arr[i] <= 10^9

解题思路

所有子数组的按位或总数最多为 O(n * 30),因为或只会增加 1 位、最多 30 位,每个位置产生

的"不同或值"集合是有界的。维护"以当前元素结尾的所有子数组的按位或集合",每个集合大小

不超过 30。

解法一(滚动集合,主提交):用 Set<Integer> cur 记录以 arr[i] 结尾的子数组或值,

每步 cur = {x | newCur = x | arr[i], x in cur} ∪ {arr[i]},把 cur 并入全局答案集合。

每个集合最大 30 个元素,总复杂度 O(n * 30)。

解法二(不等式剪枝的暴力两两或,对照实现):f[i] 表示以 i 结尾的最长"或值不重复增长"

的起点集合,思想同解一,仅以数组形式保存,便于理解去重逻辑。

代码实现

java 复制代码
import java.util.HashSet;
import java.util.Set;

class Solution {

    // 解法一:滚动集合(推荐提交)
    public int subarrayBitwiseORs(int[] arr) {
        Set<Integer> ans = new HashSet<>();
        Set<Integer> cur = new HashSet<>();   // 以当前元素结尾的子数组或值集合
        for (int x : arr) {
            Set<Integer> nxt = new HashSet<>();
            nxt.add(x);
            for (int v : cur) {
                nxt.add(v | x);
            }
            cur = nxt;
            ans.addAll(cur);
        }
        return ans.size();
    }

    // 解法二:数组滚动 + 去重(对照实现)
    public int subarrayBitwiseORs2(int[] arr) {
        Set<Integer> ans = new HashSet<>();
        Set<Integer> cur = new HashSet<>();
        for (int x : arr) {
            Set<Integer> nxt = new HashSet<>();
            for (int v : cur) {
                nxt.add(v | x);
            }
            nxt.add(x);
            // 把新集合赋值给 cur(避免复用同一对象导致并发修改)
            cur.clear();
            cur.addAll(nxt);
            ans.addAll(cur);
        }
        return ans.size();
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {{0}, {1, 1, 2}, {1, 2, 4}, {3, 4, 5}, {1, 2, 3, 4, 5}};
        // 期望值由 Python 核对:case4={3,4,5,7} 共 4 个;case5={1,2,3,4,5,7} 共 6 个
        int[] exp = {1, 3, 6, 4, 6};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            int g1 = s.subarrayBitwiseORs(arrs[t]);
            int g2 = s.subarrayBitwiseORs2(arrs[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 898-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0898 PASS ==");
        } else {
            System.out.println("== 0898 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n * 30) O(n * 30)(答案集合)
解法二 O(n * 30) O(n * 30)

899. Orderly Queue(有序队列)

题目描述

给定一个字符串 s 和一个整数 k。你可以从 s 的前 k 个字母中选择一个,并把它加到

字符串的末尾。

返回在应用上述步骤的任意数量的移动后,字典序最小的字符串。

示例 1

  • 输入 :s = "cba", k = 1
  • 输出:"acb"
  • 解释 :第一步移动第一个字符 "c" 到最后得到 "bac",第二步移动 "b" 得到 "acb"。

示例 2

  • 输入 :s = "baaca", k = 3
  • 输出 :"aaabc"
    提示:
  • 1 <= k <= s.length <= 1000
  • s 只由小写字母组成

解题思路

分两种情况:

  • 当 k == 1 时,每次只能移动首字符到末尾,能得到的字符串就是原串的所有循环移位,
    取其中字典序最小者;
  • 当 k >= 2 时,任意两个相邻字符都可以交换(用 k=2 就能实现任意排列),等价于对
    s 的字符排序,最小字典序就是升序排列后的串。
    解法一(分类讨论,主提交):k >= 2 直接排序返回;k == 1 枚举 n 个循环移位取最小。
    解法二(模拟旋转,对照实现):k == 1 用双倍串 s + s 在滑动窗口内取最小子串;其余
    同解法一。两种实现对 k=1 分支的实现方式不同,便于对照。

代码实现

java 复制代码
import java.util.Arrays;

class Solution {

    // 解法一:分类讨论(推荐提交)
    public String orderlyQueue(String s, int k) {
        if (k > 1) {
            char[] cs = s.toCharArray();
            Arrays.sort(cs);
            return new String(cs);
        }
        // k == 1:所有循环移位中取最小
        String best = s;
        int n = s.length();
        for (int i = 1; i < n; ++i) {
            String rotated = s.substring(i) + s.substring(0, i);
            if (rotated.compareTo(best) < 0) {
                best = rotated;
            }
        }
        return best;
    }

    // 解法二:双倍串滑动窗口(对照实现)
    public String orderlyQueue2(String s, int k) {
        if (k > 1) {
            char[] cs = s.toCharArray();
            Arrays.sort(cs);
            return new String(cs);
        }
        String doubled = s + s;
        int n = s.length();
        String best = s;
        for (int i = 0; i < n; ++i) {
            String cand = doubled.substring(i, i + n);
            if (cand.compareTo(best) < 0) {
                best = cand;
            }
        }
        return best;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] ss = {"cba", "baaca", "abc", "zzz", "bca"};
        int[] ks = {1, 3, 1, 1, 1};
        String[] exp = {"acb", "aaabc", "abc", "zzz", "abc"};
        for (int t = 0; t < ss.length; ++t) {
            ++total;
            String g1 = s.orderlyQueue(ss[t], ks[t]);
            String g2 = s.orderlyQueue2(ss[t], ks[t]);
            if (g1.equals(exp[t]) && g2.equals(exp[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 899-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0899 PASS ==");
        } else {
            System.out.println("== 0899 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n^2)(k=1 时枚举)或 O(n log n)(k>1) O(n)
解法二 O(n^2)(k=1 时) 或 O(n log n) O(n)

900. RLE Iterator(RLE 迭代器)

题目描述

我们可以使用游程编码(即 RLE)来编码一个整数序列。在偶数长度编码的序列 encoding

(从 0 开始)中,元素偶数索引构成计数序列,奇数索引构成值序列。例如,encoding = [3, 8, 0, 9, 2, 5] 是序列 [8, 8, 8, 5, 5] 的 RLE 编码(它有三个 8,0 个 9,两个 5)。

给定一个编码的序列 encoding,实现 RLEIterator 类:

  • RLEIterator(int[] encoding) 对编码后的整数序列构造对象;
  • int next(int n) 以这种方式消费后面的 n 个元素(即返回下一个消费的元素),如果剩余
    的元素少于 n 个,则返回 -1。

示例 1

  • 输入 :"RLEIterator","`next`","`next`","`next`","`next`", \[\[3,8,0,9,2,5],2,1,1,2]
  • 输出 :`null`,8,8,5,-1
  • 解释 :
    RLEIterator 由 encoding = 3,8,0,9,2,5 初始化,这映射到序列 8,8,8,5,5。
    可以看下表:
  1. next(2) 消费 2 个元素,返回 8。
  2. next(1) 消费接下来的 1 个元素,返回 8。
  3. next(1) 消费接下来的 1 个元素,返回 5。
  4. next(2) 还剩 1 个元素,返回 -1。
    提示:
  • 2 <= encoding.length <= 1000
  • encoding.length 是偶数
  • 0 <= encoding[i] <= 10^9
  • 1 <= n <= 10^9
  • 每个测试用例最多调用 1000 次 next

解题思路

双指针扫描编码数组:i 指向当前"计数"位置,cnt 记录当前位置还剩余多少可消费元素。

解法一(双指针 + 计数,主提交):next(n) 循环:若当前剩余 cnt 不够,n 先减去 cnt,

跳到下一段(i += 2,cnt 重置为该段计数);找到足够的一段后 cnt -= n,返回该段的值。

若 i 超出数组仍不够,返回 -1。均摊 O(1)。

解法二(一次性映射,对照实现):直接构造展开数组再按游标取(数据范围大时不适用),

仅在小数据下对照,说明"惰性消费"是本题要点。

代码实现

java 复制代码
class RLEIterator {

    private int[] encoding;
    private int i;       // 当前段计数位置(偶数下标)
    private int cnt;     // 当前段剩余可消费个数

    public RLEIterator(int[] encoding) {
        this.encoding = encoding;
        this.i = 0;
        this.cnt = 0;
    }

    public int next(int n) {
        while (i < encoding.length) {
            long remain = encoding[i] - cnt;   // 当前段剩余个数
            if (n <= remain) {
                cnt += n;
                int val = encoding[i + 1];     // 当前段的值
                if (cnt == encoding[i]) {      // 本段恰好消费完,跳到下一段
                    cnt = 0;
                    i += 2;
                }
                return val;
            } else {
                // 当前段不够:消费整段并前进
                n -= remain;
                cnt = 0;
                i += 2;
            }
        }
        return -1;
    }

    // 解法二:展开式对照(仅小数据)
    static class RLEIterator2 {

        private int[] seq;
        private int pos;

        public RLEIterator2(int[] encoding) {
            java.util.List<Integer> list = new java.util.ArrayList<>();
            for (int j = 0; j + 1 < encoding.length; j += 2) {
                for (int c = 0; c < encoding[j]; ++c) {
                    list.add(encoding[j + 1]);
                }
            }
            seq = new int[list.size()];
            for (int j = 0; j < list.size(); ++j) {
                seq[j] = list.get(j);
            }
        }

        public int next(int n) {
            if (pos + n > seq.length) {
                pos = seq.length;
                return -1;
            }
            pos += n;
            return seq[pos - 1];
        }
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        int pass = 0, total = 0;
        {
            int[] enc = {3, 8, 0, 9, 2, 5};
            RLEIterator it = new RLEIterator(enc);
            int[] ans = {it.next(2), it.next(1), it.next(1), it.next(2)};
            ++total;
            if (ans[0] == 8 && ans[1] == 8 && ans[2] == 5 && ans[3] == -1) {
                ++pass;
            } else {
                System.out.println("FAIL 900-1: " + java.util.Arrays.toString(ans));
            }
        }
        {
            int[] enc = {2, 7, 1, 9};
            RLEIterator it = new RLEIterator(enc);
            ++total;
            if (it.next(1) == 7 && it.next(2) == 9 && it.next(2) == -1) {
                ++pass;
            } else {
                System.out.println("FAIL 900-2");
            }
        }
        {
            int[] enc = {3, 8, 0, 9, 2, 5};
            RLEIterator2 it = new RLEIterator2(enc);
            ++total;
            if (it.next(2) == 8 && it.next(1) == 8 && it.next(1) == 5 && it.next(2) == -1) {
                ++pass;
            } else {
                System.out.println("FAIL 900-3");
            }
        }
        if (pass == total) {
            System.out.println("== 0900 PASS ==");
        } else {
            System.out.println("== 0900 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 均摊 O(1)(next) O(1)
解法二 O(totalLen) 预构建 O(totalLen)
相关推荐
aloha_7892 小时前
Java 基础、集合、异常与 JUnit 5 速查
java·junit·sqlserver
知守观2 小时前
设计模式实战:从真实老项目中看标准模板方法与工厂的落地
java·spring·设计模式
独隅3 小时前
Claude API 实战教程:从入门到完整项目
android·java·数据库·claude·claude code
Wang's Blog3 小时前
Java 项目实战: 交友项目 - 圈子功能实现:总结
java·开发语言·交友
SL_staff3 小时前
JVS私有化文档系统上线避坑指南:5大运维与业务协同断点及检测加固实践
java·开源·全栈
FinelyYang5 小时前
考试系统简答题 AI 阅卷:覆盖度驱动、异步评分与人机协同落地实践
java·大数据·人工智能
hhhjhl5 小时前
Yup 跨端三方库仓颉适配全流程
java·开发语言
用户3126874877205 小时前
Java 对象内存布局:压缩指针、对齐填充与 JOL 实测
java
东华果汁哥5 小时前
【Spring AI 实战】从零开始调用大模型接口,构建 Java AI 应用
java·人工智能·spring