LeetCode626~650题目解析

LeetCode 626-650. 题解合集

文章目录


626. Exchange Seats(换座位)

题目类型:数据库(SQL)

题目描述

表:Seat

text 复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| id          | int     |
| student     | varchar |
+-------------+---------+

id 是该表的主键(唯一值)列。

该表的每一行都表示学生的姓名和 ID。

ID 序列始终从 1 开始并连续增加。

编写解决方案来交换每两个连续的学生的座位号。如果学生的数量是奇数,则最后一个学生的 id 不交换。

按 id 升序 返回结果表。

示例 1:

text 复制代码
输入:
Seat 表:
+----+---------+
| id | student |
+----+---------+
| 1  | Abbot   |
| 2  | Doris   |
| 3  | Emerson |
| 4  | Green   |
| 5  | Jeames  |
+----+---------+
输出:
+----+---------+
| id | student |
+----+---------+
| 1  | Doris   |
| 2  | Abbot   |
| 3  | Green   |
| 4  | Emerson |
| 5  | Jeames  |
+----+---------+
解释:
请注意,如果学生人数为奇数,则不需要更换最后一名学生的座位。

解题思路

解法一(自连接 + 配对 id + COALESCE) :相邻奇偶座位互换。把每个 id 映射到它的"搭档"id:

(id + 1) ^ 1 - 1 恰好把奇数映射到下一个偶数、把偶数映射到上一个奇数(例如 1 → 2、2 → 1、3 → 4、4 → 3)。

用 Seat 左连接 Seat(别名 s1/s2,连接条件为 (s1.id + 1) ^ 1 - 1 = s2.id),

COALESCE(s2.student, s1.student) 在没有搭档(最后一个落单的奇数)时保留原名。最后按 id 升序输出。

解法二(直接改 id 后重排) :不换人、改座位号。非末尾奇数加 1、偶数减 1、末尾奇数不变,

再按新 id 排序,效果与互换学生相同。判断末尾用子查询 MAX(id)。

解法三(RANK + 异或翻转) :把 0 起始的下标 (id - 1) 做异或翻转(^ 1 交换 0/1、2/3...),

再用 RANK() OVER (ORDER BY (id - 1) ^ 1) 重新生成连续座位号,一行完成成对交换,末尾自然保留。

解法四(窗口函数判断末尾) :用 ROW_NUMBER() OVER (ORDER BY id) != COUNT(id) OVER () 判断是否为最后一行,

避免子查询二次扫描,其余逻辑与解法二一致。

代码实现

sql 复制代码
-- 解法一:自连接 + 配对 id
SELECT s1.id, COALESCE(s2.student, s1.student) AS student
FROM
    Seat AS s1
    LEFT JOIN Seat AS s2 ON (s1.id + 1) ^ 1 - 1 = s2.id
ORDER BY 1;
sql 复制代码
-- 解法二:直接改 id 后重排
SELECT
    id + (
        CASE
            WHEN id % 2 = 1
            AND id != (SELECT MAX(id) FROM Seat) THEN 1
            WHEN id % 2 = 0 THEN -1
            ELSE 0
        END
    ) AS id,
    student
FROM Seat
ORDER BY 1;
sql 复制代码
-- 解法三:RANK + 异或翻转
SELECT
    RANK() OVER (ORDER BY (id - 1) ^ 1) AS id,
    student
FROM Seat;
sql 复制代码
-- 解法四:窗口函数判断末尾
SELECT
    CASE
        WHEN id & 1 = 0 THEN id - 1
        WHEN ROW_NUMBER() OVER (ORDER BY id) != COUNT(id) OVER () THEN id + 1
        ELSE id
    END AS id,
    student
FROM Seat
ORDER BY 1;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n)(排序) O(n)
解法二 O(n log n)(排序 + 子查询) O(1)
解法三 O(n log n)(排序窗口) O(n)
解法四 O(n log n)(排序窗口) O(n)

627. Swap Salary(变更性别)

题目类型:数据库(SQL)

题目描述

表:Salary

text 复制代码
+-------------+----------+
| Column Name | Type     |
+-------------+----------+
| id          | int      |
| name        | varchar  |
| sex         | ENUM     |
| salary      | int      |
+-------------+----------+

id 是这个表的主键(具有唯一值的列)。

sex 这一列的值是 ENUM 类型,只能从 ('m', 'f') 中取。

本表包含公司雇员的信息。

请你编写一个解决方案来交换所有的 'f' 和 'm'(即,将所有 'f' 变为 'm',反之亦然),

仅使用单个 UPDATE 语句,且不产生中间临时表。

注意,你必须仅使用一条 UPDATE 语句,且不能 使用 SELECT 语句。

示例 1:

text 复制代码
输入:
Salary 表:
+----+------+-----+--------+
| id | name | sex | salary |
+----+------+-----+--------+
| 1  | A    | m   | 2500   |
| 2  | B    | f   | 1500   |
| 3  | C    | m   | 5500   |
| 4  | D    | f   | 500    |
+----+------+-----+--------+
输出:
+----+------+-----+--------+
| id | name | sex | salary |
+----+------+-----+--------+
| 1  | A    | f   | 2500   |
| 2  | B    | m   | 1500   |
| 3  | C    | f   | 5500   |
| 4  | D    | m   | 500    |
+----+------+-----+--------+
解释:
(1, A) 和 (3, C) 从 'm' 变为 'f'。
(2, B) 和 (4, D) 从 'f' 变为 'm'。

解题思路

解法一(IF 函数,主提交) :sex 只有两个合法值 'm' / 'f',交换就是"取另一个值"。

SET sex = IF(sex = 'f', 'm', 'f') 对每一行就地翻转,一条 UPDATE 完成,最直观。

解法二(CASE WHEN) :用 CASE 表达式等价实现条件翻转:

CASE sex WHEN 'f' THEN 'm' ELSE 'f' END。与解法一逻辑完全一致,适合不习惯 IF 的写法。

两种写法都不产生中间表、不使用 SELECT,符合题目约束。

代码实现

sql 复制代码
-- 解法一:IF 函数
UPDATE Salary
SET sex = IF(sex = 'f', 'm', 'f');
sql 复制代码
-- 解法二:CASE WHEN
UPDATE Salary
SET sex = CASE sex WHEN 'f' THEN 'm' ELSE 'f' END;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n)(扫描全表逐行更新) O(1)
解法二 O(n)(扫描全表逐行更新) O(1)

628. Maximum Product of Three Numbers(三个数的最大乘积)

题目描述

给你一个整型数组 nums,在数组中找出由三个数组成的最大乘积,并返回这个最大乘积。

示例 1

  • 输入 :nums = 1,2,-3
  • 输出:-6
  • 解释:只有三个数字 1、2 和 -3,因此最大乘积是 1 * 2 * -3 = -6。

示例 2

  • 输入 :nums = 1,2,3,4
  • 输出:24
  • 解释:最大的乘积来自三个最大的数字:2 * 3 * 4 = 24。

示例 3

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

解题思路

三个数乘积最大,可能来自三个最大的正数,也可能来自两个绝对值很大的负数再乘一个最大的正数。

解法一(排序,主提交):升序排序后,比较 nums[n-1]*nums[n-2]*nums[n-3](三个最大数)与

nums[n-1]*nums[0]*nums[1](两个最小负数 × 最大正数)取较大者。时间复杂度 O(n log n),空间 O(log n)(排序栈)。

解法二(线性维护极值):不排序,一趟扫描同时维护最小的两个数 mi1/mi2 与最大的三个数 mx1/mx2/mx3,

最后比较 mi1*mi2*mx1 与 mx1*mx2*mx3。时间复杂度 O(n),空间 O(1)。

代码实现

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

class Solution {

    // 解法一:排序后比较两种情况
    public int maximumProduct(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        int a = nums[n - 1] * nums[n - 2] * nums[n - 3];
        int b = nums[n - 1] * nums[0] * nums[1];
        return Math.max(a, b);
    }

