LeetCode 1661. 每台机器的进程平均运行时间
题目描述
题意分析
题目目标:Activity 表用
(machine_id, process_id, activity_type, timestamp)描述事件,每个流程恰好有一条start和一条end。要求输出每台机器上「所有流程耗时」的平均值,保留 3 位小数。
核心约束:一个流程的耗时不是一行数据,而是同一
(machine_id, process_id)下两行数据的差值;也就是说输入的粒度是「事件」,而答案的粒度是「机器」,中间还夹着一层「流程」。凡是输入粒度和输出粒度不一致的题,都要先想清楚中间那层怎么合并掉。
边界处理:题目保证每个流程都成对出现,所以不必考虑「只有 start 没有 end」的残缺数据;也保证每台机器至少跑过一个流程,因此分母不会为 0。真正要盯住的是小数位数:结果必须是 3 位小数,而不是数据库默认的精度。
实现取舍:把两行合成一行有两条路——自连接(把 start 行和 end 行按键配对)或条件聚合(用 CASE WHEN 把符号翻转后直接求和)。两者结果一致,前者更直白,后者只扫一遍表。
解法:哈希表配对 start/end(代码视角)
核心思路
最朴素的想法是「先按机器分组,再在组内按流程分组,最后在流程里找 end 减 start」——三层嵌套,SQL 写起来会变成子查询套子查询,可读性很差。
瓶颈在于「流程」这一层被当成了一个真实的分组层级。观察一下:每个流程只有两行,且这两行的角色由
activity_type唯一确定,所以完全不需要真的分组,只要把它们并排放到同一行上,耗时就退化成一次减法。
于是不变量是:自连接之后的结果集里,每一行恰好代表一个完整流程,且该行同时持有它的开始时刻
s.timestamp与结束时刻e.timestamp。连接键是(machine_id, process_id)这个二元组,它是流程的唯一标识。有了这条不变量,外层就只剩一层GROUP BY machine_id的普通平均。
换成代码视角,等价的状态定义是:哈希表
start[(machine_id, process_id)] = timestamp,扫到end事件时用timestamp - start[key]得到该流程耗时,累加进total[machine_id]并让cnt[machine_id]自增,最后逐机器输出total / cnt。SQL 的自连接就是这张哈希表的声明式写法。
解题步骤
第一步:把 Activity 表当成两张表看待。 别名
s只取activity_type = 'start'的行,别名e只取activity_type = 'end'的行。为什么要起别名:自连接的本质是同一张表扮演两个角色,不起别名数据库无法区分你说的是哪一侧。
第二步:用
machine_id和process_id同时作为连接条件。 只连machine_id会让机器上的所有 start 和所有 end 交叉相乘,得到 $k^2$ 行垃圾数据;只连process_id会让不同机器上同编号的流程串到一起。两个字段缺一不可,因为流程的唯一标识是这个二元组。
第三步:把
activity_type的过滤写进ON子句。 写在ON里意味着「配对时就只认这类行」,配对完成后结果集天然干净;写在WHERE里在内连接下等价,但一旦将来改成外连接语义就会变,所以养成写在ON里的习惯。
第四步:
e.timestamp - s.timestamp求单个流程耗时,AVG求机器平均。 此时每行就是一个流程,AVG的分母自动等于该机器的流程数,不需要再手写COUNT。
第五步:
ROUND(..., 3)卡住输出精度。 题目明确要求 3 位小数,浮点相减会产生0.8080000000000001这类尾巴,不ROUND会被判错。
以
Activity = [[0,0,start,0.712], [0,0,end,1.520], [0,1,start,3.140], [0,1,end,4.120], [1,0,start,0.550], [1,0,end,1.550], [1,1,start,0.430], [1,1,end,1.420]]走一遍:先看自连接的中间结果。s侧有 4 行(0-0、0-1、1-0、1-1 的 start),e侧同样 4 行。按(machine_id, process_id)配对,每个 s 行只能命中唯一一个 e 行,于是中间结果恰好 4 行:(0, 0, 0.712, 1.520)、(0, 1, 3.140, 4.120)、(1, 0, 0.550, 1.550)、(1, 1, 0.430, 1.420)。注意这里验证了不变量:4 个流程 → 4 行,没有膨胀也没有丢失。
接着算差值列:0.808、0.980、1.000、0.990。再按
s.machine_id分组:机器 0 拿到 {0.808, 0.980},平均(0.808 + 0.980) / 2 = 0.894;机器 1 拿到 {1.000, 0.990},平均(1.000 + 0.990) / 2 = 0.995。最后ROUND到 3 位,输出[[0, 0.894], [1, 0.995]],与期望一致。
代码实现
SELECT
s.machine_id,
ROUND(AVG(e.timestamp - s.timestamp), 3) AS processing_time
FROM Activity s
JOIN Activity e
ON e.machine_id = s.machine_id
AND e.process_id = s.process_id
AND s.activity_type = 'start'
AND e.activity_type = 'end'
GROUP BY s.machine_id;
复杂度分析
- 时间复杂度:$O(n)$。凭什么:
(machine_id, process_id)是流程的唯一键,等值连接可以走哈希连接,构建哈希表与探测各扫一遍表,分组聚合同样是一次哈希聚合,全程没有嵌套循环,$n$ 为 Activity 的行数。- 空间复杂度:$O(n)$。凭什么:哈希连接需要把一侧(约 $n/2$ 行的 start 事件)全部装进哈希表,分组聚合再额外维护机器数量级的累加器,机器数不超过 $n$。
关键点总结
- 输入粒度和输出粒度不一致时,先补上中间那层。 本题输入是事件、输出是机器、中间是流程,认出这三层之后写法自然收敛为「先把流程拼成一行,再按机器聚合」。
- 自连接的连接键必须是中间实体的完整主键。 少写一个字段就会产生笛卡尔膨胀,这类 bug 不会报错,只会悄悄把答案改大。
- 过滤条件优先写进
ON。 内连接下与WHERE等价,但语义更贴近「配对规则」,改成外连接时也不会翻车。AVG天然带分母,别手写除法。 只要保证「一行 = 一个被平均的对象」,AVG就不会数错个数。- 输出精度是题目要求的一部分。 浮点减法的尾差是判错重灾区,
ROUND要写在聚合外层而不是差值里面,否则先舍入再平均会引入额外误差。- 面试视角:面试官问这题时,真正想听的是「你能不能把行转列」。先说清楚「一个流程分散在两行,我要把它压成一行」,再给自连接写法,最后主动补一句「也可以用
SUM(CASE WHEN activity_type='end' THEN timestamp ELSE -timestamp END) / COUNT(DISTINCT process_id)单次扫表完成」,这套「先讲不变量再讲两种实现」的答法比直接甩 SQL 分高得多。
易错点总结
- 错误写法:
ON e.machine_id = s.machine_id而漏掉process_id→ 用例machine 0有 2 个流程时,2 个 start 会分别匹配 2 个 end,中间结果从 2 行膨胀到 4 行,多出1.520-3.140 = -1.62这样的负耗时,机器 0 的平均值直接变成负数。- 错误写法:
ON e.process_id = s.process_id而漏掉machine_id→ 机器 0 的流程 0 会和机器 1 的流程 0 配对,算出1.550 - 0.712这种跨机器耗时,两台机器的结果全部污染。- 错误写法:只在
ON里写s.activity_type = 'start',忘了限制e.activity_type = 'end'→ e 侧同时包含 start 和 end 行,每个流程配出 2 行,其中一行差值恒为 0,机器 0 的平均值被拉低成(0.808 + 0 + 0.980 + 0) / 4 = 0.447。- 错误写法:
s.timestamp - e.timestamp把减号方向写反 → 输出-0.894,符号错误,题目期望正数耗时。- 错误写法:省略
ROUND(..., 3)→ 机器 0 输出0.8940000000000001,与期望的0.894不匹配被判错。- 错误写法:
ROUND(e.timestamp - s.timestamp, 3)写在AVG里面 → 先对每个流程舍入再平均,当耗时是0.8085这类值时,累积误差会让末位与期望差 1。- 错误写法:
GROUP BY s.machine_id, s.process_id→ 输出粒度变成流程而不是机器,结果行数从 2 行变成 4 行。- 错误写法:
SELECT machine_id不加s.前缀 → 自连接两侧都有同名列,数据库直接报Column 'machine_id' in field list is ambiguous。- 错误写法:结果列不起别名或起成
avg_time→ 判题按列名processing_time校验,列名不符直接失败。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 175. 组合两个表 | 简单 | 用左连接保留无匹配行,而本题的成对数据保证内连接不会丢行 |
| 182. 查找重复的电子邮箱 | 简单 | 分组后用 HAVING 过滤组,本题分组后直接聚合不需要过滤 |
| 197. 上升的温度 | 简单 | 同样是自连接,但连接键是「日期相差 1 天」的函数条件而非等值主键 |
| 570. 至少有5名直接下属的经理 | 中等 | 自连接的两侧是上下级两种角色,聚合的是计数而非平均 |
| 1280. 学生们参加各科测试的次数 | 简单 | 需要先用交叉连接补全不存在的组合,本题所有组合都真实存在 |