领导,请停止对我的侵犯!
我在网上看到一个帖子,标题就很有味儿:“领导,请停止对我的侵犯。”先别紧张,这里说的不是啥悬疑剧情,说白了,就是晚上10点突然被拉去开复盘会。白天开不够,非得挑人脑子快关机的时候狠狠干一票,属实有点离谱。
发帖那位网友讲得很清楚,他不是不想复盘,也不是摆烂,更不是对工作没责任心。他只是想留一点活人该有的时间。白天忙成陀螺,晚上刚准备洗澡、吃口热饭,结果手机一震:来,开会。那一刻,别说复盘了,魂都快盘没了。
说实话,我特别能共情。工作当然要认真,但认真不等于24小时待命。成年人上班是为了生活,不是把生活整个打包献祭给工位。真想把事做好,白天把节奏理顺,比半夜抓人开会强太多了。
面试题:敲击计数器
面试里碰到“敲击计数器”这题,我一般不会一上来就讲队列、环形数组这些名词,先把题意掰正:系统不断收到一次次敲击事件,给你一个时间戳 timestamp,表示这一秒发生了一次 hit;然后随时会问一句,最近 300 秒一共敲了多少次。
这题看着像计数,实际考的是“窗口”。
最容易想到的写法,就是来一次敲击就记一次时间。查询的时候,把 300 秒之外的数据都清掉,剩下多少条就是多少次。
先看一个直观版本,代码不复杂,但很好讲清楚思路:
import java.util.ArrayDeque;
import java.util.Deque;
classHitCounter{
privatefinal Deque<Integer> queue = new ArrayDeque<>();
publicvoidhit(int timestamp){
queue.addLast(timestamp);
clearExpired(timestamp);
}
publicintgetHits(int timestamp){
clearExpired(timestamp);
return queue.size();
}
privatevoidclearExpired(int timestamp){
while (!queue.isEmpty() && timestamp - queue.peekFirst() >= 300) {
queue.pollFirst();
}
}
}
这个版本很适合先把题做出来。queue 里存的是每一次敲击的时间,窗口往前滑的时候,把过期数据从队头弹掉。 如果面试官没继续追问,到这里其实已经够了。
但这题通常还会补一刀:如果某一秒内有很多次敲击怎么办?比如同一秒打进来几万次,你还一条条往队列里塞,空间就不太好看了。
这时候就该把“按事件存”换成“按秒聚合存”。
classHitCounter{
privatefinalint[] times = newint[300];
privatefinalint[] hits = newint[300];
publicvoidhit(int timestamp){
int idx = timestamp % 300;
if (times[idx] != timestamp) {
times[idx] = timestamp;
hits[idx] = 1;
} else {
hits[idx]++;
}
}
publicintgetHits(int timestamp){
int sum = 0;
for (int i = 0; i < 300; i++) {
if (timestamp - times[i] < 300) {
sum += hits[i];
}
}
return sum;
}
}
这里真正关键的是这两行:
int idx = timestamp % 300;
if (times[idx] != timestamp)
意思很直接:数组只保留最近 300 秒的位置,同一个位置会被 300 秒之后的新时间覆盖。 但覆盖之前要先确认,这个槽位里是不是当前这一秒的数据,不是的话就重置,是的话直接累加。
这个写法有点像线上做限流、滑动窗口统计时常见的桶计数。 优点也很明显:
hit()是 O(1)getHits()固定扫 300 个桶,也是 O(1)空间稳定,不会因为敲击次数暴涨而膨胀
这题不难,难点主要在于你是不是能从“记录每次点击”自然过渡到“记录每秒汇总”。前者是把题做对,后者是把题做得像线上可用。