TL;DR
Sliding-window queue (deque) β O(1) amortized per ping, O(W) space for pings inside the window.
Approach 1 β Brute force: keep everything, rescan every time
This is a design problem with no classic βbrute forceβ beyond the naive design: store every ping in a list and, on each ping, count how many recorded times fall inside the window.
from typing import List
class RecentCounter:
def __init__(self) -> None:
self.times: List[int] = []
def ping(self, t: int) -> int:
self.times.append(t)
lo = t - 3000
return sum(1 for x in self.times if x >= lo)
Complexity: O(n) per ping (n = pings so far), O(n) space β O(nΒ²) total over all calls.
With 10^4 pings thatβs ~10^8 element checks in the worst case; it may scrape by, but every ping re-examines ancient history that can never be relevant again.
Approach 2 β Binary search over the sorted history
The insight: ping times arrive strictly increasing, so self.times is already sorted β the windowβs contents are a suffix of the list. Find where t - 3000 would insert with binary search; everything from there to the end is in the window. (Binary search: repeatedly halve a sorted range to locate a boundary in O(log n).)
import bisect
from typing import List
class RecentCounter:
def __init__(self) -> None:
self.times: List[int] = []
def ping(self, t: int) -> int:
self.times.append(t)
i = bisect.bisect_left(self.times, t - 3000)
return len(self.times) - i
Walkthrough on ping(1), ping(100), ping(3001), ping(3002):
ping(1): times = [1]; boundary for β2999 is index 0 β 1 β 0 = 1.
ping(100): times = [1, 100]; boundary for β2900 is index 0 β 2 β 0 = 2.
ping(3001): times = [1, 100, 3001]; boundary for 1 is index 0 (1 β₯ 1 stays in) β 3 β 0 = 3.
ping(3002): times = [1, 100, 3001, 3002]; boundary for 2 is index 1 (the ping at 1 ages out) β 4 β 1 = 3.
Returns 1, 2, 3, 3 β matches the expected output.
Complexity: O(log n) per ping, but space stays O(n) β the dead prefix is never reclaimed.
Approach 3 β Sliding-window queue (deque)
The insight: because times only increase, once a ping falls outside a window it is outside every future window β it can be discarded forever. Keep only in-window pings in a FIFO queue: append the new time, evict expired times from the front, and the queueβs length is the answer.
from collections import deque
class RecentCounter:
def __init__(self) -> None:
self.window: deque[int] = deque()
def ping(self, t: int) -> int:
self.window.append(t)
lo = t - 3000
while self.window and self.window[0] < lo:
self.window.popleft()
return len(self.window)
Walkthrough on ping(1), ping(100), ping(3001), ping(3002):
ping(1): window = [1]; lo = β2999, nothing evicted β 1.
ping(100): window = [1, 100]; lo = β2900, nothing evicted β 2.
ping(3001): window = [1, 100, 3001]; lo = 1, front is 1 which is β₯ 1, kept β 3.
ping(3002): window = [1, 100, 3001, 3002]; lo = 2, front 1 < 2 β evict it; new front 100 β₯ 2 stays β length 3 β 3.
Complexity: each ping is appended once and popped at most once over the counterβs whole lifetime, so O(1) amortized per ping. Space is O(W) β only the pings inside the current 3000 ms window (at most all 10^4 in pathological inputs, but typically far fewer).
Common pitfalls
- The window is inclusive on both ends: evict while the front is
< t - 3000, not <= β a ping at exactly t - 3000 still counts.
- Forgetting to count the current ping (it must be appended before you measure, or add 1 afterwards).
- Using
list.pop(0) instead of deque.popleft() β popping the front of a Python list shifts every element and quietly reintroduces O(n) per eviction.
- Reaching for binary search first: itβs a fine answer, but it never frees expired entries, which matters in a long-running counter.
Pattern takeaway
When events arrive in increasing order and queries ask about a trailing window, expired data is expired forever β so a FIFO queue that evicts from the front maintains exactly the live set, with each element paying O(1) amortized for its one entry and one exit. This βmonotonic time β sliding-window dequeβ move recurs in rate limiters, moving averages, and every sliding-window problem in this bank.