933. Number of Recent Calls


Problem Description

In this problem, you are tasked with designing a RecentCounter class that tracks the number of requests received within a specific time frame. The time frame in question is the last 3000 milliseconds, which equates to the last 3 seconds. This can be likened to monitoring network traffic, where you're interested in understanding the number of events or requests that have occurred in a sliding window of time for performance analysis or rate limiting.

The class should support two operations:

  1. RecentCounter() is the constructor that initializes the counter. When a RecentCounter object is created, it starts with zero recent requests.

  2. ping(int t) is a method that is called each time a new request is received. The request comes with a timestamp t (in milliseconds), which is strictly increasing with each call. The purpose of ping is to add the new request and then return the count of all recent requests within the last 3000 milliseconds, which is the time range from t - 3000 to t, inclusive of both ends of the interval.

Intuition

The key to solving this problem is to maintain a dynamic list of timestamped requests, ensuring that only those within the last 3000 milliseconds are counted. Because the input guarantees that each t in ping is increasing, we can use a queue to keep track of the timestamps of the recent requests.

The intuition for using a queue comes from its FIFO (First-In-First-Out) property, which naturally aligns with the progression of time in our problem. When a new request arrives with timestamp t, we add it to the end of the queue. Then we need to remove all requests from the front of the queue that are outside the 3000 millisecond window (i.e., older than t - 3000).

By doing so, our queue always contains only those requests that are within the 3000 millisecond window. The length of the queue after this cleaning process gives us the number of recent requests, which is what the ping method returns.

To implement this idea in code, we utilize the deque data structure from Python's collections module because it allows for efficient addition and removal of elements from both ends.

Learn more about Queue and Data Stream patterns.

Solution Approach

The solution is implemented as a class, RecentCounter, containing 2 methods: the constructor __init__ and the method ping.

  • In the __init__ method, we initialize a queue (specifically a deque) to store the timestamps of incoming requests. We start with an empty deque since there are no requests yet. A deque is used here because it supports fast appends and pops from both ends.

  • The ping method is where the main logic is executed:

    1. First, we append the new request's timestamp t to the end of the deque with self.q.append(t). This means we are adding the current request to our list of recent requests.

    2. Next, we enter a while loop, which will continue to execute as long as the front element of the deque (the oldest request) is less than t - 3000. This check is done with self.q[0] < t - 3000. Requests that fall outside the last 3000 milliseconds do not belong to the 'recent' category anymore and should be removed.

    3. For each loop iteration, if the condition is met (i.e., the request is too old), we remove the oldest request from the front of the deque with self.q.popleft(). Since requests are always timestamped in increasing order, once we find a request that is within the time frame, we can be sure that all subsequent requests are also within it, and there is no need to check the rest of the deque.

    4. Finally, once we have cleaned up all the old requests, the remaining length of the deque represents the count of recent requests. We return this count with return len(self.q).

This implementation ensures that we're always working with a list of requests that have occurred within the last 3000 milliseconds. The time complexity for the ping method is O(N), where N is the number of calls to this method. However, because we are only keeping recent requests within the deque, this method is effective and efficient for a large number of requests, as long as the number of concurrent recent requests is reasonable.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's use the solution approach outlined above on a small example to illustrate how the RecentCounter class works in practice. For this example, assume we receive a sequence of timestamped requests at the following times (in milliseconds): 1, 100, 3001, 3002.

  1. We create a RecentCounter object.
  2. We call ping(1). The deque now has one element: [1].
    • Since there are no pings older than 3000 milliseconds, we return len(self.q) which is 1.
  3. Next, we call ping(100). The deque becomes: [1, 100].
    • Both pings are within 3000 milliseconds from the current ping time, so the number of recent requests is 2.
  4. Then we call ping(3001). The deque is updated to: [1, 100, 3001].
    • Now, we check for timestamps older than 3001 - 3000 which is 1. Since 1 is within the range, we do not remove it. The deque remains [1, 100, 3001].
    • The len(self.q) now returns 3, as these are the recent pings within the last 3000 milliseconds.
  5. Lastly, we call ping(3002). The deque first receives the timestamp 3002: [1, 100, 3001, 3002].
    • We then remove any timestamps older than 3002 - 3000, which is 2. After removal, the deque is [100, 3001, 3002].
    • We return len(self.q), which is 3, as the count of recent requests.

Throughout this process, the RecentCounter maintains a list of timestamps that are within the window of the last 3000 milliseconds. By adding new timestamps to the deque and removing the old ones, we ensure that we always have an accurate count of the recent requests when ping is called.

Solution Implementation

