LeetCode 576-600. 题解合集
文章目录
- [LeetCode 576-600. 题解合集](#LeetCode 576-600. 题解合集)
-
- [576. Out of Boundary Paths(出界的路径数)](#576. Out of Boundary Paths(出界的路径数))
- [577. Employee Bonus(员工奖金)](#577. Employee Bonus(员工奖金))
- [578. Get Highest Answer Rate Question(查询回答率最高的问题)](#578. Get Highest Answer Rate Question(查询回答率最高的问题))
- [579. Find Cumulative Salary of an Employee(查询员工的累计薪水)](#579. Find Cumulative Salary of an Employee(查询员工的累计薪水))
- [580. Count Student Number in Departments(统计各专业学生人数)](#580. Count Student Number in Departments(统计各专业学生人数))
- [581. Shortest Unsorted Continuous Subarray(最短无序连续子数组)](#581. Shortest Unsorted Continuous Subarray(最短无序连续子数组))
- [582. Kill Process(杀掉进程)](#582. Kill Process(杀掉进程))
- [583. Delete Operation for Two Strings(两个字符串的删除操作)](#583. Delete Operation for Two Strings(两个字符串的删除操作))
- [584. Find Customer Referee(寻找用户推荐人)](#584. Find Customer Referee(寻找用户推荐人))
- [585. Investments in 2016(2016 年的投资)](#585. Investments in 2016(2016 年的投资))
- [586. Customer Placing the Largest Number of Orders(订单最多的客户)](#586. Customer Placing the Largest Number of Orders(订单最多的客户))
- [587. Erect the Fence(安装栅栏)](#587. Erect the Fence(安装栅栏))
- [588. Design In-Memory File System(设计内存文件系统)](#588. Design In-Memory File System(设计内存文件系统))
- [589. N-ary Tree Preorder Traversal(N 叉树的前序遍历)](#589. N-ary Tree Preorder Traversal(N 叉树的前序遍历))
- [590. N-ary Tree Postorder Traversal(N 叉树的后序遍历)](#590. N-ary Tree Postorder Traversal(N 叉树的后序遍历))
- [591. Tag Validator(标签验证器)](#591. Tag Validator(标签验证器))
- [592. Fraction Addition and Subtraction(分数加减运算)](#592. Fraction Addition and Subtraction(分数加减运算))
- [593. Valid Square(有效的正方形)](#593. Valid Square(有效的正方形))
- [594. Longest Harmonious Subsequence(最长和谐子序列)](#594. Longest Harmonious Subsequence(最长和谐子序列))
- [595. Big Countries(大的国家)](#595. Big Countries(大的国家))
- [596. Classes More Than 5 Students(超过 5 名学生的课)](#596. Classes More Than 5 Students(超过 5 名学生的课))
- [597. Friend Requests I: Overall Acceptance Rate(好友申请 I:总体通过率)](#597. Friend Requests I: Overall Acceptance Rate(好友申请 I:总体通过率))
- [598. Range Addition II(区间加法 II)](#598. Range Addition II(区间加法 II))
- [599. Minimum Index Sum of Two Lists(两个列表的最小索引总和)](#599. Minimum Index Sum of Two Lists(两个列表的最小索引总和))
- [600. Non-negative Integers without Consecutive Ones(不含连续1的非负整数)](#600. Non-negative Integers without Consecutive Ones(不含连续1的非负整数))
576. Out of Boundary Paths(出界的路径数)
题目描述
给你一个大小为 m x n 的网格和一个球。球的起始坐标为 [startRow, startColumn]。你可以将球移到在四个方向上相邻的单元格内(可以穿过网格边界到达网格之外)。你最多可以移动 maxMove 次球。
给你五个整数 m、n、maxMove、startRow 以及 startColumn,找出并返回可以将球移出边界的路径数量。因为答案可能非常大,返回对 10^9 + 7 取余后的结果。
示例
- 输入 :
m= 2,n= 2,maxMove= 2,startRow= 0,startColumn= 0 - 输出: 6
- 输入 :
m= 1,n= 3,maxMove= 3,startRow= 0,startColumn= 1 - 输出 : 12
提示: 1 <= m, n <= 500 <= maxMove <= 500 <= startRow < m,0 <= startColumn < n
解题思路
解法一(三维 DP):dp[k][i][j] 表示移动恰好 k 步到达 (i, j) 的路径数。初始 dp[0][startRow][startColumn] = 1;
每步由四邻域累加转移得到 dp[k+1],当从一个格子走出边界时(相邻格越界),把该格路径数计入答案 ans(走出即出界)。
累计每次出界路径数,取模返回。(主提交采用此解法。)
解法二(记忆化搜索):dfs(i, j, moves) 表示从 (i, j)、剩余 moves 步能走出边界的路径数;越界返回 1,moves == 0 返回 0,否则枚举四方向递归并记忆化。时间复杂度相同。
代码实现
java
import java.util.*;
class Solution {
private static final int MOD = 1_000_000_007;
private static final int[][] DIRS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
// 主提交:解法一 三维 DP(滚动两层)
public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
int[][] dp = new int[m][n];
dp[startRow][startColumn] = 1;
int ans = 0;
for (int step = 0; step < maxMove; ++step) {
int[][] ndp = new int[m][n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (dp[i][j] == 0) continue;
for (int[] d : DIRS) {
int ni = i + d[0], nj = j + d[1];
if (ni < 0 || ni >= m || nj < 0 || nj >= n) {
ans = (ans + dp[i][j]) % MOD; // 走出边界,计入答案
} else {
ndp[ni][nj] = (ndp[ni][nj] + dp[i][j]) % MOD;
}
}
}
}
dp = ndp;
}
return ans;
}
// 解法二:记忆化搜索
private int m, n, mm;
private Integer[][][] memo;
public int findPaths2(int m, int n, int maxMove, int startRow, int startColumn) {
this.m = m; this.n = n; this.mm = maxMove;
memo = new Integer[m][n][maxMove + 1];
return dfs(startRow, startColumn, maxMove);
}
private int dfs(int i, int j, int moves) {
if (i < 0 || i >= m || j < 0 || j >= n) return 1; // 越界即出界
if (moves == 0) return 0;
if (memo[i][j][moves] != null) return memo[i][j][moves];
long sum = 0;
for (int[] d : DIRS) sum += dfs(i + d[0], j + d[1], moves - 1);
return memo[i][j][moves] = (int) (sum % MOD);
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.findPaths(2, 2, 2, 0, 0) == 6) ++pass; else System.out.println("FAIL test1");
++total; if (sol.findPaths(1, 3, 3, 0, 1) == 12) ++pass; else System.out.println("FAIL test2");
++total; if (sol.findPaths(1, 1, 1, 0, 0) == 4) ++pass; else System.out.println("FAIL test3");
++total; if (sol.findPaths(2, 2, 0, 0, 0) == 0) ++pass; else System.out.println("FAIL test4");
// 解法二
++total; if (sol.findPaths2(2, 2, 2, 0, 0) == 6) ++pass; else System.out.println("FAIL test5");
++total; if (sol.findPaths2(1, 3, 3, 0, 1) == 12) ++pass; else System.out.println("FAIL test6");
++total; if (sol.findPaths2(2, 1, 10, 0, 0) == 30) ++pass; else System.out.println("FAIL test7");
if (pass == total) System.out.println("== 0576 PASS =="); else { System.out.println("== 0576 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(maxMove·m·n) | O(m·n)(滚动) |
| 解法二 | O(maxMove·m·n) | O(maxMove·m·n)(记忆表) |
577. Employee Bonus(员工奖金)
题目类型:数据库(SQL)
题目描述
表:Employee
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| empId | int |
| name | varchar |
| supervisor | int |
| salary | int |
+-------------+---------+
empId 是该表中具有唯一值的列。该表的每一行都表示员工的 id 和姓名,以及他们经理的 id 和他们的工资。
表:Bonus
+-------------+------+
| Column Name | Type |
+-------------+------+
| empId | int |
| bonus | int |
+-------------+------+
empId 是该表具有唯一值的列。empId 是 Employee 表中 empId 的外键(reference 列)。该表的每一行都包含一个员工的 id 和他们各自的奖金。
编写一个解决方案来报告满足以下任一条件的每个员工的姓名和奖金金额:
- 奖金少于
1000的员工。 - 没有任何奖金的员工。
以任意顺序返回结果表。结果格式如下所示。
示例 1:
输入:
Employee table:
+-------+--------+------------+--------+
| empId | name | supervisor | salary |
+-------+--------+------------+--------+
| 3 | Brad | null | 4000 |
| 1 | John | 3 | 1000 |
| 2 | Dan | 3 | 2000 |
| 4 | Thomas | 3 | 4000 |
+-------+--------+------------+--------+
Bonus table:
+-------+-------+
| empId | bonus |
+-------+-------+
| 2 | 500 |
| 4 | 2000 |
+-------+-------+
输出:
+------+-------+
| name | bonus |
+------+-------+
| Brad | null |
| John | null |
| Dan | 500 |
+------+-------+
解题思路
解法一(左连接 + IFNULL 过滤,推荐):需要列出奖金少于 1000 的员工,没有奖金记录的也要算作 0 。内连接会丢掉没有奖金的人,因此用 Employee LEFT JOIN Bonus(以 empId 关联),再用 IFNULL(bonus, 0) < 1000 筛选。NULL 被当成 0,符合「未发奖金」的语义。
解法二(显式 IS NULL 条件):也可以不借助 IFNULL,直接用 bonus < 1000 OR bonus IS NULL 表达两个条件的并集,语义更直白。
代码实现
sql
-- 解法一:左连接 + IFNULL 过滤(推荐)
SELECT name, bonus
FROM Employee
LEFT JOIN Bonus USING (empId)
WHERE IFNULL(bonus, 0) < 1000;
sql
-- 解法二:OR 显式补 NULL
SELECT e.name, b.bonus
FROM Employee AS e
LEFT JOIN Bonus AS b ON e.empId = b.empId
WHERE b.bonus < 1000 OR b.bonus IS NULL;
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + m)(一次左连接,n 为 Employee 行数,m 为 Bonus 行数) |
O(n)(连接结果) |
| 解法二 | O(n + m) | O(n) |
578. Get Highest Answer Rate Question(查询回答率最高的问题)
题目类型:数据库(SQL)
题目描述
SurveyLog 表:
+-------------+------+
| Column Name | Type |
+-------------+------+
| id | int |
| action | ENUM |
| question_id | int |
| answer_id | int |
| q_num | int |
| timestamp | int |
+-------------+------+
这张表可能包含重复项。action 是一个 ENUM(category) 数据,可以是 "show"、"answer" 或者 "skip"。这张表的每一行表示:ID = id 的用户对 question_id 的问题在 timestamp 时间进行了 action 操作。如果用户对应的操作是 "answer",answer_id 将会是对应答案的 id,否则值为 null。q_num 是该问题在当前会话中的数字顺序。
回答率是指:同一问题编号中回答次数占显示次数的比率。
编写一个解决方案以报告回答率 最高的问题。如果有多个问题具有相同的最大回答率 ,返回 question_id 最小的那个。查询结果如下例所示。
示例 1:
输入:
SurveyLog table:
+----+--------+-------------+-----------+-------+-----------+
| id | action | question_id | answer_id | q_num | timestamp |
+----+--------+-------------+-----------+-------+-----------+
| 5 | show | 285 | null | 1 | 123 |
| 5 | answer | 285 | 124124 | 1 | 124 |
| 5 | show | 369 | null | 2 | 125 |
| 5 | skip | 369 | null | 2 | 126 |
+----+--------+-------------+-----------+-------+-----------+
输出:
+------------+
| survey_log |
+------------+
| 285 |
+------------+
解释:
问题 285 显示 1 次、回答 1 次。回答率为 1.0。
问题 369 显示 1 次、回答 0 次。回答率为 0.0。
问题 285 回答率最高。
解题思路
解法一(GROUP BY 聚合,推荐):回答率是 answer 次数除以 show 次数,按题分组即可。SUM(action = 'answer') / SUM(action = 'show') 在分组内算出比率,按比率降序、question_id 升序(并列时取最小),LIMIT 1 取出题目。
解法二(窗口函数):用 SUM(...) OVER (PARTITION BY question_id) 给每一行带上该题的比率,外层去重后按比率降序取第一。结果与分组聚合相同,便于与其他窗口列一起使用。
代码实现
sql
-- 解法一:GROUP BY 聚合(推荐)
SELECT question_id AS survey_log
FROM SurveyLog
GROUP BY 1
ORDER BY SUM(action = 'answer') / SUM(action = 'show') DESC, 1
LIMIT 1;
sql
-- 解法二:窗口函数
WITH T AS (
SELECT
question_id AS survey_log,
(SUM(action = 'answer') OVER (PARTITION BY question_id)) /
(SUM(action = 'show') OVER (PARTITION BY question_id)) AS ratio
FROM SurveyLog
)
SELECT survey_log
FROM T
ORDER BY ratio DESC, 1
LIMIT 1;
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n)(分组后排序,n 为 SurveyLog 行数) |
O(k)(k 为不同问题数) |
| 解法二 | O(n log n) | O(n)(窗口结果行数) |
579. Find Cumulative Salary of an Employee(查询员工的累计薪水)
题目类型:数据库(SQL)
题目描述
表:Employee
+-------------+------+
| Column Name | Type |
+-------------+------+
| id | int |
| month | int |
| salary | int |
+-------------+------+
(id, month) 是该表的主键(具有唯一值的列的组合)。表中的每一行表示 2020 年期间员工一个月的工资。
编写一个解决方案,在一个统一的表中计算出每个员工的累计工资汇总 。员工的累计工资汇总可以计算如下:
- 对于该员工工作的每个月,将该月 和前两个月 的工资加 起来。这是他们当月的 3 个月总工资和 。如果员工在前几个月没有为公司工作,那么他们在前几个月的有效工资为
0。 - 不要 在摘要中包括员工最近一个月的 3 个月总工资和。
- 不要 包括雇员没有工作的任何一个月的 3 个月总工资和。
返回按 id 升序排序 的结果表。如果 id 相等,请按 month 降序排序。结果格式如下所示。
示例 1:
输入:
Employee table:
+----+-------+--------+
| id | month | salary |
+----+-------+--------+
| 1 | 1 | 20 |
| 2 | 1 | 20 |
| 1 | 2 | 30 |
| 2 | 2 | 30 |
| 3 | 2 | 40 |
| 1 | 3 | 40 |
| 3 | 3 | 60 |
| 1 | 4 | 60 |
| 3 | 4 | 70 |
| 1 | 7 | 90 |
| 1 | 8 | 90 |
+----+-------+--------+
输出:
+----+-------+--------+
| id | month | Salary |
+----+-------+--------+
| 1 | 7 | 90 |
| 1 | 4 | 130 |
| 1 | 3 | 90 |
| 1 | 2 | 50 |
| 1 | 1 | 20 |
| 2 | 1 | 20 |
| 3 | 3 | 100 |
| 3 | 2 | 40 |
+----+-------+--------+
解释 :员工 1 有 5 条工资记录,不包括最近一个月的 8。第 7 个月累计为 90 + 0 + 0 = 90,第 4 个月为 60 + 40 + 30 = 130,第 3 个月为 40 + 30 + 20 = 90,第 2 个月为 30 + 20 + 0 = 50,第 1 个月为 20 + 0 + 0 = 20。员工 2、3 依此类推。
解题思路
解法一(窗口范围累加 + NOT IN 排除最近月,推荐):每位员工排除最近一个月后,对每个剩下的月份求当月及前两个月的工资和。窗口的 RANGE 2 PRECEDING 正好按月份值 取近三个月,而不是按行数(缺月自动视为 0)。先用子查询去掉每人的 MAX(month),再按 id 分区、按 month 排序做范围累加,最后按 id、month 降序输出。
解法二(RANK() 排名过滤):方法一用 NOT IN 去掉最近月,也可以先算出累计工资与按月份倒序的排名 RANK() OVER (... ORDER BY month DESC),最近月排名为 1,外层过滤 rk > 1 即可,过滤写法更直观。
代码实现
sql
-- 解法一:RANGE 窗口累加 + NOT IN 排除最近月(推荐)
SELECT
id,
month,
SUM(salary) OVER (
PARTITION BY id
ORDER BY month
RANGE 2 PRECEDING
) AS Salary
FROM Employee
WHERE
(id, month) NOT IN (
SELECT id, MAX(month)
FROM Employee
GROUP BY id
)
ORDER BY id, month DESC;
sql
-- 解法二:RANK 排名过滤最近月
WITH T AS (
SELECT
id,
month,
SUM(salary) OVER (
PARTITION BY id
ORDER BY month
RANGE 2 PRECEDING
) AS salary,
RANK() OVER (
PARTITION BY id
ORDER BY month DESC
) AS rk
FROM Employee
)
SELECT id, month, salary
FROM T
WHERE rk > 1
ORDER BY 1, 2 DESC;
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n)(排序 + 窗口计算,n 为 Employee 行数) |
O(n)(窗口结果) |
| 解法二 | O(n log n) | O(n) |
580. Count Student Number in Departments(统计各专业学生人数)
题目类型:数据库(SQL)
题目描述
表:Student
+--------------+---------+
| Column Name | Type |
+--------------+---------+
| student_id | int |
| student_name | varchar |
| gender | varchar |
| dept_id | int |
+--------------+---------+
student_id 是该表的主键(具有唯一值的列)。dept_id 是 Department 表中 dept_id 的外键。该表的每一行都表示学生的姓名、性别和所属系的 id。
表:Department
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| dept_id | int |
| dept_name | varchar |
+-------------+---------+
dept_id 是该表的主键(具有唯一值的列)。该表的每一行包含一个部门的 id 和名称。
编写解决方案,为 Department 表中的所有部门 (甚至是没有当前学生的部门)报告各自的部门名称和每个部门的学生人数。按 student_number 降序 返回结果表。如果是平局,则按 dept_name 的字母顺序排序。结果格式如下所示。
示例 1:
输入:
Student 表:
+------------+--------------+--------+---------+
| student_id | student_name | gender | dept_id |
+------------+--------------+--------+---------+
| 1 | Jack | M | 1 |
| 2 | Jane | F | 1 |
| 3 | Mark | M | 2 |
+------------+--------------+--------+---------+
Department 表:
+---------+-------------+
| dept_id | dept_name |
+---------+-------------+
| 1 | Engineering |
| 2 | Science |
| 3 | Law |
+---------+-------------+
输出:
+-------------+----------------+
| dept_name | student_number |
+-------------+----------------+
| Engineering | 2 |
| Science | 1 |
| Law | 0 |
+-------------+----------------+
解题思路
解法一(左连接 + 分组统计,推荐):要每个系的学生人数,没有学生的系也应为 0 。内连接会丢掉空系,因此 Department LEFT JOIN Student(以 dept_id 关联),按系分组 COUNT(student_id) 统计学生数,再按人数降序、系名升序排序。左连接保证空系留下一行,COUNT 计为 0。
解法二(先聚合再外连接):也可以在子查询中先按 dept_id 在 Student 上分组计数,再与 Department 外连接,逻辑上先缩小数据量再关联,两种写法结果一致。
代码实现
sql
-- 解法一:左连接 + 分组统计(推荐)
SELECT dept_name, COUNT(student_id) AS student_number
FROM Department
LEFT JOIN Student USING (dept_id)
GROUP BY dept_id
ORDER BY 2 DESC, 1;
sql
-- 解法二:先分组计数再外连接
SELECT d.dept_name, IFNULL(t.cnt, 0) AS student_number
FROM Department AS d
LEFT JOIN (
SELECT dept_id, COUNT(1) AS cnt
FROM Student
GROUP BY dept_id
) AS t ON d.dept_id = t.dept_id
ORDER BY student_number DESC, dept_name;
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + m + k log k)(n 为 Student 行数,m 为 Department 行数,k 为系数量) |
O(k)(分组结果) |
| 解法二 | O(n + m + k log k) | O(k) |
581. Shortest Unsorted Continuous Subarray(最短无序连续子数组)
题目描述
给你一个整数数组 nums,你需要找出一个连续子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
请你找出符合题意的最短子数组,并输出它的长度。
示例
- 输入 :
nums= 2,6,4,8,10,9,15 - 输出: 5
- 解释: 你只需要对 6, 4, 8, 10, 9 进行升序排序,那么整个表都会变为升序排序。
- 输入 :
nums= 1,2,3,4 - 输出: 0
- 输入 :
nums= 1 - 输出 : 0
提示: 1 <= nums.length <= 10^4-10^5 <= nums[i] <= 10^5- 进阶:你可以设计一个时间复杂度为
O(n)的解决方案吗?
解题思路
解法一(排序比较):复制原数组排序,从两端同时扫描找第一个与排序后不同的位置 left 与 right,长度 = right - left + 1(若 left >= right 返回 0)。(主提交采用此解法。)
解法二(O(n) 双端扫描):从左到右维护遇到的最大值 max,记录最后一次 nums[i] < max 的位置(该位置右侧需重排);从右到左维护最小值 min,记录最后一次 nums[i] > min 的位置(该位置左侧需重排)。两端位置差 +1 即为答案。O(n) 时间、O(1) 空间。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 排序后比较
public int findUnsortedSubarray(int[] nums) {
int n = nums.length;
int[] sorted = nums.clone();
Arrays.sort(sorted);
int l = 0, r = n - 1;
while (l < n && nums[l] == sorted[l]) ++l;
while (r > l && nums[r] == sorted[r]) --r;
return r - l + 1;
}
// 解法二:O(n) 双端扫描(进阶)
public int findUnsortedSubarray2(int[] nums) {
int n = nums.length;
int max = nums[0], right = -1;
for (int i = 1; i < n; ++i) {
if (nums[i] < max) right = i; // 出现逆序,右边界至少到 i
else max = Math.max(max, nums[i]);
}
if (right == -1) return 0;
int min = nums[n - 1], left = -1;
for (int i = n - 2; i >= 0; --i) {
if (nums[i] > min) left = i; // 出现逆序,左边界至少到 i
else min = Math.min(min, nums[i]);
}
return right - left + 1;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.findUnsortedSubarray(new int[]{2,6,4,8,10,9,15}) == 5) ++pass; else System.out.println("FAIL test1");
++total; if (sol.findUnsortedSubarray(new int[]{1,2,3,4}) == 0) ++pass; else System.out.println("FAIL test2");
++total; if (sol.findUnsortedSubarray(new int[]{1}) == 0) ++pass; else System.out.println("FAIL test3");
++total; if (sol.findUnsortedSubarray(new int[]{2,1}) == 2) ++pass; else System.out.println("FAIL test4");
// 解法二
++total; if (sol.findUnsortedSubarray2(new int[]{2,6,4,8,10,9,15}) == 5) ++pass; else System.out.println("FAIL test5");
++total; if (sol.findUnsortedSubarray2(new int[]{1,2,3,4}) == 0) ++pass; else System.out.println("FAIL test6");
++total; if (sol.findUnsortedSubarray2(new int[]{1,3,2,2,2}) == 4) ++pass; else System.out.println("FAIL test7");
if (pass == total) System.out.println("== 0581 PASS =="); else { System.out.println("== 0581 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n)(排序) | O(n)(副本) |
| 解法二 | O(n) | O(1) |
582. Kill Process(杀掉进程)
题目描述
系统中存在 n 个进程,形成一个有根树结构。给你两个整数数组 pid 和 ppid,其中 pid[i] 是第 i 个进程的 ID,ppid[i] 是第 i 个进程的父进程 ID。
每一个进程只有一个父进程,但是可能会有一个或者多个子进程。只有一个进程的 ppid[i] = 0,意味着这个进程没有父进程。
当一个进程被杀掉的时候,它所有的子进程和后代进程都要被杀掉。
给你一个整数 kill 表示要杀掉进程的 ID,返回被杀掉的进程的 ID 列表。可以按任意顺序返回答案。
示例
- 输入:pid = 1,3,10,5, ppid = 3,0,5,3, kill = 5
- 输出: 5,10
- 解释: 涂为红色的进程是应该被杀掉的进程。
- 输入:pid = 1, ppid = 0, kill = 1
- 输出 : 1
提示: n == pid.length,n == ppid.length1 <= n <= 5 * 10^41 <= pid[i] <= 5 * 10^4,0 <= ppid[i] <= 5 * 10^4- 仅有一个进程没有父进程;
pid中的所有值互不相同;题目数据保证kill在pid中
解题思路
解法一(建树 + DFS):以 ppid -> 子进程列表 建邻接表(从 pid 与 ppid 同步构造),从 kill 出发做 DFS/BFS 收集所有被波及的节点。(主提交采用此解法。)
解法二(逆映射 + BFS):Map<Integer, Integer> 记录 pid -> ppid,从 kill 开始 BFS,每次从 pid 列表中筛出父进程为当前节点者入队。正确但需要每次全表扫描,复杂度更高。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 邻接表 + DFS
public List<Integer> killProcess(List<Integer> pid, List<Integer> ppid, int kill) {
Map<Integer, List<Integer>> children = new HashMap<>();
for (int i = 0; i < pid.size(); ++i) {
children.computeIfAbsent(ppid.get(i), k -> new ArrayList<>()).add(pid.get(i));
}
List<Integer> ans = new ArrayList<>();
Deque<Integer> stack = new ArrayDeque<>();
stack.push(kill);
while (!stack.isEmpty()) {
int cur = stack.pop();
ans.add(cur);
List<Integer> subs = children.get(cur);
if (subs != null) for (int s : subs) stack.push(s);
}
return ans;
}
// 解法二:逆映射 + 循环扩展(思路参考)
public List<Integer> killProcess2(List<Integer> pid, List<Integer> ppid, int kill) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < pid.size(); ++i) map.put(pid.get(i), ppid.get(i));
List<Integer> ans = new ArrayList<>(Arrays.asList(kill));
boolean changed = true;
while (changed) {
changed = false;
for (int p : pid) {
if (!ans.contains(p)) {
Integer parent = map.get(p);
if (parent != null && ans.contains(parent)) {
ans.add(p);
changed = true;
}
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
List<Integer> p1 = Arrays.asList(1, 3, 10, 5);
List<Integer> pp1 = Arrays.asList(3, 0, 5, 3);
Set<Integer> r1 = new HashSet<>(sol.killProcess(p1, pp1, 5));
++total; if (r1.equals(new HashSet<>(Arrays.asList(5, 10)))) ++pass; else System.out.println("FAIL test1: " + r1);
List<Integer> p2 = Arrays.asList(1);
List<Integer> pp2 = Arrays.asList(0);
++total; if (sol.killProcess(p2, pp2, 1).equals(Arrays.asList(1))) ++pass; else System.out.println("FAIL test2");
List<Integer> p3 = Arrays.asList(1, 2, 3);
List<Integer> pp3 = Arrays.asList(0, 1, 1);
Set<Integer> r3 = new HashSet<>(sol.killProcess(p3, pp3, 1));
++total; if (r3.equals(new HashSet<>(Arrays.asList(1, 2, 3)))) ++pass; else System.out.println("FAIL test3: " + r3);
// 解法二
Set<Integer> r4 = new HashSet<>(sol.killProcess2(p1, pp1, 5));
++total; if (r4.equals(new HashSet<>(Arrays.asList(5, 10)))) ++pass; else System.out.println("FAIL test4");
if (pass == total) System.out.println("== 0582 PASS =="); else { System.out.println("== 0582 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n²)(最坏每次全扫) | O(n) |
583. Delete Operation for Two Strings(两个字符串的删除操作)
题目描述
给定两个单词 word1 和 word2,返回使得 word1 和 word2 相同所需的最小步数。
每步可以删除任意一个字符串中的一个字符。
示例
- 输入 :
word1= "sea",word2= "eat" - 输出: 2
- 解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat" 变为 "ea"。
- 输入 :
word1= "leetcode",word2= "etco" - 输出 : 4
提示: 1 <= word1.length, word2.length <= 500word1和word2只包含小写英文字母
解题思路
解法一(LCS + 减法):两个字符串最终相同,保留的公共子序列越长,删除越少。设 lcs 为两者最长公共子序列长度,答案 = len1 + len2 - 2 * lcs。
用经典二维 DP 求 LCS:dp[i][j] 表示 word1[0..i) 与 word2[0..j) 的 LCS 长度。(主提交采用此解法。)
解法二(编辑距离变体 DP):dp[i][j] 直接表示使 word1[0..i) 与 word2[0..j) 相同的最小删除次数:字符相等时 dp[i-1][j-1],否则 min(dp[i-1][j], dp[i][j-1]) + 1。不需要显式求 LCS。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 LCS
public int minDistance(String word1, String word2) {
int n = word1.length(), m = word2.length();
int[][] dp = new int[n + 1][m + 1];
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return n + m - 2 * dp[n][m];
}
// 解法二:直接 DP 最小删除次数
public int minDistance2(String word1, String word2) {
int n = word1.length(), m = word2.length();
int[][] dp = new int[n + 1][m + 1];
for (int i = 1; i <= n; ++i) dp[i][0] = i; // 删光 word1
for (int j = 1; j <= m; ++j) dp[0][j] = j; // 删光 word2
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1;
}
}
}
return dp[n][m];
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.minDistance("sea", "eat") == 2) ++pass; else System.out.println("FAIL test1");
++total; if (sol.minDistance("leetcode", "etco") == 4) ++pass; else System.out.println("FAIL test2");
++total; if (sol.minDistance("a", "a") == 0) ++pass; else System.out.println("FAIL test3");
++total; if (sol.minDistance("a", "ab") == 1) ++pass; else System.out.println("FAIL test4");
// 解法二
++total; if (sol.minDistance2("sea", "eat") == 2) ++pass; else System.out.println("FAIL test5");
++total; if (sol.minDistance2("leetcode", "etco") == 4) ++pass; else System.out.println("FAIL test6");
++total; if (sol.minDistance2("a", "ab") == 1) ++pass; else System.out.println("FAIL test7");
if (pass == total) System.out.println("== 0583 PASS =="); else { System.out.println("== 0583 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n·m) | O(n·m)(可优化 O(m)) |
| 解法二 | O(n·m) | O(n·m)(可优化 O(m)) |
584. Find Customer Referee(寻找用户推荐人)
题目类型:数据库(SQL)
题目描述
表:Customer
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| id | int |
| name | varchar |
| referee_id | int |
+-------------+---------+
在 SQL 中,id 是该表的主键列。该表的每一行表示一个客户的 id、姓名以及推荐他们的客户的 id。
找出以下客户的姓名:
- 被任何
id != 2的用户推荐。 - 没有被任何用户推荐。
以任意顺序返回结果表。结果格式如下所示。
示例 1:
输入:
Customer 表:
+----+------+------------+
| id | name | referee_id |
+----+------+------------+
| 1 | Will | null |
| 2 | Jane | null |
| 3 | Alex | 2 |
| 4 | Bill | null |
| 5 | Zack | 1 |
| 6 | Mark | 2 |
+----+------+------------+
输出:
+------+
| name |
+------+
| Will |
| Jane |
| Bill |
| Zack |
+------+
解题思路
解法一(IFNULL 条件过滤,推荐):筛出推荐人不是 2 的客户。referee_id 为 NULL 时,<> 2 在 SQL 中结果为 NULL(不是真),会把这些人丢掉。用 IFNULL(referee_id, 0) != 2 把 NULL 归一为 0 再比较即可同时覆盖「未被推荐」的情况。
解法二(显式 OR IS NULL):条件写成 referee_id != 2 OR referee_id IS NULL,显式保留未知推荐人,语义更直白。注意不能用 referee_id = NULL 或 NOT IN (2)(后者同样会因为 NULL 不匹配而丢行)。
代码实现
sql
-- 解法一:IFNULL 条件过滤(推荐)
SELECT name
FROM Customer
WHERE IFNULL(referee_id, 0) != 2;
sql
-- 解法二:OR IS NULL 显式保留
SELECT name
FROM Customer
WHERE referee_id != 2 OR referee_id IS NULL;
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n)(一次全表扫描,n 为 Customer 行数) |
O(1)(常数辅助空间) |
| 解法二 | O(n) | O(1) |
585. Investments in 2016(2016 年的投资)
题目类型:数据库(SQL)
题目描述
Insurance 表:
+-------------+-------+
| Column Name | Type |
+-------------+-------+
| pid | int |
| tiv_2015 | float |
| tiv_2016 | float |
| lat | float |
| lon | float |
+-------------+-------+
pid 是这张表的主键(具有唯一值的列)。表中的每一行都包含一条保险信息,其中:pid 是投保人的投保编号,tiv_2015 是该投保人在 2015 年的总投保金额,tiv_2016 是该投保人在 2016 年的总投保金额,lat 是投保人所在城市的纬度,lon 是投保人所在城市的经度。题目数据确保 lat 不为空。
编写解决方案报告 2016 年(tiv_2016)所有满足下述条件的投保人的投保金额之和:
- 他在
2015年的投保额(tiv_2015)至少跟一个其他投保人在2015年的投保额相同。 - 他所在的城市必须与其他投保人都不同(也就是说
(lat, lon)不能跟其他任何一个投保人完全相同)。
tiv_2016 四舍五入的两位小数。查询结果格式如下例所示。
示例 1:
输入:
Insurance 表:
+-----+----------+----------+-----+-----+
| pid | tiv_2015 | tiv_2016 | lat | lon |
+-----+----------+----------+-----+-----+
| 1 | 10 | 5 | 10 | 10 |
| 2 | 20 | 20 | 20 | 20 |
| 3 | 10 | 30 | 20 | 20 |
| 4 | 10 | 40 | 40 | 40 |
+-----+----------+----------+-----+-----+
输出:
+----------+
| tiv_2016 |
+----------+
| 45.00 |
+----------+
解释 :表中的第一条记录和最后一条记录都满足两个条件:tiv_2015 值为 10,与第三条和第四条记录相同,且其位置是唯一的。第二条记录不符合任何一个条件。结果是第一条和最后一条记录的 tiv_2016 之和,即 5 + 40 = 45。
解题思路
解法一(窗口函数计数,推荐):2016 年投保额要满足两个条件:tiv_2015 有人相同(出现次数 > 1),且 (lat, lon) 独一无二(出现次数 = 1)。对每行再扫全表是平方级,这里用窗口函数一次扫描完成:按 tiv_2015 分区计数得 cnt1,按 (lat, lon) 分区计数得 cnt2,留下 cnt1 > 1 AND cnt2 = 1 的行,对其 tiv_2016 求和并 ROUND(..., 2) 四舍五入。
解法二(相关子查询):也可以分别用 IN(tiv_2015 出现在他人中)与 NOT IN((lat, lon) 未出现在他人中)子查询筛选,关联方式更贴近条件语义,但在大表上不如窗口函数高效。
代码实现
sql
-- 解法一:窗口函数计数(推荐)
WITH T AS (
SELECT
tiv_2016,
COUNT(1) OVER (PARTITION BY tiv_2015) AS cnt1,
COUNT(1) OVER (PARTITION BY lat, lon) AS cnt2
FROM Insurance
)
SELECT ROUND(SUM(tiv_2016), 2) AS tiv_2016
FROM T
WHERE cnt1 > 1 AND cnt2 = 1;
sql
-- 解法二:相关子查询
SELECT ROUND(SUM(tiv_2016), 2) AS tiv_2016
FROM Insurance AS i
WHERE
tiv_2015 IN (SELECT tiv_2015 FROM Insurance WHERE pid <> i.pid)
AND (lat, lon) NOT IN (
SELECT lat, lon FROM Insurance WHERE pid <> i.pid
);
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n)(窗口分区排序,n 为 Insurance 行数) |
O(n)(窗口结果) |
| 解法二 | O(n²)(每行两次相关子查询) | O(1) |
586. Customer Placing the Largest Number of Orders(订单最多的客户)
题目类型:数据库(SQL)
题目描述
表:Orders
+-----------------+----------+
| Column Name | Type |
+-----------------+----------+
| order_number | int |
| customer_number | int |
+-----------------+----------+
在 SQL 中,Order_number 是该表的主键。此表包含关于订单 ID 和客户 ID 的信息。
查找下了最多订单 的客户的 customer_number。测试用例生成后,恰好有一个客户比任何其他客户下了更多的订单。查询结果格式如下所示。
示例 1:
输入:
Orders 表:
+--------------+-----------------+
| order_number | customer_number |
+--------------+-----------------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 3 |
+--------------+-----------------+
输出:
+-----------------+
| customer_number |
+-----------------+
| 3 |
+-----------------+
解释 :customer_number 为 3 的顾客有两个订单,比顾客 1 或者 2 都要多,因为他们只有一个订单。所以结果是该顾客的 customer_number,也就是 3。
进阶 :如果有多位顾客订单数并列最多,你能找到他们所有的 customer_number 吗?
解题思路
解法一(分组 + 排序取首行,推荐):按客户分组计数,再按次数降序取第一。GROUP BY customer_number 后 ORDER BY COUNT(1) DESC,LIMIT 1 取一条记录。题目保证答案唯一,因此可以直接取首行。
解法二(HAVING 过滤达到最大值):先求出最大订单数 MAX(cnt),再在分组结果中用 HAVING COUNT(1) = max_cnt 筛出达到该值的客户。该写法天然支持进阶场景:若有并列最多,会把所有并列客户都返回。
解法三(窗口排名):用 RANK() OVER (ORDER BY COUNT(1) DESC) 对每个客户排名,取排名为 1 的客户,同样可应对并列需求。
代码实现
sql
-- 解法一:分组 + 排序取首行(推荐)
SELECT customer_number
FROM Orders
GROUP BY customer_number
ORDER BY COUNT(1) DESC
LIMIT 1;
sql
-- 解法二:HAVING 过滤最大计数(支持并列,应对进阶)
SELECT customer_number
FROM Orders
GROUP BY customer_number
HAVING COUNT(1) = (
SELECT COUNT(1) AS cnt
FROM Orders
GROUP BY customer_number
ORDER BY cnt DESC
LIMIT 1
);
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n)(分组 + 排序,n 为 Orders 行数) |
O(k)(k 为客户数) |
| 解法二 | O(n log n)(分组两次) | O(k) |
| 解法三 | O(n log n) | O(k) |
587. Erect the Fence(安装栅栏)
题目描述
给定一个数组 trees,其中 trees[i] = [xi, yi] 表示树在花园中的位置。
你被要求用最短长度的绳子把整个花园围起来,因为绳子很贵。只有把所有的树都围起来,花园才围得很好。
返回恰好位于围栏周边的树木的坐标。
示例
- 输入:points = \[1,1,2,2,2,0,2,4,3,3,4,2]
- 输出: \[1,1,2,0,3,3,2,4,4,2]
- 输入:points = \[1,2,2,2,4,2]
- 输出 : \[4,2,2,2,1,2]
提示: 1 <= points.length <= 3000points[i].length == 20 <= xi, yi <= 100- 所有给定的点都是唯一的。
解题思路
解法一(Andrew 凸包):把点按 (x, y) 排序,分别求下凸壳与上凸壳,最后合并(首尾点重复需去重)。
用栈维护凸壳,判断叉积 cross(o, a, b) <= 0 则弹栈(不保留共线中间点,本方法对 cnt == 1/2 的处理也要考虑重复点)。
注意:当点数 ≤ 2 时直接全部返回。共线点序列较长时需用 >= 0 保证顶点不被漏掉。(主提交采用此解法。)
解法二(暴力 O(n³)):枚举任意两点组成的直线,若所有点都在这条直线的一侧(含线上),则该两点在凸包边上;收集所有凸包边上的点并去重。可读性高但 O(n³)。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 Andrew 凸包
public int[][] outerTrees(int[][] trees) {
int n = trees.length;
if (n <= 3) return trees.clone();
Arrays.sort(trees, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
// 下凸壳
List<int[]> lower = new ArrayList<>();
for (int[] p : trees) {
while (lower.size() >= 2 && cross(lower.get(lower.size() - 2), lower.get(lower.size() - 1), p) < 0) {
lower.remove(lower.size() - 1);
}
lower.add(p);
}
// 上凸壳
List<int[]> upper = new ArrayList<>();
for (int i = n - 1; i >= 0; --i) {
int[] p = trees[i];
while (upper.size() >= 2 && cross(upper.get(upper.size() - 2), upper.get(upper.size() - 1), p) < 0) {
upper.remove(upper.size() - 1);
}
upper.add(p);
}
// 合并去重(首尾点重复)
Set<String> seen = new HashSet<>();
List<int[]> res = new ArrayList<>();
for (int[] p : lower) {
String key = p[0] + "," + p[1];
if (seen.add(key)) res.add(p);
}
for (int i = 1; i < upper.size() - 1; ++i) { // upper 首尾与 lower 重复
int[] p = upper.get(i);
String key = p[0] + "," + p[1];
if (seen.add(key)) res.add(p);
}
return res.toArray(new int[res.size()][]);
}
// 叉积:ab x ac
private int cross(int[] a, int[] b, int[] c) {
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
}
// 解法二:暴力枚举直线(凸包边判定)
public int[][] outerTrees2(int[][] trees) {
int n = trees.length;
if (n <= 2) return trees.clone();
Set<String> onHull = new HashSet<>();
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
int[] a = trees[i], b = trees[j];
int left = 0, right = 0;
for (int[] p : trees) {
int cr = cross(a, b, p);
if (cr > 0) ++left;
else if (cr < 0) ++right;
if (left > 0 && right > 0) break;
}
if (left == 0 || right == 0) {
onHull.add(a[0] + "," + a[1]);
onHull.add(b[0] + "," + b[1]);
}
}
}
int[][] res = new int[onHull.size()][2];
int idx = 0;
for (String s : onHull) {
String[] xy = s.split(",");
res[idx++] = new int[]{Integer.parseInt(xy[0]), Integer.parseInt(xy[1])};
}
return res;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] r1 = sol.outerTrees(new int[][]{{1,1},{2,2},{2,0},{2,4},{3,3},{4,2}});
++total; if (r1.length == 5) ++pass; else System.out.println("FAIL test1 len=" + r1.length);
int[][] r2 = sol.outerTrees(new int[][]{{1,2},{2,2},{4,2}});
++total; if (r2.length == 3) ++pass; else System.out.println("FAIL test2 len=" + r2.length);
int[][] r3 = sol.outerTrees(new int[][]{{0,0},{0,1},{1,0},{1,1}});
++total; if (r3.length == 4) ++pass; else System.out.println("FAIL test3 len=" + r3.length);
// 解法二
int[][] r4 = sol.outerTrees2(new int[][]{{1,1},{2,2},{2,0},{2,4},{3,3},{4,2}});
++total; if (r4.length == 5) ++pass; else System.out.println("FAIL test4 len=" + r4.length);
int[][] r5 = sol.outerTrees2(new int[][]{{0,0},{0,1},{1,0},{1,1}});
++total; if (r5.length == 4) ++pass; else System.out.println("FAIL test5 len=" + r5.length);
if (pass == total) System.out.println("== 0587 PASS =="); else { System.out.println("== 0587 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n)(排序) | O(n) |
| 解法二 | O(n³) | O(n) |
588. Design In-Memory File System(设计内存文件系统)
题目描述
设计一个内存文件系统,模拟以下功能:
实现文件系统类:
FileSystem()初始化系统对象List<String> ls(String path):如果path是一个文件路径,则返回一个仅包含该文件名称的列表;如果path是一个目录路径,则返回该目录中文件和目录名的列表。答案应该按字典顺序排列。void mkdir(String path):根据给定的路径创建一个新目录。给定的目录路径不存在。如果路径中的中间目录不存在,您也应该创建它们。void addContentToFile(String filePath, String content):如果filePath不存在,则创建包含给定内容content的文件;如果filePath已经存在,将给定的内容content附加到原始内容。String readContentFromFile(String filePath):返回filePath下的文件内容。
示例
- 输入 :"FileSystem","ls","mkdir","`addContentToFile`","ls","`readContentFromFile`"
\[\],\["/"\],\["/`a`/`b`/`c`"\],\["/`a`/`b`/`c`/`d`","hello"\],\["/"\],\["/`a`/`b`/`c`/`d`"\]
- 输出 : `null`,\[,
null,null,"`a`","hello"]
提示: - 所有路径的输入前后都带斜杠;
1 <= path.length <= 100 - 路径中至少包含一个目录名/文件名(形如
/a,不会是/) 1 <= content.length <= 50,ls返回值不超过 50 项- 测试用例保证
readContentFromFile/ls的路径不包含不存在项
解题思路
解法一(Path 拆分 + 字典树节点):每个节点表示一个路径分量,children 为有序 Map(TreeMap<String, Node> 天然字典序),content 字符串仅文件拥有(目录为 null)。
ls(path):按 / 拆路径沿 children 下钻,若终点是文件返回 [文件名],是目录返回 children.keySet() 列表。
mkdir(path):逐分量创建不存在的中间节点。addContentToFile:同样逐层建节点,最后把 content 追加到文件节点的 content。(主提交采用此解法。)
解法二(Map + 延迟展开):用 Map<String, Map<String, ...>> 或全局 Map<String, Node> 直接索引绝对路径,实现更简单但路径解析逻辑需自己处理。这里采用解法一为骨架,本质等价。
代码实现
java
import java.util.*;
class FileSystem {
private static class Node {
Map<String, Node> children = new TreeMap<>(); // 字典序
String content = null; // 文件内容,目录为 null
}
private Node root = new Node();
public FileSystem() {}
private List<String> split(String path) {
// "/a/b/c" -> ["a","b","c"]
List<String> parts = new ArrayList<>();
for (String p : path.split("/")) {
if (!p.isEmpty()) parts.add(p);
}
return parts;
}
public List<String> ls(String path) {
Node cur = root;
List<String> parts = split(path);
for (String p : parts) cur = cur.children.get(p);
if (cur.content != null) {
// 文件路径:返回仅含自身名字的列表
return Collections.singletonList(parts.get(parts.size() - 1));
}
return new ArrayList<>(cur.children.keySet());
}
public void mkdir(String path) {
Node cur = root;
for (String p : split(path)) {
cur = cur.children.computeIfAbsent(p, k -> new Node());
}
}
public void addContentToFile(String filePath, String content) {
Node cur = root;
List<String> parts = split(filePath);
for (int i = 0; i < parts.size() - 1; ++i) {
cur = cur.children.computeIfAbsent(parts.get(i), k -> new Node());
}
String name = parts.get(parts.size() - 1);
Node file = cur.children.computeIfAbsent(name, k -> new Node());
file.content = (file.content == null ? "" : file.content) + content;
}
public String readContentFromFile(String filePath) {
Node cur = root;
for (String p : split(filePath)) cur = cur.children.get(p);
return cur.content;
}
public static void main(String[] args) {
int pass = 0, total = 0;
FileSystem fs = new FileSystem();
++total; if (fs.ls("/").isEmpty()) ++pass; else System.out.println("FAIL test1");
fs.mkdir("/a/b/c");
fs.addContentToFile("/a/b/c/d", "hello");
List<String> ls = fs.ls("/");
++total; if (ls.size() == 1 && ls.get(0).equals("a")) ++pass; else System.out.println("FAIL test2: " + ls);
++total; if (fs.readContentFromFile("/a/b/c/d").equals("hello")) ++pass; else System.out.println("FAIL test3");
// 追加内容 + 文件路径 ls
fs.addContentToFile("/a/b/c/d", "world");
++total; if (fs.readContentFromFile("/a/b/c/d").equals("helloworld")) ++pass; else System.out.println("FAIL test4");
List<String> lsf = fs.ls("/a/b/c/d");
++total; if (lsf.size() == 1 && lsf.get(0).equals("d")) ++pass; else System.out.println("FAIL test5: " + lsf);
// 目录字典序:先建 z 再建 m
fs.mkdir("/a/z");
fs.mkdir("/a/m");
List<String> lsd = fs.ls("/a");
++total; if (lsd.equals(Arrays.asList("b", "m", "z"))) ++pass; else System.out.println("FAIL test6: " + lsd);
if (pass == total) System.out.println("== 0588 PASS =="); else { System.out.println("== 0588 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | ls/mkdir/add/read 均为 O(L)(L 为路径分量数) | O(节点数 × 路径长) |
| 解法二 | O(L) | O(节点数 × 路径长) |
589. N-ary Tree Preorder Traversal(N 叉树的前序遍历)
题目描述
给定一个 N 叉树,返回其节点值的前序遍历。
N 叉树在输入中按层序遍历进行序列化表示,每组子节点由空值分隔(请参见示例)。
示例
- 输入 :
root= 1,`null`,3,2,4,`null`,5,6 - 输出: 1,3,5,6,2,4
- 输入 :
root= 1,`null`,2,3,4,5,`null`,`null`,6,7,`null`,8,`null`,9,10,`null`,`null`,11,`null`,12,`null`,13,`null`,`null`,14 - 输出 : 1,2,3,6,7,11,14,4,8,12,5,9,13,10
提示: - 节点总数在范围
[0, 10^4]内。 0 <= Node.val <= 10^4- N 叉树的高度小于或等于 1000。
解题思路
解法一(递归 DFS):前序顺序为「节点自身 -> 依次所有子节点」。空树返回空列表。(主提交采用此解法。)
解法二(迭代 + 栈):栈后进先出,先压入根节点;每次弹出节点加入答案,再把其子节点逆序 压栈,保证最先访问第一个子节点。也可用 LinkedList 的 push/add 调整。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 递归
public List<Integer> preorder(Node root) {
List<Integer> ans = new ArrayList<>();
dfs(root, ans);
return ans;
}
private void dfs(Node node, List<Integer> ans) {
if (node == null) return;
ans.add(node.val);
if (node.children != null) {
for (Node c : node.children) dfs(c, ans);
}
}
// 解法二:迭代 + 栈(子节点逆序压栈)
public List<Integer> preorder2(Node root) {
List<Integer> ans = new ArrayList<>();
if (root == null) return ans;
Deque<Node> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
Node cur = stack.pop();
ans.add(cur.val);
if (cur.children != null) {
for (int i = cur.children.size() - 1; i >= 0; --i) {
stack.push(cur.children.get(i));
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
// [1,null,3,2,4,null,5,6]
Node n5 = new Node(5), n6 = new Node(6);
Node n3 = new Node(3, Arrays.asList(n5, n6));
Node n2 = new Node(2), n4 = new Node(4);
Node root1 = new Node(1, Arrays.asList(n3, n2, n4));
++total; if (sol.preorder(root1).equals(Arrays.asList(1, 3, 5, 6, 2, 4))) ++pass; else System.out.println("FAIL test1");
++total; if (sol.preorder(null).isEmpty()) ++pass; else System.out.println("FAIL test2");
// 解法二
++total; if (sol.preorder2(root1).equals(Arrays.asList(1, 3, 5, 6, 2, 4))) ++pass; else System.out.println("FAIL test3");
++total; if (sol.preorder2(null).isEmpty()) ++pass; else System.out.println("FAIL test4");
if (pass == total) System.out.println("== 0589 PASS =="); else { System.out.println("== 0589 FAIL =="); System.exit(1); }
}
}
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) { val = _val; }
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(h)(递归栈,h 为树高) |
| 解法二 | O(n) | O(n)(栈) |
590. N-ary Tree Postorder Traversal(N 叉树的后序遍历)
题目描述
给定一个 N 叉树,返回其节点值的后序遍历。
N 叉树在输入中按层序遍历进行序列化表示,每组子节点由空值分隔(请参见示例)。
示例
- 输入 :
root= 1,`null`,3,2,4,`null`,5,6 - 输出: 5,6,3,2,4,1
- 输入 :
root= 1,`null`,2,3,4,5,`null`,`null`,6,7,`null`,8,`null`,9,10,`null`,`null`,11,`null`,12,`null`,13,`null`,`null`,14 - 输出 : 2,6,14,11,7,3,12,8,4,13,9,10,5,1
提示: - 节点总数在范围
[0, 10^4]内。 0 <= Node.val <= 10^4- N 叉树的高度小于或等于 1000。
解题思路
解法一(递归 DFS):后序顺序为「依次所有子节点 -> 节点自身」。(主提交采用此解法。)
解法二(迭代:先序反转):先按「根 -> 子节点从左到右」的变体先序入栈(即普通先序的子节点正序压栈、弹出顺序反转),得到「根、最右子、...」的序列,最后整体反转即为后序。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 递归
public List<Integer> postorder(Node root) {
List<Integer> ans = new ArrayList<>();
dfs(root, ans);
return ans;
}
private void dfs(Node node, List<Integer> ans) {
if (node == null) return;
if (node.children != null) {
for (Node c : node.children) dfs(c, ans);
}
ans.add(node.val);
}
// 解法二:迭代(先序变体后反转)
public List<Integer> postorder2(Node root) {
List<Integer> ans = new ArrayList<>();
if (root == null) return ans;
Deque<Node> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
Node cur = stack.pop();
ans.add(cur.val);
if (cur.children != null) {
for (Node c : cur.children) stack.push(c); // 正序压栈
}
}
Collections.reverse(ans); // 反转 => 后序
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
// [1,null,3,2,4,null,5,6]
Node n5 = new Node(5), n6 = new Node(6);
Node n3 = new Node(3, Arrays.asList(n5, n6));
Node n2 = new Node(2), n4 = new Node(4);
Node root1 = new Node(1, Arrays.asList(n3, n2, n4));
++total; if (sol.postorder(root1).equals(Arrays.asList(5, 6, 3, 2, 4, 1))) ++pass; else System.out.println("FAIL test1");
++total; if (sol.postorder(null).isEmpty()) ++pass; else System.out.println("FAIL test2");
Node single = new Node(7);
++total; if (sol.postorder(single).equals(Arrays.asList(7))) ++pass; else System.out.println("FAIL test3");
// 解法二
++total; if (sol.postorder2(root1).equals(Arrays.asList(5, 6, 3, 2, 4, 1))) ++pass; else System.out.println("FAIL test4");
++total; if (sol.postorder2(single).equals(Arrays.asList(7))) ++pass; else System.out.println("FAIL test5");
if (pass == total) System.out.println("== 0590 PASS =="); else { System.out.println("== 0590 FAIL =="); System.exit(1); }
}
}
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) { val = _val; }
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(h)(递归栈) |
| 解法二 | O(n) | O(n)(栈 + 结果) |
591. Tag Validator(标签验证器)
题目描述
给定一个表示代码片段的字符串,你需要实现一个验证器来解析这段代码,并返回它是否合法。合法的代码片段需要遵守以下的所有规则:
- 代码必须被合法的闭合标签包围。否则,代码是无效的。
- 闭合标签(不一定合法)要严格符合格式:
<TAG_NAME>TAG_CONTENT</TAG_NAME>。起始和结束标签中的TAG_NAME应当相同。当且仅当TAG_NAME和TAG_CONTENT都是合法的,闭合标签才是合法的。 - 合法的
TAG_NAME仅含有大写字母,长度在范围[1, 9]之间。 - 合法的
TAG_CONTENT可以包含其他合法的闭合标签、cdata(规则 7)和任意字符,除了不匹配的<、不匹配的起始和结束标签、不匹配的或带有不合法TAG_NAME的闭合标签。 - 一个起始标签,如果没有具有相同
TAG_NAME的结束标签与之匹配,是不合法的。反之亦然。也需要考虑标签嵌套的问题。 - 一个
<,如果你找不到一个后续的>与之匹配,是不合法的。当你找到一个<或</时,所有直到下一个>前的字符,都应当被解析为TAG_NAME(不一定合法)。 - cdata 有如下格式:
<![CDATA[CDATA_CONTENT]]>,CDATA_CONTENT的范围被定义成<![CDATA[和后续的第一个]]>之间的字符。 CDATA_CONTENT可以包含任意字符,cdata 的功能是阻止验证器解析CDATA_CONTENT。
示例
- 输入 :code = "
This is thefirstline<!CDATA\[\<\`div\`\>]> " - 输出 :
true - 输入 :code = "
>> !cdata\[] <!CDATA\[\<\`div\`\>>]]>]]>>] " - 输出 :
true
提示: code.length <= 500- code 仅由数字、字母、
<、>、[、]、!、空格和\n组成。
解题思路
解法一(栈模拟 + 规则翻译):用栈存 TAG_NAME。顺序扫描:
- 遇到
<![CDATA[:必须至少已在某个标签内(栈非空),跳到]]>之后的字符继续;若找不到]]>或栈空则非法。 - 遇到
</:找>截出结束标签名,按规则 3 校验 TAG_NAME,与栈顶比较(须匹配),弹栈。 - 遇到
<:找>截出起始标签名,按规则 3 校验,压栈。 - 其他字符:普通内容。
细节:起始标签出现时栈为空表示最外层开始(此后内容必须完整);最终要求扫描完且栈空,且保证「代码被闭合标签包围」(即code必须以<开始、以>结束,且扫描过程中</>的匹配不越界)。(主提交采用此解法。)
解法二(正则化简):先剥掉所有 cdata 块和匹配的标签对进行递归下降,不再展开。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 栈模拟
public boolean isValid(String code) {
Deque<String> stack = new ArrayDeque<>();
int i = 0, n = code.length();
// 规则 6 隐含:首个字符必须是 '<',末尾字符必须来自闭合标签的 '>'
while (i < n) {
if (i > 0 && stack.isEmpty()) return false; // 顶层内容必须被标签包围
if (code.startsWith("<![CDATA[", i)) {
if (stack.isEmpty()) return false; // cdata 必须在标签内
int end = code.indexOf("]]>", i + 9);
if (end == -1) return false;
i = end + 3;
} else if (code.startsWith("</", i)) {
int end = code.indexOf('>', i + 2);
if (end == -1) return false;
String name = code.substring(i + 2, end);
if (!validName(name) || stack.isEmpty() || !stack.pop().equals(name)) return false;
i = end + 1;
} else if (code.charAt(i) == '<') {
int end = code.indexOf('>', i + 1);
if (end == -1) return false;
String name = code.substring(i + 1, end);
if (!validName(name)) return false;
stack.push(name);
i = end + 1;
} else {
++i;
}
}
return stack.isEmpty();
}
private boolean validName(String tag) {
if (tag.length() < 1 || tag.length() > 9) return false;
for (char c : tag.toCharArray()) {
if (c < 'A' || c > 'Z') return false;
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.isValid("<DIV>This is the first line <![CDATA[<div>]]></DIV>") == true) ++pass; else System.out.println("FAIL test1");
++total; if (sol.isValid("<DIV>>> ![cdata[]] <![CDATA[<div>]>]]>]]>>]</DIV>") == true) ++pass; else System.out.println("FAIL test2");
++total; if (sol.isValid("<A> <B> </A> </B>") == false) ++pass; else System.out.println("FAIL test3");
++total; if (sol.isValid("<DIV> unmatched tag </DIV>") == true) ++pass; else System.out.println("FAIL test4");
++total; if (sol.isValid("hello world") == false) ++pass; else System.out.println("FAIL test5");
++total; if (sol.isValid("<a><a></a></a>") == false) ++pass; else System.out.println("FAIL test6");
++total; if (sol.isValid("<A></A>") == true) ++pass; else System.out.println("FAIL test7");
if (pass == total) System.out.println("== 0591 PASS =="); else { System.out.println("== 0591 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n)(栈) |
| 解法二 | O(n)(指数级回溯时退化) | O(n) |
592. Fraction Addition and Subtraction(分数加减运算)
题目描述
给定一个表示分数加减运算的字符串 expression,你需要返回一个字符串形式的计算结果。
这个结果应该是不可约分的分数,即最简分数。如果最终结果是一个整数,例如 2,你需要将它转换成分数形式,其分母为 1。所以在上述例子中,2 应该被转换为 2/1。
示例
- 输入:expression = "-1/2+1/2"
- 输出: "0/1"
- 输入:expression = "-1/2+1/2+1/3"
- 输出: "1/3"
- 输入:expression = "1/3-1/2"
- 输出 : "-1/6"
提示: - 输入和输出字符串只包含
'0'到'9'的数字,以及'/'、'+'和'-'。 - 输入和输出分数格式均为
±分子/分母。如果输入的第一个分数或者输出的分数是正数,则'+'会被省略掉。 - 输入只包含合法的最简分数,每个分数的分子与分母的范围是
[1,10]。如果分母是 1,意味着这个分数实际上是一个整数。 - 输入的分数个数范围是
[1,10]。最终结果的分子与分母保证是 32 位整数范围内的有效整数。
解题思路
解法一(扫描解析 + 通分):逐个分数解析出符号、分子、分母。用当前累计分子 num 与累计分母 den,每读入一个分数 a/b 即 num = num*b ± a*den、den = den*b,最后对 num/den 约分(除以最大公约数),输出 "num/den"(num == 0 时输出 "0/1")。(主提交采用此解法。)
解法二(正则拆分):用 Pattern 按 ([+-]?\d+/\d+) 拆分出所有分数再逐个累加。解析更简洁,运算逻辑同解法一。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 扫描解析
public String fractionAddition(String expression) {
int num = 0, den = 1; // 累计分子/分母(保持通分状态)
int i = 0, n = expression.length();
while (i < n) {
int sign = 1;
if (expression.charAt(i) == '+') { sign = 1; ++i; }
else if (expression.charAt(i) == '-') { sign = -1; ++i; }
int a = 0;
while (i < n && Character.isDigit(expression.charAt(i))) {
a = a * 10 + (expression.charAt(i) - '0');
++i;
}
++i; // 跳过 '/'
int b = 0;
while (i < n && Character.isDigit(expression.charAt(i))) {
b = b * 10 + (expression.charAt(i) - '0');
++i;
}
// num/den ± a/b
num = num * b + sign * a * den;
den = den * b;
int g = gcd(Math.abs(num), den);
num /= g;
den /= g;
}
if (num == 0) return "0/1";
return num + "/" + den;
}
private int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
// 解法二:正则拆分(语义同上)
public String fractionAddition2(String expression) {
java.util.regex.Matcher m = java.util.regex.Pattern
.compile("([+-]?\\d+/\\d+)")
.matcher(expression);
long num = 0, den = 1;
while (m.find()) {
String[] parts = m.group(1).split("/");
long a = Long.parseLong(parts[0]);
long b = Long.parseLong(parts[1]);
num = num * b + a * den;
den = den * b;
long g = gcd((int) Math.abs(num), (int) den);
num /= g;
den /= g;
}
if (num == 0) return "0/1";
return num + "/" + den;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.fractionAddition("-1/2+1/2").equals("0/1")) ++pass; else System.out.println("FAIL test1: " + sol.fractionAddition("-1/2+1/2"));
++total; if (sol.fractionAddition("-1/2+1/2+1/3").equals("1/3")) ++pass; else System.out.println("FAIL test2");
++total; if (sol.fractionAddition("1/3-1/2").equals("-1/6")) ++pass; else System.out.println("FAIL test3");
++total; if (sol.fractionAddition("1/2+1/2").equals("1/1")) ++pass; else System.out.println("FAIL test4");
// 解法二
++total; if (sol.fractionAddition2("-1/2+1/2+1/3").equals("1/3")) ++pass; else System.out.println("FAIL test5");
++total; if (sol.fractionAddition2("1/3-1/2").equals("-1/6")) ++pass; else System.out.println("FAIL test6");
if (pass == total) System.out.println("== 0592 PASS =="); else { System.out.println("== 0592 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(n)(拆分数组) |
593. Valid Square(有效的正方形)
题目描述
给定 2D 空间中四个点的坐标 p1、p2、p3 和 p4,如果这四个点构成一个正方形,则返回 true。
点的坐标 pi 表示为 [xi, yi]。输入没有任何顺序。
一个有效的正方形有四条等边和四个等角(90 度角)。
示例
- 输入 :
p1= 0,0,p2= 1,1, p3 = 1,0, p4 = 0,1 - 输出 :
true - 输入 :
p1= 0,0,p2= 1,1, p3 = 1,0, p4 = 0,12 - 输出 :
false - 输入 :
p1= 1,0,p2= -1,0, p3 = 0,1, p4 = 0,-1 - 输出 :
true
提示: p1.length == p2.length == p3.length == p4.length == 2-10^4 <= xi, yi <= 10^4
解题思路
解法一(距离集合):正方形四点的 6 条两两距离中,恰好有 4 条相等(边长)且另外 2 条相等(对角线)。用集合收集 6 个距离平方:
若集合大小为 2,且较长距离是较短距离的 2 倍(边长² × 2 = 对角线²),且无任何点重合(无距离 0),即为正方形。(主提交采用此解法。)
解法二(三点验证):任选一个点作为锚点,从剩下三点中找出与锚点距离最远的点作为对角线端点(剩下两点是邻点),验证三条距离满足勾股定理(邻边相等且垂直),再对剩余三点递归验证。逻辑较繁琐,使用较少。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 距离集合
public boolean validSquare(int[] p1, int[] p2, int[] p3, int[] p4) {
long[] d = {
dist2(p1, p2), dist2(p1, p3), dist2(p1, p4),
dist2(p2, p3), dist2(p2, p4), dist2(p3, p4)
};
Set<Long> set = new HashSet<>();
for (long v : d) {
if (v == 0) return false; // 点重合
set.add(v);
}
if (set.size() != 2) return false;
// 集合两个值 a < b,需 b == 2*a(对角线² = 2 * 边长²)
long[] vals = new long[2];
int idx = 0;
for (long v : set) vals[idx++] = v;
long a = Math.min(vals[0], vals[1]);
long b = Math.max(vals[0], vals[1]);
return b == 2 * a;
}
private long dist2(int[] a, int[] b) {
long dx = a[0] - b[0], dy = a[1] - b[1];
return dx * dx + dy * dy;
}
// 解法二:三点勾股验证(锚点 p1,找最远点为对角点)
public boolean validSquare2(int[] p1, int[] p2, int[] p3, int[] p4) {
int[][] pts = {p1, p2, p3, p4};
// 以 p1 为锚,找到最远点
int far = 1;
long maxD = -1;
for (int i = 1; i < 4; ++i) {
long d = dist2(p1, pts[i]);
if (d > maxD) { maxD = d; far = i; }
}
int[] diag = pts[far];
int[] a1 = null, a2 = null;
for (int i = 1; i < 4; ++i) {
if (i == far) continue;
if (a1 == null) a1 = pts[i]; else a2 = pts[i];
}
// 邻边相等 + 勾股(垂直)
long s1 = dist2(p1, a1), s2 = dist2(p1, a2);
if (s1 != s2 || s1 == 0) return false;
if (s1 + s2 != maxD) return false;
// 验证剩下三点的顺序(a1, diag, a2)也构成直角
long d1 = dist2(a1, diag), d2 = dist2(diag, a2);
return d1 == s1 && d2 == s2;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.validSquare(new int[]{0,0}, new int[]{1,1}, new int[]{1,0}, new int[]{0,1}) == true) ++pass; else System.out.println("FAIL test1");
++total; if (sol.validSquare(new int[]{0,0}, new int[]{1,1}, new int[]{1,0}, new int[]{0,12}) == false) ++pass; else System.out.println("FAIL test2");
++total; if (sol.validSquare(new int[]{1,0}, new int[]{-1,0}, new int[]{0,1}, new int[]{0,-1}) == true) ++pass; else System.out.println("FAIL test3");
++total; if (sol.validSquare(new int[]{0,0}, new int[]{0,0}, new int[]{0,0}, new int[]{0,0}) == false) ++pass; else System.out.println("FAIL test4");
// 解法二
++total; if (sol.validSquare2(new int[]{0,0}, new int[]{1,1}, new int[]{1,0}, new int[]{0,1}) == true) ++pass; else System.out.println("FAIL test5");
++total; if (sol.validSquare2(new int[]{0,0}, new int[]{1,1}, new int[]{1,0}, new int[]{0,12}) == false) ++pass; else System.out.println("FAIL test6");
if (pass == total) System.out.println("== 0593 PASS =="); else { System.out.println("== 0593 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(1) | O(1) |
| 解法二 | O(1) | O(1) |
594. Longest Harmonious Subsequence(最长和谐子序列)
题目描述
和谐数组是指一个数组里元素的最大值和最小值之间的差别正好是 1。
给你一个整数数组 nums,请你在所有可能的子序列中找到最长的和谐子序列的长度。
数组的子序列是一个由数组派生出来的序列,它可以通过删除一些元素或不删除元素、且不改变其余元素的顺序而得到。
示例
- 输入 :
nums= 1,3,2,2,5,2,3,7 - 输出: 5
- 解释: 最长和谐子序列是 3,2,2,2,3。
- 输入 :
nums= 1,2,3,4 - 输出: 2
- 解释: 最长和谐子序列是 1,2、2,3 和 3,4,长度都为 2。
- 输入 :
nums= 1,1,1,1 - 输出: 0
- 解释 : 数组中没有和谐子序列。
提示: 1 <= nums.length <= 2 * 10^4-10^9 <= nums[i] <= 10^9
解题思路
解法一(哈希计数):统计每个数值的出现次数。对每个键 x,若 x + 1 也存在,则 cnt[x] + cnt[x+1] 是候选长度,取最大值。
子序列不要求连续,因此频数之和即为可构成长度。(主提交采用此解法。)
解法二(排序 + 双指针):排序后用滑动窗口统计「值为 x 与 x+1」的两段长度和。思路等价,适合展示单调性。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 哈希计数
public int findLHS(int[] nums) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int x : nums) cnt.put(x, cnt.getOrDefault(x, 0) + 1);
int ans = 0;
for (Map.Entry<Integer, Integer> e : cnt.entrySet()) {
int x = e.getKey();
if (cnt.containsKey(x + 1)) {
ans = Math.max(ans, e.getValue() + cnt.get(x + 1));
}
}
return ans;
}
// 解法二:排序 + 双指针(相邻值窗口)
public int findLHS2(int[] nums) {
Arrays.sort(nums);
int ans = 0;
// 窗口 [i, j] 内值只可能是 nums[i] 与 nums[i]+1
for (int i = 0, j = 0; j < nums.length; ++j) {
while (nums[j] - nums[i] > 1) ++i;
if (nums[j] - nums[i] == 1) {
ans = Math.max(ans, j - i + 1);
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.findLHS(new int[]{1,3,2,2,5,2,3,7}) == 5) ++pass; else System.out.println("FAIL test1");
++total; if (sol.findLHS(new int[]{1,2,3,4}) == 2) ++pass; else System.out.println("FAIL test2");
++total; if (sol.findLHS(new int[]{1,1,1,1}) == 0) ++pass; else System.out.println("FAIL test3");
++total; if (sol.findLHS(new int[]{1,2}) == 2) ++pass; else System.out.println("FAIL test4");
// 解法二
++total; if (sol.findLHS2(new int[]{1,3,2,2,5,2,3,7}) == 5) ++pass; else System.out.println("FAIL test5");
++total; if (sol.findLHS2(new int[]{1,1,1,1}) == 0) ++pass; else System.out.println("FAIL test6");
if (pass == total) System.out.println("== 0594 PASS =="); else { System.out.println("== 0594 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n log n)(排序) | O(1) |
595. Big Countries(大的国家)
题目类型:数据库(SQL)
题目描述
World 表:
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| name | varchar |
| continent | varchar |
| area | int |
| population | int |
| gdp | bigint |
+-------------+---------+
name 是该表的主键(具有唯一值的列)。这张表的每一行提供:国家名称、所属大陆、面积、人口和 GDP 值。
如果一个国家满足下述两个条件之一,则认为该国是大国:
- 面积至少为
300万平方公里(即3000000 km²),或者 - 人口至少为
2500万(即25000000)。
编写解决方案找出大国 的国家名称、人口和面积。按任意顺序返回结果表。
示例:
输入:
World 表:
+-------------+-----------+---------+------------+--------------+
| name | continent | area | population | gdp |
+-------------+-----------+---------+------------+--------------+
| Afghanistan | Asia | 652230 | 25500100 | 20343000000 |
| Albania | Europe | 28748 | 2831741 | 12960000000 |
| Algeria | Africa | 2381741 | 37100000 | 188681000000 |
| Andorra | Europe | 468 | 78115 | 3712000000 |
| Angola | Africa | 1246700 | 20609294 | 100990000000 |
+-------------+-----------+---------+------------+--------------+
输出:
+-------------+------------+---------+
| name | population | area |
+-------------+------------+---------+
| Afghanistan | 25500100 | 652230 |
| Algeria | 37100000 | 2381741 |
+-------------+------------+---------+
解题思路
解法一(WHERE + OR,推荐):大国定义为面积或人口达到阈值,一行过滤即可。WHERE area >= 3000000 OR population >= 25000000 选出符合任一条件的国家。两个谓词用或连接,避免漏掉只满足一侧的行。注意:面积和人口条件都满足的国家只输出一次。
解法二(UNION 合并两次查询):也可以把两个条件写成两次查询再用 UNION 合并,UNION 自动去重,不会出现重复行。当优化器对 OR 不走索引时,两次范围扫描再合并可能更稳,结果集合与方法一相同。
代码实现
sql
-- 解法一:WHERE + OR(推荐)
SELECT name, population, area
FROM World
WHERE area >= 3000000 OR population >= 25000000;
sql
-- 解法二:UNION 合并(自动去重)
SELECT name, population, area
FROM World
WHERE area >= 3000000
UNION
SELECT name, population, area
FROM World
WHERE population >= 25000000;
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n)(一次扫描,n 为 World 行数;有索引可加速) |
O(1) |
| 解法二 | O(n)(两次扫描 + 去重) | O(k)(k 为大国数量) |
596. Classes More Than 5 Students(超过 5 名学生的课)
题目类型:数据库(SQL)
题目描述
表:Courses
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| student | varchar |
| class | varchar |
+-------------+---------+
(student, class) 是该表的主键(不同值的列的组合)。该表的每一行表示学生的名字和他们注册的课程。
查询至少有 5 个学生 的所有课程。以任意顺序返回结果表。
示例 1:
输入:
Courses 表:
+---------+----------+
| student | class |
+---------+----------+
| A | Math |
| B | English |
| C | Math |
| D | Biology |
| E | Math |
| F | Computer |
| G | Math |
| H | Math |
| I | Math |
+---------+----------+
输出:
+---------+
| class |
+---------+
| Math |
+---------+
解释 :数学课有 6 个学生,所以我们包括它。英语课、生物课、计算机课各只有 1 名学生,所以我们不包括它们。
解题思路
解法一(GROUP BY + HAVING,推荐):班级至少五名学生,按课分组计数,用 HAVING 过滤即可,不必先做子查询。GROUP BY class 后 HAVING COUNT(1) >= 5,HAVING 作用在分组结果上,与 WHERE 过滤行的时机不同(WHERE 是在分组前过滤行)。
解法二(子查询 + WHERE):也可以先在子查询中按 class 分组计数得到各课人数表 T,外层再 WHERE t.cnt >= 5 过滤。写法上多一层嵌套,逻辑等价。
代码实现
sql
-- 解法一:GROUP BY + HAVING(推荐)
SELECT class
FROM Courses
GROUP BY 1
HAVING COUNT(1) >= 5;
sql
-- 解法二:子查询 + WHERE 过滤
SELECT class
FROM (
SELECT class, COUNT(1) AS cnt
FROM Courses
GROUP BY class
) AS T
WHERE cnt >= 5;
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n)(一次分组聚合,n 为 Courses 行数) |
O(k)(k 为课程数) |
| 解法二 | O(n) | O(k) |
597. Friend Requests I: Overall Acceptance Rate(好友申请 I:总体通过率)
题目类型:数据库(SQL)
题目描述
表:FriendRequest
+----------------+---------+
| Column Name | Type |
+----------------+---------+
| sender_id | int |
| send_to_id | int |
| request_date | date |
+----------------+---------+
该表可能包含重复项(换句话说,在 SQL 中,该表没有主键)。该表包含发送请求的用户的 ID、接受请求的用户的 ID 以及请求的日期。
表:RequestAccepted
+----------------+---------+
| Column Name | Type |
+----------------+---------+
| requester_id | int |
| accepter_id | int |
| accept_date | date |
+----------------+---------+
该表可能包含重复项(换句话说,在 SQL 中,该表没有主键)。该表包含发送请求的用户的 ID、接受请求的用户的 ID 以及请求通过的日期。
求出好友申请的通过率,用 2 位小数表示。通过率由接受好友申请的数目除以申请总数。
提示:
- 通过的好友申请不一定都在表
FriendRequest中。你只需要统计总的被通过的申请数(不管它们在不在表FriendRequest中),并将它除以申请总数,得到通过率。 - 一个好友申请发送者有可能会给接受者发几条好友申请,也有可能一个好友申请会被通过好几次。这种情况下,重复的好友申请只统计一次。
- 如果一个好友申请都没有,你应该返回
accept_rate为0.00。
示例 1:
输入:
FriendRequest 表:
+-----------+------------+--------------+
| sender_id | send_to_id | request_date |
+-----------+------------+--------------+
| 1 | 2 | 2016/06/01 |
| 1 | 3 | 2016/06/01 |
| 1 | 4 | 2016/06/01 |
| 2 | 3 | 2016/06/02 |
| 3 | 4 | 2016/06/09 |
+-----------+------------+--------------+
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 |
| 3 | 4 | 2016/06/10 |
+--------------+-------------+-------------+
输出:
+-------------+
| accept_rate |
+-------------+
| 0.8 |
+-------------+
解释 :总共有 5 个请求,有 4 个不同的通过请求,所以通过率是 0.80。
解题思路
解法一(分别去重计数后相除,推荐):接受率是「不同的接受对」除以「不同的请求对」,两表各用 COUNT(DISTINCT ...) 去重计数再相除。用 IFNULL(..., 0) 处理除数为 0(没有请求)的情况,再 ROUND(..., 2) 保留两位小数。注意重复的好友申请只统计一次,所以必须 DISTINCT 两列的组合。
解法二(内连接去重计数):先分别求出去重后的请求对集合与接受对集合,再通过 RIGHT JOIN(以请求为主体)计算匹配数,COUNT(accepter)/COUNT(requester) 得出比率。结果与方法一相同,但写法更绕,方法一更简洁。
代码实现
sql
-- 解法一:分别去重计数后相除(推荐)
SELECT
ROUND(
IFNULL(
(
SELECT COUNT(DISTINCT requester_id, accepter_id)
FROM RequestAccepted
) / (SELECT COUNT(DISTINCT sender_id, send_to_id) FROM FriendRequest),
0
),
2
) AS accept_rate;
sql
-- 解法二:去重集合 + 连接计数
SELECT ROUND(
COUNT(DISTINCT a.requester_id, a.accepter_id) /
COUNT(DISTINCT r.sender_id, r.send_to_id),
2
) AS accept_rate
FROM FriendRequest AS r
RIGHT JOIN RequestAccepted AS a ON
r.sender_id = a.requester_id AND r.send_to_id = a.accepter_id;
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + m)(两次去重计数,n 为 RequestAccepted 行数,m 为 FriendRequest 行数) |
O(n + m)(去重哈希) |
| 解法二 | O(n + m) | O(n + m) |
598. Range Addition II(区间加法 II)
题目描述
给你一个 m x n 的矩阵 M 和一个操作数组 op。矩阵初始化时所有的单元格都为 0。ops[i] = [ai, bi] 意味着当所有的 0 <= x < ai 和 0 <= y < bi 时,M[x][y] 应该加 1。
在执行完所有操作后,计算并返回矩阵中最大整数的个数。
示例
- 输入 :
m= 3,n= 3,ops= \[2,2,3,3] - 输出: 4
- 解释: M 中最大的整数是 2,而且 M 中有 4 个值为 2 的元素。因此返回 4。
- 输入 :
m= 3,n= 3,ops= \[\] - 输出 : 9
提示: 1 <= m, n <= 4 * 10^40 <= ops.length <= 10^4ops[i].length == 21 <= ai <= m,1 <= bi <= n
解题思路
解法一(交集求最小矩形):每次操作 [ai, bi] 都会给左上角 ai x bi 的区域加 1。所有操作叠加后,被加得最多的区域是所有操作覆盖区域的交集 ,即左上角 min(ai) x min(bi) 的矩形。
因此答案 = minRow * minCol(当 ops 为空时 = m * n)。(主提交采用此解法。)
解法二(模拟差分):对 m x n 网格做二维差分后前缀和求出最大值个数。正确但没必要,复杂度 O(m·n) 最坏 1.6e9 不可行,仅思路参考。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 交集求最小矩形
public int maxCount(int m, int n, int[][] ops) {
int minA = m, minB = n;
for (int[] op : ops) {
minA = Math.min(minA, op[0]);
minB = Math.min(minB, op[1]);
}
return minA * minB;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.maxCount(3, 3, new int[][]{{2,2},{3,3}}) == 4) ++pass; else System.out.println("FAIL test1");
++total; if (sol.maxCount(3, 3, new int[][]{}) == 9) ++pass; else System.out.println("FAIL test2");
++total; if (sol.maxCount(3, 3, new int[][]{{2,2},{3,3},{3,3},{3,3},{2,2},{3,3},{3,3},{3,3},{2,2},{3,3},{3,3},{3,3}}) == 4) ++pass; else System.out.println("FAIL test3");
++total; if (sol.maxCount(1, 1, new int[][]{{1,1}}) == 1) ++pass; else System.out.println("FAIL test4");
if (pass == total) System.out.println("== 0598 PASS =="); else { System.out.println("== 0598 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(k)(k 为 ops 长度) | O(1) |
| 解法二 | O(m·n + k) | O(m·n) |
599. Minimum Index Sum of Two Lists(两个列表的最小索引总和)
题目描述
给定两个字符串数组 list1 和 list2,找到索引和最小的公共字符串。
公共字符串是同时出现在 list1 和 list2 中的字符串。
具有最小索引和的公共字符串是指,如果它在 list1[i] 和 list2[j] 中出现,那么 i + j 应该是所有其他公共字符串中的最小值。
返回所有具有最小索引和的公共字符串。以任何顺序返回答案。
示例
- 输入:list1 = "Shogun", "Tapioca Express", "Burger King", "KFC",list2 = "Piatti", "The Grill at Torrey Pines", "Hungry Hunter Steakhouse", "Shogun"
- 输出: "Shogun"
- 输入:list1 = "happy","sad","good", list2 = "sad","happy","good"
- 输出: "sad","happy"
- 解释 : "happy" 索引和 = 0 + 1 = 1;"sad" 索引和 = 1 + 0 = 1;"good" 索引和 = 2 + 2 = 4。
提示: 1 <= list1.length, list2.length <= 10001 <= list1[i].length, list2[i].length <= 30list1[i]和list2[i]由空格' '和英文字母组成list1的所有字符串都是独一无二的,list2的所有字符串也都是独一无二的。
解题思路
解法一(哈希 + 一次线性扫描):把 list1 的每个字符串映射到其下标。遍历 list2,若当前字符串在 map 中,则 idxSum = i + map.get(s):
- 等于当前最小:加入结果集;
- 小于当前最小:清空结果集、更新最小并加入。
一趟 O(n + m)。(主提交采用此解法。)
解法二(暴力双循环):枚举所有(i, j)对,只统计两个字符串相等且i + j最小的组合,O(n·m)。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 哈希 + 单趟
public String[] findRestaurant(String[] list1, String[] list2) {
Map<String, Integer> idx = new HashMap<>();
for (int i = 0; i < list1.length; ++i) idx.put(list1[i], i);
List<String> ans = new ArrayList<>();
int minSum = Integer.MAX_VALUE;
for (int j = 0; j < list2.length; ++j) {
Integer i = idx.get(list2[j]);
if (i == null) continue;
int sum = i + j;
if (sum < minSum) {
ans.clear();
ans.add(list2[j]);
minSum = sum;
} else if (sum == minSum) {
ans.add(list2[j]);
}
}
return ans.toArray(new String[0]);
}
// 解法二:暴力双循环
public String[] findRestaurant2(String[] list1, String[] list2) {
List<String> ans = new ArrayList<>();
int minSum = Integer.MAX_VALUE;
for (int i = 0; i < list1.length; ++i) {
for (int j = 0; j < list2.length; ++j) {
if (list1[i].equals(list2[j])) {
int sum = i + j;
if (sum < minSum) {
ans.clear();
ans.add(list1[i]);
minSum = sum;
} else if (sum == minSum) {
ans.add(list1[i]);
}
}
}
}
return ans.toArray(new String[0]);
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
String[] l1 = {"Shogun", "Tapioca Express", "Burger King", "KFC"};
String[] l2 = {"Piatti", "The Grill at Torrey Pines", "Hungry Hunter Steakhouse", "Shogun"};
++total; if (Arrays.equals(sol.findRestaurant(l1, l2), new String[]{"Shogun"})) ++pass; else System.out.println("FAIL test1");
String[] l3 = {"happy", "sad", "good"};
String[] l4 = {"sad", "happy", "good"};
Set<String> r2 = new HashSet<>(Arrays.asList(sol.findRestaurant(l3, l4)));
++total; if (r2.equals(new HashSet<>(Arrays.asList("sad", "happy")))) ++pass; else System.out.println("FAIL test2: " + r2);
// 解法二
++total; if (Arrays.equals(sol.findRestaurant2(l1, l2), new String[]{"Shogun"})) ++pass; else System.out.println("FAIL test3");
Set<String> r4 = new HashSet<>(Arrays.asList(sol.findRestaurant2(l3, l4)));
++total; if (r4.equals(new HashSet<>(Arrays.asList("sad", "happy")))) ++pass; else System.out.println("FAIL test4");
if (pass == total) System.out.println("== 0599 PASS =="); else { System.out.println("== 0599 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + m) | O(n) |
| 解法二 | O(n·m) | O(1) |
600. Non-negative Integers without Consecutive Ones(不含连续1的非负整数)
题目描述
给定一个正整数 n,请你统计在 [0, n] 范围的非负整数中,有多少个整数的二进制表示中不存在连续的 1。
示例
- 输入 :
n= 5 - 输出: 5
- 解释 : 下面列出范围在 0, 5 的非负整数与其对应的二进制表示:
0: 0, 1: 1, 2: 10, 3: 11, 4: 100, 5: 101
其中,只有整数 3 违反规则(有两个连续的 1),其他 5 个满足规则。 - 输入 :
n= 1 - 输出: 2
- 输入 :
n= 2 - 输出 : 3
提示: 1 <= n <= 10^9
解题思路
解法一(数位 DP,记忆化搜索):把 n 转成二进制字符串 s,dfs(i, pre, limit) 表示从第 i 位开始、上一位数字为 pre(0 或 1)、是否受限 limit 的合法方案数。
当前位可填 0 或(pre == 0 && !limit 时)1,递归累加。用 memo[i][pre][limit] 记忆化。最后统计到 0 为止的所有方案数(n == 0 时的空串也是一种,注意边界)。(主提交采用此解法。)
解法二(组合计数,斐波那契转移):不依赖数位 DP 的状态机,直接数出二进制位长 L 下无连续 1 的个数满足斐波那契规律(f[0]=1, f[1]=2, f[2]=3, ...,f[i] = f[i-1] + f[i-2])。再顺着 n 的二进制位,遇到 11 提前停止(该位以后全部小于 n 直接计数),1 位出现时累加该位后面的方案数。O(L)。
代码实现
java
import java.util.*;
class Solution {
// 主提交:解法一 数位 DP(记忆化搜索)
public int findIntegers(int n) {
String s = Integer.toBinaryString(n);
int L = s.length();
int[][][] memo = new int[L][2][2];
for (int[][] a : memo) for (int[] b : a) Arrays.fill(b, -1);
return dfs(0, 0, 1, s, memo);
}
private int dfs(int i, int pre, int limit, String s, int[][][] memo) {
if (i == s.length()) return 1;
if (memo[i][pre][limit] != -1) return memo[i][pre][limit];
int up = limit == 1 ? (s.charAt(i) - '0') : 1;
int ans = 0;
for (int d = 0; d <= up; ++d) {
if (pre == 1 && d == 1) continue; // 连续 1 非法
ans += dfs(i + 1, d, limit == 1 && d == up ? 1 : 0, s, memo);
}
return memo[i][pre][limit] = ans;
}
// 解法二:组合计数(斐波那契)
public int findIntegers2(int n) {
int[] fib = new int[32]; // fib[i] 表示长度为 i 的 01 串中无连续 1 的个数(含全 0)
fib[0] = 1;
fib[1] = 2; // "0", "1"
for (int i = 2; i < 32; ++i) fib[i] = fib[i - 1] + fib[i - 2];
int ans = 0, preBit = 0;
// 从高位到低位扫描 n 的二进制
for (int i = 31; i >= 0; --i) {
if (((n >> i) & 1) == 1) {
// 该位填 0 时,低位任意(无连续 1 约束只受前面影响,但此处去掉该位为 1 的情况后,低 i 位合法数为 fib[i])
ans += fib[i];
if (preBit == 1) return ans; // 出现 "11",后续全取 0 位也超限,停止
preBit = 1;
} else {
preBit = 0;
}
}
return ans + 1; // 加上 n 自身(n 本身无连续 1 时)
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.findIntegers(5) == 5) ++pass; else System.out.println("FAIL test1");
++total; if (sol.findIntegers(1) == 2) ++pass; else System.out.println("FAIL test2");
++total; if (sol.findIntegers(2) == 3) ++pass; else System.out.println("FAIL test3");
++total; if (sol.findIntegers(10) == 8) ++pass; else System.out.println("FAIL test4");
++total; if (sol.findIntegers(1000000000) == 2178309) ++pass; else System.out.println("FAIL test5");
// 解法二
++total; if (sol.findIntegers2(5) == 5) ++pass; else System.out.println("FAIL test6");
++total; if (sol.findIntegers2(1) == 2) ++pass; else System.out.println("FAIL test7");
++total; if (sol.findIntegers2(10) == 8) ++pass; else System.out.println("FAIL test8");
++total; if (sol.findIntegers2(1000000000) == 2178309) ++pass; else System.out.println("FAIL test9");
if (pass == total) System.out.println("== 0600 PASS =="); else { System.out.println("== 0600 FAIL =="); System.exit(1); }
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(L)(L ≤ 31,记忆化状态数 O(2L·2)) | O(L) |
| 解法二 | O(L) | O(L)(斐波那契表,可省) |