Skip to content

LeetCode 362: Design Hit Counter

A clear explanation of designing a hit counter for the last 5 minutes using a queue with compressed timestamps.

Problem Restatement

We need to design a hit counter.

The counter supports two operations:

MethodMeaning
hit(timestamp)Record one hit at timestamp
getHits(timestamp)Return how many hits happened in the past 300 seconds

The timestamp is given in seconds.

Calls arrive in chronological order, so timestamps are monotonically increasing. Multiple hits may happen at the same timestamp. The time window is the past 5 minutes, which means the past 300 seconds.

For a query at time t, we count hits whose timestamp is greater than:

t - 300

So at time 301, a hit at time 1 is expired because:

301 - 1 = 300

The hit at time 1 is no longer inside the last 300 seconds.

Input and Output

MethodInputOutput
HitCounter()NoneInitializes the counter
hit(timestamp)Integer timestampNone
getHits(timestamp)Integer timestampNumber of recent hits

Example class shape:

class HitCounter:

    def __init__(self):
        ...

    def hit(self, timestamp: int) -> None:
        ...

    def getHits(self, timestamp: int) -> int:
        ...

Examples

Example calls:

counter = HitCounter()

counter.hit(1)
counter.hit(2)
counter.hit(3)

counter.getHits(4)

counter.hit(300)

counter.getHits(300)
counter.getHits(301)

Expected results:

3
4
3

Step by step:

OperationResultWhy
hit(1)Record hit at 1
hit(2)Record hit at 2
hit(3)Record hit at 3
getHits(4)3Hits at 1, 2, 3 are inside the window
hit(300)Record hit at 300
getHits(300)4Hits at 1, 2, 3, 300 are inside the window
getHits(301)3Hit at 1 is expired

At timestamp 301, the valid timestamps are:

2, 3, 300

because a hit must be greater than:

301 - 300 = 1

First Thought: Store Every Hit

The simplest design is to store every hit timestamp in a list.

class HitCounter:

    def __init__(self):
        self.hits = []

    def hit(self, timestamp: int) -> None:
        self.hits.append(timestamp)

    def getHits(self, timestamp: int) -> int:
        count = 0

        for hit_time in self.hits:
            if hit_time > timestamp - 300:
                count += 1

        return count

This works, but getHits scans all hits ever recorded.

If the system receives many hits, old hits stay in the list forever and every query becomes slower.

Key Insight

Since timestamps arrive in chronological order, old hits are always at the front.

When a hit becomes older than the 300 second window, it will never become valid again.

So we can delete expired hits from the front.

A queue fits this exactly.

To handle many hits at the same timestamp more efficiently, store pairs:

(timestamp, count)

For example, three hits at timestamp 10 become:

(10, 3)

instead of:

10, 10, 10

We also keep a running total of hits currently in the queue.

Algorithm

Maintain:

VariableMeaning
queueTimestamp groups inside or near the active window
totalNumber of hits currently stored in queue

For hit(timestamp):

  1. If the queue tail has the same timestamp, increase its count.
  2. Otherwise, append a new pair [timestamp, 1].
  3. Increase total.

For getHits(timestamp):

  1. While the queue is not empty and the front timestamp is expired:
    • Remove the front pair.
    • Subtract its count from total.
  2. Return total.

A hit expires when:

hit_time <= timestamp - 300

Equivalently:

timestamp - hit_time >= 300

Correctness

The queue stores hit groups in chronological order because calls are made with monotonically increasing timestamps.

When getHits(timestamp) is called, any hit at time hit_time <= timestamp - 300 lies outside the past 300 seconds. Since the queue is sorted by time, all such expired hits appear at the front. The algorithm removes exactly those expired groups.

After removing expired groups, every remaining group has timestamp greater than timestamp - 300, so every remaining hit belongs to the required window.

The variable total is increased once for every recorded hit and decreased by the exact count of each expired group. Therefore, after cleanup, total equals the number of hits in the past 300 seconds.

So getHits returns the correct count.

Complexity

Let g be the number of timestamp groups currently stored.

OperationTimeSpace
hitO(1)O(1) extra
getHitsAmortized O(1)O(g) total

A single expired group is removed only once across the whole lifetime of the object.

So although one getHits call may remove several groups, the amortized cost per operation is constant.

Implementation

from collections import deque

class HitCounter:

    def __init__(self):
        self.queue = deque()
        self.total = 0

    def hit(self, timestamp: int) -> None:
        if self.queue and self.queue[-1][0] == timestamp:
            self.queue[-1][1] += 1
        else:
            self.queue.append([timestamp, 1])

        self.total += 1

    def getHits(self, timestamp: int) -> int:
        while self.queue and self.queue[0][0] <= timestamp - 300:
            _, count = self.queue.popleft()
            self.total -= count

        return self.total

Code Explanation

The queue stores grouped hits:

self.queue = deque()

Each item is:

[timestamp, count]

The running total stores the current number of non-expired hits:

self.total = 0

When a hit arrives, we merge it with the last group if the timestamp is the same:

if self.queue and self.queue[-1][0] == timestamp:
    self.queue[-1][1] += 1

Otherwise, we add a new group:

self.queue.append([timestamp, 1])

Every hit increases the total:

self.total += 1

Before answering a query, we remove expired groups:

while self.queue and self.queue[0][0] <= timestamp - 300:

For each removed group, subtract its count:

_, count = self.queue.popleft()
self.total -= count

Then return the remaining total:

return self.total

Alternative Implementation: Fixed 300 Buckets

Since the window is exactly 300 seconds, another common solution uses two arrays of length 300.

Each bucket stores:

ArrayMeaning
times[i]Timestamp currently stored in bucket i
hits[i]Number of hits at that timestamp

The bucket index is:

timestamp % 300

Code:

class HitCounter:

    def __init__(self):
        self.times = [0] * 300
        self.hits = [0] * 300

    def hit(self, timestamp: int) -> None:
        index = timestamp % 300

        if self.times[index] != timestamp:
            self.times[index] = timestamp
            self.hits[index] = 1
        else:
            self.hits[index] += 1

    def getHits(self, timestamp: int) -> int:
        total = 0

        for i in range(300):
            if timestamp - self.times[i] < 300:
                total += self.hits[i]

        return total

This version has:

OperationTime
hitO(1)
getHitsO(300), treated as O(1)

It uses fixed memory, which is useful when the hit volume is very large.

Testing

def run_tests():
    counter = HitCounter()

    counter.hit(1)
    counter.hit(2)
    counter.hit(3)

    assert counter.getHits(4) == 3

    counter.hit(300)

    assert counter.getHits(300) == 4
    assert counter.getHits(301) == 3

    counter = HitCounter()

    counter.hit(1)
    counter.hit(1)
    counter.hit(1)

    assert counter.getHits(1) == 3
    assert counter.getHits(300) == 3
    assert counter.getHits(301) == 0

    counter = HitCounter()

    counter.hit(100)
    counter.hit(200)
    counter.hit(399)

    assert counter.getHits(399) == 2
    assert counter.getHits(400) == 2
    assert counter.getHits(500) == 1
    assert counter.getHits(699) == 0

    print("all tests passed")

run_tests()

Test meaning:

TestWhy
Standard sequenceChecks the main example behavior
Boundary at 300 secondsConfirms expiration uses <= timestamp - 300
Multiple hits at same timestampChecks grouped counts
Far future queryConfirms old hits are removed correctly