最近在开发者广场上刷到一个帖子:某大厂程序员吐槽自己人生很失败——赚了一千多万买房,结果房价下跌赔了本;孩子成绩全班倒数;媳妇整天抱怨,家里就像开了个7×24小时的报警器。

我觉得这哥们不是失败,而是把压力打包成了全家共享套餐。房子这事真像线上发布,遇到回滚谁也挡不住;孩子成绩更像版本迭代,得慢慢调参;至于媳妇抱怨,先把“倾听功能”打开,比解释更有用。你说人到中年,最怕的不是跌一跤,是跌了还要强装没事。
算法题:最近时刻
那天加完班回家都快凌晨一点了,刚躺下,产品在群里给我来一句:“东哥,你那个最近时刻的算法是不是有问题?用户说早上九点和晚上九点,差了12个小时,不是最近吧?”我整个人一个激灵,这不对啊,按理说应该是0点那种跨天才容易算错,怎么九点还能玩出花来。
先说一下题目:给你一堆「HH:MM」格式的时间点,让你计算这些时间点两两之间的最小时间差,注意是“钟表上的时间”,会绕一圈。比如 ["23:59","00:00"],肉眼一看就知道最近差1分钟,不是1439分钟。
我当时写第一版的时候,想得特别简单粗暴:两个for循环,全都减一遍,绝对值取最小,完事。那会儿还沾沾自喜,觉得多朴素的写法,多有教学意义。结果线上一跑,直接超时,典型的“自己感动自己”。这种“默认写法会坑你”的体验,我在SpringBoot默认配置里踩过一大堆,惨案就不展开说了。
后来我冷静了一下,想了想菜市场买菜的场景:阿姨不会一根一根地数菜叶子,她会先拿秤,统一按斤算。时间也一样,别老盯着“09:30”、“23:59”这几个字符看,直接全换成分钟就行了——0点是0,00:01是1,01:00是60,以此类推,23:59就是23*60+59=1439。
所以第一步一定是写个小工具函数,把"HH:MM"转成分钟。那会儿我困得眼睛都睁不开了,但这个小函数还是得写完整,不然第二天肯定自己看不懂自己在干嘛。代码大概这样:
def to_minutes(t: str) -> int:
# t 形如 "HH:MM"
h, m = t.split(":")
return int(h) * 60 + int(m)
这个地方其实就已经有坑了,我第一次写的时候还很中二,拆完之后忘了 int(),结果拿字符串去乘60,Python也不报错,直接给我字符串重复……然后我还奇怪结果怎么这么长一串,print了一屏幕。那一刻我就意识到:半夜写代码真的容易变成Bug生成器。
分钟有了之后就简单了,把所有时间转成分钟,然后排个序。为什么要排序?因为你要找最近的两个点嘛,直觉上肯定是“相邻的更可能近”,就像考试排队报分数,排名相邻的两个同学成绩最接近,不会跨着十几个人再去对比。
于是主函数我就这么写了一版:
from typing import List
def find_min_difference(time_points: List[str]) -> int:
# 1. 全部转成分钟
minutes = [to_minutes(t) for t in time_points]
minutes.sort()
# 2. 相邻的差值
ans = 24 * 60 # 最大就这么多
for i in range(1, len(minutes)):
diff = minutes[i] - minutes[i - 1]
if diff < ans:
ans = diff
# 3. 别忘了最后一个和第一个跨天那一对
wrap_diff = 24 * 60 - minutes[-1] + minutes[0]
ans = min(ans, wrap_diff)
return ans
写完我自己在本地随手测了几个:
["23:59", "00:00"] → 1
["01:00", "01:10", "03:00"] → 10 都没问题,看起来人模狗样的。
真正的问题出在产品跟我说的那个“早九晚九”的场景。当时测试给了一组奇怪的数据:["09:00", "21:00", "09:00"]。你们看出来了吗?这个列表里有两个一模一样的时间点。我之前的写法,算完相邻差值是12小时,然后跨天那一对也是12小时,答案就一直是720分钟,完全不知道有“0分钟”这种更优解。
后来一拍脑袋:哎哟,24小时总共就1440分钟,你时间点数量如果超过1440个,那肯定至少有两个一模一样的,鸽笼原理知道吧,这就是数学老师在远方默默地对你竖起大拇指的那种场景。
于是我又给函数加了一点小心机:
def find_min_difference(time_points: List[str]) -> int:
# 如果时间点数量超过1440,必然有重复,直接返回0
if len(time_points) > 24 * 60:
return 0
minutes = sorted(to_minutes(t) for t in time_points)
ans = 24 * 60
for i in range(1, len(minutes)):
diff = minutes[i] - minutes[i - 1]
if diff == 0:
# 提前结束,已经不可能更小了
return 0
if diff < ans:
ans = diff
wrap_diff = 24 * 60 - minutes[-1] + minutes[0]
return min(ans, wrap_diff)
你看,逻辑其实一点都不高级:
- 先把时间拉直成0~1439的一条线;
- 排序之后,只看相邻的就够了;
- 最后别忘了把这条线首尾连成一个圈,算一下“最后一个到第一个”的距离;
- 如果时间点特别多或者出现重复,那最近时刻就是0,直接溜。
这个题最容易翻车的地方,其实就两个:第一是忘了“首尾跨天那一下”,第二是没考虑重复时间。前者会被23:59/00:00教做人,后者会被那种“列表里复制了一堆一样时间点”的测试数据拿捏。
我那天把这个版本发上去之后,测试又造了一堆妖魔鬼怪的用例,全过了。手机一扔,人往床上一躺,脑子里只有一个念头:以后再有人跟我说“这个题很简单”的时候,我都要先打个问号。
行了,最近时刻就先聊到这。如果你有类似的代码故事或想分享技术心得,欢迎来云栈社区交流。等会儿我去给咖啡续个杯,脑子又开始打转了,想起昨天有人问我“时间复杂度到底算不算挂科预警”这个问题,回头有空再吐槽。