Design Hit Counter

Build a class HitCounter that counts how many hits (for example page views) arrived during the last five minutes.

  • HitCounter() creates a counter with no hits.
  • hit(timestamp) records one hit at time timestamp, measured in seconds. Several hits may share the same timestamp.
  • getHits(timestamp) returns the number of hits recorded in the 300 seconds ending at timestamp, that is, hits whose time t satisfies timestamp - 300 < t <= timestamp.

Calls arrive in time order: the timestamp passed to any call is never smaller than the one passed to the previous call.

Each test is a list of operations and a matching list of arguments; the expected output lists every return value, with null for the constructor and for hit.

Example 1

Input: ["HitCounter","hit","hit","hit","hit","getHits","getHits","getHits"] [[],[5],[10],[10],[200],[304],[305],[306]] Output: [null,null,null,null,null,4,3,3] Why: At time 305 the window covers seconds 6 through 305, so the hit at second 5 has expired while the hits at 10, 10 and 200 still count.

Example 2

Input: ["HitCounter","hit","hit","hit","getHits","hit","getHits","getHits"] [[],[100],[100],[100],[100],[250],[399],[400]] Output: [null,null,null,null,3,null,4,1] Why: Hits that share a timestamp all count. At time 400 the window starts after second 100, so everything from second 100 has expired.

Example 3

Input: ["HitCounter","getHits","hit","getHits"] [[],[1],[1],[1]] Output: [null,0,null,1] Why: A query before any hit returns 0.

Constraints

  • 1 <= timestamp <= 2 · 109
  • Timestamps passed to successive calls never decrease.
  • At most 3000 calls are made to hit and getHits combined.

Pattern Path · Design · problem 165 of 169

Once a hit is older than 300 seconds it can never count again, because time only moves forward.
Store hit timestamps in arrival order. Where do the expired ones sit in that sequence?
Use a queue: append on hit, and in getHits pop from the front while the oldest timestamp is <= timestamp - 300. The queue length is the answer.
Target: O(1) amortised per call time, O(hits in the last 300 seconds) extra space.

Start simple

Keep every hit forever and scan the whole list on each getHits call: O(total hits) per query and unbounded memory.

The key idea

Because timestamps never decrease, the hits form a sorted sequence and expiry always happens at the oldest end. That is exactly the shape of a queue.

hit(t) appends t to the back. getHits(t) first removes hits from the front while they are at or before t - 300. Those hits are outside the window now, and since later queries only have larger timestamps, they will never be inside a window again, so throwing them away is safe. Whatever remains is precisely the set of hits in the window, so the answer is the queue length.

Each hit is appended once and removed at most once, so the total work over all calls is linear and each call is amortised O(1). Memory holds only the hits from the last 300 seconds.

If a single second can contain millions of hits, switch to 300 buckets indexed by t % 300, each storing the second it represents and a count. Then memory is fixed at 300 entries and getHits sums the buckets whose second is still inside the window.

Complexity

  • Time: O(1) amortised per call
  • Space: O(hits in the last 300 seconds)

Python

from collections import deque

class HitCounter:
    WINDOW = 300

    def __init__(self):
        self.hits = deque()          # timestamps of hits, oldest first

    def hit(self, timestamp):
        self.hits.append(timestamp)

    def getHits(self, timestamp):
        # Hits at or before timestamp - 300 can never count again: drop them.
        while self.hits and self.hits[0] <= timestamp - self.WINDOW:
            self.hits.popleft()
        return len(self.hits)

JavaScript

class HitCounter {
  constructor() {
    this.hits = [];   // timestamps of hits, oldest first
    this.head = 0;    // index of the oldest hit still in the window
  }

  hit(timestamp) {
    this.hits.push(timestamp);
  }

  getHits(timestamp) {
    // Hits at or before timestamp - 300 can never count again: skip them.
    while (this.head < this.hits.length && this.hits[this.head] <= timestamp - 300) {
      this.head++;
    }
    return this.hits.length - this.head;
  }
}

What the interviewer is checking

It tests whether you exploit the time-ordered input to discard stale data, and whether you can discuss memory trade-offs when traffic is heavy.

Mistakes to avoid

  • Off-by-one on the window edge: a hit exactly 300 seconds old has expired (timestamp - 300 is excluded).
  • Counting distinct timestamps instead of hits when several hits share a second.
  • Only evicting old hits inside hit(), so getHits after a long quiet period still counts expired hits.
  • Using list.pop(0) in Python, which makes each eviction O(n); use a deque or a moving head index.

Follow-ups to expect

  • What if one second can have millions of hits? (300 buckets of (second, count).)
  • What if calls come from many threads? (Lock or per-shard counters.)
  • How would you support any window length? (Prefix counts plus binary search.)

In real software

Rate limiters, request-per-minute dashboards and abuse detection all keep sliding-window counts like this.

Practice next

Next Post Previous Post
No Comment
Add Comment
comment url