933. Number of Recent Calls
Problem Description
You need to implement a RecentCounter
class that tracks the number of recent requests within a sliding time window of 3000 milliseconds.
The class should support two operations:
-
Constructor
RecentCounter()
: Creates a new counter that starts with zero requests recorded. -
Method
ping(int t)
: Records a new request at timet
(in milliseconds) and returns how many requests occurred in the last 3000 milliseconds, including the current request. More specifically, it counts all requests that happened in the time interval[t - 3000, t]
.
Key constraints:
- Each call to
ping
will have a strictly increasing value oft
(times are always in ascending order) - The time window is inclusive on both ends
Example walkthrough:
- If you call
ping(1)
, it records a request at time 1 and returns 1 (only this request is in range[1-3000, 1]
=[-2999, 1]
) - If you then call
ping(100)
, it records a request at time 100 and returns 2 (both requests at times 1 and 100 are in range[100-3000, 100]
=[-2900, 100]
) - If you call
ping(3001)
, it records a request at time 3001 and returns 3 (requests at 1, 100, and 3001 are all in range[3001-3000, 3001]
=[1, 3001]
) - If you call
ping(3002)
, it records a request at time 3002 and returns 3 (requests at 100, 3001, and 3002 are in range[3002-3000, 3002]
=[2, 3002]
; the request at time 1 is now outside the window)
The solution uses a queue (deque) to efficiently maintain only the requests within the current time window. When a new request comes in, it's added to the queue, and any requests older than 3000 milliseconds are removed from the front of the queue. The number of remaining elements in the queue represents the count of recent requests.
Intuition
The key insight is recognizing that we need to maintain a sliding window of requests that occurred within the last 3000 milliseconds. Since time values are guaranteed to be strictly increasing with each ping
call, we can think of this problem as maintaining a moving window that slides forward in time.
When we receive a new request at time t
, we need to:
- Add this new request to our collection
- Remove any requests that are now too old (occurred before
t - 3000
) - Count the remaining requests
The critical observation is that once a request becomes "too old" for a particular ping
call, it will remain too old for all future ping
calls (since time values are strictly increasing). This means we never need to reconsider old requests once they fall outside the window - they can be permanently discarded.
This naturally leads us to use a queue data structure:
- New requests are added to the back of the queue (most recent)
- Old requests are removed from the front of the queue (oldest)
- The queue maintains requests in chronological order
Using a deque (double-ended queue) is particularly efficient because:
append()
to add new requests isO(1)
popleft()
to remove old requests isO(1)
- We only keep requests that are currently relevant (within the 3000ms window)
The beauty of this approach is its simplicity - we're essentially maintaining a "live" window of relevant requests. As time moves forward, the window slides along, naturally dropping old requests and including new ones. The count of requests in the window at any moment is simply the size of our queue.
Learn more about Queue and Data Stream patterns.
Solution Approach
The implementation uses a deque (double-ended queue) to efficiently manage the sliding window of requests:
Data Structure Choice:
- We use
deque()
from Python's collections module to store timestamps of requests - The deque maintains timestamps in chronological order (oldest at front, newest at back)
Implementation Details:
-
Initialization (
__init__
):self.q = deque()
- Creates an empty deque to store request timestamps
- No requests are recorded initially
-
Processing a new request (
ping
):def ping(self, t: int) -> int: self.q.append(t) while self.q[0] < t - 3000: self.q.popleft() return len(self.q)
The method performs three key operations:
- Step 1: Add the new request
self.q.append(t)
adds the current timestamp to the back of the queue- This is
O(1)
operation
- Step 2: Remove outdated requests
- The while loop checks if the oldest request (at
self.q[0]
) is outside the 3000ms window - If
self.q[0] < t - 3000
, that request is too old and gets removed withpopleft()
- This continues until all requests in the queue are within the valid time range
[t - 3000, t]
- Each removal is
O(1)
, and we only remove requests once (amortizedO(1)
per request)
- The while loop checks if the oldest request (at
- Step 3: Return the count
len(self.q)
gives us the number of requests in the valid time window- This is
O(1)
operation
- Step 1: Add the new request
Time Complexity:
- Each
ping
operation has amortizedO(1)
time complexity - While the while loop might iterate multiple times in a single call, each timestamp is added once and removed at most once throughout all operations
Space Complexity:
O(n)
wheren
is the maximum number of requests that can occur within a 3000ms window- In the worst case, if requests come every millisecond, we'd store at most 3001 timestamps
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a sequence of ping
calls to see how the queue-based solution maintains the sliding window:
Initial state: queue = []
Call 1: ping(1)
- Add timestamp 1 to queue:
queue = [1]
- Check if front element (1) is outside window
[1-3000, 1]
=[-2999, 1]
- Is 1 < -2999? No, so keep it
- Queue now contains:
[1]
- Return: 1 request in window
Call 2: ping(100)
- Add timestamp 100 to queue:
queue = [1, 100]
- Check if front element (1) is outside window
[100-3000, 100]
=[-2900, 100]
- Is 1 < -2900? No, so keep it
- Queue now contains:
[1, 100]
- Return: 2 requests in window
Call 3: ping(3001)
- Add timestamp 3001 to queue:
queue = [1, 100, 3001]
- Check if front element (1) is outside window
[3001-3000, 3001]
=[1, 3001]
- Is 1 < 1? No, so keep it
- Queue now contains:
[1, 100, 3001]
- Return: 3 requests in window
Call 4: ping(3002)
- Add timestamp 3002 to queue:
queue = [1, 100, 3001, 3002]
- Check if front element (1) is outside window
[3002-3000, 3002]
=[2, 3002]
- Is 1 < 2? Yes, so remove it
- Queue becomes:
[100, 3001, 3002]
- Check if new front element (100) is outside window
- Is 100 < 2? No, so keep it
- Queue now contains:
[100, 3001, 3002]
- Return: 3 requests in window
Call 5: ping(5000)
- Add timestamp 5000 to queue:
queue = [100, 3001, 3002, 5000]
- Check if front element (100) is outside window
[5000-3000, 5000]
=[2000, 5000]
- Is 100 < 2000? Yes, so remove it
- Queue becomes:
[3001, 3002, 5000]
- Check if new front element (3001) is outside window
- Is 3001 < 2000? No, so keep it
- Queue now contains:
[3001, 3002, 5000]
- Return: 3 requests in window
Notice how the queue efficiently maintains only the relevant timestamps. Old timestamps are removed from the front as they fall outside the 3000ms window, while new timestamps are added to the back. The queue size always represents the exact count of requests within the current time window.
Solution Implementation
1from collections import deque
2from typing import Optional
3
4
5class RecentCounter:
6 """
7 A class to count recent requests within a sliding time window of 3000ms.
8 Each ping represents a request at time t (in milliseconds).
9 """
10
11 def __init__(self) -> None:
12 """
13 Initialize the RecentCounter with an empty queue to store timestamps.
14 """
15 self.request_queue = deque()
16
17 def ping(self, t: int) -> int:
18 """
19 Record a request at time t and return the number of requests
20 that have happened in the past 3000 milliseconds (including current request).
21
22 Args:
23 t: The timestamp of the current request in milliseconds.
24 It's guaranteed that every call uses a larger t value than before.
25
26 Returns:
27 The number of requests in the time window [t-3000, t].
28 """
29 # Add the current timestamp to the queue
30 self.request_queue.append(t)
31
32 # Remove all timestamps that are outside the 3000ms window
33 # Keep removing from the front while timestamp is older than t-3000
34 while self.request_queue[0] < t - 3000:
35 self.request_queue.popleft()
36
37 # Return the count of requests within the valid time window
38 return len(self.request_queue)
39
40
41# Your RecentCounter object will be instantiated and called as such:
42# obj = RecentCounter()
43# param_1 = obj.ping(t)
44
1class RecentCounter {
2 // Array to store timestamps of requests
3 private int[] timestamps = new int[10010];
4 // Current index pointing to the next position to insert
5 private int currentIndex;
6
7 /**
8 * Initialize the counter with zero recent requests
9 */
10 public RecentCounter() {
11 currentIndex = 0;
12 }
13
14 /**
15 * Records a request at the given timestamp and returns the number of requests
16 * that have happened in the past 3000 milliseconds (including the new request)
17 *
18 * @param t The timestamp of the current request in milliseconds
19 * @return The number of requests in the time range [t - 3000, t]
20 */
21 public int ping(int t) {
22 // Store the current timestamp at the current index
23 timestamps[currentIndex++] = t;
24
25 // Find the first index where timestamp >= (t - 3000)
26 int firstValidIndex = binarySearch(t - 3000);
27
28 // Return the count of valid requests (from firstValidIndex to currentIndex-1)
29 return currentIndex - firstValidIndex;
30 }
31
32 /**
33 * Binary search to find the leftmost index where timestamps[index] >= target
34 *
35 * @param target The target value to search for
36 * @return The index of the first element >= target
37 */
38 private int binarySearch(int target) {
39 int left = 0;
40 int right = currentIndex;
41
42 // Binary search for the leftmost position where timestamps[mid] >= target
43 while (left < right) {
44 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
45
46 if (timestamps[mid] >= target) {
47 // Move right boundary to mid (keep searching left)
48 right = mid;
49 } else {
50 // Move left boundary past mid
51 left = mid + 1;
52 }
53 }
54
55 return left;
56 }
57}
58
59/**
60 * Your RecentCounter object will be instantiated and called as such:
61 * RecentCounter obj = new RecentCounter();
62 * int param_1 = obj.ping(t);
63 */
64
1class RecentCounter {
2private:
3 std::queue<int> timestamps; // Queue to store timestamps of recent requests
4
5public:
6 /**
7 * Constructor: Initializes an empty RecentCounter
8 */
9 RecentCounter() {
10 // Queue is automatically initialized as empty
11 }
12
13 /**
14 * Records a new request at timestamp t and returns the number of requests
15 * that have happened in the past 3000 milliseconds (including the new request).
16 *
17 * @param t - The timestamp of the current request in milliseconds
18 * @return The number of requests in the time window [t-3000, t]
19 */
20 int ping(int t) {
21 // Add the current timestamp to the queue
22 timestamps.push(t);
23
24 // Remove all timestamps that are outside the 3000ms window
25 // Keep removing from front while timestamp is older than (t - 3000)
26 while (!timestamps.empty() && timestamps.front() < t - 3000) {
27 timestamps.pop();
28 }
29
30 // Return the count of requests within the valid time window
31 return timestamps.size();
32 }
33};
34
35/**
36 * Your RecentCounter object will be instantiated and called as such:
37 * RecentCounter* obj = new RecentCounter();
38 * int param_1 = obj->ping(t);
39 */
40
1// Queue to store timestamps of recent requests within the 3000ms window
2let requestQueue: number[] = [];
3
4/**
5 * Initializes the counter by clearing the request queue
6 */
7function RecentCounter(): void {
8 requestQueue = [];
9}
10
11/**
12 * Records a request at timestamp t and returns the number of requests
13 * that have happened in the past 3000 milliseconds (including the current request)
14 *
15 * @param t - The timestamp of the current request in milliseconds
16 * @returns The number of requests in the range [t - 3000, t]
17 */
18function ping(t: number): number {
19 // Add the current timestamp to the queue
20 requestQueue.push(t);
21
22 // Remove all timestamps that are outside the 3000ms window
23 // Keep removing from the front while timestamps are older than (t - 3000)
24 while (requestQueue[0] < t - 3000) {
25 requestQueue.shift();
26 }
27
28 // Return the count of requests within the valid time window
29 return requestQueue.length;
30}
31
Time and Space Complexity
Time Complexity:
__init__()
:O(1)
- Simply initializes an empty deque.ping(t)
:O(n)
in the worst case, wheren
is the number of outdated pings that need to be removed. However, amortized time complexity isO(1)
because each element is added once and removed at most once throughout all operations.
Space Complexity:
O(w)
wherew
is the maximum number of pings in any 3000ms window. In the worst case, if all pings happen within a 3000ms interval, the space would beO(n)
wheren
is the total number of pings within that window. The deque only stores pings from the last 3000 milliseconds, so the space is bounded by the number of pings that can occur in this time frame.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. IndexError when checking empty queue
The most common mistake is not handling the case when the queue becomes empty after removing outdated requests:
Problematic Code:
def ping(self, t: int) -> int:
self.q.append(t)
while self.q[0] < t - 3000: # IndexError if queue becomes empty!
self.q.popleft()
return len(self.q)
Why it fails:
If all existing requests get removed (though this won't happen with the problem constraints since we just added one), or more commonly, if someone modifies the code and checks before appending, accessing self.q[0]
on an empty queue raises an IndexError
.
Solution:
def ping(self, t: int) -> int:
self.q.append(t)
while self.q and self.q[0] < t - 3000: # Check if queue is non-empty first
self.q.popleft()
return len(self.q)
2. Using a list instead of deque
Some might use a regular Python list and pop from the front:
Problematic Code:
def __init__(self):
self.q = [] # Using list instead of deque
def ping(self, t: int) -> int:
self.q.append(t)
while self.q[0] < t - 3000:
self.q.pop(0) # O(n) operation!
return len(self.q)
Why it fails:
While this works correctly, pop(0)
on a list is O(n) because it needs to shift all remaining elements. This degrades performance significantly with many requests.
Solution:
Always use collections.deque
with popleft()
for O(1) removal from the front.
3. Incorrect boundary condition
Using wrong comparison operator for the time window:
Problematic Code:
while self.q[0] <= t - 3000: # Wrong: removes requests at exactly t-3000 self.q.popleft()
Why it fails:
The time window [t-3000, t]
is inclusive on both ends. A request at exactly t-3000
should be counted. Using <=
would incorrectly remove it.
Solution:
Use strict inequality <
to keep requests at time t-3000
:
while self.q[0] < t - 3000: # Correct: keeps requests at t-3000 self.q.popleft()
4. Not leveraging the monotonic property
Some solutions might try to binary search or iterate through the entire queue:
Problematic Code:
def ping(self, t: int) -> int:
self.q.append(t)
count = 0
for timestamp in self.q: # Unnecessary iteration
if timestamp >= t - 3000:
count += 1
return count
Why it's inefficient: Since timestamps are strictly increasing, once we find the first valid timestamp, all subsequent ones are also valid. We should remove invalid ones and count the rest, not iterate through everything.
Solution: Stick to the original approach of removing from the front and returning the queue length.
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
Median of Data Stream Given a stream of numbers find the median number at any given time accurate to 1 decimal place Example add_number 1 add_number 2 add_number 3 get_median 2 0 add_number 4 get_median 2 5 Try it yourself Explanation Intuition Brute force way is to sort the entire list every time we get a new number This would be O N log N for each add_number operation One step up is to notice that the list is sorted before we add any new number to it So every
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!