LeetCode 1344. 时钟指针的夹角
题目描述
题意分析
给定
hour和minutes两个整数,表示一个 12 小时制时钟上的时刻,要返回时针与分针之间较小的那个夹角,单位是度,允许 $10^{-5}$ 的误差。题目要求「较小的夹角」,这是第一个必须抓住的点。两根指针把整个表盘分成两段弧,度数之和恒为 360°,所以答案永远落在
[0°, 180°]区间内,超过 180° 的那一段必须换成它的补角。第二个必须抓住的点藏在物理常识里:时针不是每小时跳一格,而是随着分钟连续移动的。3 点半时时针并不指着 3,而是在 3 和 4 的正中间。把时针当成整点位置来算是这道题最主要的错误来源,也正是它被定为「中等」而不是「简单」的唯一原因。
约束里
1 <= hour <= 12、0 <= minutes <= 59。注意hour的上界是 12 而不是 11,所以会出现hour = 12的输入,它在角度上等价于 0 点,需要折算。输入规模是常数,没有任何数据结构或遍历的余地,这是一道纯粹的公式推导题——「算法」全部体现在把物理运动翻译成算式的过程里。
边界要留意四点:
hour = 12要折成 0;分针可能反超时针,两个角度谁大谁小不确定,所以要取绝对值;夹角可能恰好是 0°(12:00)或 180°(6:00),都是合法答案;返回值是浮点数,因为 0.5°/分钟的时针速度会产生半度的小数。
解法:角度计算
核心思路
先把表盘建成一个统一的坐标系:以 12 点方向为 0°,顺时针为正方向,一整圈是 360°。这样每根指针在任一时刻都对应一个
[0°, 360°)的绝对角度,两者相减就是夹角——这一步「统一基准」是把物理问题代数化的关键,没有它就只能凭直觉数格子。分针的角度最好算。它走完一圈 60 分钟对应 360°,所以速度是
360 / 60 = 6度每分钟,且不受小时数影响。于是分针角度是6 × minutes。时针要拆成两部分。整点部分:走完一圈 12 小时对应 360°,所以每小时
360 / 12 = 30度,贡献30 × (hour mod 12)。连续部分:时针在一小时内均匀走完这 30°,而这一小时有 60 分钟,所以速度是30 / 60 = 0.5度每分钟,贡献0.5 × minutes。合起来时针角度是30 × (hour mod 12) + 0.5 × minutes。
hour mod 12把输入归一化到表盘的0..11:12 点与 0 点方向相同,12 mod 12 = 0。即使多出的一整圈在本题最后也可能被补角消掉,先归一化仍能保证两根指针的绝对角都落在同一标准区间,推导和中间量更清楚。两个角度都确定后,令
diff = |时针角度 - 分针角度|。取绝对值是必要的:谁在前谁在后随时刻变化,比如 3:00 时时针 90°、分针 0°,而 3:30 时时针 105°、分针 180°,正负号会翻转。此时
diff落在[0°, 360°)内,它是两根指针沿某一方向的夹角。另一个方向的夹角是360 - diff,题目要较小的那个,所以答案是min(diff, 360 - diff)。这一步的不变量是:两段弧之和恒为 360°,取较小者必然落在[0°, 180°]。正确性来自两个事实:匀速运动公式精确给出了两根指针相对 12 点方向的角度;圆上连接这两个方向的两段弧长度分别是
diff与360 - diff。因此取二者较小值,恰好就是题目要求的夹角。直接用浮点数最自然,因为时针每分钟移动 0.5°,奇数分钟会产生半度。也可以把所有角度先放大 2 倍用整数计算、最后除以 2.0,但本题没有精度累积,
double/float64更简洁。
解题步骤
- 先算分针角度
6.0 * minutes。 系数 6 来自「一圈 360° ÷ 60 分钟」。这里写成6.0是为了让角度类型一眼可见;由于乘积本身是整数,写6 * minutes后再赋给浮点变量也不会损失精度。- 再算时针角度
30.0 * (hour % 12) + 0.5 * minutes。 前项系数 30 来自「一圈 360° ÷ 12 小时」,后项系数 0.5 来自「每小时 30° ÷ 60 分钟」。这个加法项是全题的核心:它表达了时针的连续移动,漏掉它会让 3:30 这类时刻算出完全错误的结果。- 对小时数取模 12。
hour的取值范围含 12,而表盘上 12 点就是 0 点。取模后hour % 12落在[0, 11],与「30° 一格」的编号一一对应。- 计算
diff = |hourAngle - minuteAngle|。 取绝对值是因为两根指针的先后关系不固定,直接相减会出现负值,导致后面的min比较把360 - diff这个大于 360 的数错当成候选。- 返回
min(diff, 360.0 - diff)。 这一步把「沿某方向的夹角」修正为「较小夹角」。判据很直观:diff超过 180° 时,360 - diff必然小于 180°,反之则diff本身更小;恰好 180° 时两者相等,取谁都对。- 用
double/float64保存角度。0.5 * minutes可能产生半度;若改用整数,就必须统一保存两倍角度,不能在中途做整数除法。以
hour = 3、minutes = 30走一遍(正确答案是 75.0)。分针角度:
6.0 × 30 = 180.0,分针指向表盘正下方的 6 点位置,符合直觉。时针角度:整点部分
30.0 × (3 % 12) = 90.0,连续部分0.5 × 30 = 15.0,合计105.0。105° 落在 3 点(90°)与 4 点(120°)的正中间,正是「三点半时针在 3 和 4 中间」的直观描述。这一步如果漏掉连续部分,时针会停在 90°。
diff = |105.0 - 180.0| = 75.0。这里时针角度小于分针角度,减出来是负的 -75,绝对值把它扳正——这正是必须取绝对值的实例。
min(75.0, 360.0 - 75.0) = min(75.0, 285.0) = 75.0。返回 75.0,与预期一致。若漏掉时针的连续移动,会得到|90 - 180| = 90,偏差整整 15°。再看三个边界用例。
hour = 12、minutes = 0时两角都是 0°,答案为 0;hour = 12、minutes = 30时两角分别为 15° 和 180°,答案为 165°,说明 12 必须按表盘位置归一化为 0。
hour = 3、minutes = 15:分针6 × 15 = 90°,时针90 + 0.5 × 15 = 97.5°,diff = 7.5,返回 7.5。这个.5就是必须用浮点数的直接证据——用int会得到 7 甚至 0。
hour = 6、minutes = 0:分针 0°,时针 180°,diff = 180,min(180, 180) = 180,两针正对,返回 180.0,是答案的上界。
代码实现
class Solution {
public double angleClock(int hour, int minutes) {
// 分针每分钟走 360 / 60 = 6 度。
double minuteAngle = 6.0 * minutes;
// 时针每小时走 30 度,且在一小时内以 0.5 度每分钟连续推进。
double hourAngle = 30.0 * (hour % 12) + 0.5 * minutes;
// 谁在前不确定,取绝对值。
double diff = Math.abs(hourAngle - minuteAngle);
// 两段弧之和为 360,取较小的那段。
return Math.min(diff, 360.0 - diff);
}
}
import "math"
func angleClock(hour int, minutes int) float64 {
minuteAngle := 6.0 * float64(minutes)
hourAngle := 30.0*float64(hour%12) + 0.5*float64(minutes)
diff := math.Abs(hourAngle - minuteAngle)
return math.Min(diff, 360.0-diff)
}
复杂度分析
- 时间复杂度:$O(1)$,全程只有固定的几次乘法、加法、取模、取绝对值和一次比较,运算次数与输入数值大小无关,没有任何循环或递归。
- 空间复杂度:$O(1)$,只用了
minuteAngle、hourAngle、diff三个浮点标量,不随输入变化。
关键点总结
- 把物理运动拆成「匀速 × 时间」再求和:时针角度 = 整点贡献 + 分钟贡献,两项分别由 30°/小时 和 0.5°/分钟 两个速率导出。凡是多个部件按不同速率联动的题(钟表、追及、传送带),都可以套这个「各自定速率、统一定基准」的框架。
- 圆周上的夹角要取补角修正:任意两个方向把圆分成两段,和为 360°,「较小夹角」就是
min(d, 360 - d)。这条在几何题、方向题里反复出现,记住比现推快。- 周期量先取模归位:
hour % 12把 12 点折回 0 点,使两个绝对角都处于同一圈内,补角公式的前提清晰可见。- 半度不能被整数除法截断:直接用浮点数最简洁;若面试官要求避免浮点中间量,就统一计算两倍角度,最后除以 2.0。
- 面试视角:这题代码只有四行,面试官真正在看的是你会不会主动指出「时针是连续移动的」。开口第一句就该说「三点半的时针在 3 和 4 中间,所以时针角度要加上
0.5 × minutes」,然后再补「答案取较小夹角,所以最后要和补角比一次」「hour = 12要取模归零」。这三句说完基本就满分了。反过来,如果上来就写公式而不解释 0.5 的来历,很容易被追问到卡壳。
易错点总结
- 错误写法:时针角度只写
30 * (hour % 12),漏掉0.5 * minutes→ 用例hour = 3, minutes = 30会算出|90 - 180| = 90,而正确答案是 75,偏差 15°;所有非整点时刻都会出错。- 错误写法:不取绝对值,直接用
hourAngle - minuteAngle→ 用例hour = 3, minutes = 30得到 -75,随后min(-75, 435)返回 -75,出现负角度。- 错误写法:省掉
min(diff, 360 - diff),直接返回diff→ 用例hour = 12, minutes = 30中diff = |15 - 180| = 165恰好正确,但用例hour = 1, minutes = 57中diff = |58.5 - 342| = 283.5,超过 180°,正确答案应是 76.5。- 错误写法:全程使用
int类型,写成30 * (hour % 12) + minutes / 2→ 用例hour = 3, minutes = 15中15 / 2整除为 7,时针角度算成 97 而非 97.5,返回 7 而正确答案是 7.5,超出 $10^{-5}$ 的误差容忍。- 错误写法:分针角度写成整数表达式
minutes / 60 * 360→minutes < 60时前半段整除恒为 0,分针被错误地固定在 12 点方向。- 错误写法:时针分钟贡献用整数表达式
minutes * 30 / 60→ 用例minutes = 15得到 7 而不是 7.5,最终夹角误差为 0.5°;应使用0.5 * minutes或两倍角度。- 错误写法:把时针速度记成 0.5 度每小时或 6 度每分钟 → 用例
hour = 3, minutes = 30中若时针用 6 度每分钟,角度变成90 + 180 = 270,diff = 90,答案错得离谱;速率一定要从「30° 一格 ÷ 60 分钟」现推。- 错误写法:认为答案一定小于 180 而写成
diff % 180→ 用例hour = 6, minutes = 0中diff = 180,取模后变成 0,而正确答案就是 180。- 错误写法:把「较小夹角」理解成「顺时针从时针到分针」的角 → 用例
hour = 3, minutes = 0中顺时针从时针 90° 到分针 0° 是 270°,而正确答案是 90,方向性的定义在本题里是多余且有害的。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 539. 最小时间差 | 中等 | 同样是环形量取最小差,排序后还要额外比较首尾跨越午夜的一对 |
| LCR 035. 最小时间差 | 中等 | 与 539 同题,适合再练一遍「化为分钟数 + 环形首尾」的套路 |
| 949. 给定数字能组成的最大时间 | 中等 | 时钟主题但转为枚举全排列并校验合法性,考的是边界判定而非公式 |
| 1360. 日期之间隔几天 | 简单 | 时间换算题,闰年规则是主要坑点,与本题的「12 点折 0 点」同属归一化思路 |
| 1041. 困于环中的机器人 | 中等 | 方向用 90° 的模 4 周期表示,靠一轮后是否回到原点或转向判断是否有界 |
| 874. 模拟行走机器人 | 中等 | 方向数组配合模 4 转向,是把「角度」离散化成四个方向的典型简化 |
| 7. 整数反转 | 中等 | 纯算术推导,核心在溢出判断而非公式本身 |
| 9. 回文数 | 简单 | 只用整数运算反转一半数字,考的是避免转字符串的数学技巧 |
| 172. 阶乘后的零 | 中等 | 把「数零的个数」推导成统计因子 5 的个数,同样是把现象翻译成算式 |
| 258. 各位相加 | 简单 | 数根公式 1 + (n-1) % 9,与本题一样能把过程压成一行常数运算 |
| 326. 3 的幂 | 简单 | 可用取模循环,也可用「最大 3 的幂能否整除」的常数解,体现数学化的收益 |
| 593. 有效的正方形 | 中等 | 平面几何判定,靠边长集合的多重集比较避开浮点误差 |
| 1071. 字符串的最大公因子 | 简单 | 把字符串周期性问题化归为长度的最大公约数,同属「找到隐藏的数学量」 |
| 412. Fizz Buzz | 简单 | 周期性判断的入门题,与本题的模运算归位思路一脉相承 |