1from collections import deque
2
3class RecentCounter:
4    def __init__(self):
5        # Initialize a double-ended queue to keep track of ping times
6        self.queue = deque()
7
8    def ping(self, t: int) -> int:
9        # Add the new ping time to the queue
10        self.queue.append(t)
11
12        # Remove ping times that are older than 3000ms from the current time
13        while self.queue[0] < t - 3000:
14            self.queue.popleft()
15
16        # Return the number of pings within the last 3000ms
17        return len(self.queue)
18
19# Example usage:
20# recent_counter = RecentCounter()
21# count = recent_counter.ping(1)  # Assuming 't' is a timestamp in milliseconds
22
1import java.util.Deque;
2import java.util.ArrayDeque;
3
4class RecentCounter {
5    // Queue to store the timestamps of the pings.
6    private Deque<Integer> queue = new ArrayDeque<>();
7
8    /**
9     * Constructor initializes the RecentCounter with an empty queue.
10     */
11    public RecentCounter() {
12    }
13
14    /**
15     * Records a new ping, and returns the number of pings in the last 3000 milliseconds.
16     *
17     * @param t The timestamp of the ping.
18     * @return The count of pings in the last 3000 milliseconds.
19     */
20    public int ping(int t) {
21        // Note: Timestamp here is assumed to be in milliseconds.
22
23        // Offer/add the new ping's timestamp to the end of the queue.
24        queue.offer(t);
25
26        // Continue removing(polling) pings from the start of the queue if they are older
27        // than 3000 milliseconds compared to the current ping's timestamp.
28        while (!queue.isEmpty() && queue.peekFirst() < t - 3000) {
29            queue.pollFirst();
30        }
31
32        // After removing old pings, return the size of the queue,
33        // which is the count of recent pings.
34        return queue.size();
35    }
36}
37
38/**
39 * The RecentCounter object will be instantiated and called as such:
40 * RecentCounter obj = new RecentCounter();
41 * int param_1 = obj.ping(t);
42 */
43
1#include <deque>
2
3class RecentCounter {
4private:
5    std::deque<int> pings;
6
7public:
8    // Constructor of RecentCounter initializes a new instance.
9    RecentCounter() {
10        // Nothing needed here since the deque 'pings' automatically initializes
11    }
12
13    // Method to record a new ping and return the count of pings in the last 3000 milliseconds.
14    int ping(int t) {
15        // Add the new ping timestamp to the deque
16        pings.push_back(t);
17
18        // Remove any pings that are no longer within the past 3000 milliseconds
19        while (!pings.empty() && pings.front() < t - 3000) {
20            pings.pop_front();
21        }
22
23        // Return the count of pings within the past 3000 milliseconds
24        return pings.size();
25    }
26};
27
28/*
29 * Use of the RecentCounter class:
30 *
31 * RecentCounter* obj = new RecentCounter();
32 * int param_1 = obj->ping(t);
33 * delete obj; // Once done, remember to delete the object to free memory
34 */
35
1// Array to hold timestamps of recent pings.
2let recentPings: number[] = [];
3
4/**
5 * Records a ping with the current timestamp and returns the number
6 * of pings that occurred in the last 3000 milliseconds.
7 * @param {number} t - The current timestamp in milliseconds
8 * @returns {number} The number of pings in the last 3000 milliseconds
9 */
10function ping(t: number): number {
11    // Record the new ping by adding the timestamp to the array.
12    recentPings.push(t);
13
14    // Remove pings from the array that are older than 3000 milliseconds from the current timestamp.
15    while (recentPings.length > 0 && recentPings[0] < t - 3000) {
16        recentPings.shift();
17    }
18
19    // Return the number of recent pings.
20    return recentPings.length;
21}
22

Time and Space Complexity

Time Complexity

The time complexity of the ping method is O(N), where N is the number of calls to the method that fall within the 3000 ms window relative to the current call. This is because in the worst-case scenario, all N calls might have occurred within the past 3000 ms, which means the while loop will have to run N times to remove all outdated requests (requests older than 3000 ms before the current timestamp t). However, it is important to note that each individual call to ping is generally O(1) on average because the while loop doesn't always iterate over all elements. Over time, each element is added once and removed once, leading to an amortized time complexity of O(1).

Space Complexity

The space complexity is O(N), where N is the number of all pings that are within the 3000 ms time window relative to the current call. This is due to the data structure (a deque) used to store the timestamps of pings. The space used by the deque will grow in direct proportion to the number of elements within the time window.

Learn more about how to find time and space complexity quickly using problem constraints.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

Which type of traversal does breadth first search do?


Recommended Readings

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


Siddharth Narayan

Intro to deque, learn about available methods

Fri Sep 13 2024