LeetCode601~625题目解析

LeetCode 601-625. 题解合集

文章目录


601. Human Traffic of Stadium(体育馆的人流量)

题目类型:数据库(SQL)

题目描述

表:Stadium

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| id            | int     |
| visit_date    | date    |
| people        | int     |
+---------------+---------+

visit_date 是该表中具有唯一值的列。每日人流量信息被记录在这三列信息中:序号 (id)、日期 (visit_date)、人流量 (people)。每天只有一行记录,日期随着 id 的增加而增加。

编写解决方案找出每行的人数大于或等于 100 且 id 连续的三行或更多行记录。返回按 visit_date 升序排列的结果表。

示例 1:

复制代码
输入:
Stadium 表:
+------+------------+-----------+
| id   | visit_date | people    |
+------+------------+-----------+
| 1    | 2017-01-01 | 10        |
| 2    | 2017-01-02 | 109       |
| 3    | 2017-01-03 | 150       |
| 4    | 2017-01-04 | 99        |
| 5    | 2017-01-05 | 145       |
| 6    | 2017-01-06 | 1455      |
| 7    | 2017-01-07 | 199       |
| 8    | 2017-01-09 | 188       |
+------+------------+-----------+
输出:
+------+------------+-----------+
| id   | visit_date | people    |
+------+------------+-----------+
| 5    | 2017-01-05 | 145       |
| 6    | 2017-01-06 | 1455      |
| 7    | 2017-01-07 | 199       |
| 8    | 2017-01-09 | 188       |
+------+------------+-----------+

解释 :id 为 5、6、7、8 的四行 id 连续,并且每行都有 >= 100 的人数记录。即使第 7 行和第 8 行的 visit_date 不是连续的,输出也应当包含第 8 行,因为我们只需要考虑 id 连续的记录。不输出 id 为 2 和 3 的行,因为至少需要三条 id 连续的记录。

解题思路

解法一(窗口函数:差值分组,推荐):题目要求连续至少三天 people >= 100。先筛出达标行后,连续 id 与按 id 排序的行号之差为常数 :用 id - ROW_NUMBER() OVER (ORDER BY id) 得到分组键 rk,同一段连续编号共享同一个 rk;再用 COUNT(1) OVER (PARTITION BY rk) 统计该连续段长度,保留长度至少为 3 的段即可覆盖任意长度的连续区间。

解法二(差值分组 + HAVING 过滤):与方法一相同的思想,先算 id_diff = id - ROW_NUMBER(),再用 GROUP BY id_diff HAVING COUNT(*) > 2 找出「足够长」的连续段集合,最后 IN 回表过滤,语义更贴近集合判断。

代码实现

sql 复制代码
-- 解法一:ROW_NUMBER 差值分组 + 窗口计数(推荐)
WITH
    S AS (
        SELECT
            *,
            id - (ROW_NUMBER() OVER (ORDER BY id)) AS rk
        FROM Stadium
        WHERE people >= 100
    ),
    T AS (SELECT *, COUNT(1) OVER (PARTITION BY rk) AS cnt FROM S)
SELECT id, visit_date, people
FROM T
WHERE cnt >= 3
ORDER BY 1;
sql 复制代码
-- 解法二:差值分组 + HAVING 过滤
WITH
    Consecutive AS (
        SELECT
            *,
            id - ROW_NUMBER() OVER () AS id_diff
        FROM Stadium
        WHERE people >= 100
    )
SELECT id, visit_date, people
FROM Consecutive
WHERE
    id_diff IN (
        SELECT id_diff
        FROM Consecutive
        GROUP BY id_diff
        HAVING COUNT(*) > 2
    )
ORDER BY visit_date;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n)(排序 + 窗口,n 为 Stadium 行数) O(n)(窗口结果)
解法二 O(n log n) O(n)

602. Friend Requests II: Who Has the Most Friends(好友申请 II:谁有最多的好友)

题目类型:数据库(SQL)

题目描述

RequestAccepted 表:

复制代码
+----------------+---------+
| Column Name    | Type    |
+----------------+---------+
| requester_id   | int     |
| accepter_id    | int     |
| accept_date    | date    |
+----------------+---------+

(requester_id, accepter_id) 是这张表的主键(具有唯一值的列的组合)。这张表包含发送好友请求的人的 ID,接收好友请求的人的 ID,以及好友请求通过的日期。

编写解决方案,找出拥有最多的好友的人和他拥有的好友数目。生成的测试用例保证拥有最多好友数目的只有 1 个人。

示例 1:

复制代码
输入:
RequestAccepted 表:
+--------------+-------------+-------------+
| requester_id | accepter_id | accept_date |
+--------------+-------------+-------------+
| 1            | 2           | 2016/06/03  |
| 1            | 3           | 2016/06/08  |
| 2            | 3           | 2016/06/08  |
| 3            | 4           | 2016/06/09  |
+--------------+-------------+-------------+
输出:
+----+-----+
| id | num |
+----+-----+
| 3  | 3   |
+----+-----+

解释:编号为 3 的人是编号为 1、2 和 4 的人的好友,所以他总共有 3 个好友,比其他人都多。

进阶:在真实世界里,可能会有多个人拥有好友数相同且最多,你能找到所有这些人吗?

解题思路

解法一(UNION ALL 双向合并 + 分组计数,推荐):好友关系是无向 的,表中只记录一次接受。若只按 requester_id 或只按 accepter_id 分组,会漏掉另一侧的度数。正确做法是把 (requester_id, accepter_id) 与对向 (accepter_id, requester_id) 用 UNION ALL 合并成两行,这样每个人作为「起点」出现的次数正好等于其好友数;再按起点 GROUP BY 计数、按人数降序取一行即可。

解法二(进阶:并列全取):把「取一行」改为「与最大计数相等」的过滤------先求出最大好友数 MAX(num),再用子查询或 HAVING 保留计数等于最大值的所有用户,即可处理多个人并列最多的场景。

代码实现

sql 复制代码
-- 解法一:UNION ALL 双向合并 + 分组计数(推荐)
WITH
    T AS (
        SELECT requester_id, accepter_id FROM RequestAccepted
        UNION ALL
        SELECT accepter_id, requester_id FROM RequestAccepted
    )
SELECT requester_id AS id, COUNT(1) AS num
FROM T
GROUP BY 1
ORDER BY 2 DESC
LIMIT 1;
sql 复制代码
-- 解法二(进阶):并列最多全取
WITH
    T AS (
        SELECT requester_id AS id FROM RequestAccepted
        UNION ALL
        SELECT accepter_id FROM RequestAccepted
    ),
    C AS (SELECT id, COUNT(1) AS num FROM T GROUP BY id)
SELECT id, num
FROM C
WHERE num = (SELECT MAX(num) FROM C);

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n)(n 为接受记录数,分组 + 排序) O(n)(合并后行数)
解法二 O(n log n) O(k)(k 为去重后用户数)

603. Consecutive Available Seats(连续空余座位)

题目类型:数据库(SQL)

题目描述

表:Cinema

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| seat_id     | int  |
| free        | bool |
+-------------+------+

seat_id 是该表的自动递增主键列。该表的每一行表示第 i 个座位是否空闲:1 表示空闲,0 表示被占用。

查找电影院所有连续可用的座位。返回按 seat_id 升序排序的结果表。测试用例的生成使得两个以上的座位连续可用。

示例 1:

复制代码
输入:
Cinema 表:
+---------+------+
| seat_id | free |
+---------+------+
| 1       | 1    |
| 2       | 0    |
| 3       | 1    |
| 4       | 1    |
| 5       | 1    |
+---------+------+
输出:
+---------+
| seat_id |
+---------+
| 3       |
| 4       |
| 5       |
+---------+

解题思路

解法一(自连接 + 去重,推荐):空座若要被列入结果,必须与左右至少一侧 相邻且该侧也为空。用自连接把 seat_id 差为 1 的两行配对,条件为两侧 free 都为真,配对结果里每个座位可能出现两次(与左邻配一次、与右邻配一次),DISTINCT 去重后按 seat_id 升序输出。

解法二(LAG/LEAD 窗口函数):自连接会生成配对行,窗口 LAG(free) 与 LEAD(free) 可在一行内读到前后座位的 free 值,若自身 free 与前一或后一之和为 2,则该座属于连续空座。WHERE a = 2 OR b = 2 过滤。

解法三(三连窗口求和):也可以只用一个窗口:SUM(free) OVER (... ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) 一次看「当前座 + 前后各一」的窗口和,当前座为空且窗口和大于 1,说明至少一侧邻座也为空。

代码实现

sql 复制代码
-- 解法一:自连接 + DISTINCT(推荐)
SELECT DISTINCT a.seat_id
FROM
    Cinema AS a
    JOIN Cinema AS b ON ABS(a.seat_id - b.seat_id) = 1 AND a.free AND b.free
ORDER BY 1;
sql 复制代码
-- 解法二:LAG / LEAD 窗口函数
WITH
    T AS (
        SELECT
            seat_id,
            (free + (LAG(free) OVER (ORDER BY seat_id))) AS a,
            (free + (LEAD(free) OVER (ORDER BY seat_id))) AS b
        FROM Cinema
    )
SELECT seat_id
FROM T
WHERE a = 2 OR b = 2;
sql 复制代码
-- 解法三:三连窗口求和
WITH
    T AS (
        SELECT
            *,
            SUM(free = 1) OVER (
                ORDER BY seat_id
                ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
            ) AS cnt
        FROM Cinema
    )
SELECT seat_id
FROM T
WHERE free = 1 AND cnt > 1
ORDER BY 1;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n²)(自连接笛卡尔积最坏,n 为座位数) O(n)(配对结果)
解法二 O(n log n)(排序 + 窗口) O(n)
解法三 O(n log n) O(n)

604. Design Compressed String Iterator(迭代压缩字符串)

题目描述

设计并实现一个迭代压缩字符串的数据结构。给定的压缩字符串的形式是,每个字母后面紧跟一个正整数,表示该字母在原始未压缩字符串中出现的次数。

设计一个数据结构,它支持如下两种操作:next 和 hasNext。

  • next():如果原始字符串中仍有未压缩字符,则返回下一个字符,否则返回空格。
  • hasNext():如果原始字符串中存在未压缩的字母,则返回 true,否则返回 false。

示例

  • 输入 :"StringIterator","`next`","`next`","`next`","`next`","`next`","`next`","`hasNext`","`next`","`hasNext`"

    \["L1e2t1C1o1d1e1"\], \[\], \[\], \[\], \[\], \[\], \[\], \[\], \[\], \[\]

  • 输出 : `null`, "L", "e", "e", "`t`", "C", "o", `true`, "`d`", `true`
    提示:
  • 1 <= compressedString.length <= 1000
  • compressedString 由小写字母、大写字母和数字组成。
  • 在 compressedString 中,单个字符的重复次数在 [1, 10^9] 范围内。
  • next 和 hasNext 的操作数最多为 100。

解题思路

解法一(预解析存储,主提交):构造时把「字母 + 十进制次数」拆成 (c, x) 存入两个平行列表 cs / ns,

用下标 p 指向当前组。next() 取出 cs[p] 并把 ns[p] 减一,减到 0 则前进到下一组;

hasNext() 判断 p 未越界且当前组剩余次数大于 0。每次操作 O(1),构造 O(len)。

此解法把「重复次数可能很大(可达 10^9)」也正确支持,不会展开存储大量重复字符。

思路二(惰性解析):不预先建表,只保存原串与一个游标,在 next() 时按需读取字母并累计其后数字;

状态只有「当前字符 + 剩余次数」两个变量,空间 O(1)。两种写法结果等价,这里采用解法一为骨架。

代码实现

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

class StringIterator {

    private final List<Character> cs = new ArrayList<>();
    private final List<Integer> ns = new ArrayList<>();
    private int p; // 当前组下标

    public StringIterator(String compressedString) {
        int n = compressedString.length();
        int i = 0;
        while (i < n) {
            char c = compressedString.charAt(i++); // 字母
            int x = 0;
            while (i < n && Character.isDigit(compressedString.charAt(i))) {
                x = x * 10 + (compressedString.charAt(i) - '0');
                ++i;
            }
            cs.add(c);
            ns.add(x);
        }
    }

    public char next() {
        if (!hasNext()) {
            return ' ';
        }
        char ans = cs.get(p);
        ns.set(p, ns.get(p) - 1);
        if (ns.get(p) == 0) {
            ++p;
        }
        return ans;
    }

    public boolean hasNext() {
        return p < ns.size() && ns.get(p) > 0;
    }

