We are building a rate limiter for log messages. When a message arrives, we need to decide: has this exact message been printed within the last 10 seconds? If yes, suppress it. If no (or we have never seen it), print it and record the timestamp.
The important detail is the 10-second window. If "foo" was last printed at time 1, it can be printed again at time 11 or later, but not at time 10. The check is currentTimestamp >= lastTimestamp + 10, or equivalently, currentTimestamp - lastTimestamp >= 10.
Since messages arrive in chronological order, we never receive a message with a timestamp earlier than one we have already processed. We can overwrite a message's stored timestamp without corrupting any earlier decision.
The operations we need are fast lookup by message string and storage of one timestamp per message. A hash map provides both in O(1).
0 <= timestamp <= 10^9 -- Timestamps fit in a 32-bit signed integer, and timestamp + 10 stays well below the 2^31 - 1 limit, so there is no overflow risk in the cooldown check.timestamp is non-decreasing -- We never receive out-of-order messages, so we do not need to handle retroactive changes to a rate-limit window. This ordering also lets the sliding-window approach below evict expired entries from one end only.1 <= message.length <= 30 -- Messages are short strings, so hashing one is a bounded-cost operation we can treat as O(1).10^4 calls -- An O(n) scan per call would reach 10^8 operations, which is borderline. A hash map brings each call to O(1).Store every printed (timestamp, message) pair. When a new message arrives, scan the stored pairs to find whether the same message was printed within the last 10 seconds.
This wastes both time and space. We keep every printed pair even though only the most recent occurrence of each message affects the decision, and we re-scan the entire history on every call.
shouldPrintMessage(timestamp, message) call:timestamp - foundTimestamp < 10), return false.Input:
For each call, we scan the stored list backward to find the most recent occurrence of the same message. If that occurrence is within 10 seconds, reject; otherwise add the pair and allow.
(1, "foo"): list empty, no match. Add (1, "foo"). Return true. List: [(1,"foo")].(2, "bar"): scan back, no "bar". Add (2, "bar"). Return true. List: [(1,"foo"),(2,"bar")].(3, "foo"): scan back, match at timestamp 1. 3 - 1 = 2 < 10, still in cooldown. Return false. List unchanged.(8, "bar"): scan back, match at timestamp 2. 8 - 2 = 6 < 10. Return false. List unchanged.(10, "foo"): scan back, match at timestamp 1. 10 - 1 = 9 < 10. Return false. List unchanged.(11, "foo"): scan back, match at timestamp 1. 11 - 1 = 10, not less than 10, so the cooldown has expired. Add (11, "foo"). Return true.The returned sequence is [true, true, false, false, false, true].
Output:
The linear search exists only because a list cannot find a specific message's last print time directly. A hash map keyed by the message string removes that scan, dropping each call to O(1).
Use a hash map where the key is the message string and the value is the next allowed timestamp for that message. When a message arrives at time t, check whether t is at least the stored next-allowed time. If the message is not in the map, it has never been printed, so allow it.
When a message is allowed, set its next-allowed timestamp to t + 10. Each subsequent call then needs one map lookup and one comparison, with no scanning and no stored history.
Storing the cooldown expiry rather than the last-seen time makes the check a single comparison, timestamp >= map.get(message), instead of timestamp - map.get(message) >= 10. Both are equivalent; the expiry form avoids a subtraction.
The approach relies on timestamps arriving in non-decreasing order. Once we write map[message] = t + 10, no later call can carry a timestamp smaller than t, so the stored expiry can never become stale relative to a future call. A message is allowed exactly when its previous expiry has passed, which is the definition of the 10-second cooldown. If timestamps could arrive out of order, a late-arriving earlier event might need to overwrite a larger stored value, and a single timestamp per message would no longer suffice.
shouldPrintMessage(timestamp, message) call:timestamp < nextAllowed, return false (still in cooldown).map[message] = timestamp + 10.The space bound names the limitation of this approach: it stores one entry per distinct message and never removes any of them. A message printed once at timestamp 5 keeps its map entry forever, even if it never appears again. For a bounded test that is fine, but a logger that runs for months over a large vocabulary of distinct messages accumulates an entry per message and the map grows without bound.
The cooldown only depends on messages seen in the last 10 seconds. Anything older than timestamp - 10 can never cause a rejection again, so its entry is dead weight. The next approach bounds memory to the messages active within the current window.
A message can only suppress a future message if it was printed within the last 10 seconds. So the only state worth keeping is the set of messages printed in the current 10-second window. Track that set, and as time advances, discard messages whose print time has fallen out of the window.
A queue holds the printed (timestamp, message) events in arrival order. Because timestamps are non-decreasing, the oldest events sit at the front, so expiry happens from the front only. A companion set holds the messages currently in the queue, giving O(1) membership checks. On each call, first evict everything older than the window, then test membership.
(timestamp, message) events and a set of the messages currently in the queue.shouldPrintMessage(timestamp, message) call:frontTimestamp <= timestamp - 10, remove the front event and erase its message from the set. (An event printed at frontTimestamp blocks reprints until frontTimestamp + 10; once timestamp >= frontTimestamp + 10, it can no longer block anything.)message is in the set, it was printed within the window, so return false.(timestamp, message) to the back of the queue, add message to the set, and return true.Because messages arrive in non-decreasing timestamp order, a single message is never in the queue more than once: it is only enqueued when absent from the set, and it stays in the set until evicted.
We trace the same calls. The queue holds events still inside the window; inWindow is the set of their messages.
(1, "foo"): queue empty, nothing to evict. "foo" not in set. Enqueue. Queue: [(1,foo)], set {foo}. Return true.(2, "bar"): front is (1,foo); 1 <= 2 - 10 = -8? No, keep it. "bar" not in set. Enqueue. Queue: [(1,foo),(2,bar)], set {foo,bar}. Return true.(3, "foo"): evict check 1 <= 3 - 10 = -7? No. "foo" is in set. Return false. State unchanged.(8, "bar"): evict check 1 <= 8 - 10 = -2? No. "bar" is in set. Return false.(10, "foo"): evict check 1 <= 10 - 10 = 0? No. "foo" is in set. Return false.(11, "foo"): evict check 1 <= 11 - 10 = 1? Yes. Remove (1,foo), set becomes {bar}. Next front (2,bar): 2 <= 1? No, stop. "foo" not in set now. Enqueue. Queue: [(2,bar),(11,foo)], set {bar,foo}. Return true.The returned sequence is [true, true, false, false, false, true], matching the other approaches. After the last call the queue holds only (2,bar) and (11,foo), not the rejected attempts, so memory tracks the active window rather than all history.
Approach 2 is the standard answer for the LeetCode constraints because it is shorter and the bounded 10^4 calls make its unbounded growth a non-issue. Approach 3 is what a long-running service would use, since its memory is tied to recent activity instead of total history. The tradeoff is more state (a queue plus a set) and amortized rather than strict O(1) per call.