LeetCode 197. 上升的温度
题目描述
题意分析
表
Weather每行是某一天的记录,包含唯一id、日期recordDate和当天温度temperature。要求找出所有「与前一天相比温度更高」的日期,输出这些行的id,顺序不限。「前一天」这三个字是全题的重心。表里只有一行一天的孤立记录,行与行之间没有任何指针把今天和昨天串起来,所以必须自己造出这种关联。关系代数里能把同一张表的两行摆到同一行上比较的手段只有一个:把表和它自己连接起来,用连接条件规定「左边那行的日期比右边那行晚一天」。
约束还透露了一个容易被忽略的信号:
recordDate只保证唯一,不保证连续。也就是说数据里完全可能出现 2015-01-01 之后直接跳到 2015-01-05 的情况。这意味着不能用「按日期排序后比较相邻行」这种朴素思路,必须真正按「日期差恰好为 1 天」去匹配,日期不连续的那一对根本不该配上。另一个信号是日期是
date类型而不是整数,所以「差一天」不能写成减法,得用数据库提供的日期差函数;同时也不能对日期做字符串比较后加一,跨月跨年会直接错。边界有三处:整张表只有一行时没有任何一对能配上,结果为空集,这是合法输出而不是异常;温度相等的两天不算上升,条件必须是严格大于;某一天没有前一天记录时,它自然不会出现在连接结果里,无需额外处理。
解法:SQL 查询建模
核心思路
把全表拉到应用层排序也能处理,但除了比较温度,还必须校验两行日期恰好相差一天;它增加了数据传输和应用代码,而这条跨行关系可以直接交给数据库完成。
关键观察是:「今天」和「昨天」是同一张表的两行。把
Weather起别名today、yesterday,一次自连接就把原本分散在两行里的日期和温度放到同一行中比较。查询不变量是:连接结果中的每一行都恰好代表一对日历日期相隔一天的记录,
today是较晚日期,yesterday是较早日期。因此温度条件比较的一定是今天与真正的前一天。把「差一天」放进
ON而不是WHERE,是因为它属于「哪两行该配对」的配对规则;把温度比较放进WHERE,是因为它属于「配好的对里保留哪些」的过滤规则。对INNER JOIN而言两者结果等价,但按语义分开写,读起来才是「先配对、再筛选」。日期配对写成
yesterday.recordDate = DATE_SUB(today.recordDate, INTERVAL 1 DAY):对每个今天精确计算日历上的前一天,再按日期等值连接。日期缺失时匹配不到,跨月跨年则由日期函数正确处理;同时被连接的yesterday.recordDate保持为裸列,存在索引时可以用于查找。
解题步骤
- 确定输出粒度:结果一行对应一个升温的
today,所以只选择today.id。- 让表自连接:
today表示待输出的今天,yesterday表示它的前一天。- 写配对条件:
yesterday.recordDate = DATE_SUB(today.recordDate, INTERVAL 1 DAY)。它匹配的是日历上恰好相差一天,而不是排序后相邻或 id 相邻。- 写过滤条件:
today.temperature > yesterday.temperature。严格大于才表示升温,持平不能输出。- 确认无需去重:
recordDate唯一,所以每个today最多匹配一个yesterday。- 确认无需排序:题目明确说明结果顺序任意,不加
ORDER BY可以省掉一次排序开销。以下面这组用例走一遍。表内四行:
(1, 2015-01-01, 10)、(2, 2015-01-02, 25)、(3, 2015-01-03, 20)、(4, 2015-01-04, 30)。自连接按日期条件留下三对:
today=2 / yesterday=1、today=3 / yesterday=2、today=4 / yesterday=3。id=1因为没有 2014-12-31 的记录,找不到前一天并自然被排除。再比较温度:25 > 10,输出
id=2;20 > 25 不成立;30 > 20,输出id=4。最终结果为
{2, 4}。再换一组带缺口的数据验证配对规则:
(1, 2015-01-01, 10)、(2, 2015-01-05, 40)。两行日期相差 4 天,ON条件不成立,连接结果为空,最终输出空集——尽管温度确实从 10 涨到了 40,但它们不是相邻两天,正确地没有被计入。
代码实现
-- today 与日历上的前一天等值连接,再比较温度。
SELECT today.id
FROM Weather AS today
JOIN Weather AS yesterday
ON yesterday.recordDate = DATE_SUB(today.recordDate, INTERVAL 1 DAY)
WHERE today.temperature > yesterday.temperature;
复杂度分析
- 时间复杂度:有
recordDate索引时,每个今天可做一次索引等值查找,约为 $O(n \log n)$;没有可用索引时,执行计划最坏可能退化为 $O(n^2)$ 扫描。- 空间复杂度:查询无需分组、排序或窗口缓冲,流式执行时额外空间为 $O(1)$;具体仍取决于连接计划。
关键点总结
- 只要题目要求「本行与另一行比较」,第一反应就该是自连接:SQL 无法在单行视角看到别的行,把表和自己连起来是把跨行关系拍平成同行关系的通用手段。
- 配对条件放
ON、筛选条件放WHERE,即便在INNER JOIN下结果相同,也要按语义分开——换成LEFT JOIN时两者不再等价,养成习惯能避免以后踩坑。- 日期运算一律走日期函数,不要退化成整数加减或字符串比较,跨月跨年是这类写法的必然反例。
- 「相邻」在数据里未必是「连续」,不能假设日期没有缺口,这是本题拒绝「排序后比较相邻行」思路的根本原因。
- 将函数放在已知的
today.recordDate一侧、让被查找的yesterday.recordDate保持裸列,能给索引等值查找留下机会。
易错点总结
- 把日期当整数写
today.recordDate = yesterday.recordDate + 1:2015-01-31到2015-02-01的跨月边界不能靠数值加一表达,应使用DATE_SUB或DATE_ADD。- 日期方向写反:若写成
today.recordDate = DATE_SUB(yesterday.recordDate, INTERVAL 1 DAY),today实际成了较早的一天,温度比较也随之反向。- 把比较写成
>=:用例(1, 01-01, 20)、(2, 01-02, 20),温度持平不算上升,>=会多输出id=2。- 输出
yesterday.id而不是today.id:(1,01-01,10)、(2,01-02,25)会返回 1,正确答案是升温当天的 id 2。- 误用
LEFT JOIN且把温度条件也写进ON:没有前一天或没有升温的today仍会被保留,若外层不再过滤就会输出不合格日期。- 靠
ORDER BY recordDate后比较相邻行(如用LAG但不校验日期差):用例(1, 01-01, 10)、(2, 01-05, 40),排序后id=2的上一行是id=1,LAG认为温度从 10 升到 40 而输出id=2,但这两天相隔 4 天,正确答案是空集。- 给两个别名起了相同的名字或干脆不起别名:
FROM Weather, Weather会报「表名重复」,而只写一个Weather则无法引用两行,WHERE temperature > temperature恒为假返回空集。- 按
id相差 1 配对:id 只保证唯一,不代表日期连续;id=1的 01-01 与id=2的 01-05 不能比较为相邻两天。- 漏掉日期配对条件:查询会在所有日期的笛卡尔积上比较温度,既产生重复 id,也会比较相隔多天的记录。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 181. 超过经理收入的员工 | 简单 | 同样是自连接,但配对条件来自显式外键 managerId,不需要构造日期关系 |
| 182. 查找重复的电子邮箱 | 简单 | 跨行关系用 GROUP BY + HAVING COUNT(*) > 1 表达,比自连接更省一次扫描 |
| 196. 删除重复的电子邮箱 | 简单 | 自连接的结果不是查询而是 DELETE,要处理「保留 id 最小那行」的不对称条件 |
| 180. 连续出现的数字 | 中等 | 需要三表自连接凑出连续三行,考察把「相邻」推广到「连续 k 个」 |
| 176. 第二高的薪水 | 中等 | 重点在无结果时必须返回 NULL 而非空集,考察子查询与 LIMIT OFFSET
|
| 178. 分数排名 | 中等 | 并列不跳号的排名,考察 DENSE_RANK 与自连接计数两种写法的取舍 |
| 184. 部门工资最高的员工 | 中等 | 分组内取最大值且允许并列,考察相关子查询与 IN (SELECT ...) 的组合 |