Facebook Pixel

362. Design Hit Counter πŸ”’

Problem Description

You need to design a hit counter system that tracks the number of hits received within the past 5 minutes (300 seconds).

The system should support two main operations:

  1. Recording hits: When a hit occurs at a specific timestamp (in seconds), the system should record it. Multiple hits can happen at the same timestamp.

  2. Querying hit count: Given a timestamp, the system should return the total number of hits that occurred in the past 300 seconds from that timestamp. Specifically, it counts all hits in the time range [timestamp - 299, timestamp].

Key constraints and assumptions:

  • Timestamps are provided in seconds
  • Calls to the system happen in chronological order (timestamps are monotonically increasing)
  • Multiple hits may arrive at the same timestamp

The HitCounter class needs three methods:

  • HitCounter(): Initializes the hit counter system
  • hit(timestamp): Records a hit at the given timestamp
  • getHits(timestamp): Returns the count of all hits in the past 300 seconds from the given timestamp

For example, if hits occurred at timestamps 1, 2, 3, and 301, calling getHits(301) would return 1 (only the hit at timestamp 301 is within the past 300 seconds), while getHits(303) would still return 1 since the hit at timestamp 1 is now more than 300 seconds old.

The solution uses a list to store all timestamps and binary search (bisect_left) to efficiently find the boundary of the 5-minute window. Since timestamps are monotonically increasing, the list remains sorted, making binary search an optimal approach for finding how many hits fall within the valid time window.

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

Intuition

The key insight comes from recognizing that we need to efficiently count hits within a sliding time window. Since we're told that timestamps arrive in chronological order (monotonically increasing), we can leverage this property to optimize our solution.

Initially, we might think about storing each hit with its timestamp and then iterating through all stored hits to count those within the 300-second window. However, this would be inefficient for the getHits operation, especially as the number of hits grows.

The breakthrough comes from realizing that since timestamps are already sorted (due to chronological ordering), we don't need to search through all hits - we just need to find the boundary point where hits become "too old" to count.

Think of it this way: if we have hits at timestamps [1, 2, 3, 100, 200, 301, 302] and we want to count hits at timestamp 302, we need hits from timestamp 3 onwards (since 302 - 300 + 1 = 3). All hits before timestamp 3 are outside our window.

This naturally leads us to binary search. We can use binary search to find the first timestamp that is within our valid window (greater than or equal to timestamp - 300 + 1). Once we find this position, all hits from this position to the end of our list are valid hits within the 300-second window.

The formula timestamp - 300 + 1 gives us the earliest valid timestamp. We add 1 because we want the past 300 seconds inclusive. For example, at timestamp 300, we want to include hits from timestamp 1 to 300 (which is exactly 300 seconds).

Using bisect_left to find this boundary position gives us an O(log n) solution for getHits, which is much more efficient than iterating through all hits. The total number of valid hits is then simply the total length of our list minus the position of the first valid hit.

Solution Approach

The solution uses a simple array-based approach combined with binary search to efficiently handle both recording hits and querying hit counts.

Data Structure:

  • We use a single array ts to store all timestamps when hits occur
  • Since timestamps arrive in chronological order, this array is naturally sorted

Implementation Details:

  1. Initialization (__init__):

    • Create an empty list ts to store all hit timestamps
    • This list will maintain chronological order automatically due to the problem constraints
  2. Recording Hits (hit):

    • Simply append the new timestamp to the ts array
    • Time complexity: O(1) for appending
    • Since timestamps are monotonically increasing, no sorting is needed
  3. Querying Hit Count (getHits):

    • Calculate the earliest valid timestamp: timestamp - 300 + 1
    • Use bisect_left to find the first position in ts where the timestamp is greater than or equal to our cutoff
    • bisect_left(self.ts, timestamp - 300 + 1) returns the leftmost insertion position for the cutoff value
    • The number of valid hits equals the total length of ts minus this position
    • Time complexity: O(log n) where n is the number of hits stored

Why Binary Search Works:

  • The sorted nature of ts allows us to use binary search
  • bisect_left efficiently finds the boundary between "expired" hits and "valid" hits
  • All timestamps before the found position are outside the 300-second window
  • All timestamps from the found position onwards are within the window

Example Walkthrough: If ts = [1, 2, 100, 301, 302] and we call getHits(302):

  • Calculate cutoff: 302 - 300 + 1 = 3
  • bisect_left(ts, 3) returns position 2 (where value 100 is located)
  • Valid hits: len(ts) - 2 = 5 - 2 = 3 (timestamps 100, 301, and 302)

