刚开始刷 LeetCode 的读者看到 981 这题,多半会把它归到“设计题”一类:题面很短,意思也好懂,实现一个 TimeMap,支持 set(key, value, timestamp) 和 get(key, timestamp),其中 get 要返回该 key 在给定时间戳或之前最近一次写入的 value。很多人看完第一反应是“HashMap 套一个按时间排序的数组不就行了”,这个方向没有错,但把它真正落到 Java 代码上,不同写法的时间和内存差距非常明显。尤其当你把“内存表现出色”作为目标时,这题反而是我做过的最适合用来理解 Java 对象内存布局的题目之一。
我最初提交的版本和大多数人一样,十分钟 AC 就划走了。后来再刷到它,发现朴素实现虽然能过,但每条记录背后藏着一堆看不见的包装对象,数据量一大内存就会很难看。这篇文章没有铺垫,直接从“先 AC”开始,到“把内存压下去”结束,把二分边界和 Java 存储结构都讲透,适合想夯实设计题底层逻辑、或者正准备面试时被问到“还能怎么优化”的读者。
1. 题目在考什么:不是“会不会排序”,是“会不会做版本化读取”
1.1 如果系统要回看历史快照,你会怎么设计
抛开 LeetCode 题目的包装,981 描述的场景其实非常常见:某个 key 会在不同时间点被写入不同值,而你查询时希望拿到“在某个时间点之前最后一次生效的值”。
典型业务例子就是配置中心。一份配置被修改多次,每次改动都会留下一个时间戳,线上实例收到“配置变更通知”时,可能因为网络原因晚到一会儿,这时候它查到的不应该是最新的,而是“消息里携带的时间戳那一刻应该是什么配置”。另一个常见例子是价格快照:商品价格在 9 点改成 100 元、10 点改成 120 元,用户 9:30 加购,结算时要按 9:30 的价格计算,而不是查询当下 12 点的价格。
这类需求本质上是带版本的数据读取。版本不一定是自增数字,用时间戳作为版本号更自然,也更好对账。所以 981 不是一道“背个数据结构就能过”的题,它是在考察你能不能抽象出“按 key 索引 + 按时间追加 + 按时间查询”的模型。
1.2 原题里最容易忽略的两个约束条件
很多解法写得不对,不是因为代码写得差,而是漏看了题面约束。
第一个约束:同一个 key
