This is a design problem rather than a single-answer algorithm, so there is no input array to trace β the interesting part is the data layout that makes get fast. The optimal solution below steps through that layout directly: the per-key store rows, and the binary search get runs over them.
Store and Linear Scan
Time set: O(1), get: O(n)Space O(n)For each key, keep a list of (timestamp, value) pairs in the order they were set. Since the problem guarantees timestamps for a given key are strictly increasing, that list comes for free already sorted by timestamp. A correct get can simply scan the list from the most recent entry backwards and return the first value whose timestamp is <= timestamp. This is correct, but for a key with many versions, every get call can cost O(n).
class TimeMap: def __init__(self): self.store: dict[str, list[tuple[int, str]]] = {}
def set(self, key: str, value: str, timestamp: int) -> None: self.store.setdefault(key, []).append((timestamp, value))
def get(self, key: str, timestamp: int) -> str: result = "" for ts, val in reversed(self.store.get(key, [])): if ts <= timestamp: result = val break return resultBinary Search Over Stored Timestamps
OptimalTime set: O(1), get: O(log n)Space O(n)The scan in get is wasteful precisely because the list is already sorted by timestamp β that is a binary search opportunity. Keep the timestamps and values for each key in parallel, already-sorted lists (appending is O(1) since new timestamps are always larger than existing ones), then binary search for the rightmost timestamp that does not exceed the query. That is the same βfind the last index satisfying a conditionβ binary search used in find-minimum-in-rotated-sorted-array, just applied to a per-key timestamp list instead of the whole input array.
class TimeMap: def __init__(self): self.timestamps: dict[str, list[int]] = {} self.values: dict[str, list[str]] = {}
def set(self, key: str, value: str, timestamp: int) -> None: self.timestamps.setdefault(key, []).append(timestamp) self.values.setdefault(key, []).append(value)
def get(self, key: str, timestamp: int) -> str: ts_list = self.timestamps.get(key, []) val_list = self.values.get(key, []) lo, hi = 0, len(ts_list) - 1 result = "" while lo <= hi: mid = (lo + hi) // 2 if ts_list[mid] <= timestamp: result = val_list[mid] # candidate answer, but a later index might be closer lo = mid + 1 else: hi = mid - 1 return resultThe mechanism is the layout plus the search acting together, so the trace below replays the statement example on the real store (trace data lives in this folderβs data.ts). Watch lo/mid/hi narrow the row, the mid chip being compared, and the candidate upgrade each time a newer entry satisfies the <= query condition β the returned value is always the rightmost chip whose timestamp does not exceed the query:
store["foo"] Β· pairs already sorted by timestamp
every other key gets its own rowThe store starts empty. Each key will own two parallel lists β `timestamps` and `values` β where one index in both lists describes one set call. Timestamps for a key are strictly increasing, so each list is born sorted, and that sorted order is what turns `get` from a scan into a binary search.
All four get results above β bar, bar, bar2, bar2 β match the expected output [null, null, "bar", "bar", null, "bar2", "bar2"].
Why itβs correct: because timestamps for a key are strictly increasing, the stored list is sorted, so βthe value with the largest timestamp_prev <= timestampβ is exactly the rightmost element satisfying ts_list[mid] <= timestamp. The loop keeps every index that satisfies the condition as a candidate (result = val_list[mid]) while continuing to search further right for a better one, and discards indices that fail the condition along with everything after them. Complexity: set only appends, O(1). get binary searches a list of at most n timestamps, giving O(log n) time; storage is O(n) across all keys.