    public static void main(String[] args) {
        int pass = 0, total = 0;
        StringIterator it = new StringIterator("L1e2t1C1o1d1e1");
        ++total; if (it.next() == 'L') ++pass; else System.out.println("FAIL test1");
        ++total; if (it.next() == 'e') ++pass; else System.out.println("FAIL test2");
        ++total; if (it.next() == 'e') ++pass; else System.out.println("FAIL test3");
        ++total; if (it.next() == 't') ++pass; else System.out.println("FAIL test4");
        ++total; if (it.next() == 'C') ++pass; else System.out.println("FAIL test5");
        ++total; if (it.next() == 'o') ++pass; else System.out.println("FAIL test6");
        ++total; if (it.hasNext()) ++pass; else System.out.println("FAIL test7");
        ++total; if (it.next() == 'd') ++pass; else System.out.println("FAIL test8");
        ++total; if (it.hasNext()) ++pass; else System.out.println("FAIL test9");
        ++total; if (it.next() == 'e') ++pass; else System.out.println("FAIL test10");
        ++total; if (!it.hasNext()) ++pass; else System.out.println("FAIL test11");
        ++total; if (it.next() == ' ') ++pass; else System.out.println("FAIL test12");
        // 空压缩串
        StringIterator it2 = new StringIterator("");
        ++total; if (!it2.hasNext()) ++pass; else System.out.println("FAIL test13");
        ++total; if (it2.next() == ' ') ++pass; else System.out.println("FAIL test14");
        // 大次数
        StringIterator it3 = new StringIterator("a3b1");
        ++total; if (it3.next() == 'a') ++pass; else System.out.println("FAIL test15");
        ++total; if (it3.next() == 'a') ++pass; else System.out.println("FAIL test16");
        ++total; if (it3.next() == 'a') ++pass; else System.out.println("FAIL test17");
        ++total; if (it3.next() == 'b') ++pass; else System.out.println("FAIL test18");
        ++total; if (!it3.hasNext()) ++pass; else System.out.println("FAIL test19");
        if (pass == total) {
            System.out.println("== 0604 PASS ==");
        } else {
            System.out.println("== 0604 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 构造 O(n),next/hasNext O(1) O(n)(n 为压缩串长度)
思路二 构造 O(1),next 均摊 O(1) O(1)

605. Can Place Flowers(种花问题)

题目描述

假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。

另有一个数 n,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true,不能则返回 false。

示例

  • 输入 :flowerbed = 1,0,0,0,1, n = 1
  • 输出 : true
  • 输入 :flowerbed = 1,0,0,0,1, n = 2
  • 输出 : false
    提示:
  • 1 <= flowerbed.length <= 2 * 10^4
  • flowerbed[i] 为 0 或 1
  • flowerbed 中不存在相邻的两朵花
  • 0 <= n <= flowerbed.length

解题思路

解法一(贪心扫描,主提交):花不能与已有花相邻,能放则尽早放绝对不劣。遍历每个位置 i,

若 flowerbed[i] 为 0 且左右(越界视为 0)也都是 0,则在此种下(置 1)并 n--。

从左到右「尽量早种」不会妨碍后面更多位置,最后判断剩余 n <= 0。时间复杂度 O(m),空间 O(1)。

解法二(连续空段计数):把数组按已有的 1 切成若干全 0 段:首段(第一个 1 之前)长度 l 可种 l/2,

中间段(两个 1 之间)长度 l 可种 (l-1)/2,末段(最后一个 1 之后)长度 l 可种 l/2;

若全为 0 则一整段长度 m 可种 (m+1)/2。累加可种数量,判断是否不小于 n。

代码实现

java 复制代码
class Solution {

    public boolean canPlaceFlowers(int[] flowerbed, int n) {
        int m = flowerbed.length;
        for (int i = 0; i < m; ++i) {
            int l = (i == 0) ? 0 : flowerbed[i - 1];
            int r = (i == m - 1) ? 0 : flowerbed[i + 1];
            if (l + flowerbed[i] + r == 0) {
                flowerbed[i] = 1;
                --n;
            }
        }
        return n <= 0;
    }

    public boolean canPlaceFlowers2(int[] flowerbed, int n) {
        int m = flowerbed.length;
        int cnt = 0;
        int i = 0;
        while (i < m && flowerbed[i] == 0) {
            ++i;
        }
        if (i == m) { // 全 0
            cnt = (m + 1) / 2;
            return cnt >= n;
        }
        cnt += i / 2;          // 首段
        int j = i;
        while (j < m) {
            int k = j + 1;
            while (k < m && flowerbed[k] == 0) {
                ++k;
            }
            if (k < m) {       // 中间段(含左右边界 1)
                cnt += (k - j - 1 - 1) / 2;
            } else {           // 末段
                cnt += (k - j - 1) / 2;
            }
            j = k;
        }
        return cnt >= n;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.canPlaceFlowers(new int[] {1, 0, 0, 0, 1}, 1)) ++pass; else System.out.println("FAIL test1");
        ++total; if (!sol.canPlaceFlowers(new int[] {1, 0, 0, 0, 1}, 2)) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.canPlaceFlowers(new int[] {1, 0, 0, 0, 0, 1}, 1)) ++pass; else System.out.println("FAIL test3");
        ++total; if (!sol.canPlaceFlowers(new int[] {1, 0, 0, 0, 0, 1}, 2)) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.canPlaceFlowers(new int[] {0, 0, 1, 0, 1}, 1)) ++pass; else System.out.println("FAIL test5");
        ++total; if (!sol.canPlaceFlowers(new int[] {0, 0, 1, 0, 1}, 2)) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.canPlaceFlowers(new int[] {0, 0, 0}, 2)) ++pass; else System.out.println("FAIL test7");
        ++total; if (!sol.canPlaceFlowers(new int[] {0, 0, 0}, 3)) ++pass; else System.out.println("FAIL test8");
        // 解法二
        ++total; if (sol.canPlaceFlowers2(new int[] {1, 0, 0, 0, 1}, 1)) ++pass; else System.out.println("FAIL test9");
        ++total; if (!sol.canPlaceFlowers2(new int[] {1, 0, 0, 0, 1}, 2)) ++pass; else System.out.println("FAIL test10");
        ++total; if (sol.canPlaceFlowers2(new int[] {0, 0, 0}, 2)) ++pass; else System.out.println("FAIL test11");
        ++total; if (!sol.canPlaceFlowers2(new int[] {0, 0, 0}, 3)) ++pass; else System.out.println("FAIL test12");
        ++total; if (sol.canPlaceFlowers2(new int[] {1, 0, 0, 0, 0, 1}, 1)) ++pass; else System.out.println("FAIL test13");
        ++total; if (sol.canPlaceFlowers2(new int[] {1}, 0)) ++pass; else System.out.println("FAIL test14");
        if (pass == total) {
            System.out.println("== 0605 PASS ==");
        } else {
            System.out.println("== 0605 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

606. Construct String from Binary Tree(根据二叉树创建字符串)

题目描述

给定二叉树的根节点 root,你的任务是按照一组特定的格式规则创建该树的字符串表示。该表示应基于二叉树的前序遍历,并且必须遵循以下规则:

  • 节点表示:树中的每个节点都应使用其整数值表示。
  • 子节点的括号表示:如果一个节点至少有一个子节点(左子节点或右子节点),则其子节点应使用括号表示。
  • 如果一个节点存在左子节点,则应将左子节点的表示放在括号中,并紧跟在当前节点的值之后。
  • 如果一个节点存在右子节点,则也应将右子节点的表示放在括号中。右子节点对应的括号应位于左子节点对应括号之后。
  • 省略空括号:最终的树字符串表示中,应省略所有空括号对(即 ()),但有一种特殊情况除外:当一个节点存在右子节点但不存在左子节点时,必须保留一对空括号,以表示左子节点缺失。

示例

  • 输入 :root = 1,2,3,4
  • 输出: "1(2(4))(3)"
  • 输入 :root = 1,2,3,`null`,4
  • 输出 : "1(2()(4))(3)"
    提示:
  • 树中节点的数量范围为 [1, 10^4]
  • -1000 <= Node.val <= 1000

解题思路

解法一(递归,主提交):前序遍历加括号可以还原结构,但空括号规则不对称:仅有左子树时要写一对括号,

仅有右子树时左侧的空括号不能省。递归按三种情形输出:叶子只写值;right 为空只包左子树;

否则左右都包(左子树为空时自然形成 (),正好表示左子缺失)。时间复杂度 O(n),递归栈 O(n)。

解法二(显式栈前序):为避免极端链状树(节点数可达 10^4)递归栈溢出,用带状态帧的显式栈模拟后序式回填。

栈元素 (node, state):state=0 进入节点写值,非叶子则写下左括号、压入结束帧和自己的左孩子;

state=1 左子树结束补右括号,若有右孩子再写左括号并压入右孩子;state=2 补右孩子的右括号。

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

代码实现

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

class Solution {

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

    private static class Frame {
        TreeNode node;
        int state;
        Frame(TreeNode node, int state) {
            this.node = node;
            this.state = state;
        }
    }

    public String tree2str(TreeNode root) {
        if (root == null) {
            return "";
        }
        if (root.left == null && root.right == null) {
            return root.val + "";
        }
        if (root.right == null) {
            return root.val + "(" + tree2str(root.left) + ")";
        }
        return root.val + "(" + tree2str(root.left) + ")(" + tree2str(root.right) + ")";
    }

    public String tree2str2(TreeNode root) {
        if (root == null) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        Deque<Frame> stk = new java.util.ArrayDeque<>();
        stk.push(new Frame(root, 0));
        while (!stk.isEmpty()) {
            Frame cur = stk.pop();
            TreeNode node = cur.node;
            if (cur.state == 0) {
                sb.append(node.val);
                if (node.left == null && node.right == null) {
                    continue;
                }
                sb.append('(');
                stk.push(new Frame(node, 1));
                if (node.left != null) {
                    stk.push(new Frame(node.left, 0));
                }
                continue;
            }
            if (cur.state == 1) {
                sb.append(')');
                if (node.right != null) {
                    sb.append('(');
                    stk.push(new Frame(node, 2));
                    stk.push(new Frame(node.right, 0));
                }
                continue;
            }
            sb.append(')');
        }
        return sb.toString();
    }

    private static TreeNode mk(Integer[] a) {
        if (a == null || a.length == 0 || a[0] == null) {
            return null;
        }
        TreeNode root = new TreeNode(a[0]);
        java.util.Queue<TreeNode> q = new java.util.ArrayDeque<>();
        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;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.tree2str(mk(new Integer[] {1, 2, 3, 4})).equals("1(2(4))(3)")) ++pass; else System.out.println("FAIL test1");
        ++total; if (sol.tree2str(mk(new Integer[] {1, 2, 3, null, 4})).equals("1(2()(4))(3)")) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.tree2str(mk(new Integer[] {1})).equals("1")) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.tree2str(mk(new Integer[] {1, 2})).equals("1(2)")) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.tree2str(mk(new Integer[] {1, null, 2})).equals("1()(2)")) ++pass; else System.out.println("FAIL test5");
        // 解法二
        ++total; if (sol.tree2str2(mk(new Integer[] {1, 2, 3, 4})).equals("1(2(4))(3)")) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.tree2str2(mk(new Integer[] {1, 2, 3, null, 4})).equals("1(2()(4))(3)")) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.tree2str2(mk(new Integer[] {1, null, 2})).equals("1()(2)")) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.tree2str2(mk(new Integer[] {1})).equals("1")) ++pass; else System.out.println("FAIL test9");
        if (pass == total) {
            System.out.println("== 0606 PASS ==");
        } else {
            System.out.println("== 0606 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(n)(递归栈)
解法二 O(n) O(n)(显式栈 + 结果串)

607. Sales Person(销售员)

题目类型:数据库(SQL)

题目描述

表:SalesPerson

复制代码
+-----------------+---------+
| Column Name     | Type    |
+-----------------+---------+
| sales_id        | int     |
| name            | varchar |
| salary          | int     |
| commission_rate | int     |
| hire_date       | date    |
+-----------------+---------+

sales_id 是该表的主键列(具有唯一值的列)。该表的每一行都显示了销售人员的姓名和 ID,以及他们的工资、佣金率和雇佣日期。

表:Company

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| com_id      | int     |
| name        | varchar |
| city        | varchar |
+-------------+---------+

com_id 是该表的主键列(具有唯一值的列)。该表的每一行都表示公司的名称和 ID,以及公司所在的城市。

表:Orders

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| order_id    | int  |
| order_date  | date |
| com_id      | int  |
| sales_id    | int  |
| amount      | int  |
+-------------+------+

order_id 是该表的主键列(具有唯一值的列)。com_id 是 Company 表中 com_id 的外键(reference 列)。sales_id 是来自销售员表 sales_id 的外键(reference 列)。该表的每一行包含一个订单的信息。

编写解决方案,找出没有任何与名为 "RED" 的公司相关的订单的所有销售人员的姓名。以任意顺序返回结果表。

示例 1:

复制代码
输入:
SalesPerson 表:
+----------+------+--------+-----------------+------------+
| sales_id | name | salary | commission_rate | hire_date  |
+----------+------+--------+-----------------+------------+
| 1        | John | 100000 | 6               | 4/1/2006   |
| 2        | Amy  | 12000  | 5               | 5/1/2010   |
| 3        | Mark | 65000  | 12              | 12/25/2008 |
| 4        | Pam  | 25000  | 25              | 1/1/2005   |
| 5        | Alex | 5000   | 10              | 2/3/2007   |
+----------+------+--------+-----------------+------------+
Company 表:
+--------+--------+----------+
| com_id | name   | city     |
+--------+--------+----------+
| 1      | RED    | Boston   |
| 2      | ORANGE | New York |
| 3      | YELLOW | Boston   |
| 4      | GREEN  | Austin   |
+--------+--------+----------+
Orders 表:
+----------+------------+--------+----------+--------+
| order_id | order_date | com_id | sales_id | amount |
+----------+------------+--------+----------+--------+
| 1        | 1/1/2014   | 3      | 4        | 10000  |
| 2        | 2/1/2014   | 4      | 5        | 5000   |
| 3        | 3/1/2014   | 1      | 1        | 50000  |
| 4        | 4/1/2014   | 1      | 4        | 25000  |
+----------+------------+--------+----------+--------+
输出:
+------+
| name |
+------+
| Amy  |
| Mark |
| Alex |
+------+

解释 :根据表 orders 中的订单 '3' 和 '4',只有 'John' 和 'Pam' 两个销售员曾经向公司 'RED' 销售过。所以我们需要输出表 salesperson 中所有其他人的名字。

解题思路

解法一(左连接 + 分组统计,推荐):要找「从未卖过 RED 公司」的销售。只选「卖过 RED」再做差集,会漏掉从未有订单的人,因此必须从 SalesPerson 出发左连接 。SalesPerson LEFT JOIN Orders LEFT JOIN Company(都按外键关联),按 sales_id 分组,用 SUM(c.name = 'RED') 统计每位销售员名下 RED 订单数:无订单时左连接产生的 NULL 由 IFNULL(..., 0) 归零,HAVING IFNULL(SUM(c.name = 'RED'), 0) = 0 即从未卖过 RED。

解法二(NOT IN 子查询):先查出「卖过 RED」的销售员集合------Orders JOIN Company 后筛出 name = 'RED' 的 sales_id,再取 SalesPerson 中 sales_id NOT IN 该集合的名字。逻辑直观,注意 sales_id 无 NULL 时 NOT IN 安全。

代码实现

sql 复制代码
-- 解法一:左连接 + 分组统计(推荐)
SELECT s.name
FROM
    SalesPerson AS s
    LEFT JOIN Orders USING (sales_id)
    LEFT JOIN Company AS c USING (com_id)
GROUP BY sales_id
HAVING IFNULL(SUM(c.name = 'RED'), 0) = 0;
sql 复制代码
-- 解法二:NOT IN 排除卖过 RED 的销售
SELECT name
FROM SalesPerson
WHERE
    sales_id NOT IN (
        SELECT o.sales_id
        FROM Orders AS o
            JOIN Company AS c ON o.com_id = c.com_id
        WHERE c.name = 'RED'
    );

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n + m + k)(n 为销售人数,m 为订单数,k 为公司数) O(n)(分组结果)
解法二 O(n + m + k) O(m)(子查询结果)

608. Tree Node(树节点)

题目类型:数据库(SQL)

题目描述

表:Tree

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| id          | int  |
| p_id        | int  |
+-------------+------+

id 是该表中具有唯一值的列。该表的每行包含树中节点的 id 及其父节点的 id 信息。给定的结构总是一个有效的树。

树中的每个节点可以是以下三种类型之一:

  • "Leaf":节点是叶子节点。
  • "Root":节点是树的根节点。
  • "Inner":节点既不是叶子节点也不是根节点。

编写一个解决方案来报告树中每个节点的类型。以任意顺序返回结果表。

示例 1:

复制代码
输入:
Tree table:
+----+------+
| id | p_id |
+----+------+
| 1  | null |
| 2  | 1    |
| 3  | 1    |
| 4  | 2    |
| 5  | 2    |
+----+------+
输出:
+----+-------+
| id | type  |
+----+-------+
| 1  | Root  |
| 2  | Inner |
| 3  | Leaf  |
| 4  | Leaf  |
| 5  | Leaf  |
+----+-------+

解释:节点 1 是根节点,因为它的父节点为空,并且它有子节点 2 和 3。节点 2 是一个内部节点,因为它有父节点 1 和子节点 4 和 5。节点 3、4 和 5 是叶子节点,因为它们有父节点而没有子节点。

示例 2:

复制代码
输入:
Tree table:
+----+------+
| id | p_id |
+----+------+
| 1  | null |
+----+------+
输出:
+----+-------+
| id | type  |
+----+-------+
| 1  | Root  |
+----+-------+

解释:如果树中只有一个节点,则只需要输出其根属性。

解题思路

解法一(CASE + 子查询,推荐):节点类型由「是否有父、是否当过父」两个条件决定,用 CASE WHEN 分层判断:p_id IS NULL → 'Root';否则若 id IN (SELECT p_id FROM Tree)(即做过别人的父节点)→ 'Inner';其余 → 'Leaf'。一次扫描表加一个子查询即可,写法直白。

解法二(左连接 + 计数):把 Tree 左连接 Tree AS child ON Tree.id = child.p_id,按 id 分组:p_id IS NULL 为根;COUNT(child.id) > 0 说明有子节点(结合有父)为内部节点;两者均否则为叶。用聚合视角等价表达三种类型,避免子查询。

代码实现

sql 复制代码
-- 解法一:CASE + 子查询(推荐)
SELECT
    id,
    CASE
        WHEN p_id IS NULL THEN 'Root'
        WHEN id IN (SELECT p_id FROM Tree) THEN 'Inner'
        ELSE 'Leaf'
    END AS type
FROM Tree;
sql 复制代码
-- 解法二:左连接 + 聚合判断
SELECT
    t.id,
    CASE
        WHEN t.p_id IS NULL THEN 'Root'
        WHEN COUNT(c.id) > 0 THEN 'Inner'
        ELSE 'Leaf'
    END AS type
FROM
    Tree AS t
    LEFT JOIN Tree AS c ON t.id = c.p_id
GROUP BY t.id;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n²)(IN 子查询最坏,n 为节点数) O(n)(子查询结果)
解法二 O(n log n)(连接 + 分组) O(n)

