LeetCode 362. 敲击计数器
题目描述
题意分析
hit(timestamp)记录一次敲击,getHits(timestamp)返回最近五分钟的敲击总数。查询时刻为t时,只保留满足t - 300 < timestamp <= t的记录;所有调用的时间戳单调不减,同一秒允许有多次敲击。
解法:队列压缩计数
核心思路
[!blue]
时间只会向前推进,旧记录是否过期只由它的时间戳决定,适合用按时间排列的队列维护。过期记录一定集中在队首,而且一旦过期就不会重新有效,可以永久移除。
如果每次敲击都存一条记录,同一秒的密集请求会占用很多空间。它们会在同一时刻过期,可以合并成
(时间戳, 次数):时间与队尾相同就增加队尾次数,否则追加一组。时间戳单调不减,保证需要合并的组只能位于队尾。另外维护
total,始终等于队列中各组次数之和。写入时加一,移除过期组时扣掉整组次数。查询只需先清理,再返回total,不必重新遍历队列求和。两种操作都先清理,避免长时间只有写入、没有查询时积累历史组。清理条件是
当前时间 - 队首时间 >= 300,并且要循环移除,直到队首有效或队列为空;此时后面的时间更新,也都处于有效窗口内。
解题步骤
- 用空队列和
total = 0初始化计数器。- 每次调用先移除全部相差至少 300 秒的队首组,并同步减少
total。- 对
hit,合并同秒队尾或追加新组,再令total加一。- 对
getHits,直接返回清理后的total;队列为空时自然返回 0。
代码实现
// hit 时若与队尾时间戳相同则累加,否则新入队。
class HitCounter {
private final Deque<int[]> queue = new ArrayDeque<>();
private int total = 0;
public void hit(int timestamp) {
// 写入和查询都先清理窗口之外的时间组
evictExpired(timestamp);
if (!queue.isEmpty() && queue.peekLast()[0] == timestamp) {
queue.peekLast()[1]++;
} else {
queue.addLast(new int[] {
timestamp,
1
});
}
total++;
}
public int getHits(int timestamp) {
// 写入和查询都先清理窗口之外的时间组
evictExpired(timestamp);
return total;
}
private void evictExpired(int timestamp) {
while (!queue.isEmpty() && timestamp - queue.peekFirst()[0] >= 300) {
// 相差三百秒已过期,扣掉整组次数再丢弃该组
total -= queue.pollFirst()[1];
}
}
}
// hit 时若与队尾时间戳相同则累加,否则新入队。
type HitCounter struct {
times []hitPair
total int
}
type hitPair struct {
time int
count int
}
func Constructor() HitCounter {
return HitCounter{}
}
func (h *HitCounter) Hit(timestamp int) {
// 写入和查询都先清理窗口之外的时间组
h.evictExpired(timestamp)
if len(h.times) > 0 && h.times[len(h.times)-1].time == timestamp {
h.times[len(h.times)-1].count++
} else {
h.times = append(h.times, hitPair{time: timestamp, count: 1})
}
h.total++
}
func (h *HitCounter) GetHits(timestamp int) int {
// 写入和查询都先清理窗口之外的时间组
h.evictExpired(timestamp)
return h.total
}
func (h *HitCounter) evictExpired(timestamp int) {
for len(h.times) > 0 && timestamp-h.times[0].time >= 300 {
// 相差三百秒已过期,扣掉整组次数再丢弃该组
h.total -= h.times[0].count
h.times = h.times[1:]
}
}
复杂度分析
- 时间复杂度:一次调用最坏清理 $O(W)$ 个组,
W = 300为窗口秒数。每个时间组只入队、出队一次,所以整个调用序列中,每次操作均摊 $O(1)$。- 空间复杂度:$O(W)$,整数秒窗口最多三百个时间组。
关键点总结
[!green]
- 同一秒次数与组数量分开,密集敲击不增加相同时间组。
- 队列按时间有序,只需从队首清理,就能保留完整有效窗口。
- 队列变化时同步维护
total,查询无需再次求和。- 写入也清理,空间才只与窗口秒数有关。
易错点总结
[!yellow]
- 过期条件用严格大于三百,会多保留边界敲击。
- 只删队首一次,可能残留其他过期组。
- 移除队列组却不扣总数,会返回过大的累计值。
- 不能用队列长度当答案,一组可能代表同一秒的多次敲击。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 933. 最近的请求次数 | 简单 | 同样删除时间窗口外的请求,原题时间戳严格递增且窗口为3000毫秒,本题按秒统计命中。 |
| 346. 数据流中的移动平均值 | 简单 | 同样维护最近窗口,本题窗口按时间限定,移动平均题按元素个数限定。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!