This approach elegantly handles the sliding window requirement without explicitly removing old entries, making it both space and time efficient for the given constraints.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to understand how the hit counter works:

Initial State:

  • Create a HitCounter with an empty timestamp list: ts = []

Step 1: Recording hits

  • hit(1): Add timestamp 1 β†’ ts = [1]
  • hit(2): Add timestamp 2 β†’ ts = [2, 2]
  • hit(3): Add timestamp 3 β†’ ts = [1, 2, 3]
  • hit(300): Add timestamp 300 β†’ ts = [1, 2, 3, 300]
  • hit(301): Add timestamp 301 β†’ ts = [1, 2, 3, 300, 301]

Step 2: Query at timestamp 300

  • Call getHits(300)
  • Calculate cutoff: 300 - 300 + 1 = 1
  • Use bisect_left(ts, 1) to find where timestamp 1 would be inserted
  • bisect_left([1, 2, 3, 300, 301], 1) returns position 0
  • Valid hits: len(ts) - 0 = 5 - 0 = 5
  • Result: All 5 hits are within the window [1, 300]

Step 3: Query at timestamp 301

  • Call getHits(301)
  • Calculate cutoff: 301 - 300 + 1 = 2
  • Use bisect_left(ts, 2) to find where timestamp 2 would be inserted
  • bisect_left([1, 2, 3, 300, 301], 2) returns position 1
  • Valid hits: len(ts) - 1 = 5 - 1 = 4
  • Result: 4 hits are within the window [2, 301] (timestamp 1 is excluded)

Step 4: Add more hits and query

  • hit(302): Add timestamp 302 β†’ ts = [1, 2, 3, 300, 301, 302]
  • Call getHits(302)
  • Calculate cutoff: 302 - 300 + 1 = 3
  • bisect_left([1, 2, 3, 300, 301, 302], 3) returns position 2
  • Valid hits: 6 - 2 = 4
  • Result: 4 hits are within the window [3, 302] (timestamps 1 and 2 are excluded)

The binary search efficiently finds the boundary between expired and valid hits without examining every element, giving us O(log n) query performance.

Solution Implementation