609. Find Duplicate File in System(在系统中查找重复文件)

题目描述

给你一个目录信息列表 paths,包括目录路径,以及该目录中的所有文件及其内容,请你按路径返回文件系统中的所有重复文件。答案可按任意顺序返回。

一组重复的文件至少包括两个具有完全相同内容的文件。

输入列表中的单个目录信息字符串的格式如下:

"root/d1/d2/.../dm f1.txt(f1_content) f2.txt(f2_content) ... fn.txt(fn_content)"

这意味着,在目录 root/d1/d2/.../dm 下,有 n 个文件的内容分别是 (f1_content, f2_content ... fn_content)。

注意:n >= 1 且 m >= 0。如果 m = 0,则表示该目录是根目录。

输出是由重复文件路径组构成的列表。其中每个组由所有具有相同内容文件的文件路径组成。

文件路径是具有下列格式的字符串:"directory_path/file_name.txt"。

示例

  • 输入 :paths = "`root`/`a` 1.txt(abcd) 2.txt(efgh)","`root`/`c` 3.txt(abcd)","`root`/`c`/`d` 4.txt(efgh)","`root` 4.txt(efgh)"
  • 输出 : \["`root`/`a`/2.txt","`root`/`c`/`d`/4.txt","`root`/4.txt","`root`/`a`/1.txt","`root`/`c`/3.txt"]
    提示:
  • 1 <= paths.length <= 2 * 10^4
  • 1 <= paths[i].length <= 3000
  • 1 <= sum(paths[i].length) <= 5 * 10^5
  • paths[i] 由英文字母、数字、字符 '/'、'.'、'('、')' 和 ' ' 组成
  • 你可以假设在同一目录中没有任何文件或目录共享相同的名称。
  • 你可以假设每个给定的目录信息代表一个唯一的目录。目录路径和文件信息用单个空格分隔。

解题思路

解法一(哈希表 + 字符串解析,主提交):重复由文件内容相等定义,路径格式把目录和「文件名(内容)」挤在同一字符串里。

遍历每个 paths[i],先按空格拆出目录路径与各文件信息;对每个文件信息,用 indexOf('(') 定位,

左侧为文件名、括号内为内容,拼成完整路径 目录路径 + '/' + 文件名 存入以「内容」为键的哈希表桶。

最后所有桶大小大于 1 的就是重复组。时间复杂度 O(总字符数),空间复杂度 O(总字符数)。

解法二(两阶段:内容分组 + 排序去重):先把所有「内容→路径」映射收集完后,对每个桶排序并去重(题目保证同一目录下无重名,桶内天然不重复),再过滤长度大于 1 的桶。

相较于解法一只是排序开销不同,结果一致;本题桶内本来不会重复,故以解法一为准。

代码实现

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

class Solution {