    // 解法二:一趟扫描维护最大三数、最小两数
    public int maximumProduct2(int[] nums) {
        final int inf = 1 << 30;
        int mi1 = inf, mi2 = inf;
        int mx1 = -inf, mx2 = -inf, mx3 = -inf;
        for (int x : nums) {
            if (x < mi1) {
                mi2 = mi1;
                mi1 = x;
            } else if (x < mi2) {
                mi2 = x;
            }
            if (x > mx1) {
                mx3 = mx2;
                mx2 = mx1;
                mx1 = x;
            } else if (x > mx2) {
                mx3 = mx2;
                mx2 = x;
            } else if (x > mx3) {
                mx3 = x;
            }
        }
        return Math.max(mi1 * mi2 * mx1, mx1 * mx2 * mx3);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.maximumProduct(new int[] {1, 2, -3}) == -6) ++pass; else System.out.println("FAIL test1: " + sol.maximumProduct(new int[] {1, 2, -3}));
        ++total; if (sol.maximumProduct(new int[] {1, 2, 3, 4}) == 24) ++pass; else System.out.println("FAIL test2: " + sol.maximumProduct(new int[] {1, 2, 3, 4}));
        ++total; if (sol.maximumProduct(new int[] {-1, -2, -3}) == -6) ++pass; else System.out.println("FAIL test3: " + sol.maximumProduct(new int[] {-1, -2, -3}));
        ++total; if (sol.maximumProduct(new int[] {-100, -98, -1, 2, 3, 4}) == 39200) ++pass; else System.out.println("FAIL test4: " + sol.maximumProduct(new int[] {-100, -98, -1, 2, 3, 4}));
        ++total; if (sol.maximumProduct(new int[] {-5, -4, 1, 2}) == 40) ++pass; else System.out.println("FAIL test5: " + sol.maximumProduct(new int[] {-5, -4, 1, 2}));
        // 解法二
        ++total; if (sol.maximumProduct2(new int[] {1, 2, -3}) == -6) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.maximumProduct2(new int[] {1, 2, 3, 4}) == 24) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.maximumProduct2(new int[] {-100, -98, -1, 2, 3, 4}) == 39200) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.maximumProduct2(new int[] {-5, -4, 1, 2}) == 40) ++pass; else System.out.println("FAIL test9");
        if (pass == total) {
            System.out.println("== 0628 PASS ==");
        } else {
            System.out.println("== 0628 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

629. K Inverse Pairs Array(K 个逆序对数组)

题目描述

对于一个整数数组 nums,逆序对是一对满足 0 <= i < j < nums.length 且 nums[i] > nums[j] 的整数对 [i, j]。

给你两个整数 n 和 k,找出所有包含从 1 到 n 的数字,且恰好拥有 k 个逆序对的不同的数组的个数。

由于答案可能很大,只需要返回对 10^9 + 7 取余的结果。

示例 1

  • 输入 :n = 3, k = 0
  • 输出:1
  • 解释:只有数组 1,2,3 包含了从 1 到 3 的整数并且正好拥有 0 个逆序对。

示例 2

  • 输入 :n = 3, k = 1
  • 输出:2
  • 解释 :数组 1,3,2 和 2,1,3 都有 1 个逆序对。
    提示:
  • 1 <= n <= 1000
  • 0 <= k <= 1000

解题思路

解法一(DP + 前缀和优化,主提交):令 f[i][j] 表示长度为 i、恰好有 j 个逆序对的排列数。

把数字 i 插入长度为 i-1 的排列中:插在最后不新增逆序,插在最前新增 i-1 个,因此

f[i][j] = Σ f[i-1][j-t](t = 0..min(j, i-1)),即前一行一段区间之和。对 f[i-1] 维护前缀和 s,

转移降到 O(1);再用两个一维数组滚动,总复杂度 O(nk),空间 O(k)。

解法二(暴力枚举全排列):对小 n 直接枚举 1..n 的所有排列并统计逆序对数,仅用于验证思路,

复杂度 O(n!·n),只适用于 n <= 8 的规模。

代码实现

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

class Solution {

    public int kInversePairs(int n, int k) {
        final int mod = (int) 1e9 + 7;
        int[] f = new int[k + 1];
        int[] s = new int[k + 2];
        f[0] = 1;
        Arrays.fill(s, 1);
        s[0] = 0;
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= k; ++j) {
                f[j] = (s[j + 1] - s[Math.max(0, j - (i - 1))] + mod) % mod;
            }
            for (int j = 1; j <= k + 1; ++j) {
                s[j] = (s[j - 1] + f[j - 1]) % mod;
            }
        }
        return f[k];
    }

    // 解法二:暴力枚举全排列(仅小规模验证用)
    public int kInversePairsBrute(int n, int k) {
        int[] a = new int[n];
        for (int i = 0; i < n; ++i) {
            a[i] = i + 1;
        }
        int[] cnt = new int[] {0};
        perm(a, 0, k, cnt);
        return cnt[0];
    }

    private void perm(int[] a, int p, int k, int[] cnt) {
        if (p == a.length) {
            int inv = 0;
            for (int i = 0; i < a.length; ++i) {
                for (int j = i + 1; j < a.length; ++j) {
                    if (a[i] > a[j]) {
                        ++inv;
                    }
                }
            }
            if (inv == k) {
                cnt[0]++;
            }
            return;
        }
        for (int i = p; i < a.length; ++i) {
            int t = a[p];
            a[p] = a[i];
            a[i] = t;
            perm(a, p + 1, k, cnt);
            t = a[p];
            a[p] = a[i];
            a[i] = t;
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.kInversePairs(3, 0) == 1) ++pass; else System.out.println("FAIL test1: " + sol.kInversePairs(3, 0));
        ++total; if (sol.kInversePairs(3, 1) == 2) ++pass; else System.out.println("FAIL test2: " + sol.kInversePairs(3, 1));
        ++total; if (sol.kInversePairs(3, 2) == 2) ++pass; else System.out.println("FAIL test3: " + sol.kInversePairs(3, 2));
        ++total; if (sol.kInversePairs(4, 3) == 6) ++pass; else System.out.println("FAIL test4: " + sol.kInversePairs(4, 3));
        ++total; if (sol.kInversePairs(1, 0) == 1) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.kInversePairs(2, 1) == 1) ++pass; else System.out.println("FAIL test6");
        // 暴力对照(n <= 7)
        ++total;
        boolean bOk = true;
        for (int n = 1; n <= 7; ++n) {
            for (int k = 0; k <= n * (n - 1) / 2; ++k) {
                if (sol.kInversePairs(n, k) != sol.kInversePairsBrute(n, k)) {
                    bOk = false;
                    System.out.println("FAIL brute n=" + n + " k=" + k + " dp=" + sol.kInversePairs(n, k) + " brute=" + sol.kInversePairsBrute(n, k));
                }
            }
        }
        if (bOk) ++pass; else System.out.println("FAIL test7 (brute)");
        // 大用例不超时即可
        ++total; if (sol.kInversePairs(1000, 1000) >= 0) ++pass; else System.out.println("FAIL test8");
        if (pass == total) {
            System.out.println("== 0629 PASS ==");
        } else {
            System.out.println("== 0629 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(nk) O(k)
解法二 O(n!·n)(仅小规模验证) O(n)

630. Course Schedule III(课程表 III)

题目描述

这里有 n 门不同的在线课程,按从 1 到 n 编号。给你一个数组 courses,

其中 courses[i] = [durationi, lastDayi] 表示第 i 门课将会持续 durationi 天课,

并且必须在不晚于 lastDayi 的时候完成。

你的学期从第 1 天开始,且不能同时修读两门及两门以上的课程。

返回你最多可以修读的课程数目。

示例 1

  • 输入:courses = \[100, 200, 200, 1300, 1000, 1250, 2000, 3200]
  • 输出:3

示例 2

  • 输入:courses = \[1,2]
  • 输出:1

示例 3

  • 输入:courses = \[3,2,4,3]
  • 输出 :0
    提示:
  • 1 <= courses.length <= 10^4
  • 1 <= durationi, lastDayi <= 10^4

解题思路

解法一(贪心 + 大根堆,主提交):每门课有时长与截止日期,求最多能上几门。

按截止时间升序排序后,依次尝试修读每门课:把课程时长加入 s 并入堆;

若 s > lastDay(当前总时长超过这门课的截止日),则弹出堆中时长最大的那门课并从 s 扣除,

为后面截止更紧的课腾出时间。堆中剩下的课程数即为最大可选门数。

正确性:先处理截止早的课(早截止的课越早决定越好),遇到冲突时优先丢弃持续时间最长的课,

这样既保证了已选课程都能按时完成,又给未来课程留出最大空间。

解法二(DFS 暴力枚举):枚举课程子集的选取与否,检查是否每门都满足截止且互不重叠,

仅用于小规模验证,复杂度 O(2^n·n)。

代码实现

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

class Solution {

    public int scheduleCourse(int[][] courses) {
        Arrays.sort(courses, (a, b) -> a[1] - b[1]);
        PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());
        int s = 0;
        for (int[] e : courses) {
            int duration = e[0], last = e[1];
            pq.offer(duration);
            s += duration;
            while (s > last) {
                s -= pq.poll();
            }
        }
        return pq.size();
    }

    // 解法二:DFS 暴力枚举子集,仅小规模验证
    public int scheduleCourseBrute(int[][] courses) {
        int n = courses.length;
        int best = 0;
        for (int mask = 0; mask < (1 << n); ++mask) {
            int day = 0, cnt = 0;
            boolean ok = true;
            for (int i = 0; i < n; ++i) {
                if ((mask & (1 << i)) != 0) {
                    ++cnt;
                    day += courses[i][0];
                    if (day > courses[i][1]) {
                        ok = false;
                        break;
                    }
                }
            }
            if (ok) {
                best = Math.max(best, cnt);
            }
        }
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.scheduleCourse(new int[][] {{100, 200}, {200, 1300}, {1000, 1250}, {2000, 3200}}) == 3) ++pass;
        else System.out.println("FAIL test1: " + sol.scheduleCourse(new int[][] {{100, 200}, {200, 1300}, {1000, 1250}, {2000, 3200}}));
        ++total; if (sol.scheduleCourse(new int[][] {{1, 2}}) == 1) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.scheduleCourse(new int[][] {{3, 2}, {4, 3}}) == 0) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.scheduleCourse(new int[][] {{5, 5}, {4, 6}, {2, 6}}) == 2) ++pass; else System.out.println("FAIL test4: " + sol.scheduleCourse(new int[][] {{5, 5}, {4, 6}, {2, 6}}));
        ++total; if (sol.scheduleCourse(new int[][] {{1, 1}, {1, 2}, {1, 3}, {1, 4}}) == 4) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.scheduleCourse(new int[][] {{7, 17}, {3, 12}, {10, 20}, {9, 10}, {5, 20}, {10, 19}, {4, 18}}) == 4) ++pass;
        else System.out.println("FAIL test6: " + sol.scheduleCourse(new int[][] {{7, 17}, {3, 12}, {10, 20}, {9, 10}, {5, 20}, {10, 19}, {4, 18}}));
        // 暴力对照(n <= 12)
        ++total;
        boolean bOk = true;
        int[][] small1 = {{3, 2}, {4, 3}};
        int[][] small2 = {{5, 5}, {4, 6}, {2, 6}};
        int[][] small3 = {{2, 3}, {1, 5}, {3, 6}, {1, 2}};
        int[][][] cases = {small1, small2, small3};
        for (int[][] c : cases) {
            if (sol.scheduleCourse(c) != sol.scheduleCourseBrute(c)) {
                bOk = false;
                System.out.println("FAIL brute: " + Arrays.deepToString(c));
            }
        }
        if (bOk) ++pass; else System.out.println("FAIL test7 (brute)");
        if (pass == total) {
            System.out.println("== 0630 PASS ==");
        } else {
            System.out.println("== 0630 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n) O(n)
解法二 O(2^n·n)(仅小规模验证) O(n)

631. Design Excel Sum Formula(设计 Excel 求和公式)

题目描述

请你设计 Excel 中的基本功能,并实现求和公式。

实现 Excel 类:

  • Excel(int height, char width):用高度 height 和宽度 width 初始化对象。
    该表格是一个大小为 height x width 的整数矩阵 mat,行下标范围是 [1, height],列下标范围是 ['A', width]。
    初始情况下,所有的值都应该为零。
  • void set(int row, char column, int val):将 mat[row][column] 的值更改为 val。
  • int get(int row, char column):返回 mat[row][column] 的值。
  • int sum(int row, char column, List<String> numbers):将 mat[row][column] 的值设为由 numbers 表示的单元格的和,
    并返回 mat[row][column] 的值。此求和公式应该长期作用于该单元格,直到该单元格被另一个值或另一个求和公式覆盖。
    其中,numbers[i] 的格式可以为:
  • "ColRow":表示某个单元格,例如 "F7" 表示单元格 mat[7]['F']。
  • "ColRow1:ColRow2":表示一组单元格(始终为矩形),"ColRow1" 为左上角,"ColRow2" 为右下角,例如 "B3:F7"。
    注意:可以假设不会出现循环求和引用。

示例 1

  • 输入 :

    "Excel", "`set`", "`sum`", "`set`", "get"

    \[3, "C"\], \[1, "A", 2\], \[3, "C", \["A1", "A1:B2"\]\], \[2, "B", 2\], \[3, "C"\]

  • 输出 :

    `null`, `null`, 4, `null`, 6

    提示:
  • 1 <= height <= 26
  • 'A' <= width <= 'Z'
  • 1 <= row <= height
  • 'A' <= column <= width
  • -100 <= val <= 100
  • 1 <= numbers.length <= 26
  • numbers 单元格位置不会重复(以任意形式)------即公式不会同时引用两次相同的单元格

解题思路

解法一(公式记录 + 递归求值,主提交):表格本身不大,但求和公式必须长期生效------被引用格改值后,

求和格要跟着变。每个格子维护一个值:要么是常数,要么是公式(由若干单元格/矩形组成的列表)。

get 时若格子是公式,递归累加每个引用格(矩形按行列展开成单格);set 时直接覆盖为常数并清除原公式;

sum 时记录公式列表并立即返回计算结果。题目保证无环,递归不会死循环。

解法二(公式格改为即时缓存 + 增量更新):把每个公式格的结果缓存下来,并在 set 修改某个格子时

检查所有公式格是否引用了它,只重算受影响的公式格。当 get 调用频繁时更高效;本题调用量在 500 以内,

解法一的按需递归已足够,两种写法结果一致,以解法一为骨架。

代码实现

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

class Excel {

    private final int[][] mat;
    private final Map<String, List<String>> formula = new HashMap<>();

    public Excel(int height, char width) {
        mat = new int[height + 1][width - 'A' + 1];
    }

    public void set(int row, char column, int val) {
        mat[row][column - 'A'] = val;
        formula.remove(key(row, column));
    }

    public int get(int row, char column) {
        String k = key(row, column);
        if (formula.containsKey(k)) {
            return sumOf(formula.get(k));
        }
        return mat[row][column - 'A'];
    }

    public int sum(int row, char column, List<String> numbers) {
        // 先清掉该格的旧公式,避免覆盖后残留
        formula.put(key(row, column), new ArrayList<>(numbers));
        int v = sumOf(numbers);
        mat[row][column - 'A'] = v;
        return v;
    }

    private int sumOf(List<String> numbers) {
        int s = 0;
        for (String item : numbers) {
            int colon = item.indexOf(':');
            if (colon < 0) {
                s += valOfCell(item);
            } else {
                String a = item.substring(0, colon);
                String b = item.substring(colon + 1);
                int r1 = rowOf(a), c1 = colOf(a);
                int r2 = rowOf(b), c2 = colOf(b);
                for (int r = r1; r <= r2; ++r) {
                    for (int c = c1; c <= c2; ++c) {
                        s += get(r, (char) ('A' + c));
                    }
                }
            }
        }
        return s;
    }

    private int valOfCell(String cell) {
        return get(rowOf(cell), (char) ('A' + colOf(cell)));
    }

    private String key(int row, char column) {
        return column + Integer.toString(row);
    }

    private static int rowOf(String cell) {
        int i = 0;
        while (i < cell.length() && cell.charAt(i) >= 'A' && cell.charAt(i) <= 'Z') {
            ++i;
        }
        return Integer.parseInt(cell.substring(i));
    }

    private static int colOf(String cell) {
        return cell.charAt(0) - 'A';
    }

    public static void main(String[] args) {
        int pass = 0, total = 0;
        // 官方示例
        Excel excel = new Excel(3, 'C');
        excel.set(1, 'A', 2);
        ++total; if (excel.get(1, 'A') == 2) ++pass; else System.out.println("FAIL test1");
        ++total; if (excel.sum(3, 'C', java.util.Arrays.asList("A1", "A1:B2")) == 4) ++pass;
        else System.out.println("FAIL test2: " + excel.sum(3, 'C', java.util.Arrays.asList("A1", "A1:B2")));
        excel.set(2, 'B', 2);
        ++total; if (excel.get(3, 'C') == 6) ++pass; else System.out.println("FAIL test3: " + excel.get(3, 'C'));
        // 覆盖公式为常数
        Excel e2 = new Excel(5, 'E');
        e2.sum(1, 'A', java.util.Arrays.asList("B1", "C1"));
        e2.set(1, 'A', 7);
        ++total; if (e2.get(1, 'A') == 7) ++pass; else System.out.println("FAIL test4: " + e2.get(1, 'A'));
        // 矩形求和 + 链式引用
        Excel e3 = new Excel(4, 'D');
        e3.set(1, 'A', 1);
        e3.set(1, 'B', 2);
        e3.set(2, 'A', 3);
        e3.set(2, 'B', 4);
        ++total; if (e3.sum(3, 'C', java.util.Arrays.asList("A1:B2")) == 10) ++pass;
        else System.out.println("FAIL test5: " + e3.sum(3, 'C', java.util.Arrays.asList("A1:B2")));
        e3.sum(4, 'D', java.util.Arrays.asList("C3", "A1"));
        ++total; if (e3.get(4, 'D') == 11) ++pass; else System.out.println("FAIL test6: " + e3.get(4, 'D'));
        // set 修改被引用格,公式格跟着变
        e3.set(2, 'B', 100);
        ++total; if (e3.get(3, 'C') == 106) ++pass; else System.out.println("FAIL test7: " + e3.get(3, 'C'));
        if (pass == total) {
            System.out.println("== 0631 PASS ==");
        } else {
            System.out.println("== 0631 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 get/sum O(引用格数),set O(1) O(单元格数 + 公式引用数)
解法二 set O(公式格数 × 引用数),get O(1) O(单元格数 + 公式引用数)

632. Smallest Range Covering Elements from K Lists(最小区间)

题目描述

你有 k 个非递减排列的整数列表。找到一个最小区间,使得 k 个列表中的每个列表至少有一个数包含在其中。

我们定义如果 b-a < d-c 或者在 b-a == d-c 时 a < c,则区间 [a,b] 比 [c,d] 小。

示例 1

  • 输入 :nums = \[4,10,15,24,26, 0,9,12,20, 5,18,22,30]
  • 输出:20,24

示例 2

  • 输入 :nums = \[1,2,3,1,2,3,1,2,3]
  • 输出 :1,1
    提示:
  • nums.length == k
  • 1 <= k <= 3500
  • 1 <= nums[i].length <= 50
  • -10^5 <= nums[i][j] <= 10^5
  • nums[i] 按非递减顺序排列

解题思路

解法一(排序 + 滑动窗口 + 计数,主提交):把所有 (值, 所属列表编号) 二元组收集成一维数组并按值排序,

问题转化为「最短窗口恰好覆盖 k 个不同编号」。右端每次扩张一个元素,用 cnt 统计窗口内各编号出现次数;

当窗口已覆盖全部 k 个编号时,尝试移动左端收缩窗口(每次移除一个元素并更新计数),期间不断更新最小区间。

时间复杂度 O(L log L)(L 为总元素个数),空间 O(L)。

解法二(k 路指针 + 最小堆):为每个列表维护一个指针,每次取出当前 k 个指针指向值中的最小值所在列表,

用「当前 k 个值」的最小值与最大值构成候选区间,然后该列表指针后移。时间复杂度 O(L log k),

与解法一思路等价但需要额外维护指针,代码较繁琐,故以解法一为骨架。

代码实现

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

class Solution {

    public int[] smallestRange(List<List<Integer>> nums) {
        int n = 0;
        for (List<Integer> v : nums) {
            n += v.size();
        }
        int[][] t = new int[n][2];
        int k = nums.size();
        for (int i = 0, j = 0; i < k; ++i) {
            for (int x : nums.get(i)) {
                t[j++] = new int[] {x, i};
            }
        }
        Arrays.sort(t, (a, b) -> a[0] - b[0]);
        int j = 0;
        Map<Integer, Integer> cnt = new HashMap<>();
        int[] ans = new int[] {-1000000, 1000000};
        for (int[] e : t) {
            int b = e[0];
            int v = e[1];
            cnt.merge(v, 1, Integer::sum);
            while (cnt.size() == k) {
                int a = t[j][0];
                int w = t[j][1];
                int x = b - a - (ans[1] - ans[0]);
                if (x < 0 || (x == 0 && a < ans[0])) {
                    ans[0] = a;
                    ans[1] = b;
                }
                if (cnt.merge(w, -1, Integer::sum) == 0) {
                    cnt.remove(w);
                }
                ++j;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<List<Integer>> l1 = new ArrayList<>();
        l1.add(Arrays.asList(4, 10, 15, 24, 26));
        l1.add(Arrays.asList(0, 9, 12, 20));
        l1.add(Arrays.asList(5, 18, 22, 30));
        ++total; if (Arrays.equals(sol.smallestRange(l1), new int[] {20, 24})) ++pass;
        else System.out.println("FAIL test1: " + Arrays.toString(sol.smallestRange(l1)));
        List<List<Integer>> l2 = new ArrayList<>();
        l2.add(Arrays.asList(1, 2, 3));
        l2.add(Arrays.asList(1, 2, 3));
        l2.add(Arrays.asList(1, 2, 3));
        ++total; if (Arrays.equals(sol.smallestRange(l2), new int[] {1, 1})) ++pass;
        else System.out.println("FAIL test2: " + Arrays.toString(sol.smallestRange(l2)));
        List<List<Integer>> l3 = new ArrayList<>();
        l3.add(Arrays.asList(1));
        l3.add(Arrays.asList(2));
        l3.add(Arrays.asList(3));
        ++total; if (Arrays.equals(sol.smallestRange(l3), new int[] {1, 3})) ++pass;
        else System.out.println("FAIL test3: " + Arrays.toString(sol.smallestRange(l3)));
        List<List<Integer>> l4 = new ArrayList<>();
        l4.add(Arrays.asList(1, 2));
        l4.add(Arrays.asList(1, 3));
        l4.add(Arrays.asList(1, 4));
        ++total; if (Arrays.equals(sol.smallestRange(l4), new int[] {1, 1})) ++pass;
        else System.out.println("FAIL test4: " + Arrays.toString(sol.smallestRange(l4)));
        List<List<Integer>> l5 = new ArrayList<>();
        l5.add(Arrays.asList(-5, -4, -3, -2, -1));
        l5.add(Arrays.asList(-10, -9, 0, 1, 2));
        ++total; if (Arrays.equals(sol.smallestRange(l5), new int[] {-1, 0})) ++pass;
        else System.out.println("FAIL test5: " + Arrays.toString(sol.smallestRange(l5)));
        if (pass == total) {
            System.out.println("== 0632 PASS ==");
        } else {
            System.out.println("== 0632 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(L log L)(L 为总元素数) O(L)
解法二 O(L log k) O(k)

633. Sum of Square Numbers(平方数之和)

题目描述

给定一个非负整数 c,你要判断是否存在两个整数 a 和 b,使得 a^2 + b^2 = c。

示例 1

  • 输入 :c = 5
  • 输出 :true
  • 解释:1 * 1 + 2 * 2 = 5

示例 2

  • 输入 :c = 3
  • 输出 :false
    提示:
  • 0 <= c <= 2^31 - 1

解题思路

解法一(双指针,主提交):a、b 的取值范围都在 [0, ⌊√c⌋] 内。令 a = 0、b = (int) Math.sqrt(c),

计算 s = a*a + b*b:s == c 直接返回 true;s < c 说明太小,a 加一;s > c 说明太大,b 减一。

因为平方和随 a、b 单调,指针相向移动不会漏解。用 long 防止溢出,时间复杂度 O(⌊√c⌋),空间 O(1)。

解法二(枚举 + 哈希表):枚举所有 a * a <= c 的值并存入集合,再检查 c - b*b 是否在集合中,

等价于「两数之和」思路。时间复杂度 O(⌊√c⌋),空间 O(⌊√c⌋)。

代码实现

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

class Solution {

    public boolean judgeSquareSum(int c) {
        long a = 0;
        long b = (long) Math.sqrt(c);
        while (a <= b) {
            long s = a * a + b * b;
            if (s == c) {
                return true;
            }
            if (s < c) {
                ++a;
            } else {
                --b;
            }
        }
        return false;
    }

    public boolean judgeSquareSum2(int c) {
        Set<Long> set = new HashSet<>();
        for (long a = 0; a * a <= c; ++a) {
            set.add(a * a);
            if (set.contains(c - a * a)) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.judgeSquareSum(5)) ++pass; else System.out.println("FAIL test1");
        ++total; if (!sol.judgeSquareSum(3)) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.judgeSquareSum(0)) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.judgeSquareSum(1)) ++pass; else System.out.println("FAIL test4");
        ++total; if (!sol.judgeSquareSum(2147483647)) ++pass; else System.out.println("FAIL test5");
        // 解法二
        ++total; if (sol.judgeSquareSum2(5)) ++pass; else System.out.println("FAIL test6");
        ++total; if (!sol.judgeSquareSum2(3)) ++pass; else System.out.println("FAIL test7");
        // 2147483646 = 2 × 3 × 7 × 7 × 2434789... ,含 4k+3 型质因子奇次幂,不可分解为两平方和
        ++total; if (!sol.judgeSquareSum2(2147483646)) ++pass; else System.out.println("FAIL test8");
        if (pass == total) {
            System.out.println("== 0633 PASS ==");
        } else {
            System.out.println("== 0633 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(⌊√c⌋) O(1)
解法二 O(⌊√c⌋) O(⌊√c⌋)

634. Find the Derangement of An Array(寻找数组的错位排列)

题目描述

在组合数学中,如果一个排列中所有元素都不在原先的位置上,那么这个排列就被称为错位排列。

给定一个从 1 到 n 升序排列的数组,返回不同的错位排列的数量。

由于答案可能非常大,你只需要将答案对 10^9 + 7 取余输出即可。

示例 1

  • 输入 :n = 3
  • 输出: 2
  • 解释: 原始的数组为 1,2,3。两个错位排列的数组为 2,3,1 和 3,1,2。

示例 2

  • 输入 :n = 2
  • 输出 : 1
    提示:
  • 1 <= n <= 10^6

解题思路

解法一(递推 DP,主提交):错排数 !n 满足经典递推 D[i] = (i - 1) * (D[i - 1] + D[i - 2]):

把数字 i 放到位置 j(1 <= j < i),若 j 也放到位置 i,则剩余 i-2 个错排 D[i-2];

若 j 不放回位置 i,则相当于把 j 视为与 i 交换关系的错排,共 D[i-1] 种。

边界 D[0] = 1(空排列)、D[1] = 0,从小到大连乘并取模。时间复杂度 O(n),空间 O(1)。

解法二(容斥公式):错排数 !n = n! * Σ_{i=0..n} (-1)^i / i!,取模需预计算阶乘与逆元,

思路直观但需要预处理 n 以内阶乘逆元,复杂度和空间都 O(n);递推版更简洁,以解法一为骨架。

代码实现

java 复制代码
class Solution {

    public int findDerangement(int n) {
        final int mod = (int) 1e9 + 7;
        if (n == 1) {
            return 0;
        }
        long d0 = 1; // D[0] = 1
        long d1 = 0; // D[1] = 0
        long ans = 0;
        for (int i = 2; i <= n; ++i) {
            ans = (i - 1) * (d0 + d1) % mod;
            d0 = d1;
            d1 = ans;
        }
        return (int) ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.findDerangement(3) == 2) ++pass; else System.out.println("FAIL test1: " + sol.findDerangement(3));
        ++total; if (sol.findDerangement(2) == 1) ++pass; else System.out.println("FAIL test2: " + sol.findDerangement(2));
        ++total; if (sol.findDerangement(1) == 0) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.findDerangement(4) == 9) ++pass; else System.out.println("FAIL test4: " + sol.findDerangement(4));
        ++total; if (sol.findDerangement(5) == 44) ++pass; else System.out.println("FAIL test5: " + sol.findDerangement(5));
        ++total; if (sol.findDerangement(1000000) >= 0) ++pass; else System.out.println("FAIL test6 (big n)");
        if (pass == total) {
            System.out.println("== 0634 PASS ==");
        } else {
            System.out.println("== 0634 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

635. Design Log Storage System(设计日志存储系统)

题目描述

你将获得多条日志,每条日志都有唯一的 id 和 timestamp,timestamp 是形如 Year:Month:Day:Hour:Minute:Second

的字符串(例如 2017:01:01:23:59:59),所有值域都是零填充的十进制数。

实现 LogSystem 类:

  • LogSystem() 初始化 LogSystem 对象
  • void put(int id, string timestamp) 给定日志的 id 和 timestamp,将这个日志存入存储系统中。
  • int[] retrieve(string start, string end, string granularity) 返回在给定时间区间 [start, end](包含两端)内
    的所有日志的 id。granularity 表示考虑的时间粒度(例如精确到 Day、Minute 等),忽略更细粒度的时间字段。

示例

  • 输入 :

    "LogSystem", "put", "put", "put", "retrieve", "retrieve"

    \[, 1, "2017:01:01:23:59:59", 2, "2017:01:01:22:59:59", 3, "2016:01:01:00:00:00",

    "2016:01:01:01:01:01", "2017:01:01:23:00:00", "Year"\], \["2016:01:01:01:01:01", "2017:01:01:23:00:00", "Hour"\]

  • 输出 :

    `null`, `null`, `null`, `null`, \[3, 2, 1\], \[2, 1\]

    提示:
  • 1 <= id <= 2000
  • 1 <= timestamp.length <= 19
  • granularity 是这些值 ["Year", "Month", "Day", "Hour", "Minute", "Second"] 之一
  • 最多调用 500 次 put 和 retrieve

解题思路

解法一(字符串前缀比较,主提交):时间戳是定长字符串,字典序与时间序一致。按粒度取前缀长度:

Year=4、Month=7、Day=10、Hour=13、Minute=16、Second=19,将 start 与 end 截断到该长度,

每条日志的时间戳也截断到相同长度后做字典序比较,落在 [start[:i], end[:i]] 之间即命中。

put 直接追加,retrieve 遍历日志,复杂度 O(L·n)(n 为日志数),空间 O(n)。

解法二(有序集合二分):将所有日志按「完整时间戳」排序存储,retrieve 时把 start[:i]-00:00:00(下界)与

end[:i]-59:59:59(上界)之间的日志二分截取。日志量小时两种写法差别不大,以解法一为骨架。

代码实现

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

class LogSystem {

    private final List<int[]> logs = new ArrayList<>();
    private final Map<String, Integer> d = new HashMap<>();

    public LogSystem() {
        d.put("Year", 4);
        d.put("Month", 7);
        d.put("Day", 10);
        d.put("Hour", 13);
        d.put("Minute", 16);
        d.put("Second", 19);
    }

    public void put(int id, String timestamp) {
        logs.add(new int[] {id, logs.size()});
        times.add(timestamp);
    }

    private final List<String> times = new ArrayList<>();

    public List<Integer> retrieve(String start, String end, String granularity) {
        List<Integer> ans = new ArrayList<>();
        int i = d.get(granularity);
        String s = start.substring(0, i);
        String e = end.substring(0, i);
        List<String[]> hit = new ArrayList<>();
        for (int k = 0; k < times.size(); ++k) {
            String t = times.get(k).substring(0, i);
            if (s.compareTo(t) <= 0 && t.compareTo(e) <= 0) {
                hit.add(new String[] {times.get(k), String.valueOf(logs.get(k)[0])});
            }
        }
        // 按时间戳升序返回,与题目示例输出一致
        hit.sort((a, b) -> a[0].compareTo(b[0]));
        for (String[] x : hit) {
            ans.add(Integer.parseInt(x[1]));
        }
        return ans;
    }

    public static void main(String[] args) {
        int pass = 0, total = 0;
        LogSystem logSystem = new LogSystem();
        logSystem.put(1, "2017:01:01:23:59:59");
        logSystem.put(2, "2017:01:01:22:59:59");
        logSystem.put(3, "2016:01:01:00:00:00");
        List<Integer> r1 = logSystem.retrieve("2016:01:01:01:01:01", "2017:01:01:23:00:00", "Year");
        ++total; if (r1.equals(java.util.Arrays.asList(3, 2, 1))) ++pass;
        else System.out.println("FAIL test1: " + r1);
        List<Integer> r2 = logSystem.retrieve("2016:01:01:01:01:01", "2017:01:01:23:00:00", "Hour");
        ++total; if (r2.equals(java.util.Arrays.asList(2, 1))) ++pass;
        else System.out.println("FAIL test2: " + r2);
        LogSystem ls2 = new LogSystem();
        ls2.put(10, "2020:01:01:00:00:00");
        ls2.put(11, "2020:01:02:00:00:00");
        List<Integer> r3 = ls2.retrieve("2020:01:01:00:00:00", "2020:01:01:23:59:59", "Day");
        ++total; if (r3.equals(java.util.Arrays.asList(10))) ++pass;
        else System.out.println("FAIL test3: " + r3);
        List<Integer> r4 = ls2.retrieve("2020:01:01:00:00:00", "2020:01:02:00:00:00", "Second");
        ++total; if (r4.equals(java.util.Arrays.asList(10, 11))) ++pass;
        else System.out.println("FAIL test4: " + r4);
        if (pass == total) {
            System.out.println("== 0635 PASS ==");
        } else {
            System.out.println("== 0635 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 put O(1),retrieve O(L·n) O(n)
解法二 put O(L log n),retrieve O(log n + m) O(n)

636. Exclusive Time of Functions(函数的独占时间)

题目描述

有一个单线程 CPU 正在运行一个含有 n 道函数的程序。每道函数都有一个位于 0 和 n-1 之间的唯一标识符。

函数调用存储在一个调用栈上:当一个函数调用开始时,它的标识符将会推入栈中;而当一个函数调用结束时,

它的标识符将会从栈中弹出。标识符位于栈顶的函数是当前正在执行的函数。

每当一个函数开始或者结束时,将会记录一条日志,包括函数标识符、是开始还是结束、以及相应的时间戳。

给你一个由日志组成的列表 logs,其中 logs[i] 表示第 i 条日志消息,

该消息是一个按 "{function_id}:{"start" | "end"}:{timestamp}" 进行格式化的字符串。

注意,函数可以调用多次,可能存在递归调用。

函数的独占时间定义是在这个函数在程序所有函数调用中执行时间的总和,调用其他函数花费的时间不算该函数的独占时间。

以数组形式返回每个函数的独占时间。

示例 1

  • 输入 :n = 2, logs = "0:`start`:0","1:`start`:2","1:`end`:5","0:`end`:6"
  • 输出:3,4

示例 2

  • 输入 :n = 1, logs = "0:`start`:0","0:`start`:2","0:`end`:5","0:`start`:6","0:`end`:6","0:`end`:7"
  • 输出:8

示例 3

  • 输入 :n = 2, logs = "0:`start`:0","0:`start`:2","0:`end`:5","1:`start`:6","1:`end`:6","0:`end`:7"
  • 输出 :7,1
    提示:
  • 1 <= n <= 100
  • 2 <= logs.length <= 500
  • 0 <= function_id < n
  • 0 <= timestamp <= 10^9
  • 两个开始事件不会在同一时间戳发生
  • 两个结束事件不会在同一时间戳发生
  • 每道函数都有一个 "start" 日志和对应的 "end" 日志

解题思路

解法一(栈 + 片段结算,主提交):函数可嵌套,日志给出起止时刻。维护一个栈保存当前在执行的函数,

以及变量 pre 表示「上一个事件发生的时刻」。顺序扫描日志:

  • start 事件:先把 [pre, cur) 这段归属时间加给栈顶函数(即把栈顶函数已执行但尚未结算的片段记账),
    然后把新函数入栈,pre = cur。
  • end 事件:把闭区间 [pre, cur] 加给栈顶函数,弹栈,并把 pre 置为 cur + 1(结束时刻本身属于该函数)。
    一次扫描,每个事件 O(1)。时间复杂度 O(len(logs)),空间 O(n)。
    解法二(时间轴逐秒模拟):从 0 到最大时间戳逐秒推进,每秒归属栈顶函数。当时间戳范围很大(可达 10^9)
    时不可行,仅作为思路保留。

代码实现

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

class Solution {

    public int[] exclusiveTime(int n, List<String> logs) {
        int[] ans = new int[n];
        java.util.Deque<int[]> stk = new java.util.ArrayDeque<>();
        int pre = 0;
        for (String log : logs) {
            String[] parts = log.split(":");
            int id = Integer.parseInt(parts[0]);
            boolean isStart = parts[1].equals("start");
            int ts = Integer.parseInt(parts[2]);
            if (isStart) {
                if (!stk.isEmpty()) {
                    ans[stk.peek()[0]] += ts - pre;
                }
                stk.push(new int[] {id, ts});
                pre = ts;
            } else {
                int top = stk.pop()[0];
                ans[top] += ts - pre + 1;
                pre = ts + 1;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (Arrays.equals(sol.exclusiveTime(2, Arrays.asList("0:start:0", "1:start:2", "1:end:5", "0:end:6")), new int[] {3, 4})) ++pass;
        else System.out.println("FAIL test1: " + Arrays.toString(sol.exclusiveTime(2, Arrays.asList("0:start:0", "1:start:2", "1:end:5", "0:end:6"))));
        ++total; if (Arrays.equals(sol.exclusiveTime(1, Arrays.asList("0:start:0", "0:start:2", "0:end:5", "0:start:6", "0:end:6", "0:end:7")), new int[] {8})) ++pass;
        else System.out.println("FAIL test2: " + Arrays.toString(sol.exclusiveTime(1, Arrays.asList("0:start:0", "0:start:2", "0:end:5", "0:start:6", "0:end:6", "0:end:7"))));
        ++total; if (Arrays.equals(sol.exclusiveTime(2, Arrays.asList("0:start:0", "0:start:2", "0:end:5", "1:start:6", "1:end:6", "0:end:7")), new int[] {7, 1})) ++pass;
        else System.out.println("FAIL test3: " + Arrays.toString(sol.exclusiveTime(2, Arrays.asList("0:start:0", "0:start:2", "0:end:5", "1:start:6", "1:end:6", "0:end:7"))));
        ++total; if (Arrays.equals(sol.exclusiveTime(3, Arrays.asList("0:start:0", "0:end:0", "1:start:1", "1:end:1", "2:start:2", "2:end:2")), new int[] {1, 1, 1})) ++pass;
        else System.out.println("FAIL test4: " + Arrays.toString(sol.exclusiveTime(3, Arrays.asList("0:start:0", "0:end:0", "1:start:1", "1:end:1", "2:start:2", "2:end:2"))));
        if (pass == total) {
            System.out.println("== 0636 PASS ==");
        } else {
            System.out.println("== 0636 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(len(logs)) O(n)
解法二 O(T)(T 为最大时间戳,不可行) O(n)

637. Average of Levels in Binary Tree(二叉树的层平均值)

题目描述

给定一个非空二叉树的根节点 root,以数组的形式返回每一层节点的平均值。

与实际答案相差 10^-5 以内的答案可以被接受。

示例 1

  • 输入 :root = 3,9,20,`null`,`null`,15,7
  • 输出:3.00000,14.50000,11.00000
  • 解释:第 0 层的平均值为 3,第 1 层的平均值为 14.5,第 2 层的平均值为 11。

示例 2

  • 输入 :root = 3,9,20,15,7
  • 输出 :3.00000,14.50000,11.00000
    提示:
  • 树中节点数量在 [1, 10^4] 范围内
  • -2^31 <= Node.val <= 2^31 - 1

解题思路

解法一(BFS 逐层累加,主提交):按层遍历二叉树。队列每次取出当前层的全部节点,累加求和后除以

该层节点个数得到平均值,并把下一层节点全部入队。注意 Node.val 可达 ±2^31-1,累加使用 long

避免整型溢出。时间复杂度 O(n),空间 O(w)(w 为最大层宽)。

解法二(DFS 递归携带层号):维护每层的节点数与和,dfs(node, depth) 累加到对应层,最后逐层求平均。

需要先知道树的层数(或边递归边扩展列表)。时间复杂度 O(n),空间 O(height)。

代码实现

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

class Solution {

    private static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) { val = x; }
    }

    public List<Double> averageOfLevels(TreeNode root) {
        List<Double> ans = new ArrayList<>();
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
        while (!q.isEmpty()) {
            int size = q.size();
            long sum = 0;
            for (int i = 0; i < size; ++i) {
                TreeNode cur = q.poll();
                sum += cur.val;
                if (cur.left != null) {
                    q.offer(cur.left);
                }
                if (cur.right != null) {
                    q.offer(cur.right);
                }
            }
            ans.add(sum * 1.0 / size);
        }
        return ans;
    }

    // 解法二:DFS 收集每层和与个数
    public List<Double> averageOfLevels2(TreeNode root) {
        List<Long> sums = new ArrayList<>();
        List<Integer> cnts = new ArrayList<>();
        dfs(root, 0, sums, cnts);
        List<Double> ans = new ArrayList<>();
        for (int i = 0; i < sums.size(); ++i) {
            ans.add(sums.get(i) * 1.0 / cnts.get(i));
        }
        return ans;
    }

    private void dfs(TreeNode node, int depth, List<Long> sums, List<Integer> cnts) {
        if (node == null) {
            return;
        }
        if (depth == sums.size()) {
            sums.add(0L);
            cnts.add(0);
        }
        sums.set(depth, sums.get(depth) + node.val);
        cnts.set(depth, cnts.get(depth) + 1);
        dfs(node.left, depth + 1, sums, cnts);
        dfs(node.right, depth + 1, sums, cnts);
    }

    private static TreeNode mk(Integer[] a) {
        if (a == null || a.length == 0 || a[0] == null) {
            return null;
        }
        TreeNode root = new TreeNode(a[0]);
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
        int i = 1;
        while (i < a.length) {
            TreeNode cur = q.poll();
            if (a[i] != null) {
                cur.left = new TreeNode(a[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < a.length && a[i] != null) {
                cur.right = new TreeNode(a[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    private static boolean close(List<Double> a, List<Double> b) {
        if (a.size() != b.size()) {
            return false;
        }
        for (int i = 0; i < a.size(); ++i) {
            if (Math.abs(a.get(i) - b.get(i)) > 1e-5) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<Double> e1 = new ArrayList<>();
        e1.add(3.0);
        e1.add(14.5);
        e1.add(11.0);
        ++total; if (close(sol.averageOfLevels(mk(new Integer[] {3, 9, 20, null, null, 15, 7})), e1)) ++pass;
        else System.out.println("FAIL test1: " + sol.averageOfLevels(mk(new Integer[] {3, 9, 20, null, null, 15, 7})));
        List<Double> e2 = new ArrayList<>();
        e2.add(3.0);
        e2.add(14.5);
        e2.add(11.0);
        ++total; if (close(sol.averageOfLevels(mk(new Integer[] {3, 9, 20, 15, 7})), e2)) ++pass;
        else System.out.println("FAIL test2: " + sol.averageOfLevels(mk(new Integer[] {3, 9, 20, 15, 7})));
        List<Double> e3 = new ArrayList<>();
        e3.add(2147483647.0);
        e3.add(2147483647.0);
        ++total; if (close(sol.averageOfLevels(mk(new Integer[] {2147483647, 2147483647, 2147483647})), e3)) ++pass;
        else System.out.println("FAIL test3 (溢出): " + sol.averageOfLevels(mk(new Integer[] {2147483647, 2147483647, 2147483647})));
        // 解法二
        ++total; if (close(sol.averageOfLevels2(mk(new Integer[] {3, 9, 20, null, null, 15, 7})), e1)) ++pass;
        else System.out.println("FAIL test4");
        ++total; if (close(sol.averageOfLevels2(mk(new Integer[] {5})), new ArrayList<Double>() {{ add(5.0); }})) ++pass;
        else System.out.println("FAIL test5");
        if (pass == total) {
            System.out.println("== 0637 PASS ==");
        } else {
            System.out.println("== 0637 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

638. Shopping Offers(大礼包)

题目描述

在 LeetCode 商店中,有 n 件在售的物品。每件物品都有对应的价格。然而,也有一些大礼包,

每个大礼包以优惠的价格捆绑销售一组物品。

给你一个整数数组 price 表示物品价格,其中 price[i] 是第 i 件物品的价格。

另有一个整数数组 needs 表示购物清单,其中 needs[i] 是需要购买第 i 件物品的数量。

还有一个数组 special 表示大礼包,special[i] 的长度为 n + 1,

其中 special[i][j] 表示第 i 个大礼包中内含第 j 件物品的数量,且 special[i][n] 为第 i 个大礼包的价格。

返回确切满足购物清单所需花费的最低价格,你可以充分利用大礼包的优惠活动。

你不能购买超出购物清单指定数量的物品,即使那样会降低整体价格。任意大礼包可无限次购买。

示例 1

  • 输入 :price = 2,5, special = \[3,0,5,1,2,10], needs = 3,2
  • 输出:14

示例 2

  • 输入 :price = 2,3,4, special = \[1,1,0,4,2,2,1,9], needs = 1,2,1
  • 输出 :11
    提示:
  • n == price.length == needs.length
  • 1 <= n <= 6
  • 0 <= price[i], needs[i] <= 10
  • 1 <= special.length <= 100
  • special[i].length == n + 1
  • 0 <= special[i][j] <= 50
  • 生成的输入对于 0 <= j <= n - 1 至少有一个 special[i][j] 非零。

解题思路

解法一(状态压缩记忆化搜索,主提交):物品种类 n <= 6、每种至多买 10 件,把各物品剩余需求

用 4 位二进制打包进一个整数 mask(needs[i] << (i * 4))。dfs(mask) 表示在剩余需求为 mask

时的最低花费:先按零售价买齐余量作为初值;再尝试每一个礼包------若礼包每种数量都不超过剩余需求,

则使用该礼包并递归 dfs(mask - 礼包分量),取最小值。记忆化避免重复计算。空间上状态数最多 11^6 ≈ 1.7M,

实际剪枝后远小于此。

解法二(DFS 不带状态压缩版):直接传剩余需求列表递归,做法等价但每次需复制列表,

状态无法用哈希表记忆(除非转字符串),仅小规模可用。以解法一为骨架。

代码实现

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

class Solution {

    private final int bits = 4;
    private int n;
    private List<Integer> price;
    private List<List<Integer>> special;
    private Map<Integer, Integer> f = new HashMap<>();

    public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {
        n = needs.size();
        this.price = price;
        this.special = special;
        int mask = 0;
        for (int i = 0; i < n; ++i) {
            mask |= needs.get(i) << (i * bits);
        }
        return dfs(mask);
    }

    private int dfs(int cur) {
        if (f.containsKey(cur)) {
            return f.get(cur);
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans += price.get(i) * (cur >> (i * bits) & 0xf);
        }
        for (List<Integer> offer : special) {
            int nxt = cur;
            boolean ok = true;
            for (int j = 0; j < n; ++j) {
                if ((cur >> (j * bits) & 0xf) < offer.get(j)) {
                    ok = false;
                    break;
                }
                nxt -= offer.get(j) << (j * bits);
            }
            if (ok) {
                ans = Math.min(ans, offer.get(n) + dfs(nxt));
            }
        }
        f.put(cur, ans);
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.shoppingOffers(Arrays.asList(2, 5), Arrays.asList(Arrays.asList(3, 0, 5), Arrays.asList(1, 2, 10)), Arrays.asList(3, 2)) == 14) ++pass;
        else System.out.println("FAIL test1: " + sol.shoppingOffers(Arrays.asList(2, 5), Arrays.asList(Arrays.asList(3, 0, 5), Arrays.asList(1, 2, 10)), Arrays.asList(3, 2)));
        ++total; if (sol.shoppingOffers(Arrays.asList(2, 3, 4), Arrays.asList(Arrays.asList(1, 1, 0, 4), Arrays.asList(2, 2, 1, 9)), Arrays.asList(1, 2, 1)) == 11) ++pass;
        else System.out.println("FAIL test2: " + sol.shoppingOffers(Arrays.asList(2, 3, 4), Arrays.asList(Arrays.asList(1, 1, 0, 4), Arrays.asList(2, 2, 1, 9)), Arrays.asList(1, 2, 1)));
        // 无需礼包
        ++total; if (sol.shoppingOffers(Arrays.asList(1), Arrays.asList(Arrays.asList(2, 2)), Arrays.asList(3)) == 3) ++pass;
        else System.out.println("FAIL test3: " + sol.shoppingOffers(Arrays.asList(1), Arrays.asList(Arrays.asList(2, 2)), Arrays.asList(3)));
        // 礼包比零售贵(不应使用)
        ++total; if (sol.shoppingOffers(Arrays.asList(1, 1), Arrays.asList(Arrays.asList(1, 1, 100)), Arrays.asList(1, 1)) == 2) ++pass;
        else System.out.println("FAIL test4: " + sol.shoppingOffers(Arrays.asList(1, 1), Arrays.asList(Arrays.asList(1, 1, 100)), Arrays.asList(1, 1)));
        // needs 全零
        ++total; if (sol.shoppingOffers(Arrays.asList(1, 1), Arrays.asList(Arrays.asList(1, 1, 1)), Arrays.asList(0, 0)) == 0) ++pass;
        else System.out.println("FAIL test5");
        if (pass == total) {
            System.out.println("== 0638 PASS ==");
        } else {
            System.out.println("== 0638 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(11^n · m)(m 为礼包数) O(11^n)
解法二 O(11^n · m · n)(无记忆化) O(n)

639. Decode Ways II(解码方法 II)

题目描述

一条包含字母 A-Z 的消息通过以下的方式进行了编码:

'A' -> "1"、'B' -> "2"、...、'Z' -> "26"。

要解码一条已编码的消息,所有的数字都必须分组,然后按原来的编码方案反向映射回字母(可能存在多种方式)。

除了上面的数字字母映射方案,编码消息中可能包含 '*' 字符,可以表示从 '1' 到 '9' 的任一数字(不包括 '0')。

给你一个字符串 s,由数字和 '*' 字符组成,返回解码该字符串的方法数目。

由于答案数目可能非常大,返回 10^9 + 7 的模。

示例 1

  • 输入 :s = "*"
  • 输出:9

示例 2

  • 输入 :s = "1*"
  • 输出:18

示例 3

  • 输入 :s = "2*"
  • 输出 :15
    提示:
  • 1 <= s.length <= 10^5
  • s[i] 是 0 - 9 中的一位数字或字符 '*'

解题思路

解法一(线性 DP + 滚动变量,主提交):令 a = dp[i-2]、b = dp[i-1],逐位考虑当前字符与前一位。

单字符解码:'*' 有 9 种(1...9),'0' 为 0 种,数字非零为 1 种。

双字符解码(i > 1 时):按前一位与当前位是否为 '*' 分类讨论:

  • **:*1~9 * 1~9 中在 11..26 范围的有 15 种;
  • *d:前 * 取 1 或 2 时双位在 10...26 内,d <= 6 时 2 种、否则 1 种;
  • d*:1 后跟 1~9 共 9 种,2 后跟 1~6 共 6 种,其余 0 种;
  • dd:常规判断 10..26。
    每步对 c 累加并对 MOD 取模,最后滚动 a=b; b=c。时间复杂度 O(n),空间 O(1)。
    解法二(完整 DP 数组版):用 dp[i] 存放到第 i 位的方法数,逻辑与解法一相同但保留全部历史,
    便于理解,空间 O(n)。以解法一为骨架。

代码实现

java 复制代码
class Solution {

    private static final int MOD = 1000000007;

    public int numDecodings(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();
        long a = 0, b = 1, c = 0;
        for (int i = 1; i <= n; i++) {
            // 1 digit
            if (cs[i - 1] == '*') {
                c = 9 * b % MOD;
            } else if (cs[i - 1] != '0') {
                c = b;
            } else {
                c = 0;
            }
            // 2 digits
            if (i > 1) {
                if (cs[i - 2] == '*' && cs[i - 1] == '*') {
                    c = (c + 15 * a) % MOD;
                } else if (cs[i - 2] == '*') {
                    if (cs[i - 1] > '6') {
                        c = (c + a) % MOD;
                    } else {
                        c = (c + 2 * a) % MOD;
                    }
                } else if (cs[i - 1] == '*') {
                    if (cs[i - 2] == '1') {
                        c = (c + 9 * a) % MOD;
                    } else if (cs[i - 2] == '2') {
                        c = (c + 6 * a) % MOD;
                    }
                } else if (cs[i - 2] != '0' && (cs[i - 2] - '0') * 10 + cs[i - 1] - '0' <= 26) {
                    c = (c + a) % MOD;
                }
            }
            a = b;
            b = c;
        }
        return (int) c;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.numDecodings("*") == 9) ++pass; else System.out.println("FAIL test1: " + sol.numDecodings("*"));
        ++total; if (sol.numDecodings("1*") == 18) ++pass; else System.out.println("FAIL test2: " + sol.numDecodings("1*"));
        ++total; if (sol.numDecodings("2*") == 15) ++pass; else System.out.println("FAIL test3: " + sol.numDecodings("2*"));
        ++total; if (sol.numDecodings("0") == 0) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.numDecodings("10") == 1) ++pass; else System.out.println("FAIL test5: " + sol.numDecodings("10"));
        ++total; if (sol.numDecodings("226") == 3) ++pass; else System.out.println("FAIL test6: " + sol.numDecodings("226"));
        ++total; if (sol.numDecodings("1*6") == 20) ++pass; else System.out.println("FAIL test7: " + sol.numDecodings("1*6"));
        if (pass == total) {
            System.out.println("== 0639 PASS ==");
        } else {
            System.out.println("== 0639 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

640. Solve the Equation(求解方程)

题目描述

求解一个给定的方程,将 x 以字符串 "x=#value" 的形式返回。

该方程仅包含 '+'、'-' 操作、变量 x 和其对应系数。

如果方程没有解或存在的解不为整数,请返回 "No solution"。

如果方程有无限解,则返回 "Infinite solutions"。

题目保证,如果方程中只有一个解,则 'x' 的值是一个整数。

示例 1

  • 输入 :equation = "x+5-3+x=6+x-2"
  • 输出 : "x=2"

示例 2

  • 输入 :equation = "x=x"
  • 输出: "Infinite solutions"

示例 3

  • 输入 :equation = "2x=x"
  • 输出 : "x=0"
    提示:
  • 3 <= equation.length <= 1000
  • equation 只有一个 '='。
  • 方程由绝对值在 [0, 100] 范围内且无任何前导零的整数和变量 'x' 组成。

解题思路

解法一(符号状态机扫描,主提交):把方程整理成 a·x + b = 0 的形式,其中 a 为 x 的系数和、

b 为常数和。以 = 为界拆成左右两边,对每一边按「当前符号 + 数字/变量」解析每一项:

若该项以 x 结尾,系数计入 a(注意 x、-x、+x 的系数为 ±1);否则作为常数计入 b。

左边扫描结束后,把 a、b 的符号取反并入右边,得到 a·x + b = 0:

  • a == 0 && b == 0:Infinite solutions
  • a == 0 && b != 0:No solution
  • 否则 x = -b / a,因为题目保证整数,直接返回。
    解析用「上一个符号位 + 数字累积」的状态机,一次扫描,时间复杂度 O(len),空间 O(1)。
    解法二(正则拆分):用正则把每一边拆成带符号项再统计,逻辑直观但表达式复杂,
    且需处理 x 系数省略的情况;两种写法结果一致,以解法一为骨架。

代码实现

java 复制代码
class Solution {

    public String solveEquation(String equation) {
        String[] sides = equation.split("=");
        int[] left = parse(sides[0]);
        int[] right = parse(sides[1]);
        // left[0]x + left[1] = right[0]x + right[1]
        int a = left[0] - right[0];   // x 系数
        int b = right[1] - left[1];   // 常数(移项到右侧)
        if (a == 0) {
            return b == 0 ? "Infinite solutions" : "No solution";
        }
        return "x=" + (b / a);
    }

    // 解析一边:返回 [x 系数, 常数和]
    private int[] parse(String s) {
        int coef = 0, cons = 0;
        int i = 0, n = s.length();
        int sign = 1; // 当前项的符号
        while (i < n) {
            char c = s.charAt(i);
            if (c == '+') {
                sign = 1;
                ++i;
            } else if (c == '-') {
                sign = -1;
                ++i;
            } else {
                // 读取数字
                int j = i;
                while (j < n && Character.isDigit(s.charAt(j))) {
                    ++j;
                }
                if (j < n && s.charAt(j) == 'x') {
                    // x 项(可能省略系数)
                    int k = i == j ? 1 : Integer.parseInt(s.substring(i, j));
                    coef += sign * k;
                    i = j + 1;
                } else {
                    int v = Integer.parseInt(s.substring(i, j));
                    cons += sign * v;
                    i = j;
                }
            }
        }
        return new int[] {coef, cons};
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.solveEquation("x+5-3+x=6+x-2").equals("x=2")) ++pass; else System.out.println("FAIL test1: " + sol.solveEquation("x+5-3+x=6+x-2"));
        ++total; if (sol.solveEquation("x=x").equals("Infinite solutions")) ++pass; else System.out.println("FAIL test2: " + sol.solveEquation("x=x"));
        ++total; if (sol.solveEquation("2x=x").equals("x=0")) ++pass; else System.out.println("FAIL test3: " + sol.solveEquation("2x=x"));
        ++total; if (sol.solveEquation("x=x+2").equals("No solution")) ++pass; else System.out.println("FAIL test4: " + sol.solveEquation("x=x+2"));
        ++total; if (sol.solveEquation("-x=-1").equals("x=1")) ++pass; else System.out.println("FAIL test5: " + sol.solveEquation("-x=-1"));
        ++total; if (sol.solveEquation("0x=0").equals("Infinite solutions")) ++pass; else System.out.println("FAIL test6: " + sol.solveEquation("0x=0"));
        ++total; if (sol.solveEquation("1+1=x").equals("x=2")) ++pass; else System.out.println("FAIL test7: " + sol.solveEquation("1+1=x"));
        ++total; if (sol.solveEquation("2+3x=4x-1").equals("x=3")) ++pass; else System.out.println("FAIL test8: " + sol.solveEquation("2+3x=4x-1"));
        if (pass == total) {
            System.out.println("== 0640 PASS ==");
        } else {
            System.out.println("== 0640 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(len) O(1)
解法二 O(len)(正则开销常数) O(len)

641. Design Circular Deque(设计循环双端队列)

题目描述

设计实现双端队列。

实现 MyCircularDeque 类:

  • MyCircularDeque(int k):构造函数,双端队列最大为 k。
  • boolean insertFront():将一个元素添加到双端队列头部。如果操作成功返回 true,否则返回 false。
  • boolean insertLast():将一个元素添加到双端队列尾部。如果操作成功返回 true,否则返回 false。
  • boolean deleteFront():从双端队列头部删除一个元素。如果操作成功返回 true,否则返回 false。
  • boolean deleteLast():从双端队列尾部删除一个元素。如果操作成功返回 true,否则返回 false。
  • int getFront():从双端队列头部获得一个元素。如果双端队列为空,返回 -1。
  • int getRear():获得双端队列的最后一个元素。如果双端队列为空,返回 -1。
  • boolean isEmpty():若双端队列为空,则返回 true,否则返回 false。
  • boolean isFull():若双端队列满了,则返回 true,否则返回 false。

示例 1

复制代码
输入
["MyCircularDeque", "insertLast", "insertLast", "insertFront", "insertFront", "getRear", "isFull", "deleteLast", "insertFront", "getFront"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
输出
[null, true, true, true, false, 2, true, true, true, 4]

提示:

  • 1 <= k <= 1000
  • 0 <= value <= 1000
  • 各操作调用次数不大于 2000 次

解题思路

解法一(数组 + front + size,主提交):定长循环双端队列要在两端 O(1) 插入删除并区分空满。

用长度 k 的数组,维护 front 与 size:

  • 头插:front = (front - 1 + k) % k,写入 q[front];
  • 尾插:写入 q[(front + size) % k];
  • 头删:front = (front + 1) % k;
  • 尾删:直接 size--(不物理清除);
  • 空满由 size == 0 / size == k 判断,避免指针相撞歧义。
    所有操作 O(1),空间 O(k)。
    解法二(双向链表 + size):用头尾哨兵节点实现双向链表,插入/删除 O(1),空间 O(k) 但常数更大。
    两种写法逻辑等价,以数组版为骨架。

代码实现

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

class MyCircularDeque {

    private final int[] q;
    private final int capacity;
    private int front;
    private int size;

    public MyCircularDeque(int k) {
        q = new int[k];
        capacity = k;
    }

    public boolean insertFront(int value) {
        if (isFull()) {
            return false;
        }
        front = (front - 1 + capacity) % capacity;
        q[front] = value;
        ++size;
        return true;
    }

    public boolean insertLast(int value) {
        if (isFull()) {
            return false;
        }
        int idx = (front + size) % capacity;
        q[idx] = value;
        ++size;
        return true;
    }

    public boolean deleteFront() {
        if (isEmpty()) {
            return false;
        }
        front = (front + 1) % capacity;
        --size;
        return true;
    }

    public boolean deleteLast() {
        if (isEmpty()) {
            return false;
        }
        --size;
        return true;
    }

    public int getFront() {
        if (isEmpty()) {
            return -1;
        }
        return q[front];
    }

    public int getRear() {
        if (isEmpty()) {
            return -1;
        }
        return q[(front + size - 1 + capacity) % capacity];
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public boolean isFull() {
        return size == capacity;
    }

    public static void main(String[] args) {
        int pass = 0, total = 0;
        // 官方示例
        MyCircularDeque dq = new MyCircularDeque(3);
        ++total; if (dq.insertLast(1)) ++pass; else System.out.println("FAIL test1");
        ++total; if (dq.insertLast(2)) ++pass; else System.out.println("FAIL test2");
        ++total; if (dq.insertFront(3)) ++pass; else System.out.println("FAIL test3");
        ++total; if (!dq.insertFront(4)) ++pass; else System.out.println("FAIL test4");
        ++total; if (dq.getRear() == 2) ++pass; else System.out.println("FAIL test5: rear=" + dq.getRear());
        ++total; if (dq.isFull()) ++pass; else System.out.println("FAIL test6");
        ++total; if (dq.deleteLast()) ++pass; else System.out.println("FAIL test7");
        ++total; if (dq.insertFront(4)) ++pass; else System.out.println("FAIL test8");
        ++total; if (dq.getFront() == 4) ++pass; else System.out.println("FAIL test9: front=" + dq.getFront());
        // 空队列
        MyCircularDeque d2 = new MyCircularDeque(2);
        ++total; if (d2.isEmpty()) ++pass; else System.out.println("FAIL test10");
        ++total; if (d2.getFront() == -1) ++pass; else System.out.println("FAIL test11");
        ++total; if (d2.getRear() == -1) ++pass; else System.out.println("FAIL test12");
        ++total; if (!d2.deleteFront() && !d2.deleteLast()) ++pass; else System.out.println("FAIL test13");
        // 环绕与容量 1
        MyCircularDeque d3 = new MyCircularDeque(1);
        ++total; if (d3.insertFront(5) && d3.getFront() == 5 && d3.isFull()) ++pass; else System.out.println("FAIL test14");
        ++total; if (!d3.insertLast(6)) ++pass; else System.out.println("FAIL test15");
        ++total; if (d3.deleteFront() && d3.isEmpty()) ++pass; else System.out.println("FAIL test16");
        if (pass == total) {
            System.out.println("== 0641 PASS ==");
        } else {
            System.out.println("== 0641 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 全部操作 O(1) O(k)
解法二 全部操作 O(1) O(k)

题目描述

为搜索引擎设计一个搜索自动补全系统。用户会输入一条语句(最少包含一个字母,以特殊字符 '#' 结尾)。

给定一个字符串数组 sentences 和一个整数数组 times,长度都为 n,

其中 sentences[i] 是之前输入的句子,times[i] 是该句子输入的相应次数。

对于除 '#' 以外的每个输入字符,返回前 3 个历史热门句子,这些句子的前缀与已经输入的句子的部分相同。

规则:

  • 一条句子的热度定义为历史上用户输入这个句子的总次数。
  • 返回前 3 的句子需要按照热度从高到低排序。如果有多条热度相同的句子,请按照 ASCII 码的顺序输出。
  • 如果满足条件的句子个数少于 3,将它们全部输出。
  • 如果输入了特殊字符 '#',表示句子结束,返回空集合,并将输入的语句存储在系统中。
    实现 AutocompleteSystem 类:
  • AutocompleteSystem(String[] sentences, int[] times):使用数组初始化对象。
  • List<String> input(char c):表示用户输入了字符 c。
    如果 c == '#',返回空数组 [],并将输入的语句存储在系统中;
    否则返回前 3 个历史热门句子。

示例 1

复制代码
输入
["AutocompleteSystem", "input", "input", "input", "input"]
[[["i love you", "island", "iroman", "i love leetcode"], [5, 3, 2, 2]], ["i"], [" "], ["a"], ["#"]]
输出
[null, ["i love you", "island", "i love leetcode"], ["i love you", "i love leetcode"], [], []]

提示:

  • 1 <= n <= 100
  • 1 <= sentences[i].length <= 100
  • 1 <= times[i] <= 50
  • c 是小写英文字母、'#' 或空格 ' '

解题思路

解法一(字典树 + 堆排序,主提交):字典树节点记录以该节点为前缀的所有句子的热度;

向前推进时维护当前输入前缀 t,input 处理 '#' 时把句子写回字典树并重置;

否则沿前缀走到节点,DFS 收集子树内的 (句子, 热度),按 (-热度, 字典序) 排序取前 3。

时间复杂度每次 input O(句子总长 × log(候选数)),空间 O(总字符数)。

解法二(每次遍历全部句子过滤前缀):维护 input 前缀,遍历 sentences 找出所有以当前前缀开头的

句子再排序取前三;实现最简单,但每次都要扫全部句子(n <= 100 时足够快)。

两种写法结果一致,以字典树版为骨架。

代码实现

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

class Trie {
    Trie[] children = new Trie[27];
    int v;
    String w = "";

    void insert(String w, int t) {
        Trie node = this;
        for (char c : w.toCharArray()) {
            int idx = c == ' ' ? 26 : c - 'a';
            if (node.children[idx] == null) {
                node.children[idx] = new Trie();
            }
            node = node.children[idx];
        }
        node.v += t;
        node.w = w;
    }

    Trie search(String pref) {
        Trie node = this;
        for (char c : pref.toCharArray()) {
            int idx = c == ' ' ? 26 : c - 'a';
            if (node.children[idx] == null) {
                return null;
            }
            node = node.children[idx];
        }
        return node;
    }
}

class AutocompleteSystem {

    private final Trie trie = new Trie();
    private StringBuilder t = new StringBuilder();

    public AutocompleteSystem(String[] sentences, int[] times) {
        int i = 0;
        for (String s : sentences) {
            trie.insert(s, times[i++]);
        }
    }

    public List<String> input(char c) {
        List<String> res = new ArrayList<>();
        if (c == '#') {
            trie.insert(t.toString(), 1);
            t = new StringBuilder();
            return res;
        }
        t.append(c);
        Trie node = trie.search(t.toString());
        if (node == null) {
            return res;
        }
        PriorityQueue<Trie> q = new PriorityQueue<>((a, b) -> a.v == b.v ? b.w.compareTo(a.w) : a.v - b.v);
        dfs(node, q);
        while (!q.isEmpty()) {
            res.add(0, q.poll().w);
        }
        return res;
    }

    private void dfs(Trie node, PriorityQueue<Trie> q) {
        if (node == null) {
            return;
        }
        if (!node.w.isEmpty()) {
            q.offer(node);
            if (q.size() > 3) {
                q.poll();
            }
        }
        for (Trie child : node.children) {
            dfs(child, q);
        }
    }

    public static void main(String[] args) {
        int pass = 0, total = 0;
        AutocompleteSystem obj = new AutocompleteSystem(
                new String[] {"i love you", "island", "iroman", "i love leetcode"},
                new int[] {5, 3, 2, 2});
        ++total; if (obj.input('i').equals(java.util.Arrays.asList("i love you", "island", "i love leetcode"))) ++pass;
        else System.out.println("FAIL test1: " + obj.input('i'));
        ++total; if (obj.input(' ').equals(java.util.Arrays.asList("i love you", "i love leetcode"))) ++pass;
        else System.out.println("FAIL test2");
        ++total; if (obj.input('a').isEmpty()) ++pass; else System.out.println("FAIL test3");
        ++total; if (obj.input('#').isEmpty()) ++pass; else System.out.println("FAIL test4");
        // 新输入的句子被保存,再输入前缀会被检索到
        ++total; if (obj.input('i').size() == 3) ++pass; else System.out.println("FAIL test5: " + obj.input('i'));
        // 空句子处理
        AutocompleteSystem obj2 = new AutocompleteSystem(new String[] {"abc", "abb"}, new int[] {2, 3});
        ++total; if (obj2.input('a').equals(java.util.Arrays.asList("abb", "abc"))) ++pass;
        else System.out.println("FAIL test6: " + obj2.input('a'));
        if (pass == total) {
            System.out.println("== 0642 PASS ==");
        } else {
            System.out.println("== 0642 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 input O(L·log C)(L 为候选句总长) O(总字符数)
解法二 input O(n· s

643. Maximum Average Subarray I(子数组最大平均数 I)

题目描述

给你一个由 n 个元素组成的整数数组 nums 和一个整数 k。

请你找出平均数最大且长度为 k 的连续子数组,并输出该最大平均数。

任何误差小于 10^-5 的答案都将被视为正确答案。

示例 1

  • 输入 :nums = 1,12,-5,-6,50,3, k = 4
  • 输出:12.75

示例 2

  • 输入 :nums = 5, k = 1
  • 输出 :5.00000
    提示:
  • n == nums.length
  • 1 <= k <= n <= 10^5
  • -10^4 <= nums[i] <= 10^4

解题思路

解法一(定长滑动窗口,主提交):定长 k 子数组的最大平均值等价于最大和。

先求前 k 个元素的和,然后窗口右移:sum += nums[i] - nums[i-k],维护最大和,最后除以 k。

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

解法二(前缀和):预处理前缀和数组 ps,枚举所有长度为 k 的窗口 ps[i+k] - ps[i] 取最大再平均。

时间复杂度 O(n),空间 O(n)。两种写法结果一致,以滑动窗口为骨架。

代码实现

java 复制代码
class Solution {

    public double findMaxAverage(int[] nums, int k) {
        int sum = 0;
        for (int i = 0; i < k; ++i) {
            sum += nums[i];
        }
        int mx = sum;
        for (int i = k; i < nums.length; ++i) {
            sum += nums[i] - nums[i - k];
            mx = Math.max(mx, sum);
        }
        return mx * 1.0 / k;
    }

    public double findMaxAverage2(int[] nums, int k) {
        int n = nums.length;
        int[] ps = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            ps[i + 1] = ps[i] + nums[i];
        }
        int mx = Integer.MIN_VALUE;
        for (int i = 0; i + k <= n; ++i) {
            mx = Math.max(mx, ps[i + k] - ps[i]);
        }
        return mx * 1.0 / k;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (Math.abs(sol.findMaxAverage(new int[] {1, 12, -5, -6, 50, 3}, 4) - 12.75) < 1e-5) ++pass;
        else System.out.println("FAIL test1: " + sol.findMaxAverage(new int[] {1, 12, -5, -6, 50, 3}, 4));
        ++total; if (Math.abs(sol.findMaxAverage(new int[] {5}, 1) - 5.0) < 1e-5) ++pass;
        else System.out.println("FAIL test2");
        ++total; if (Math.abs(sol.findMaxAverage(new int[] {-1, -2, -3, -4}, 2) + 1.5) < 1e-5) ++pass;
        else System.out.println("FAIL test3: " + sol.findMaxAverage(new int[] {-1, -2, -3, -4}, 2));
        ++total; if (Math.abs(sol.findMaxAverage(new int[] {0, 4, 0, 3, 2}, 1) - 4.0) < 1e-5) ++pass;
        else System.out.println("FAIL test4");
        // 解法二
        ++total; if (Math.abs(sol.findMaxAverage2(new int[] {1, 12, -5, -6, 50, 3}, 4) - 12.75) < 1e-5) ++pass;
        else System.out.println("FAIL test5");
        ++total; if (Math.abs(sol.findMaxAverage2(new int[] {-1, -2, -3, -4}, 2) + 1.5) < 1e-5) ++pass;
        else System.out.println("FAIL test6");
        if (pass == total) {
            System.out.println("== 0643 PASS ==");
        } else {
            System.out.println("== 0643 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

644. Maximum Average Subarray II(子数组最大平均数 II)

题目描述

给你一个包含 n 个整数的数组 nums,和一个整数 k。

请你找出长度大于等于 k 且含最大平均值的连续子数组,并输出这个最大平均值。

任何计算误差小于 10^-5 的结果都将被视为正确答案。

示例 1

  • 输入 :nums = 1,12,-5,-6,50,3, k = 4
  • 输出:12.75000

示例 2

  • 输入 :nums = 5, k = 1
  • 输出 :5.00000
    提示:
  • n == nums.length
  • 1 <= k <= n <= 10^4
  • -10^4 <= nums[i] <= 10^4

解题思路

解法一(二分答案 + 前缀和判定,主提交):长度至少为 k 的子数组最大平均值。平均值对阈值单调:

存在均值 >= v 的子数组,则答案不小于 v。于是对 v(候选答案)做二分:

把每个元素减去 v 后,问题变为「是否存在长度 >= k 的连续子数组和非负」。

用前缀和 ps 判定:对每个右端点 i(i >= k),看 ps[i] - min(ps[0..i-k]) 是否 >= 0,

其中 minPre 是截至 i-k 处前缀和的最小值(保证子数组长度 >= k)。

二分区间取 [min(nums), max(nums)],迭代约 60 次即可满足 1e-5 精度。时间复杂度 O(n·iter),空间 O(n)。

解法二(暴力枚举全部长度 >= k 的子数组):枚举起点与终点并求平均,复杂度 O(n²),

当 n <= 10^4 时约 10^8 次运算,仅作思路验证用。以解法一为骨架。

代码实现

java 复制代码
class Solution {

    public double findMaxAverage(int[] nums, int k) {
        double eps = 1e-5;
        double l = 1e10, r = -1e10;
        for (int x : nums) {
            l = Math.min(l, x);
            r = Math.max(r, x);
        }
        while (r - l >= eps) {
            double mid = (l + r) / 2;
            if (check(nums, k, mid)) {
                l = mid;
            } else {
                r = mid;
            }
        }
        return l;
    }

    private boolean check(int[] nums, int k, double v) {
        double s = 0;
        for (int i = 0; i < k; ++i) {
            s += nums[i] - v;
        }
        if (s >= 0) {
            return true;
        }
        double t = 0;
        double mi = 0;
        for (int i = k; i < nums.length; ++i) {
            s += nums[i] - v;
            t += nums[i - k] - v;
            mi = Math.min(mi, t);
            if (s >= mi) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (Math.abs(sol.findMaxAverage(new int[] {1, 12, -5, -6, 50, 3}, 4) - 12.75) < 1e-5) ++pass;
        else System.out.println("FAIL test1: " + sol.findMaxAverage(new int[] {1, 12, -5, -6, 50, 3}, 4));
        ++total; if (Math.abs(sol.findMaxAverage(new int[] {5}, 1) - 5.0) < 1e-5) ++pass;
        else System.out.println("FAIL test2");
        ++total; if (Math.abs(sol.findMaxAverage(new int[] {-1, -10, -100}, 2) + 5.5) < 1e-5) ++pass;
        else System.out.println("FAIL test3: " + sol.findMaxAverage(new int[] {-1, -10, -100}, 2));
        long t0 = System.currentTimeMillis();
        double big = sol.findMaxAverage(new int[10000], 5000);
        long t1 = System.currentTimeMillis();
        ++total; if (Math.abs(big) < 1e-9 && (t1 - t0) < 2000) ++pass; else System.out.println("FAIL test4 (big)");
        if (pass == total) {
            System.out.println("== 0644 PASS ==");
        } else {
            System.out.println("== 0644 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n·log(r-l))(约 60 次迭代) O(n)
解法二 O(n²) O(1)

645. Set Mismatch(错误的集合)

题目描述

集合 s 包含从 1 到 n 的整数。不幸的是,因为数据错误,导致集合里面某一个数字复制成了

集合里面的另外一个数字的值,导致集合丢失了一个数字并且有一个数字重复。

给定一个数组 nums 代表了该集合发生错误后的结果。

请你找出重复出现的整数,再找到丢失的整数,将它们以数组的形式返回。

示例 1

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

示例 2

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

解题思路

解法一(数学和差,主提交):[1, n] 中一数重复、一数缺失。记 s 为数组和、s2 为去重和、

s1 为 1..n 之和。重复数 = s - s2,缺失数 = s1 - s2。用布尔数组做去重统计 s2。

也可直接用哈希集合。时间复杂度 O(n),空间 O(n)。

解法二(符号标记/原地):遍历 nums,把 |nums[i]| 位置的值取负,若已为负则该数重复;

第二次遍历找出仍为正的下标,即缺失数。时间复杂度 O(n),空间 O(1)。

两种写法结果一致,以解法一为骨架。

代码实现

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

class Solution {

    public int[] findErrorNums(int[] nums) {
        int n = nums.length;
        boolean[] seen = new boolean[n + 1];
        int dup = 0;
        long s2 = 0;
        for (int x : nums) {
            if (seen[x]) {
                dup = x;
            }
            seen[x] = true;
            s2 += x;
        }
        long s1 = (long) n * (n + 1) / 2;
        int missing = (int) (s1 - s2 + dup);
        return new int[] {dup, missing};
    }

    public int[] findErrorNums2(int[] nums) {
        int n = nums.length;
        int dup = 0;
        for (int i = 0; i < n; ++i) {
            int idx = Math.abs(nums[i]) - 1;
            if (nums[idx] < 0) {
                dup = Math.abs(nums[i]);
            } else {
                nums[idx] = -nums[idx];
            }
        }
        int missing = 0;
        for (int i = 0; i < n; ++i) {
            if (nums[i] > 0) {
                missing = i + 1;
                break;
            }
        }
        return new int[] {dup, missing};
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (Arrays.equals(sol.findErrorNums(new int[] {1, 2, 2, 4}), new int[] {2, 3})) ++pass;
        else System.out.println("FAIL test1: " + Arrays.toString(sol.findErrorNums(new int[] {1, 2, 2, 4})));
        ++total; if (Arrays.equals(sol.findErrorNums(new int[] {1, 1}), new int[] {1, 2})) ++pass;
        else System.out.println("FAIL test2: " + Arrays.toString(sol.findErrorNums(new int[] {1, 1})));
        ++total; if (Arrays.equals(sol.findErrorNums(new int[] {3, 2, 3, 4, 6, 5}), new int[] {3, 1})) ++pass;
        else System.out.println("FAIL test3: " + Arrays.toString(sol.findErrorNums(new int[] {3, 2, 3, 4, 6, 5})));
        ++total; if (Arrays.equals(sol.findErrorNums2(new int[] {1, 2, 2, 4}), new int[] {2, 3})) ++pass;
        else System.out.println("FAIL test4");
        ++total; if (Arrays.equals(sol.findErrorNums2(new int[] {3, 2, 3, 4, 6, 5}), new int[] {3, 1})) ++pass;
        else System.out.println("FAIL test5");
        ++total; if (Arrays.equals(sol.findErrorNums2(new int[] {2, 2}), new int[] {2, 1})) ++pass;
        else System.out.println("FAIL test6");
        if (pass == total) {
            System.out.println("== 0645 PASS ==");
        } else {
            System.out.println("== 0645 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

646. Maximum Length of Pair Chain(最长数对链)

题目描述

给你一个由 n 个数对组成的数对数组 pairs,其中 pairs[i] = [lefti, righti] 且 lefti < righti。

现在,我们定义一种跟随关系,当且仅当 b < c 时,数对 p2 = [c, d] 才可以跟在 p1 = [a, b] 后面。

我们用这种形式来构造数对链。找出并返回能够形成的最长数对链的长度。

你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

示例 1

  • 输入:pairs = \[1,2, 2,3, 3,4]
  • 输出:2
  • 解释:最长的数对链是 1,2 -> 3,4。

示例 2

  • 输入:pairs = \[1,2,7,8,4,5]
  • 输出:3
  • 解释 :最长的数对链是 1,2 -> 4,5 -> 7,8。
    提示:
  • n == pairs.length
  • 1 <= n <= 1000
  • -1000 <= lefti < righti <= 1000

解题思路

解法一(贪心:按右端点排序,主提交):数对链要求严格递增,右端点越小越容易再接后面的数对。

按 right 升序排序后遍历:若当前数对的 left > last(上一选中的右端点),则选中它并更新 last,

同时计数加一。该贪心与「无重叠区间」同构,能证明得到最长链。时间复杂度 O(n log n),空间 O(log n)。

解法二(DP:LIS 思路):按左端点排序后,dp[i] 表示以第 i 个数对结尾的最长链长度,

dp[i] = max(dp[j] + 1)(所有 pairs[j][1] < pairs[i][0] 的 j),答案为 max(dp)。

时间复杂度 O(n²),空间 O(n)。n 较大时解法一更快,两法结果一致。

代码实现

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

class Solution {

    public int findLongestChain(int[][] pairs) {
        Arrays.sort(pairs, (a, b) -> a[1] - b[1]);
        int last = Integer.MIN_VALUE;
        int ans = 0;
        for (int[] p : pairs) {
            if (p[0] > last) {
                ++ans;
                last = p[1];
            }
        }
        return ans;
    }

    public int findLongestChain2(int[][] pairs) {
        Arrays.sort(pairs, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
        int n = pairs.length;
        int[] dp = new int[n];
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            dp[i] = 1;
            for (int j = 0; j < i; ++j) {
                if (pairs[j][1] < pairs[i][0]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            ans = Math.max(ans, dp[i]);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.findLongestChain(new int[][] {{1, 2}, {2, 3}, {3, 4}}) == 2) ++pass;
        else System.out.println("FAIL test1: " + sol.findLongestChain(new int[][] {{1, 2}, {2, 3}, {3, 4}}));
        ++total; if (sol.findLongestChain(new int[][] {{1, 2}, {7, 8}, {4, 5}}) == 3) ++pass;
        else System.out.println("FAIL test2");
        ++total; if (sol.findLongestChain(new int[][] {{5, 6}, {1, 2}, {3, 4}}) == 3) ++pass;
        else System.out.println("FAIL test3");
        ++total; if (sol.findLongestChain(new int[][] {{-10, -8}, {-5, 0}, {-4, 3}}) == 2) ++pass;
        else System.out.println("FAIL test4");
        // 解法二
        ++total; if (sol.findLongestChain2(new int[][] {{1, 2}, {2, 3}, {3, 4}}) == 2) ++pass;
        else System.out.println("FAIL test5");
        ++total; if (sol.findLongestChain2(new int[][] {{1, 2}, {7, 8}, {4, 5}}) == 3) ++pass;
        else System.out.println("FAIL test6");
        ++total; if (sol.findLongestChain2(new int[][] {{1, 10}, {2, 3}, {4, 5}}) == 2) ++pass;
        else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0646 PASS ==");
        } else {
            System.out.println("== 0646 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

647. Palindromic Substrings(回文子串)

题目描述

给你一个字符串 s,请你统计并返回这个字符串中回文子串的数目。

回文字符串是正着读和倒过来读一样的字符串。

子字符串是字符串中的由连续字符组成的一个序列。

示例 1

  • 输入 :s = "abc"
  • 输出:3
  • 解释 :三个回文子串: "a", "b", "c"

示例 2

  • 输入 :s = "aaa"
  • 输出:6
  • 解释 :6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
    提示:
  • 1 <= s.length <= 1000
  • s 由小写英文字母组成

解题思路

解法一(中心扩展,主提交):枚举 2n-1 个回文中心(奇偶长度各 n 个与 n-1 个),

对每个中心向两侧扩展,只要两端字符相等就计数加一并继续扩展,直到失配。

中心总数 O(2n),每次扩展 O(n),总复杂度 O(n²),空间 O(1)。

解法二(区间 DP):dp[i][j] 表示 s[i..j] 是否为回文,dp[i][j] = s[i]==s[j] && (j-i<2 || dp[i+1][j-1]),

统计 dp[i][j] == true 的个数。时间复杂度 O(n²),空间 O(n²)。两种写法结果一致。

代码实现

java 复制代码
class Solution {

    public int countSubstrings(String s) {
        int ans = 0;
        int n = s.length();
        for (int k = 0; k < n * 2 - 1; ++k) {
            int i = k / 2, j = (k + 1) / 2;
            while (i >= 0 && j < n && s.charAt(i) == s.charAt(j)) {
                ++ans;
                --i;
                ++j;
            }
        }
        return ans;
    }

    public int countSubstrings2(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        int ans = 0;
        for (int i = n - 1; i >= 0; --i) {
            for (int j = i; j < n; ++j) {
                if (s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1])) {
                    dp[i][j] = true;
                    ++ans;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.countSubstrings("abc") == 3) ++pass; else System.out.println("FAIL test1: " + sol.countSubstrings("abc"));
        ++total; if (sol.countSubstrings("aaa") == 6) ++pass; else System.out.println("FAIL test2: " + sol.countSubstrings("aaa"));
        ++total; if (sol.countSubstrings("a") == 1) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.countSubstrings("abba") == 6) ++pass; else System.out.println("FAIL test4: " + sol.countSubstrings("abba"));
        // 解法二
        ++total; if (sol.countSubstrings2("abc") == 3) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.countSubstrings2("aaa") == 6) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.countSubstrings2("abba") == 6) ++pass; else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0647 PASS ==");
        } else {
            System.out.println("== 0647 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

648. Replace Words(单词替换)

题目描述

在英语中,我们有一个叫做词根(root) 的概念,可以词根后面添加其他一些词组成另一个较长的单词------

我们称这个词为衍生词(derivative)。例如,词根 help,跟随着继承词 "ful",可以形成新的单词 "helpful"。

现在,给定一个由许多词根组成的词典 dictionary 和一个用空格分隔单词形成的句子 sentence。

你需要将句子中的所有衍生词用词根替换掉。如果衍生词有许多可以形成它的词根,则用最短的词根替换它。

你需要输出替换之后的句子。

示例 1

  • 输入:dictionary = "cat","bat","rat", sentence = "the cattle was rattled by the battery"
  • 输出:"the cat was rat by the bat"

示例 2

  • 输入 :dictionary = "`a`","`b`","`c`", sentence = "aadsfasf absbs bbab cadsfafs"
  • 输出 :"a a b c"
    提示:
  • 1 <= dictionary.length <= 1000
  • 1 <= dictionary[i].length <= 100
  • dictionary[i] 仅由小写字母组成。
  • 1 <= sentence.length <= 10^6
  • sentence 仅由小写字母和空格组成。
  • sentence 中单词的数量在范围 [1, 1000] 内,长度在范围 [1, 1000] 内。
  • sentence 中单词之间由一个空格隔开,没有前导或尾随空格。

解题思路

解法一(字典树 + 最短前缀匹配,主提交):把所有词根插入 26 叉字典树(isEnd 标记词根终点)。

对句子的每个单词,从根沿字符走:一旦遇到 isEnd 节点,即找到该单词的最短词根,替换之;

若走到底都没遇到词根,保留原单词。时间复杂度 O(总字符数),空间 O(词根总字符数)。

解法二(哈希集合 + 逐前缀枚举):把词根存进 HashSet,对每个单词从长度 1 开始生长前缀,

第一个命中的前缀即最短词根。实现简单,最坏 O(单词总长 × 词根数)。以解法一为骨架。

代码实现

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

class Solution {

    private static class Trie {
        Trie[] children = new Trie[26];
        boolean isEnd;
    }

    public String replaceWords(List<String> dictionary, String sentence) {
        Trie root = new Trie();
        for (String w : dictionary) {
            Trie node = root;
            for (char c : w.toCharArray()) {
                int idx = c - 'a';
                if (node.children[idx] == null) {
                    node.children[idx] = new Trie();
                }
                node = node.children[idx];
            }
            node.isEnd = true;
        }
        StringBuilder sb = new StringBuilder();
        for (String word : sentence.split(" ")) {
            Trie node = root;
            StringBuilder pref = new StringBuilder();
            boolean found = false;
            for (char c : word.toCharArray()) {
                if (node.children[c - 'a'] == null) {
                    break;
                }
                pref.append(c);
                node = node.children[c - 'a'];
                if (node.isEnd) {
                    found = true;
                    break;
                }
            }
            if (sb.length() > 0) {
                sb.append(' ');
            }
            sb.append(found ? pref.toString() : word);
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.replaceWords(java.util.Arrays.asList("cat", "bat", "rat"),
                "the cattle was rattled by the battery").equals("the cat was rat by the bat")) ++pass;
        else System.out.println("FAIL test1: " + sol.replaceWords(java.util.Arrays.asList("cat", "bat", "rat"),
                "the cattle was rattled by the battery"));
        ++total; if (sol.replaceWords(java.util.Arrays.asList("a", "b", "c"),
                "aadsfasf absbs bbab cadsfafs").equals("a a b c")) ++pass;
        else System.out.println("FAIL test2: " + sol.replaceWords(java.util.Arrays.asList("a", "b", "c"),
                "aadsfasf absbs bbab cadsfafs"));
        ++total; if (sol.replaceWords(java.util.Arrays.asList("catt", "cat", "rat"),
                "the cattle was rattled by the battery").equals("the cat was rat by the battery")) ++pass;
        else System.out.println("FAIL test3");
        // 无词根命中
        ++total; if (sol.replaceWords(java.util.Arrays.asList("x"), "a b c").equals("a b c")) ++pass;
        else System.out.println("FAIL test4");
        if (pass == total) {
            System.out.println("== 0648 PASS ==");
        } else {
            System.out.println("== 0648 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

| --- | | --- |

| 解法一 | O(L)(L 为句子的字符总数) | O(Σ|dictionary|) |

| 解法二 | O(Σ len(word) × 最长词根) | O(Σ|dictionary|) |


649. Dota2 Senate(Dota2 参议院)

题目描述

Dota2 的世界里有两个阵营:Radiant(天辉)和 Dire(夜魇)。

Dota2 参议院由来自两派的参议员组成。现在参议院希望对一个 Dota2 游戏里的改变作出决定。

他们以一个基于轮为过程的投票进行。在每一轮中,每一位参议员都可以行使两项权利中的一项:

  • 剥夺一名参议员的权利:一名参议员可以使另一名参议员在本轮及所有后续轮次中失去所有权利。
  • 宣布胜利:如果参议员发现有权利投票的参议员都是同一个阵营的,他可以宣布胜利并决定在游戏中的有关变化。
    给你一个字符串 senate 代表每个参议员的阵营。字母 'R' 和 'D' 分别代表 Radiant(天辉)和 Dire(夜魇)。
    以轮为基础的过程从给定顺序的第一个参议员开始到最后一个参议员结束。这一过程将持续到投票结束。
    所有失去权利的参议员将在过程中被跳过。
    假设每一位参议员都足够聪明,会为自己的政党做出最好的策略,你需要预测哪一方最终会宣布胜利。
    输出应该是 "Radiant" 或 "Dire"。

示例 1

  • 输入:senate = "RD"
  • 输出:"Radiant"

示例 2

  • 输入:senate = "RDD"
  • 输出 :"Dire"
    提示:
  • n == senate.length
  • 1 <= n <= 10^4
  • senate[i] 为 'R' 或 'D'

解题思路

解法一(双队列模拟轮次,主提交):参议员按顺序行使「禁止下一名对方」的权利,直到一方清空。

用两个队列分别保存 Radiant 与 Dire 阵营参议员的下标。每轮取出两队队首:下标较小者先行动,

它把对方队首弹出(被禁止),自己下标加 n 回到队尾,表示下一轮继续参战;下标较大者则被禁止。

当某队队列为空时,另一队即胜利。时间复杂度 O(n),空间 O(n)。

解法二(直接在字符串上模拟删除):反复扫描字符串,删除每个 R 之后的第一个 D(反之亦然),

直到只剩一个阵营。最坏接近 O(n²),仅作思路保留。以解法一为骨架。

代码实现

java 复制代码
import java.util.LinkedList;
import java.util.Queue;

class Solution {

    public String predictPartyVictory(String senate) {
        int n = senate.length();
        Queue<Integer> rq = new LinkedList<>();
        Queue<Integer> dq = new LinkedList<>();
        for (int i = 0; i < n; ++i) {
            if (senate.charAt(i) == 'R') {
                rq.offer(i);
            } else {
                dq.offer(i);
            }
        }
        while (!rq.isEmpty() && !dq.isEmpty()) {
            int r = rq.poll();
            int d = dq.poll();
            if (r < d) {
                rq.offer(r + n);
            } else {
                dq.offer(d + n);
            }
        }
        return rq.isEmpty() ? "Dire" : "Radiant";
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.predictPartyVictory("RD").equals("Radiant")) ++pass; else System.out.println("FAIL test1: " + sol.predictPartyVictory("RD"));
        ++total; if (sol.predictPartyVictory("RDD").equals("Dire")) ++pass; else System.out.println("FAIL test2: " + sol.predictPartyVictory("RDD"));
        ++total; if (sol.predictPartyVictory("RRDDD").equals("Radiant")) ++pass; else System.out.println("FAIL test3: " + sol.predictPartyVictory("RRDDD"));
        ++total; if (sol.predictPartyVictory("D").equals("Dire")) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.predictPartyVictory("RRR").equals("Radiant")) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.predictPartyVictory("DRDRDR").equals("Dire")) ++pass; else System.out.println("FAIL test6: " + sol.predictPartyVictory("DRDRDR"));
        if (pass == total) {
            System.out.println("== 0649 PASS ==");
        } else {
            System.out.println("== 0649 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

650. 2 Keys Keyboard(两个键的键盘)

题目描述

最初记事本上只有一个字符 'A'。你每次可以对这个记事本进行两种操作:

  • Copy All(复制全部):复制这个记事本中的所有字符(不允许仅复制部分字符)。
  • Paste(粘贴):粘贴上一次复制的字符。
    给你一个数字 n,你需要使用最少的操作次数,在记事本上输出恰好 n 个 'A'。
    返回能够打印出 n 个 'A' 的最少操作次数。

示例 1

  • 输入:3
  • 输出:3
  • 解释:最初只有一个字符 'A'。第 1 步使用 Copy All,第 2 步使用 Paste 获得 'AA',第 3 步使用 Paste 获得 'AAA'。

示例 2

  • 输入 :n = 1
  • 输出 :0
    提示:
  • 1 <= n <= 1000

解题思路

解法一(因数分解,主提交):要把文本从 1 增长到 n。若最后一次操作前文本长度为 n/j,

则最后一次把长度为 n/j 的文本粘贴到共 j 份(1 次 Copy All + j-1 次 Paste),操作数为 dfs(n/j) + j。

更直接的结论:最少操作次数等于 n 的所有质因数之和。因为每次「复制 + k 次粘贴」把一个因子 k 放大,

总和就是把 n 分解成若干个大于 1 的因子之和的最小值,即质因数分解后求和。

从 2 开始向上试除,累加因子即可,时间复杂度 O(√n),空间 O(1)。

解法二(记忆化搜索):dfs(x) 表示从 1 个字符得到 x 个字符的最少操作数,

dfs(x) = min(dfs(x/d) + d)(d 为能整除 x 的因子)。状态仅 x 取值,配合质因数结论等价。

以解法一为骨架。

代码实现

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

class Solution {

    public int minSteps(int n) {
        int ans = 0;
        for (int d = 2; d * d <= n; ++d) {
            while (n % d == 0) {
                ans += d;
                n /= d;
            }
        }
        if (n > 1) {
            ans += n;
        }
        return ans;
    }

    private int[] memo;

    public int minSteps2(int n) {
        memo = new int[n + 1];
        Arrays.fill(memo, -1);
        memo[1] = 0;
        return dfs(n);
    }

    private int dfs(int x) {
        if (memo[x] != -1) {
            return memo[x];
        }
        int best = x; // 全 1+1+...+1 分解(即每次复制一个字符粘贴)
        for (int d = 2; d * d <= x; ++d) {
            if (x % d == 0) {
                best = Math.min(best, dfs(x / d) + d);
            }
        }
        memo[x] = best;
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.minSteps(3) == 3) ++pass; else System.out.println("FAIL test1: " + sol.minSteps(3));
        ++total; if (sol.minSteps(1) == 0) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.minSteps(2) == 2) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.minSteps(4) == 4) ++pass; else System.out.println("FAIL test4: " + sol.minSteps(4));
        ++total; if (sol.minSteps(7) == 7) ++pass; else System.out.println("FAIL test5: " + sol.minSteps(7));
        ++total; if (sol.minSteps(9) == 6) ++pass; else System.out.println("FAIL test6: " + sol.minSteps(9));
        ++total; if (sol.minSteps(8) == 6) ++pass; else System.out.println("FAIL test7: " + sol.minSteps(8));
        // 解法二
        ++total; if (sol.minSteps2(3) == 3 && sol.minSteps2(9) == 6 && sol.minSteps2(8) == 6) ++pass;
        else System.out.println("FAIL test8");
        ++total; if (sol.minSteps2(1000) == sol.minSteps(1000)) ++pass; else System.out.println("FAIL test9");
        if (pass == total) {
            System.out.println("== 0650 PASS ==");
        } else {
            System.out.println("== 0650 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(√n) O(1)
解法二 O(√n · d(n)) O(√n)
相关推荐
tianbin9111 小时前
批处理与微调辅助场景:Claude 中转并发、链路缓存与压测对比
java·开发语言·缓存
frjc2 小时前
Spring AI 里 ChatClient 和 ChatModel 有什么区别?什么时候该用哪个?
java·开发语言·spring·ai
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 圈子功能的表设计方案
java·开发语言·交友
文丑颜不良啊2 小时前
LeetCode601~625题目解析
java·leetcode
文丑颜不良啊2 小时前
LeetCode651~675题目解析
java·leetcode
frjc2 小时前
Spring WebFlux SSE 长连接,可以替换为 Java 21 虚拟线程吗?
java·人工智能·spring·ai·ai编程
Xin7703 小时前
LeetCode 78. 子集(回溯法)
leetcode
INGNIGHT3 小时前
151 · 买卖股票的最佳时机 III(序列dp)
数据结构·算法·leetcode