LeetCode 570. 至少有5名直接下属的经理
题目描述
题意分析
Employee表里每行是一名员工,含id、name、department、managerId四个字段,其中managerId指向这名员工的直接上级的id。要求找出至少有 5 名直接下属的经理,输出他们的姓名。这张表是典型的自引用结构:上下级关系没有单独的关系表,而是靠同一张表里的
managerId指回id来表达。因此「某人有几名直接下属」不能在他自己那一行读到,必须反过来数有多少行的managerId等于他的id。这个视角切换是整道题的核心。「直接」两个字要读准:只统计
managerId直接指向自己的那些人,不做任何层级递归。若题目要的是「所有下属(含下属的下属)」,就必须用递归CTE了,那是完全不同的一道题。输出粒度也要先钉死:结果是每位符合条件的经理一行、只含
name。这意味着最终必须回到Employee表按id取名字——因为统计出来的只是managerId这个数字,姓名不在统计结果里。边界与约束:顶层老板的
managerId是NULL,这些行既不属于任何经理的下属计数,也不该被误算成「有一个名为 NULL 的经理」;id是主键因此唯一,回连时不会出现一对多放大;「至少 5 名」是闭区间,条件写>= 5而不是> 5;数据里可能根本没有满足条件的经理,此时返回空结果集是正确行为。
解法:分组统计 + 连接查询
核心思路
把
Employee自连接成两种角色:manager是经理,report是员工。连接条件report.managerId = manager.id让结果中的每一行恰好表示一条直接汇报关系。按
manager.id, manager.name分组后,每组包含该经理的全部直接下属,因此COUNT(*)就是直接下属人数。HAVING COUNT(*) >= 5在聚合后保留满足门槛的经理。循环关系的不变量是:连接后的每行只贡献给它直接指向的那位经理,不会沿层级继续传播;分组后的每行对应唯一经理,计数等于其直接下属数。于是筛选留下且只留下至少五名直接下属的经理。
顶层员工的
managerId为NULL,等值连接不会把它匹配成任何人的下属,无需额外处理。按 id 和 name 一起分组,也满足 MySQLONLY_FULL_GROUP_BY对输出列的要求。
解题步骤
- 把
Employee分别命名为manager与report,按report.managerId = manager.id自连接。- 按
manager.id, manager.name分组;同名经理仍由不同 id 分开,选出的name在严格分组模式下也合法。- 用
HAVING COUNT(*) >= 5筛选。聚合结果必须放在HAVING,且恰好五人也要保留。- 最终只输出
manager.name;分组已经保证每位经理只有一行,不需要DISTINCT。走一遍一份具体数据。设
Employee为:(101, John, A, NULL)、(102, Dan, A, 101)、(103, James, A, 101)、(104, Amy, A, 101)、(105, Anne, A, 101)、(106, Ron, B, 101)。自连接后,五名员工都与
id = 101的 John 配成一行;John 自己的managerId为NULL,不会作为report匹配。按 John 分组得到COUNT(*) = 5,恰好通过门槛并输出John。若删掉 Ron,只剩四条直接汇报关系,
HAVING会过滤 John。若某位下属自己还管理其他人,那些更下一层的行只计入该下属自己的分组,不会递归计给 John。
代码实现
SELECT manager.name
FROM Employee AS manager
JOIN Employee AS report
ON report.managerId = manager.id
GROUP BY manager.id, manager.name
HAVING COUNT(*) >= 5;
复杂度分析
- 时间复杂度:哈希连接与哈希聚合下期望为 $O(n)$;若执行器采用排序分组,则为 $O(n \log n)$。实际代价由索引和执行计划决定。
- 空间复杂度:最坏 $O(n)$,用于连接或聚合状态。
关键点总结
- 自引用表的第一反应是换视角:某个实体的「下属数 / 引用数」永远要从指向它的那些行去数,而不是从它自己那一行读。看清这一点,
GROUP BY managerId就是自然而然的写法。ON负责定义哪两行构成直接汇报关系,HAVING负责按分组后的COUNT(*)过滤;聚合值不能提前写进WHERE。- 自连接后的一行就是一条直接汇报关系;按经理主键分组,计数才与「直接下属人数」一一对应。
- 分组键同时写
manager.id, manager.name:id 区分同名经理,name 满足严格分组模式的输出要求。- 「至少 / 至多 / 超过」要逐字对应到
>=/<=/>。这类差一错误在 SQL 里不会报错,只会静默返回错误结果。- 本题只沿一条
managerId边连接一次,因此不会把间接下属计入。
易错点总结
- 把
COUNT(*) >= 5写进WHERE:WHERE COUNT(*) >= 5→ 直接语法错误,因为WHERE在分组之前求值,此时聚合值尚不存在。HAVING COUNT(*) > 5:某经理恰好有 5 名直接下属 → 被漏掉,「至少 5 名」被误解成「超过 5 名」。- 按
report.id分组:员工 id 是主键,每组只有一行,COUNT(*)永远达不到 5。- 连接方向写成
manager.managerId = report.id:角色完全反转,统计到的是经理的上级而非经理的下属。- 只按
manager.name分组:两个同名经理会被合并,人数可能相加后错误过线。- 误以为要统计所有层级的下属:某经理直接下属只有 2 人、但下属的下属共 6 人 → 被错误地算作符合条件,而题目只看「直接」下属。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 181. 超过经理收入的员工 | 简单 | 同为自引用表,但用自连接逐行比对薪水,不涉及分组聚合 |
| 182. 查找重复的电子邮箱 | 简单 |
GROUP BY + HAVING COUNT(*) > 1 的最小形态,无需回表翻译字段 |
| 184. 部门工资最高的员工 | 中等 | 聚合结果是组内最大值而非行数,回连时要匹配「部门 + 薪水」两个键 |
| 185. 部门工资前三高的所有员工 | 困难 | 组内取前 N 名,需要窗口函数 DENSE_RANK() 而不是简单的 HAVING 过滤 |
| 183. 从不订购的客户 | 简单 | 求的是「没有关联行」的实体,用 LEFT JOIN ... IS NULL 或 NOT IN
|
| 175. 组合两个表 | 简单 | 考的是必须用 LEFT JOIN 保留无地址的员工,与本题的内连接语义正相反 |
| 197. 上升的温度 | 简单 | 同为单表自连接,但连接条件是日期相差一天,考日期函数的用法 |
| 1376. 通知所有员工所需的时间 | 中等 | 同一份上下级结构的算法版,要沿 manager 链向上或向下递归求最长路径 |