    public List<List<String>> findDuplicate(String[] paths) {
        Map<String, List<String>> d = new HashMap<>();
        for (String p : paths) {
            String[] ps = p.split(" ");
            String dir = ps[0];
            for (int i = 1; i < ps.length; ++i) {
                int j = ps[i].indexOf('(');
                String name = ps[i].substring(0, j);
                String content = ps[i].substring(j + 1, ps[i].length() - 1);
                d.computeIfAbsent(content, k -> new ArrayList<>()).add(dir + "/" + name);
            }
        }
        List<List<String>> ans = new ArrayList<>();
        for (List<String> e : d.values()) {
            if (e.size() > 1) {
                ans.add(e);
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        List<List<String>> r1 = sol.findDuplicate(new String[] {
                "root/a 1.txt(abcd) 2.txt(efgh)", "root/c 3.txt(abcd)",
                "root/c/d 4.txt(efgh)", "root 4.txt(efgh)"});
        ++total;
        if (r1.size() == 2) {
            boolean ok = true;
            // 规范化:两个组各含 3 个与 2 个元素
            java.util.TreeSet<String> g3 = new java.util.TreeSet<>(r1.get(0));
            java.util.TreeSet<String> big0 = new java.util.TreeSet<>(r1.get(0));
            java.util.TreeSet<String> big1 = new java.util.TreeSet<>(r1.get(1));
            ok &= r1.get(0).size() == 3 || r1.get(1).size() == 3;
            ok &= (r1.get(0).size() + r1.get(1).size()) == 5;
            ok &= big0.contains("root/a/2.txt") || big1.contains("root/a/2.txt");
            ok &= big0.contains("root/c/d/4.txt") || big1.contains("root/c/d/4.txt");
            ok &= big0.contains("root/4.txt") || big1.contains("root/4.txt");
            ok &= big0.contains("root/a/1.txt") || big1.contains("root/a/1.txt");
            ok &= big0.contains("root/c/3.txt") || big1.contains("root/c/3.txt");
            if (ok) ++pass; else System.out.println("FAIL test1: " + r1);
        } else {
            System.out.println("FAIL test1: " + r1);
        }

        List<List<String>> r2 = sol.findDuplicate(new String[] {
                "root/a 1.txt(abcd) 2.txt(efgh)", "root/c 3.txt(abcd)", "root/c/d 4.txt(efgh)"});
        ++total;
        if (r2.size() == 2) {
            boolean ok = true;
            java.util.TreeSet<String> big0 = new java.util.TreeSet<>(r2.get(0));
            java.util.TreeSet<String> big1 = new java.util.TreeSet<>(r2.get(1));
            ok &= (big0.contains("root/a/2.txt") && big0.contains("root/c/d/4.txt"))
                    || (big1.contains("root/a/2.txt") && big1.contains("root/c/d/4.txt"));
            ok &= (big0.contains("root/a/1.txt") && big0.contains("root/c/3.txt"))
                    || (big1.contains("root/a/1.txt") && big1.contains("root/c/3.txt"));
            if (ok) ++pass; else System.out.println("FAIL test2: " + r2);
        } else {
            System.out.println("FAIL test2: " + r2);
        }

        // 无重复:空结果
        List<List<String>> r3 = sol.findDuplicate(new String[] {"a 1.txt(x)"});
        ++total; if (r3.isEmpty()) ++pass; else System.out.println("FAIL test3: " + r3);

        // 单目录多文件同名内容
        List<List<String>> r4 = sol.findDuplicate(new String[] {"d a.txt(1) b.txt(1)"});
        ++total;
        if (r4.size() == 1 && r4.get(0).size() == 2
                && r4.get(0).contains("d/a.txt") && r4.get(0).contains("d/b.txt")) ++pass;
        else System.out.println("FAIL test4: " + r4);

        if (pass == total) {
            System.out.println("== 0609 PASS ==");
        } else {
            System.out.println("== 0609 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(L)(L 为所有路径字符串总长) O(L)
解法二 O(L + Σk log k)(k 为各桶规模) O(L)

610. Triangle Judgement(判断三角形)

题目类型:数据库(SQL)

题目描述

表:Triangle

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| x           | int  |
| y           | int  |
| z           | int  |
+-------------+------+

在 SQL 中,(x, y, z) 是该表的主键列。该表的每一行包含三个线段的长度。

对每三个线段报告它们是否可以形成一个三角形。以任意顺序返回结果表。

示例 1:

复制代码
输入:
Triangle 表:
+----+----+----+
| x  | y  | z  |
+----+----+----+
| 13 | 15 | 30 |
| 10 | 20 | 15 |
+----+----+----+
输出:
+----+----+----+----------+
| x  | y  | z  | triangle |
+----+----+----+----------+
| 13 | 15 | 30 | No       |
| 10 | 20 | 15 | Yes      |
+----+----+----+----------+

解题思路

解法一(IF 三边不等式,推荐):三条边能否构成三角形,只需验证任意两边之和大于第三边 ,这是纯行内条件,无需过程化逻辑。对每行用 IF(x + y > z AND x + z > y AND y + z > x, 'Yes', 'No') 一次性判断并输出新列。

解法二(CASE 表达式):与解法一逻辑相同,把 IF 改写为 CASE WHEN ... THEN ... ELSE ... END,适合对可读性有要求或需要扩展多个分支的场景,结果等价。

代码实现

sql 复制代码
-- 解法一:IF 三边不等式(推荐)
SELECT
    *,
    IF(x + y > z AND x + z > y AND y + z > x, 'Yes', 'No') AS triangle
FROM Triangle;
sql 复制代码
-- 解法二:CASE 表达式
SELECT
    x,
    y,
    z,
    CASE
        WHEN x + y > z AND x + z > y AND y + z > x THEN 'Yes'
        ELSE 'No'
    END AS triangle
FROM Triangle;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n)(一次扫描,n 为 Triangle 行数) O(1)
解法二 O(n) O(1)

611. Valid Triangle Number(有效三角形的个数)

题目描述

给定一个包含非负整数的数组 nums,返回其中可以组成三角形三条边的三元组个数。

示例

  • 输入 :nums = 2,2,3,4
  • 输出: 3
  • 解释: 有效的组合是: 2,3,4 (使用第一个 2);2,3,4 (使用第二个 2);2,2,3
  • 输入 :nums = 4,2,3,4
  • 输出 : 4
    提示:
  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 1000

解题思路

一个有效三角形需要满足任意两边之和大于第三边。如果把三条边按从小到大排序 a <= b <= c,

则只需 a + b > c 成立即可(其余两式自动成立)。

解法一(排序 + 二分,主提交):先升序排序。固定前两条边 i、j(i < j),在 j + 1 .. n - 1

范围内二分查找第一个满足 nums[k] >= nums[i] + nums[j] 的位置 left,则 k ∈ [j+1, left-1] 都合法,

计数加 left - j - 1。时间复杂度 O(n² log n),空间 O(log n)(排序栈)。

解法二(排序 + 双指针):同样固定 i(从大到小枚举最大边位置也可以),固定最大边 k 从右往左、

双指针 i、j 从两端向中间扫:nums[i] + nums[j] > nums[k] 时,i 从当前到 j-1 都合法,

累加 j - i 并左移 j;否则右移 i。整体 O(n²),空间 O(log n)。

代码实现

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

class Solution {

    public int triangleNumber(int[] nums) {
        Arrays.sort(nums);
        int ans = 0;
        int n = nums.length;
        for (int i = 0; i < n - 2; ++i) {
            for (int j = i + 1; j < n - 1; ++j) {
                int left = j + 1, right = n;
                while (left < right) {
                    int mid = (left + right) >>> 1;
                    if (nums[mid] >= nums[i] + nums[j]) {
                        right = mid;
                    } else {
                        left = mid + 1;
                    }
                }
                ans += left - j - 1;
            }
        }
        return ans;
    }

    public int triangleNumber2(int[] nums) {
        Arrays.sort(nums);
        int ans = 0;
        int n = nums.length;
        for (int k = n - 1; k >= 2; --k) {
            int i = 0, j = k - 1;
            while (i < j) {
                if (nums[i] + nums[j] > nums[k]) {
                    ans += j - i;
                    --j;
                } else {
                    ++i;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.triangleNumber(new int[] {2, 2, 3, 4}) == 3) ++pass; else System.out.println("FAIL test1");
        ++total; if (sol.triangleNumber(new int[] {4, 2, 3, 4}) == 4) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.triangleNumber(new int[] {1, 1, 1}) == 1) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.triangleNumber(new int[] {1, 2, 3}) == 0) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.triangleNumber(new int[] {0, 0, 0}) == 0) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.triangleNumber(new int[] {3, 4, 5, 6}) == 4) ++pass; else System.out.println("FAIL test6");
        // 解法二
        ++total; if (sol.triangleNumber2(new int[] {2, 2, 3, 4}) == 3) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.triangleNumber2(new int[] {4, 2, 3, 4}) == 4) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.triangleNumber2(new int[] {1, 1, 1}) == 1) ++pass; else System.out.println("FAIL test9");
        ++total; if (sol.triangleNumber2(new int[] {1, 2, 3}) == 0) ++pass; else System.out.println("FAIL test10");
        ++total; if (sol.triangleNumber2(new int[] {3, 4, 5, 6}) == 4) ++pass; else System.out.println("FAIL test11");
        if (pass == total) {
            System.out.println("== 0611 PASS ==");
        } else {
            System.out.println("== 0611 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

612. Shortest Distance in a Plane(平面上的最近距离)

题目类型:数据库(SQL)

题目描述

Point2D 表:

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| x           | int  |
| y           | int  |
+-------------+------+

(x, y) 是该表的主键列(具有唯一值的列的组合)。这张表的每一行表示 X-Y 平面上一个点的位置。

p₁(x₁, y₁) 和 p₂(x₂, y₂) 这两点之间的距离是 sqrt((x2 - x1)² + (y2 - y1)²)。

编写解决方案,报告 Point2D 表中任意两点之间的最短距离。保留 2 位小数。

示例 1:

复制代码
输入:
Point2D table:
+----+----+
| x  | y  |
+----+----+
| -1 | -1 |
| 0  | 0  |
| -1 | -2 |
+----+----+
输出:
+----------+
| shortest |
+----------+
| 1.00     |
+----------+

解释:最短距离是 1.00,从点 (-1, -1) 到点 (-1, -2)。

解题思路

解法一(自连接 + 欧氏距离,推荐):平面最短距离对应所有点对的欧氏距离最小值。将 Point2D 自连接,排除同一点本身(p1.x != p2.x OR p1.y != p2.y,即 (x, y) 二元组不同),对每个点对用 SQRT(POW(p1.x - p2.x, 2) + POW(p1.y - p2.y, 2)) 计算距离,按距离升序取第一行并 ROUND(..., 2) 保留两位小数。注意:同一对点会被算两次(a→b 与 b→a),但取最小值不受影响。

解法二(无序对半连接去重):也可以限定只取无序对(如 p1.x < p2.x 或 (p1.x, p1.y) < (p2.x, p2.y) 的字典序比较),这样每个点对只计算一次。当表较大时可减少一半计算量,结果相同。

代码实现

sql 复制代码
-- 解法一:自连接 + 欧氏距离(推荐)
SELECT ROUND(SQRT(POW(p1.x - p2.x, 2) + POW(p1.y - p2.y, 2)), 2) AS shortest
FROM
    Point2D AS p1
    JOIN Point2D AS p2 ON p1.x != p2.x OR p1.y != p2.y
ORDER BY 1
LIMIT 1;
sql 复制代码
-- 解法二:无序对只算一次
SELECT ROUND(SQRT(POW(p1.x - p2.x, 2) + POW(p1.y - p2.y, 2)), 2) AS shortest
FROM
    Point2D AS p1
    JOIN Point2D AS p2 ON p1.x < p2.x OR (p1.x = p2.x AND p1.y < p2.y)
ORDER BY 1
LIMIT 1;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n²)(自连接笛卡尔积,n 为点数) O(n²)(连接结果)
解法二 O(n²) O(n²)

613. Shortest Distance in a Line(直线上的最近距离)

题目类型:数据库(SQL)

题目描述

表:Point

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| x           | int  |
+-------------+------+

在 SQL 中,x 是该表的主键列。该表的每一行表示 X 轴上一个点的位置。

找到 Point 表中任意两点之间的最短距离。保证 Point 表中至少包含两行数据。

示例 1:

复制代码
输入:
Point 表:
+----+
| x  |
+----+
| -1 |
| 0  |
| 2  |
+----+
输出:
+----------+
| shortest |
+----------+
| 1        |
+----------+

解释:点 -1 和 0 之间的最短距离为 |(-1) - 0| = 1。

进阶 :如果 Point 表按升序排列,如何优化你的解决方案?

解题思路

解法一(自连接,推荐):一维最短距离即任意两点差的最小正值 。将 Point 自连接并限定 p1.x < p2.x(保证只取有序对、差为正),对每个点对计算 p2.x - p1.x 后取 MIN 即为最短距离。

解法二(窗口函数 LAG):最短距离必出现在排序后相邻 的两点之间,不必生成笛卡尔积。按 x 升序排序,用 LAG(x) OVER (ORDER BY x) 取每行的前驱点,二者之差即与相邻点的距离;按差值降序排序后跳过第一行(LIMIT 1, 1)拿到最小正值------因为第一行的 LAG 为 NULL,排序时空值在前。此写法也正是进阶问题的答案:表已有序时相邻差天然最小。

代码实现

sql 复制代码
-- 解法一:自连接(推荐)
SELECT MIN(p2.x - p1.x) AS shortest
FROM
    Point AS p1
    JOIN Point AS p2 ON p1.x < p2.x;
sql 复制代码
-- 解法二:LAG 相邻差(进阶优化)
SELECT x - LAG(x) OVER (ORDER BY x) AS shortest
FROM Point
ORDER BY 1
LIMIT 1, 1;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n²)(自连接笛卡尔积,n 为点数) O(n²)
解法二 O(n log n)(排序 + 窗口) O(n)

614. Second Degree Follower(二级关注者)

题目类型:数据库(SQL)

题目描述

表:Follow

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| followee    | varchar |
| follower    | varchar |
+-------------+---------+

(followee, follower) 是该表的主键(具有唯一值的列的组合)。该表的每一行表示关注者关注了社交网络上的被关注者。不会有用户关注他们自己。

二级关注者是指满足以下条件的用户:

  • 关注至少一个用户;
  • 被至少一个用户关注。

编写一个解决方案来报告二级用户 及其关注者的数量。返回按 follower 字典序排序的结果表。

示例 1:

复制代码
输入:
Follow table:
+----------+----------+
| followee | follower |
+----------+----------+
| Alice    | Bob      |
| Bob      | Cena     |
| Bob      | Donald   |
| Donald   | Edward   |
+----------+----------+
输出:
+----------+-----+
| follower | num |
+----------+-----+
| Bob      | 2   |
| Donald   | 1   |
+----------+-----+

解释:用户 Bob 有 2 个关注者,Bob 是二级关注者,因为他关注了 Alice,所以我们把他包括在结果表中。用户 Donald 有 1 个关注者,Donald 是二级关注者,因为他关注了 Bob,所以我们把他包括在结果表中。用户 Alice 有 1 个关注者,但 Alice 不关注任何人,所以她不是二级关注者,不被包括。

解题思路

解法一(自连接 + 去重计数,推荐):二级关注者是「自己关注了某人、且自己也被别人关注」的用户,直接扫单表无法同时看到两跳关系。用自连接 f1.follower = f2.followee 把「我关注的人」与「被谁关注」衔接起来:取 f1.follower 作为二级用户候选,其关注的对象就是 f2.follower(即 f1 关注的人,同时又是某个 followee 的关注者)。对每个 f1.follower 用 COUNT(DISTINCT f2.follower) 统计其关注人数,即为该二级用户被要求的「关注者数量」。

解法二(EXISTS 双向过滤):先按 follower 分组统计每个用户的关注者数,再用 EXISTS 子查询分别验证「该用户关注了至少一个人」与「该用户被至少一个人关注」,两个条件同时成立即为二级关注者。逻辑上更贴近定义,但需要两次 EXISTS 扫描,写法较长。

代码实现

sql 复制代码
-- 解法一:自连接 + 去重计数(推荐)
WITH
    T AS (
        SELECT f1.follower AS follower, f2.follower AS followee
        FROM
            Follow AS f1
            JOIN Follow AS f2 ON f1.follower = f2.followee
    )
SELECT follower, COUNT(DISTINCT followee) AS num
FROM T
GROUP BY 1
ORDER BY 1;
sql 复制代码
-- 解法二:EXISTS 双向验证
SELECT
    f1.follower,
    COUNT(DISTINCT f2.followee) AS num
FROM
    Follow AS f1
    JOIN Follow AS f2 ON f1.follower = f2.follower
WHERE
    EXISTS (SELECT 1 FROM Follow f WHERE f.follower = f1.follower)
    AND EXISTS (SELECT 1 FROM Follow f WHERE f.followee = f1.follower)
GROUP BY 1
ORDER BY 1;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n²)(自连接,n 为关注关系行数) O(n)(连接结果)
解法二 O(n²)(连接 + 两个 EXISTS) O(n)

615. Average Salary: Departments VS Company(平均工资:部门与公司比较)

题目类型:数据库(SQL)

题目描述

表:Salary

复制代码
+-------------+------+
| 列名        | 类型 |
+-------------+------+
| id          | int  |
| employee_id | int  |
| amount      | int  |
| pay_date    | date |
+-------------+------+

在 SQL 中,id 是该表的主键列。该表的每一行表示一个员工一个月的薪资。employee_id 是来自 Employee 表的外键(reference 列)。

表:Employee

复制代码
+---------------+------+
| 列名          | 类型 |
+---------------+------+
| employee_id   | int  |
| department_id | int  |
+---------------+------+

在 SQL 中,employee_id 是该表的主键列。该表的每一行表示一个员工所属的部门。

找出各个部门员工的平均薪资与公司平均薪资之间的比较结果(更高 / 更低 / 相同)。以任意顺序返回结果表。

示例 1:

复制代码
输入:
Salary 表:
+----+-------------+--------+------------+
| id | employee_id | amount | pay_date   |
+----+-------------+--------+------------+
| 1  | 1           | 9000   | 2017/03/31 |
| 2  | 2           | 6000   | 2017/03/31 |
| 3  | 3           | 10000  | 2017/03/31 |
| 4  | 1           | 7000   | 2017/02/28 |
| 5  | 2           | 6000   | 2017/02/28 |
| 6  | 3           | 8000   | 2017/02/28 |
+----+-------------+--------+------------+
Employee 表:
+-------------+---------------+
| employee_id | department_id |
+-------------+---------------+
| 1           | 1             |
| 2           | 2             |
| 3           | 2             |
+-------------+---------------+
输出:
+-----------+---------------+------------+
| pay_month | department_id | comparison |
+-----------+---------------+------------+
| 2017-02   | 1             | same       |
| 2017-03   | 1             | higher     |
| 2017-02   | 2             | same       |
| 2017-03   | 2             | lower      |
+-----------+---------------+------------+

解释 :在三月,公司的平均工资是 (9000+6000+10000)/3 = 8333.33...;部门 '1' 的平均薪资是 9000,因为 9000 > 8333.33,所以比较结果为 'higher';部门 '2' 的平均薪资是 (6000 + 10000)/2 = 8000,小于公司均值,为 'lower'。二月份部门 '1' 和 '2' 都与公司的平均薪资(7000)相同,所以为 'same'。

解题思路

解法一(双窗口聚合 + DISTINCT 去重,推荐):需按月比较「部门均薪」与「公司均薪」。先把 Salary JOIN Employee 取到 department_id,构造 pay_month;然后用两个窗口函数同时取出两种均值:AVG(amount) OVER (PARTITION BY pay_date) 是当月公司均值,AVG(amount) OVER (PARTITION BY pay_date, department_id) 是当月部门均值。窗口函数会给组内每一行都复制该均值,因此外层用 SELECT DISTINCT 去重,再用 CASE 比较输出 higher / lower / same。

解法二(窗口算均值 + GROUP BY 二次聚合):与方法一同样先算出双均值,但外层不对 DISTINCT 去重,而是按 pay_month, department_id 分组后对两个窗口均值各取 AVG(窗口给组内所有行相同值,聚合后仍相同),再 CASE 比较。两种写法结果一致。

代码实现

sql 复制代码
-- 解法一:双窗口聚合 + DISTINCT(推荐)
WITH
    t AS (
        SELECT
            DATE_FORMAT(pay_date, '%Y-%m') AS pay_month,
            department_id,
            AVG(amount) OVER (PARTITION BY pay_date) AS company_avg_amount,
            AVG(amount) OVER (PARTITION BY pay_date, department_id) AS department_avg_amount
        FROM
            Salary AS s
            JOIN Employee AS e ON s.employee_id = e.employee_id
    )
SELECT DISTINCT
    pay_month,
    department_id,
    CASE
        WHEN company_avg_amount = department_avg_amount THEN 'same'
        WHEN company_avg_amount < department_avg_amount THEN 'higher'
        ELSE 'lower'
    END AS comparison
FROM t;
sql 复制代码
-- 解法二:窗口算均值 + GROUP BY 二次聚合
WITH
    S AS (
        SELECT *
        FROM
            Salary
            JOIN Employee USING (employee_id)
    ),
    T AS (
        SELECT
            DATE_FORMAT(pay_date, '%Y-%m') AS pay_month,
            department_id,
            AVG(amount) OVER (PARTITION BY pay_date, department_id) AS department_avg,
            AVG(amount) OVER (PARTITION BY pay_date) AS company_avg
        FROM S
    )
SELECT
    pay_month,
    department_id,
    CASE
        WHEN AVG(department_avg) > AVG(company_avg) THEN 'higher'
        WHEN AVG(department_avg) < AVG(company_avg) THEN 'lower'
        ELSE 'same'
    END AS comparison
FROM T
GROUP BY 1, 2;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n)(连接 + 窗口排序,n 为薪资行数) O(n)(窗口结果)
解法二 O(n log n) O(n)

616. Add Bold Tag in String(给字符串添加加粗标签)

题目描述

给定字符串 s 和字符串数组 words。

对于 s 内部的子字符串,若其存在于 words 数组中,则通过添加闭合的粗体标签 <b> 和 </b> 进行加粗标记。