1from bisect import bisect_left
2from typing import List
3
4
5class HitCounter:
6    """
7    A hit counter that tracks hits and returns the number of hits 
8    in the past 300 seconds (5 minutes).
9    """
10
11    def __init__(self):
12        """Initialize the hit counter with an empty list to store timestamps."""
13        self.timestamps: List[int] = []
14
15    def hit(self, timestamp: int) -> None:
16        """
17        Record a hit at the given timestamp.
18      
19        Args:
20            timestamp: The current timestamp (in seconds granularity)
21        """
22        self.timestamps.append(timestamp)
23
24    def getHits(self, timestamp: int) -> int:
25        """
26        Return the number of hits in the past 300 seconds from the given timestamp.
27      
28        Args:
29            timestamp: The current timestamp (in seconds granularity)
30          
31        Returns:
32            The number of hits in the range [timestamp - 299, timestamp]
33        """
34        # Find the leftmost position where (timestamp - 299) could be inserted
35        # to maintain sorted order. This gives us the index of the first hit
36        # that falls within our 300-second window
37        start_of_window = timestamp - 300 + 1
38        first_valid_index = bisect_left(self.timestamps, start_of_window)
39      
40        # All hits from first_valid_index to the end are within the window
41        return len(self.timestamps) - first_valid_index
42
43
44# Your HitCounter object will be instantiated and called as such:
45# obj = HitCounter()
46# obj.hit(timestamp)
47# param_2 = obj.getHits(timestamp)
48
1class HitCounter {
2    // List to store all hit timestamps in chronological order
3    private List<Integer> timestamps = new ArrayList<>();
4
5    /**
6     * Initialize the hit counter system
7     */
8    public HitCounter() {
9    }
10
11    /**
12     * Record a hit at the given timestamp
13     * @param timestamp - the current timestamp (in seconds granularity)
14     */
15    public void hit(int timestamp) {
16        timestamps.add(timestamp);
17    }
18
19    /**
20     * Return the number of hits in the past 5 minutes (300 seconds)
21     * @param timestamp - the current timestamp (in seconds granularity)
22     * @return number of hits in the past 300 seconds from the given timestamp
23     */
24    public int getHits(int timestamp) {
25        // Find the index of the first timestamp that is within the 300-second window
26        // We search for the leftmost position where timestamp >= (currentTime - 299)
27        int leftBoundaryIndex = binarySearchLeftmost(timestamp - 300 + 1);
28      
29        // All elements from leftBoundaryIndex to the end are within the 300-second window
30        return timestamps.size() - leftBoundaryIndex;
31    }
32
33    /**
34     * Binary search to find the leftmost index where timestamp >= target
35     * @param target - the target timestamp to search for
36     * @return the index of the first element >= target, or list size if all elements < target
37     */
38    private int binarySearchLeftmost(int target) {
39        int left = 0;
40        int right = timestamps.size();
41      
42        // Binary search for the leftmost position where timestamp >= target
43        while (left < right) {
44            int mid = (left + right) >> 1;  // Equivalent to (left + right) / 2
45          
46            if (timestamps.get(mid) >= target) {
47                // Mid element is >= target, search in left half (including mid)
48                right = mid;
49            } else {
50                // Mid element is < target, search in right half (excluding mid)
51                left = mid + 1;
52            }
53        }
54      
55        return left;
56    }
57}
58
59/**
60 * Your HitCounter object will be instantiated and called as such:
61 * HitCounter obj = new HitCounter();
62 * obj.hit(timestamp);
63 * int param_2 = obj.getHits(timestamp);
64 */
65
1class HitCounter {
2public:
3    // Constructor: Initialize the hit counter
4    HitCounter() {
5        // No initialization needed as vector is automatically initialized
6    }
7  
8    // Record a hit at the given timestamp
9    // @param timestamp: The current timestamp (in seconds)
10    void hit(int timestamp) {
11        // Add the timestamp to our sorted list of hits
12        timestamps.push_back(timestamp);
13    }
14  
15    // Get the number of hits in the past 300 seconds (5 minutes)
16    // @param timestamp: The current timestamp (in seconds)
17    // @return: Number of hits in the time window [timestamp - 299, timestamp]
18    int getHits(int timestamp) {
19        // Find the first timestamp that is within the 300-second window
20        // We look for timestamps >= (timestamp - 300 + 1) = (timestamp - 299)
21        // This gives us all hits from the last 300 seconds
22        auto startIterator = lower_bound(timestamps.begin(), 
23                                       timestamps.end(), 
24                                       timestamp - 300 + 1);
25      
26        // Calculate the count by finding the distance from start position to end
27        // All elements from startIterator to end are within the time window
28        return timestamps.end() - startIterator;
29    }
30  
31private:
32    // Store all hit timestamps in chronological order
33    // Since hits are added sequentially, the vector remains sorted
34    vector<int> timestamps;
35};
36
37/**
38 * Your HitCounter object will be instantiated and called as such:
39 * HitCounter* obj = new HitCounter();
40 * obj->hit(timestamp);
41 * int param_2 = obj->getHits(timestamp);
42 */
43
1// Array to store hit timestamps
2let hitTimestamps: number[] = [];
3
4/**
5 * Records a hit at the given timestamp
6 * @param timestamp - The timestamp of the hit in seconds
7 */
8function hit(timestamp: number): void {
9    hitTimestamps.push(timestamp);
10}
11
12/**
13 * Returns the number of hits in the past 5 minutes (300 seconds) from the given timestamp
14 * @param timestamp - The current timestamp in seconds
15 * @returns The number of hits in the past 300 seconds
16 */
17function getHits(timestamp: number): number {
18    /**
19     * Binary search to find the leftmost position where timestamp is >= targetTime
20     * @param targetTime - The timestamp to search for
21     * @returns The index of the first element >= targetTime
22     */
23    const binarySearchLeftBound = (targetTime: number): number => {
24        let left: number = 0;
25        let right: number = hitTimestamps.length;
26      
27        // Binary search for the leftmost position
28        while (left < right) {
29            const mid: number = Math.floor((left + right) / 2);
30          
31            if (hitTimestamps[mid] >= targetTime) {
32                // Move right boundary to mid (could be the answer)
33                right = mid;
34            } else {
35                // Move left boundary past mid
36                left = mid + 1;
37            }
38        }
39      
40        return left;
41    };
42  
43    // Calculate the start of the 300-second window
44    const windowStart: number = timestamp - 300 + 1;
45  
46    // Find the index of the first hit within the window
47    const firstHitIndexInWindow: number = binarySearchLeftBound(windowStart);
48  
49    // Return the count of hits from the window start to the end of array
50    return hitTimestamps.length - firstHitIndexInWindow;
51}
52

Time and Space Complexity

