Facebook Pixel

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:

  1. Constructor RecentCounter(): Creates a new counter that starts with zero requests recorded.

  2. Method ping(int t): Records a new request at time t (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 of t (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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Add this new request to our collection
  2. Remove any requests that are now too old (occurred before t - 3000)
  3. 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 is O(1)
  • popleft() to remove old requests is O(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:

  1. Initialization (__init__):

    self.q = deque()
    • Creates an empty deque to store request timestamps
    • No requests are recorded initially
  2. 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 with popleft()
      • 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 (amortized O(1) per request)
    • Step 3: Return the count
      • len(self.q) gives us the number of requests in the valid time window
      • This is O(1) operation

Time Complexity:

  • Each ping operation has amortized O(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) where n 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 Evaluator

Example 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, where n is the number of outdated pings that need to be removed. However, amortized time complexity is O(1) because each element is added once and removed at most once throughout all operations.

Space Complexity:

  • O(w) where w 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 be O(n) where n 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More