  • 如果两个这样的子字符串重叠,你应该仅使用一对闭合的粗体标签将它们包围起来。
  • 如果被粗体标签包围的两个子字符串是连续的,你应该将它们合并。
    返回添加加粗标签后的字符串 s。

示例

  • 输入 :s = "abcxyz123", words = "abc","123"
  • 输出 : "<b>abc</b>xyz<b>123</b>"
  • 输入 :s = "aaabbb", words = "aa","`b`"
  • 输出 : "<b>aaabbb</b>"
    提示:
  • 1 <= s.length <= 1000
  • 0 <= words.length <= 100
  • 1 <= words[i].length <= 1000
  • s 和 words[i] 由英文字母和数字组成
  • words 中的所有值互不相同

解题思路

解法一(布尔标记 + 暴力匹配,主提交):先在长为 n 的布尔数组 bold 上标记所有需要加粗的下标:

对每个单词 w,在 s 中从左到右用 indexOf(w, from) 找全部出现位置,把区间 [start, start + len - 1]

全部置 true。最后从左到右扫描 s:连续 true 段前后分别插入 <b> 与 </b>。

单词总长与 s 长度均在 1000 量级,暴力匹配足够快。时间复杂度 O(Σ|s|·|w|),空间 O(n)。

解法二(字典树 + 区间合并):把 words 建成字典树,从每个起点 i 沿树走一遍,可一次扫出该起点出发的

所有结束位置(isEnd 命中即记下一个 [i, j] 区间);得到全部匹配区间后按左端点排序并合并重叠/相邻区间,

再拼回加粗结果。匹配段进行为 O(n²) 最坏(每个起点最坏走到底),但避免了对每个单词重复扫 s,

单词较多时更稳。两种解法等价,任务以解法一为骨架。

代码实现

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

class Solution {

    // 解法一:布尔标记 + 暴力匹配
    public String addBoldTag(String s, String[] words) {
        int n = s.length();
        boolean[] bold = new boolean[n];
        for (String w : words) {
            int from = 0;
            while (true) {
                int idx = s.indexOf(w, from);
                if (idx < 0) {
                    break;
                }
                for (int k = idx; k < idx + w.length(); ++k) {
                    bold[k] = true;
                }
                from = idx + 1; // 允许重叠匹配
            }
        }
        StringBuilder sb = new StringBuilder();
        int i = 0;
        while (i < n) {
            if (!bold[i]) {
                sb.append(s.charAt(i));
                ++i;
                continue;
            }
            int j = i;
            while (j < n && bold[j]) {
                ++j;
            }
            sb.append("<b>").append(s, i, j).append("</b>");
            i = j;
        }
        return sb.toString();
    }

    // 解法二:前缀树 + 区间收集 + 合并
    private static class Trie {
        Trie[] children = new Trie[128];
        boolean isEnd;
        void insert(String w) {
            Trie node = this;
            for (char c : w.toCharArray()) {
                if (node.children[c] == null) {
                    node.children[c] = new Trie();
                }
                node = node.children[c];
            }
            node.isEnd = true;
        }
    }