Time Complexity:

  • hit(timestamp): O(1) - Appending to a list is a constant time operation.
  • getHits(timestamp): O(log n) - The method uses bisect_left to perform a binary search on the sorted list self.ts to find the insertion point for timestamp - 300 + 1. Binary search has logarithmic time complexity. The subtraction operation len(self.ts) - bisect_left(...) is O(1). Here, n is the length of self.ts.

Space Complexity:

  • Overall: O(n) - The space complexity is linear with respect to the number of hits stored. The list self.ts stores all timestamps from all hit() calls, where n is the total number of hits recorded. No additional data structures are used beyond the list storing timestamps.

Note: This implementation assumes that timestamps are added in non-decreasing order (monotonically increasing), which allows the list to remain sorted naturally and enables the use of binary search in getHits(). If timestamps could arrive out of order, the binary search would not work correctly without additional sorting.

Common Pitfalls

1. Memory Growth Without Cleanup

The Problem: The current solution stores all timestamps indefinitely in the timestamps list, even those that will never be queried again (older than 300 seconds from the current timestamp). This leads to unbounded memory growth in a long-running system.

Example Scenario: If your system receives 1000 hits per second, after running for just one hour, you'll have 3.6 million timestamps stored, even though you only need the last 300,000 at most.

Solution: Implement a cleanup mechanism that removes expired timestamps:

def hit(self, timestamp: int) -> None:
    """Record a hit and clean up old timestamps."""
    self.timestamps.append(timestamp)
    # Clean up old timestamps that are definitely outside any future window
    self._cleanup(timestamp)

def _cleanup(self, timestamp: int) -> None:
    """Remove timestamps older than 300 seconds."""
    cutoff = timestamp - 300 + 1
    # Find the index of first valid timestamp
    valid_start = bisect_left(self.timestamps, cutoff)
    # Keep only valid timestamps
    if valid_start > 0:
        self.timestamps = self.timestamps[valid_start:]

2. Incorrect Window Calculation

The Problem: A common mistake is using timestamp - 300 instead of timestamp - 300 + 1 or incorrectly calculating timestamp - 299. The window should include exactly 300 seconds: [timestamp - 299, timestamp].

Wrong Implementation:

# INCORRECT: This excludes hits at exactly (timestamp - 299)
first_valid_index = bisect_left(self.timestamps, timestamp - 299)

# INCORRECT: This includes hits from 301 seconds ago
first_valid_index = bisect_left(self.timestamps, timestamp - 300)

Correct Implementation:

# CORRECT: Includes hits from exactly 299 seconds ago up to current timestamp
start_of_window = timestamp - 300 + 1  # or timestamp - 299
first_valid_index = bisect_left(self.timestamps, start_of_window)

3. Handling High-Frequency Hits at Same Timestamp

The Problem: When multiple hits occur at the same timestamp, the current solution handles this correctly by appending all of them. However, in high-traffic scenarios, you might want to optimize by using a counter approach.

Optimized Solution for High-Frequency Same-Timestamp Hits:

class HitCounter:
    def __init__(self):
        self.timestamps = []
        self.hit_counts = []  # Count of hits at each timestamp
  
    def hit(self, timestamp: int) -> None:
        if self.timestamps and self.timestamps[-1] == timestamp:
            # Increment count for existing timestamp
            self.hit_counts[-1] += 1
        else:
            # Add new timestamp with count 1
            self.timestamps.append(timestamp)
            self.hit_counts.append(1)
  
    def getHits(self, timestamp: int) -> int:
        start_of_window = timestamp - 300 + 1
        first_valid_index = bisect_left(self.timestamps, start_of_window)
        # Sum all hit counts within the window
        return sum(self.hit_counts[first_valid_index:])

4. Thread Safety Issues

The Problem: In a multi-threaded environment, concurrent calls to hit() and getHits() can cause race conditions and data corruption.

Solution: Add thread synchronization:

import threading

class HitCounter:
    def __init__(self):
        self.timestamps = []
        self.lock = threading.Lock()
  
    def hit(self, timestamp: int) -> None:
        with self.lock:
            self.timestamps.append(timestamp)
  
    def getHits(self, timestamp: int) -> int:
        with self.lock:
            start_of_window = timestamp - 300 + 1
            first_valid_index = bisect_left(self.timestamps, start_of_window)
            return len(self.timestamps) - first_valid_index

These pitfalls are critical to address when moving from a LeetCode solution to a production-ready implementation.

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

Which of these properties could exist for a graph but not a tree?


Recommended Readings

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

Load More