    public String addBoldTag2(String s, String[] words) {
        Trie trie = new Trie();
        for (String w : words) {
            trie.insert(w);
        }
        int n = s.length();
        List<int[]> pairs = new ArrayList<>();
        for (int i = 0; i < n; ++i) {
            Trie node = trie;
            for (int j = i; j < n; ++j) {
                if (node.children[s.charAt(j)] == null) {
                    break;
                }
                node = node.children[s.charAt(j)];
                if (node.isEnd) {
                    pairs.add(new int[] {i, j});
                }
            }
        }
        if (pairs.isEmpty()) {
            return s;
        }
        pairs.sort((a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
        List<int[]> merged = new ArrayList<>();
        int st = pairs.get(0)[0], ed = pairs.get(0)[1];
        for (int k = 1; k < pairs.size(); ++k) {
            int a = pairs.get(k)[0], b = pairs.get(k)[1];
            if (ed + 1 < a) {
                merged.add(new int[] {st, ed});
                st = a;
                ed = b;
            } else {
                ed = Math.max(ed, b);
            }
        }
        merged.add(new int[] {st, ed});
        StringBuilder sb = new StringBuilder();
        int i = 0, k = 0;
        while (i < n) {
            if (k == merged.size()) {
                sb.append(s.substring(i));
                break;
            }
            int a = merged.get(k)[0], b = merged.get(k)[1];
            if (i < a) {
                sb.append(s, i, a);
            }
            sb.append("<b>").append(s, a, b + 1).append("</b>");
            i = b + 1;
            ++k;
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.addBoldTag("abcxyz123", new String[] {"abc", "123"}).equals("<b>abc</b>xyz<b>123</b>")) ++pass;
        else System.out.println("FAIL test1: " + sol.addBoldTag("abcxyz123", new String[] {"abc", "123"}));
        ++total; if (sol.addBoldTag("aaabbb", new String[] {"aa", "b"}).equals("<b>aaabbb</b>")) ++pass;
        else System.out.println("FAIL test2: " + sol.addBoldTag("aaabbb", new String[] {"aa", "b"}));
        ++total; if (sol.addBoldTag("abc", new String[] {}).equals("abc")) ++pass;
        else System.out.println("FAIL test3");
        ++total; if (sol.addBoldTag("abcd", new String[] {"abcd", "ab"}).equals("<b>abcd</b>")) ++pass;
        else System.out.println("FAIL test4: " + sol.addBoldTag("abcd", new String[] {"abcd", "ab"}));
        ++total; if (sol.addBoldTag("aabbcc", new String[] {"bc"}).equals("aab<b>bc</b>c")) ++pass;
        else System.out.println("FAIL test5: " + sol.addBoldTag("aabbcc", new String[] {"bc"}));
        // 解法二
        ++total; if (sol.addBoldTag2("abcxyz123", new String[] {"abc", "123"}).equals("<b>abc</b>xyz<b>123</b>")) ++pass;
        else System.out.println("FAIL test6");
        ++total; if (sol.addBoldTag2("aaabbb", new String[] {"aa", "b"}).equals("<b>aaabbb</b>")) ++pass;
        else System.out.println("FAIL test7");
        ++total; if (sol.addBoldTag2("abcd", new String[] {"abcd", "ab"}).equals("<b>abcd</b>")) ++pass;
        else System.out.println("FAIL test8");
        ++total; if (sol.addBoldTag2("abc", new String[] {}).equals("abc")) ++pass;
        else System.out.println("FAIL test9");
        if (pass == total) {
            System.out.println("== 0616 PASS ==");
        } else {
            System.out.println("== 0616 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(Σ 单词数 × s
解法二 O(总匹配扫描 + 区间合并) O(Σ 字符 + n)

617. Merge Two Binary Trees(合并二叉树)

题目描述

给你两棵二叉树:root1 和 root2。

想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。

你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;

否则,不为 null 的节点将直接作为新二叉树的节点。

返回合并后的二叉树。注意: 合并过程必须从两个树的根节点开始。

示例

  • 输入 :root1 = 1,3,2,5, root2 = 2,1,3,`null`,4,`null`,7
  • 输出 : 3,4,5,5,4,`null`,7
  • 输入:root1 = 1, root2 = 1,2
  • 输出 : 2,2
    提示:
  • 两棵树中的节点数目在范围 [0, 2000] 内
  • -10^4 <= Node.val <= 10^4

解题思路

解法一(递归,主提交):对应节点相加,缺的一侧整棵保留,结构与递归定义一致。

一侧为空则返回另一侧;否则新建节点,值为两节点之和,再递归合并左右子树。

时间复杂度 O(min(m, n))(实际遍历两树节点数较小者,一行两个都空即停),递归栈 O(min(m, n))。

解法二(迭代栈):用显式栈同时压入两棵树对应的节点对 (a, b)。弹出一对时若都不为空,

把 a.val += b.val(原地合并到 root1 上),再按左右孩子成对压栈;若 a 缺左孩子而 b 有,则直接接上 b 的子树。

避免递归栈溢出,空间 O(树高)。两种写法结果一致。

代码实现

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

class Solution {

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

    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        if (root1 == null) {
            return root2;
        }
        if (root2 == null) {
            return root1;
        }
        TreeNode node = new TreeNode(root1.val + root2.val);
        node.left = mergeTrees(root1.left, root2.left);
        node.right = mergeTrees(root1.right, root2.right);
        return node;
    }

    public TreeNode mergeTrees2(TreeNode root1, TreeNode root2) {
        if (root1 == null) {
            return root2;
        }
        Deque<TreeNode[]> stk = new ArrayDeque<>();
        stk.push(new TreeNode[] {root1, root2});
        while (!stk.isEmpty()) {
            TreeNode[] cur = stk.pop();
            TreeNode a = cur[0], b = cur[1];
            if (a == null || b == null) {
                continue;
            }
            a.val += b.val;
            if (a.left == null) {
                a.left = b.left;
            } else {
                stk.push(new TreeNode[] {a.left, b.left});
            }
            if (a.right == null) {
                a.right = b.right;
            } else {
                stk.push(new TreeNode[] {a.right, b.right});
            }
        }
        return root1;
    }

    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 ArrayDeque<>();
        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 List<Integer> lvl(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Queue<TreeNode> q = new LinkedList<>();
        if (root != null) {
            q.offer(root);
        }
        while (!q.isEmpty()) {
            TreeNode cur = q.poll();
            if (cur == null) {
                res.add(null);
                continue;
            }
            res.add(cur.val);
            q.offer(cur.left);
            q.offer(cur.right);
        }
        while (!res.isEmpty() && res.get(res.size() - 1) == null) {
            res.remove(res.size() - 1);
        }
        return res;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        // 示例 1
        TreeNode r1 = mk(new Integer[] {1, 3, 2, 5});
        TreeNode r2 = mk(new Integer[] {2, 1, 3, null, 4, null, 7});
        List<Integer> e1 = Arrays.asList(3, 4, 5, 5, 4, null, 7);
        ++total; if (lvl(sol.mergeTrees(r1, r2)).equals(e1)) ++pass; else System.out.println("FAIL test1: " + lvl(sol.mergeTrees(mk(new Integer[] {1, 3, 2, 5}), mk(new Integer[] {2, 1, 3, null, 4, null, 7}))));
        // 示例 2
        ++total; if (lvl(sol.mergeTrees(mk(new Integer[] {1}), mk(new Integer[] {1, 2}))).equals(Arrays.asList(2, 2))) ++pass;
        else System.out.println("FAIL test2");
        // 单侧为空
        TreeNode r5 = mk(new Integer[] {5, 4});
        ++total; if (lvl(sol.mergeTrees(r5, null)).equals(Arrays.asList(5, 4))) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.mergeTrees(null, null) == null) ++pass; else System.out.println("FAIL test4");
        // 解法二
        ++total; if (lvl(sol.mergeTrees2(mk(new Integer[] {1, 3, 2, 5}), mk(new Integer[] {2, 1, 3, null, 4, null, 7}))).equals(e1)) ++pass;
        else System.out.println("FAIL test5");
        ++total; if (lvl(sol.mergeTrees2(mk(new Integer[] {1}), mk(new Integer[] {1, 2}))).equals(Arrays.asList(2, 2))) ++pass;
        else System.out.println("FAIL test6");
        ++total; if (sol.mergeTrees2(null, mk(new Integer[] {7})) != null && lvl(sol.mergeTrees2(null, mk(new Integer[] {7}))).equals(Arrays.asList(7))) ++pass;
        else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0617 PASS ==");
        } else {
            System.out.println("== 0617 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(m)(m 为两树节点数较小者) O(m)(递归栈)
解法二 O(m) O(m)(显式栈)

618. Students Report By Geography(学生地理信息报告)

题目类型:数据库(SQL)

题目描述

表:student

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| name        | varchar |
| continent   | varchar |
+-------------+---------+

该表可能包含重复的行。该表的每一行表示学生的名字和他们来自的大陆。

一所学校有来自亚洲、欧洲和美洲的学生。编写解决方案实现对大洲(continent)列的透视表 操作,使得每个学生按照姓名的字母顺序 依次排列在对应的大洲下面。输出的标题应依次为美洲(America)、亚洲(Asia)和欧洲(Europe)。测试用例的生成保证来自美国的学生人数不少于亚洲或欧洲的学生人数。

示例 1:

复制代码
输入:
Student table:
+--------+-----------+
| name   | continent |
+--------+-----------+
| Jane   | America   |
| Pascal | Europe    |
| Xi     | Asia      |
| Jack   | America   |
+--------+-----------+
输出:
+---------+------+--------+
| America | Asia | Europe |
+---------+------+--------+
| Jack    | Xi   | Pascal |
| Jane    | null | null   |
+---------+------+--------+

进阶:如果不能确定哪个大洲的学生数最多,你可以写出一个查询去生成上述学生报告吗?

解题思路

解法一(ROW_NUMBER + GROUP BY 透视,推荐):要把三大洲学生按姓名排成并排三列,行数由人数最多的洲决定,条件聚合需要先对齐行号。用 ROW_NUMBER() OVER (PARTITION BY continent ORDER BY name) 给每个大洲内部按姓名升序编号,得到行号 rk;再按 rk 分组,对每个洲用 MAX(IF(continent = 'America', name, NULL)) 取出该行号上对应洲的学生名(非本洲为空),从而把一列长表「透视」成三列宽表。美洲人数最多(或有把握其行数够用)由题目保证。

解法二(进阶:行号补齐 + 全量连接):若不确定哪个洲人数最多,先取 MAX(COUNT(*)) 得到最大行数,把每个洲的编号序列补齐到该行数(用 WITH RECURSIVE 生成序号或用 UNION 补 NULL),再按行号连接各洲列。实现更复杂但无需知道哪个洲最多,可应对进阶场景。

代码实现

sql 复制代码
-- 解法一:ROW_NUMBER + GROUP BY 透视(推荐)
WITH
    T AS (
        SELECT
            *,
            ROW_NUMBER() OVER (
                PARTITION BY continent
                ORDER BY name
            ) AS rk
        FROM Student
    )
SELECT
    MAX(IF(continent = 'America', name, NULL)) AS 'America',
    MAX(IF(continent = 'Asia', name, NULL)) AS 'Asia',
    MAX(IF(continent = 'Europe', name, NULL)) AS 'Europe'
FROM T
GROUP BY rk;
sql 复制代码
-- 解法二(进阶思路):先生成补齐行号的编号序列再聚合
WITH
    T AS (
        SELECT
            *,
            ROW_NUMBER() OVER (
                PARTITION BY continent
                ORDER BY name
            ) AS rk,
            COUNT(1) OVER (PARTITION BY continent) AS total
        FROM Student
    ),
    MaxRow AS (SELECT MAX(total) AS mx FROM T)
SELECT
    MAX(IF(continent = 'America', name, NULL)) AS America,
    MAX(IF(continent = 'Asia', name, NULL)) AS Asia,
    MAX(IF(continent = 'Europe', name, NULL)) AS Europe
FROM T
WHERE rk <= (SELECT mx FROM MaxRow)
GROUP BY rk;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n)(分区排序,n 为学生行数) O(n)
解法二 O(n log n) O(n)

619. Biggest Single Number(只出现一次的最大数字)

题目类型:数据库(SQL)

题目描述

MyNumbers 表:

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| num         | int  |
+-------------+------+

该表可能包含重复项(换句话说,在 SQL 中,该表没有主键)。这张表的每一行都含有一个整数。

单一数字 是在 MyNumbers 表中只出现一次的数字。找出最大的单一数字 。如果不存在单一数字 ,则返回 null。

示例 1:

复制代码
输入:
MyNumbers 表:
+-----+
| num |
+-----+
| 8   |
| 8   |
| 3   |
| 3   |
| 1   |
| 4   |
| 5   |
| 6   |
+-----+
输出:
+-----+
| num |
+-----+
| 6   |
+-----+

解释:单一数字有 1、4、5 和 6,6 是最大的单一数字,返回 6。

示例 2:

复制代码
输入:
MyNumbers table:
+-----+
| num |
+-----+
| 8   |
| 8   |
| 7   |
| 7   |
| 3   |
| 3   |
| 3   |
+-----+
输出:
+------+
| num  |
+------+
| null |
+------+

解释 :输入的表中不存在单一数字,所以返回 null。

解题思路

解法一(分组 + 子查询,推荐):只出现一次的数中的最大者,出现多次的数应丢弃。先把 MyNumbers 按 num 分组并 HAVING COUNT(1) = 1 筛出全部「单一数」,外层再用 MAX(num) 取最大。若不存在任何单一数,MAX 作用于空集自然返回 NULL,正好符合题意。

解法二(分组 + CASE 排序取首行):不必子查询套 MAX------分组后先算频次,用 CASE WHEN COUNT(1) = 1 THEN num ELSE NULL END 把非单一数置空,再按该值降序 ORDER BY 1 DESC 取第一行,同样在无单一数时返回 NULL。

代码实现

sql 复制代码
-- 解法一:分组 HAVING + MAX 子查询(推荐)
SELECT MAX(num) AS num
FROM
    (
        SELECT num
        FROM MyNumbers
        GROUP BY 1
        HAVING COUNT(1) = 1
    ) AS t;
sql 复制代码
-- 解法二:分组 + CASE 排序取首行
SELECT
    CASE
        WHEN COUNT(1) = 1 THEN num
        ELSE NULL
    END AS num
FROM MyNumbers
GROUP BY num
ORDER BY 1 DESC
LIMIT 1;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n)(分组 + 排序,n 为 MyNumbers 行数) O(k)(k 为去重后数字数)
解法二 O(n log n) O(k)

620. Not Boring Movies(有趣的电影)

题目类型:数据库(SQL)

题目描述

表:cinema

复制代码
+----------------+----------+
| Column Name    | Type     |
+----------------+----------+
| id             | int      |
| movie          | varchar  |
| description    | varchar  |
| rating         | float    |
+----------------+----------+

id 是该表的主键(具有唯一值的列)。每行包含有关电影名称、类型和评级的信息。评级为 [0,10] 范围内的小数点后 2 位浮点数。

编写解决方案,找出所有影片描述为非 boring(不无聊)的并且 id 为奇数 的影片。返回结果按 rating 降序排列。

示例 1:

复制代码
输入:
+---------+-----------+--------------+-----------+
|   id    | movie     |  description |  rating   |
+---------+-----------+--------------+-----------+
|   1     | War       |   great 3D   |   8.9     |
|   2     | Science   |   fiction    |   8.5     |
|   3     | irish     |   boring     |   6.2     |
|   4     | Ice song  |   Fantacy    |   8.6     |
|   5     | House card|   Interesting|   9.1     |
+---------+-----------+--------------+-----------+
输出:
+---------+-----------+--------------+-----------+
|   id    | movie     |  description |  rating   |
+---------+-----------+--------------+-----------+
|   5     | House card|   Interesting|   9.1     |
|   1     | War       |   great 3D   |   9.1     |
+---------+-----------+--------------+-----------+

解释:我们有三部电影,它们的 id 是奇数:1、3 和 5。id = 3 的电影是 boring 的,所以我们不把它包括在答案中。

解题思路

解法一(WHERE 条件过滤 + ORDER BY,推荐):两个筛选条件相互独立:描述不是 boring 且 id 为奇数,一次 WHERE 即可过滤:description != 'boring' AND id % 2 = 1(也可用位运算 id & 1 = 1)。再按 rating 降序 ORDER BY rating DESC 输出。注意 description 比较需与全小写单词精确匹配。

解法二(MOD 函数版本):把 id & 1 = 1 写成 MOD(id, 2) = 1,语义更显式,适合可读性优先的场景,结果与位运算写法一致。

代码实现

sql 复制代码
-- 解法一:WHERE 过滤 + 位运算判奇(推荐)
SELECT *
FROM Cinema
WHERE description != 'boring' AND id & 1 = 1
ORDER BY 4 DESC;
sql 复制代码
-- 解法二:MOD 函数判奇
SELECT *
FROM Cinema
WHERE description <> 'boring' AND MOD(id, 2) = 1
ORDER BY rating DESC;

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n)(过滤 + 排序,n 为电影行数;rating 有索引则 O(n)) O(1)
解法二 O(n log n) O(1)

621. Task Scheduler(任务调度器)

题目描述

给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表,用字母 A 到 Z 表示,以及一个冷却时间 n。

每个周期或时间间隔允许完成一项任务。任务可以按任何顺序完成,但有一个限制:两个相同种类的任务之间必须有长度为 n 的冷却时间。

返回完成所有任务所需要的最短时间间隔。

示例

  • 输入 :tasks = "A","A","A","B","B","B", n = 2
  • 输出: 8
  • 解释: 在完成任务 A 之后,你必须等待两个间隔。对任务 B 来说也是一样。在第 3 个间隔,A 和 B 都不能完成,所以你需要待命。在第 4 个间隔,由于已经经过了 2 个间隔,你可以再次执行 A 任务。
  • 输入 :tasks = "A","C","A","B","D","B", n = 1
  • 输出: 6
  • 输入 :tasks = "A","A","A","B","B","B", n = 3
  • 输出 : 10
    提示:
  • 1 <= tasks.length <= 10^4
  • tasks[i] 是大写英文字母
  • 0 <= n <= 100

解题思路

解法一(贪心公式,主提交):相同任务之间至少间隔 n。统计每种任务出现次数,设出现次数最多的任务

出现 x 次、并列最多者共 s 种,则排满所需下界为 (x - 1) * (n + 1) + s(把最多的任务放入每个「槽块」首列,

共 x - 1 个完整块加最后一块的 s 个)。若任务总数更大,说明空格会被其他任务填满,答案应为 tasks.length。

故答案是 max(tasks.length, (x - 1) * (n + 1) + s)。时间复杂度 O(m + 26),空间 O(1)。

解法二(贪心模拟 + 最大堆):维护每种任务剩余次数的大顶堆与「冷却中」队列(记录下次可执行时间),

按时间推进:每单位时间若堆非空则取出次数最多的任务执行并进入冷却;时间到期的任务重新入堆。

模拟到全部完成,时间即答案。复杂度 O(时间 × 26 log 26),时间与 m 同阶(含待命),思路直观。

代码实现

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

class Solution {

    public int leastInterval(char[] tasks, int n) {
        int[] cnt = new int[26];
        int x = 0;
        for (char c : tasks) {
            int idx = c - 'A';
            ++cnt[idx];
            x = Math.max(x, cnt[idx]);
        }
        int s = 0;
        for (int v : cnt) {
            if (v == x) {
                ++s;
            }
        }
        return Math.max(tasks.length, (x - 1) * (n + 1) + s);
    }

    public int leastInterval2(char[] tasks, int n) {
        int[] cnt = new int[26];
        for (char c : tasks) {
            ++cnt[c - 'A'];
        }
        PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> b[1] - a[1]); // [任务, 剩余次数]
        for (int i = 0; i < 26; ++i) {
            if (cnt[i] > 0) {
                heap.offer(new int[] {i, cnt[i]});
            }
        }
        Deque<int[]> cooldown = new ArrayDeque<>(); // [任务, 剩余次数, 可执行时间]
        int time = 0;
        while (!heap.isEmpty() || !cooldown.isEmpty()) {
            ++time;
            if (!heap.isEmpty()) {
                int[] cur = heap.poll();
                --cur[1];
                if (cur[1] > 0) {
                    cooldown.offer(new int[] {cur[0], cur[1], time + n});
                }
            }
            while (!cooldown.isEmpty() && cooldown.peek()[2] == time) {
                int[] back = cooldown.poll();
                heap.offer(new int[] {back[0], back[1]});
            }
        }
        return time;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.leastInterval(new char[] {'A', 'A', 'A', 'B', 'B', 'B'}, 2) == 8) ++pass; else System.out.println("FAIL test1");
        ++total; if (sol.leastInterval(new char[] {'A', 'C', 'A', 'B', 'D', 'B'}, 1) == 6) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.leastInterval(new char[] {'A', 'A', 'A', 'B', 'B', 'B'}, 3) == 10) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.leastInterval(new char[] {'A'}, 0) == 1) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.leastInterval(new char[] {'A', 'B', 'C'}, 2) == 3) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.leastInterval(new char[] {'A', 'A', 'A'}, 1) == 5) ++pass; else System.out.println("FAIL test6");
        // 解法二
        ++total; if (sol.leastInterval2(new char[] {'A', 'A', 'A', 'B', 'B', 'B'}, 2) == 8) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.leastInterval2(new char[] {'A', 'C', 'A', 'B', 'D', 'B'}, 1) == 6) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.leastInterval2(new char[] {'A', 'A', 'A', 'B', 'B', 'B'}, 3) == 10) ++pass; else System.out.println("FAIL test9");
        ++total; if (sol.leastInterval2(new char[] {'A', 'B', 'C'}, 2) == 3) ++pass; else System.out.println("FAIL test10");
        ++total; if (sol.leastInterval2(new char[] {'A', 'A', 'A'}, 1) == 5) ++pass; else System.out.println("FAIL test11");
        if (pass == total) {
            System.out.println("== 0621 PASS ==");
        } else {
            System.out.println("== 0621 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(m + 26) O(26)
解法二 O(T log 26)(T 为总时间,含待命) O(26)

622. Design Circular Queue(设计循环队列)

题目描述

设计你的循环队列实现。循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。

它也被称为"环形缓冲器"。循环队列的一个好处是我们可以利用这个队列之前用过的空间。

你的实现应该支持如下操作:

  • MyCircularQueue(k): 构造器,设置队列长度为 k。
  • Front: 从队首获取元素。如果队列为空,返回 -1。
  • Rear: 获取队尾元素。如果队列为空,返回 -1。
  • enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
  • deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
  • isEmpty(): 检查循环队列是否为空。
  • isFull(): 检查循环队列是否已满。

示例

复制代码
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
circularQueue.enQueue(1);  // 返回 true
circularQueue.enQueue(2);  // 返回 true
circularQueue.enQueue(3);  // 返回 true
circularQueue.enQueue(4);  // 返回 false,队列已满
circularQueue.Rear();      // 返回 3
circularQueue.isFull();    // 返回 true
circularQueue.deQueue();   // 返回 true
circularQueue.enQueue(4);  // 返回 true
circularQueue.Rear();      // 返回 4

提示:

  • 所有的值都在 0 至 1000 的范围内;
  • 操作数将在 1 至 1000 的范围内;
  • 请不要使用内置的队列库。

解题思路

解法一(数组 + size 计数,主提交):用长度 k 的数组免去扩容;用 front 指向队首,size 记录元素个数,

从而「满」与「空」不会因指针相撞产生歧义。新元素写 (front + size) % k;出队 front = (front + 1) % k;

队尾取 (front + size - 1) % k。所有操作 O(1),空间 O(k)。

解法二(数组 + 双指针游标):只维护 head(或 head/tail 两指针),用左右指针之差判断空/满,

队列容量固定时下标按 % k 环绕。两种写法逻辑等价,这里采用「size 计数」版为骨架。

代码实现

java 复制代码
class MyCircularQueue {

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

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

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

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

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

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

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

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

    public static void main(String[] args) {
        int pass = 0, total = 0;
        // 官方示例
        MyCircularQueue cq = new MyCircularQueue(3);
        ++total; if (cq.enQueue(1)) ++pass; else System.out.println("FAIL test1");
        ++total; if (cq.enQueue(2)) ++pass; else System.out.println("FAIL test2");
        ++total; if (cq.enQueue(3)) ++pass; else System.out.println("FAIL test3");
        ++total; if (!cq.enQueue(4)) ++pass; else System.out.println("FAIL test4");
        ++total; if (cq.Rear() == 3) ++pass; else System.out.println("FAIL test5");
        ++total; if (cq.isFull()) ++pass; else System.out.println("FAIL test6");
        ++total; if (cq.deQueue()) ++pass; else System.out.println("FAIL test7");
        ++total; if (cq.enQueue(4)) ++pass; else System.out.println("FAIL test8");
        ++total; if (cq.Rear() == 4) ++pass; else System.out.println("FAIL test9");
        ++total; if (cq.Front() == 2) ++pass; else System.out.println("FAIL test10");
        // 空队列操作
        MyCircularQueue cq2 = new MyCircularQueue(2);
        ++total; if (cq2.isEmpty()) ++pass; else System.out.println("FAIL test11");
        ++total; if (!cq2.isFull()) ++pass; else System.out.println("FAIL test12");
        ++total; if (cq2.Front() == -1) ++pass; else System.out.println("FAIL test13");
        ++total; if (cq2.Rear() == -1) ++pass; else System.out.println("FAIL test14");
        ++total; if (!cq2.deQueue()) ++pass; else System.out.println("FAIL test15");
        // 环绕复用
        MyCircularQueue cq3 = new MyCircularQueue(3);
        cq3.enQueue(1);
        cq3.enQueue(2);
        cq3.deQueue();
        cq3.enQueue(3);
        cq3.deQueue();
        cq3.enQueue(4);
        ++total; if (cq3.Front() == 3) ++pass; else System.out.println("FAIL test16: front=" + cq3.Front());
        ++total; if (cq3.Rear() == 4) ++pass; else System.out.println("FAIL test17");
        ++total; if (!cq3.isFull()) ++pass; else System.out.println("FAIL test18");
        if (pass == total) {
            System.out.println("== 0622 PASS ==");
        } else {
            System.out.println("== 0622 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

623. Add One Row to Tree(在二叉树中增加一行)

题目描述

给定一个二叉树的根 root 和两个整数 val 和 depth,在给定的深度 depth 处添加一个值为 val 的节点行。

注意,根节点 root 位于深度 1。

加法规则如下:

  • 给定整数 depth,对于深度为 depth - 1 的每个非空树节点 cur,创建两个值为 val 的树节点作为 cur 的左子树根和右子树根。
  • cur 原来的左子树应该是新的左子树根的左子树。
  • cur 原来的右子树应该是新的右子树根的右子树。
  • 如果 depth == 1 意味着 depth - 1 根本没有深度,那么创建一个树节点,值 val 作为整个原始树的新根,而原始树就是新根的左子树。

示例

  • 输入 :root = 4,2,6,3,1,5, val = 1, depth = 2
  • 输出 : 4,1,1,2,`null`,`null`,6,3,1,5
  • 输入 :root = 4,2,`null`,3,1, val = 1, depth = 3
  • 输出 : 4,2,`null`,1,1,3,`null`,`null`,1
    提示:
  • 节点数在 [1, 10^4] 范围内
  • 树的深度在 [1, 10^4] 范围内
  • -100 <= Node.val <= 100
  • -10^5 <= val <= 10^5
  • 1 <= depth <= the depth of tree + 1

解题思路

解法一(DFS,主提交):depth == 1 时新节点直接作为整个树的新根(原树挂在左子树);否则 DFS 走到

深度 depth - 1 的节点,把原左右孩子分别挂到新建左右节点的内部,原更深子树不用动。

时间复杂度 O(n)(只访问到目标层以上节点,n 为节点数),递归栈 O(height)。

解法二(BFS):用队列逐层推进,走到第 depth - 1 层时对队内每个节点同样挂接左右新节点;

避免递归栈,适合深度较大的树。时间复杂度 O(n),空间 O(width)。

代码实现

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

class Solution {

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

    private int val;
    private int depth;

    public TreeNode addOneRow(TreeNode root, int val, int depth) {
        if (depth == 1) {
            TreeNode node = new TreeNode(val);
            node.left = root;
            return node;
        }
        this.val = val;
        this.depth = depth;
        dfs(root, 1);
        return root;
    }

    private void dfs(TreeNode root, int d) {
        if (root == null) {
            return;
        }
        if (d == depth - 1) {
            TreeNode l = new TreeNode(val);
            l.left = root.left;
            root.left = l;
            TreeNode r = new TreeNode(val);
            r.right = root.right;
            root.right = r;
            return;
        }
        dfs(root.left, d + 1);
        dfs(root.right, d + 1);
    }

    public TreeNode addOneRow2(TreeNode root, int val, int depth) {
        if (depth == 1) {
            TreeNode node = new TreeNode(val);
            node.left = root;
            return node;
        }
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int d = 1;
        while (!q.isEmpty()) {
            int sz = q.size();
            for (int i = 0; i < sz; ++i) {
                TreeNode cur = q.poll();
                if (d == depth - 1) {
                    TreeNode l = new TreeNode(val);
                    l.left = cur.left;
                    cur.left = l;
                    TreeNode r = new TreeNode(val);
                    r.right = cur.right;
                    cur.right = r;
                } else {
                    if (cur.left != null) {
                        q.offer(cur.left);
                    }
                    if (cur.right != null) {
                        q.offer(cur.right);
                    }
                }
            }
            ++d;
        }
        return root;
    }

    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 ArrayDeque<>();
        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 List<Integer> lvl(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) {
            return res;
        }
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
        while (!q.isEmpty()) {
            TreeNode cur = q.poll();
            if (cur == null) {
                res.add(null);
                continue;
            }
            res.add(cur.val);
            q.offer(cur.left);
            q.offer(cur.right);
        }
        while (!res.isEmpty() && res.get(res.size() - 1) == null) {
            res.remove(res.size() - 1);
        }
        return res;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        // 示例 1
        List<Integer> e1 = Arrays.asList(4, 1, 1, 2, null, null, 6, 3, 1, 5);
        ++total; if (lvl(sol.addOneRow(mk(new Integer[] {4, 2, 6, 3, 1, 5}), 1, 2)).equals(e1)) ++pass;
        else System.out.println("FAIL test1: " + lvl(sol.addOneRow(mk(new Integer[] {4, 2, 6, 3, 1, 5}), 1, 2)));
        // 示例 2
        List<Integer> e2 = Arrays.asList(4, 2, null, 1, 1, 3, null, null, 1);
        ++total; if (lvl(sol.addOneRow(mk(new Integer[] {4, 2, null, 3, 1}), 1, 3)).equals(e2)) ++pass;
        else System.out.println("FAIL test2: " + lvl(sol.addOneRow(mk(new Integer[] {4, 2, null, 3, 1}), 1, 3)));
        // depth == 1:新根 val=1,原树整体挂在左子树,右孩子为 null
        ++total; if (lvl(sol.addOneRow(mk(new Integer[] {4, 2, 6}), 1, 1)).equals(Arrays.asList(1, 4, null, 2, 6))) ++pass;
        else System.out.println("FAIL test3");
        // 单节点 depth=2
        ++total; if (lvl(sol.addOneRow(mk(new Integer[] {4}), 1, 2)).equals(Arrays.asList(4, 1, 1))) ++pass;
        else System.out.println("FAIL test4");
        // 解法二 BFS
        ++total; if (lvl(sol.addOneRow2(mk(new Integer[] {4, 2, 6, 3, 1, 5}), 1, 2)).equals(e1)) ++pass;
        else System.out.println("FAIL test5");
        ++total; if (lvl(sol.addOneRow2(mk(new Integer[] {4, 2, null, 3, 1}), 1, 3)).equals(e2)) ++pass;
        else System.out.println("FAIL test6");
        ++total; if (lvl(sol.addOneRow2(mk(new Integer[] {4, 2, 6}), 1, 1)).equals(Arrays.asList(1, 4, null, 2, 6))) ++pass;
        else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0623 PASS ==");
        } else {
            System.out.println("== 0623 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(height)(递归栈)
解法二 O(n) O(width)(队列宽度)

624. Maximum Distance in Arrays(数组列表中的最大距离)

题目描述

给定 m 个数组,每个数组都已经按照升序排好序了。

现在你需要从两个不同的数组中选择两个整数(每个数组选一个)并且计算它们的距离。

两个整数 a 和 b 之间的距离定义为它们差的绝对值 |a-b|。

返回最大距离。

示例

  • 输入:\[1,2,3,4,5,1,2,3]
  • 输出: 4
  • 解释: 一种得到答案 4 的方法是从第一个数组或者第三个数组中选择 1,同时从第二个数组中选择 5。
  • 输入:arrays = \[1,1]
  • 输出 : 0
    提示:
  • m == arrays.length
  • 2 <= m <= 10^5
  • 1 <= arrays[i].length <= 500
  • -10^4 <= arrays[i][j] <= 10^4
  • arrays[i] 以升序排序。
  • 所有数组中最多有 10^5 个整数。

解题思路

解法一(维护全局最值,主提交):最大距离一定是一个数组的最大值到另一个数组的最小值之间的距离。

由于必须来自「不同」数组,不能简单地取全局 max - min(可能在同一个数组里)。

从第二个数组起:先只拿「当前数组一端」与「此前已看过数组的全局另一端」求距离更新答案,

再把当前数组的两端并入全局 mi / mx,从而保证两端不来自同一数组。

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

解法二(两次扫描候选):先求全局最小值的数组下标与全局最大值的数组下标;

若二者不同,答案就是 max - min;若相同(最值同在一个数组),则退化为在该数组外再找一个

「次小/次大」综合比较,或者直接排除该数组分别计算。实现上比解法一繁琐,且要小心多个数组

同时拥有全局最值的边界情况,本任务以解法一为推荐骨架。

代码实现

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

class Solution {

    public int maxDistance(List<List<Integer>> arrays) {
        int ans = 0;
        int mi = arrays.get(0).get(0);
        int mx = arrays.get(0).get(arrays.get(0).size() - 1);
        for (int i = 1; i < arrays.size(); ++i) {
            List<Integer> arr = arrays.get(i);
            int a = Math.abs(arr.get(0) - mx);
            int b = Math.abs(arr.get(arr.size() - 1) - mi);
            ans = Math.max(ans, Math.max(a, b));
            mi = Math.min(mi, arr.get(0));
            mx = Math.max(mx, arr.get(arr.size() - 1));
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<List<Integer>> a1 = new ArrayList<>();
        a1.add(Arrays.asList(1, 2, 3));
        a1.add(Arrays.asList(4, 5));
        a1.add(Arrays.asList(1, 2, 3));
        ++total; if (sol.maxDistance(a1) == 4) ++pass; else System.out.println("FAIL test1");

        List<List<Integer>> a2 = new ArrayList<>();
        a2.add(Arrays.asList(1));
        a2.add(Arrays.asList(1));
        ++total; if (sol.maxDistance(a2) == 0) ++pass; else System.out.println("FAIL test2");

        // 最值同数组:[[1,5],[0,2]],最大距离 = 5-0=5(来自不同数组)
        List<List<Integer>> a3 = new ArrayList<>();
        a3.add(Arrays.asList(1, 5));
        a3.add(Arrays.asList(0, 2));
        ++total; if (sol.maxDistance(a3) == 5) ++pass; else System.out.println("FAIL test3: " + sol.maxDistance(a3));

        // 负数
        List<List<Integer>> a4 = new ArrayList<>();
        a4.add(Arrays.asList(-10, -1));
        a4.add(Arrays.asList(-5, 0));
        a4.add(Arrays.asList(2, 3));
        ++total; if (sol.maxDistance(a4) == 13) ++pass; else System.out.println("FAIL test4: " + sol.maxDistance(a4));

        // 三数组 [[1,4],[0,5],[2,3]]:最大 5-1=4 或 5-2=3 → 4
        List<List<Integer>> a5 = new ArrayList<>();
        a5.add(Arrays.asList(1, 4));
        a5.add(Arrays.asList(0, 5));
        a5.add(Arrays.asList(2, 3));
        ++total; if (sol.maxDistance(a5) == 4) ++pass; else System.out.println("FAIL test5: " + sol.maxDistance(a5));
        if (pass == total) {
            System.out.println("== 0624 PASS ==");
        } else {
            System.out.println("== 0624 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

625. Minimum Factorization(最小因式分解)

题目描述

给定一个正整数 num,找出最小的正整数 x 使得 x 的所有数位相乘恰好等于 num。

如果不存在这样的结果或者结果不是 32 位有符号整数,返回 0。

示例

  • 输入 :num = 48
  • 输出: 68
  • 解释: 6 × 8 = 48,且没有比 68 更小的满足条件的正整数(如 2 × 2 × 2 × 2 × 3 可拼成 2223 也满足,但 68 更小)。
  • 输入 :num = 15
  • 输出 : 35
    提示:
  • 1 <= num <= 2^31 - 1

解题思路

解法一(贪心 + 从 9 到 2 分解,主提交):要把 num 拆成若干个 2..9 的乘积,并使拼成的整数最小。

位数越少、高位越小则数越小:因此从 9 往 2 依次尽量整除(每次除净),并把每一位因子从「低位」开始

写入结果(ans = mul * i + ans,mul 为当前位的十进制权重 1、10、100...),这样先除出的因子会落在大位。

最后若 num 未被除尽(仍有大于 1 的质因子)或结果超过 2^31 - 1,返回 0。

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

解法二(DFS 回溯):枚举因子 2..9 的乘积组合,找出所有能完整分解 num 的因子序列,取拼数最小的一个;

思路直观但组合较多,仅 num 因子结构简单时适用。本题以解法一为推荐骨架。

代码实现

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

class Solution {

    public int smallestFactorization(int num) {
        if (num < 2) {
            return num;
        }
        long ans = 0;
        long mul = 1;
        for (int i = 9; i >= 2; --i) {
            while (num % i == 0) {
                num /= i;
                ans = mul * i + ans;
                mul *= 10;
            }
        }
        if (num < 2 && ans <= Integer.MAX_VALUE) {
            return (int) ans;
        }
        return 0;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.smallestFactorization(48) == 68) ++pass; else System.out.println("FAIL test1: " + sol.smallestFactorization(48));
        ++total; if (sol.smallestFactorization(15) == 35) ++pass; else System.out.println("FAIL test2: " + sol.smallestFactorization(15));
        ++total; if (sol.smallestFactorization(1) == 1) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.smallestFactorization(2) == 2) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.smallestFactorization(7) == 7) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.smallestFactorization(8) == 8) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.smallestFactorization(9) == 9) ++pass; else System.out.println("FAIL test7");
        // 质数无法拆分(除自身外无 2..9 因子)
        ++total; if (sol.smallestFactorization(13) == 0) ++pass; else System.out.println("FAIL test8: " + sol.smallestFactorization(13));
        // 6 本身是一位数,直接满足「数位相乘 = 6」
        ++total; if (sol.smallestFactorization(6) == 6) ++pass; else System.out.println("FAIL test9: " + sol.smallestFactorization(6));
        // 24 = 8×3 → 38 (3×8);也可 4×6=46、2×3×4=234... 最小为 38
        ++total; if (sol.smallestFactorization(24) == 38) ++pass; else System.out.println("FAIL test10: " + sol.smallestFactorization(24));
        // 大数溢出:num = 2^31-1 = 2147483647 无法分解
        ++total; if (sol.smallestFactorization(2147483647) == 0) ++pass; else System.out.println("FAIL test11");
        // 10 = 2×5 → 25
        ++total; if (sol.smallestFactorization(10) == 25) ++pass; else System.out.println("FAIL test12: " + sol.smallestFactorization(10));
        if (pass == total) {
            System.out.println("== 0625 PASS ==");
        } else {
            System.out.println("== 0625 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(log num) O(1)
解法二 O(8^d)(d 为因子个数) O(d)
相关推荐
文丑颜不良啊1 小时前
LeetCode651~675题目解析
java·leetcode
frjc1 小时前
Spring WebFlux SSE 长连接,可以替换为 Java 21 虚拟线程吗?
java·人工智能·spring·ai·ai编程
Xin7702 小时前
LeetCode 78. 子集(回溯法)
leetcode
INGNIGHT2 小时前
151 · 买卖股票的最佳时机 III(序列dp)
数据结构·算法·leetcode
知守观2 小时前
从 4 套 PDF 库的混战,聊聊老项目的依赖管理与“静默失败”
java·后端·设计模式
深盾科技_Virbox2 小时前
换机或重装后软件许可怎么恢复:先核对哪四项
java·服务器·数据库
Zhuo1Joy2 小时前
中间件挂了怎么办:兜底的边界与取舍
java
用户072367066602 小时前
基于 JavaAgent 的无侵入 Mock:让联调和链路测试不再等下游
java·后端