Skip to content

981. Time Based Key-Value Store (Medium)

Problem

Design a time-based key-value store:

  • set(key, value, timestamp), store the key with the value at the given timestamp.
  • get(key, timestamp), return the value associated with the key whose timestamp is the largest ≤ the query timestamp; if no such record exists, return "".

All set calls for a given key use strictly increasing timestamps.

Example

store.set("foo", "bar", 1)
store.get("foo", 1) // "bar"
store.get("foo", 3) // "bar"
store.set("foo", "bar2", 4)
store.get("foo", 4) // "bar2"
store.get("foo", 5) // "bar2"

LeetCode 981 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, linear scan per get

Per key, keep a list of (timestamp, value) entries. On get, scan linearly to find the largest timestamp ≤ query.

from collections import defaultdict
class TimeMap:
def __init__(self):
self.data = defaultdict(list) # L1: O(1) init
def set(self, key: str, value: str, timestamp: int) -> None:
self.data[key].append((timestamp, value)) # L2: O(1) amortized append
def get(self, key: str, timestamp: int) -> str:
best = ""
for ts, v in self.data[key]: # L3: scan all entries for key, O(n)
if ts <= timestamp: # L4: O(1) compare
best = v
else:
break # L5: early exit (timestamps sorted)
return best

Where the time goes, line by line

Variables: n = number of set() calls for the queried key.

LinePer-call costTimes executedContribution
L2 (set append)O(1)O(1) amortized1 per set callO(1)O(1) per set
L3/L4 (get scan)O(1)O(1)up to nO(n)O(n) per get ← dominates

set is O(1)O(1); get scans entries until it finds a timestamp exceeding the query. With strictly increasing timestamps, the early break at L5 helps in the average case but not the worst case.

Complexity

  • set: O(1)O(1).
  • get: O(n)O(n) where n is entries for that key.
  • Space: O(n)O(n).

Approach 2: Binary search per get (manual implementation)

Since timestamps are strictly increasing per key, per-key lists are sorted. Binary-search them.

from collections import defaultdict
class TimeMap:
def __init__(self):
self.data = defaultdict(list)
def set(self, key: str, value: str, timestamp: int) -> None:
self.data[key].append((timestamp, value)) # L1: O(1) amortized
def get(self, key: str, timestamp: int) -> str:
entries = self.data[key]
lo, hi = 0, len(entries) - 1 # L2: O(1)
result = ""
while lo <= hi: # L3: binary search, O(log n) steps
mid = (lo + hi) // 2 # L4: O(1) midpoint
if entries[mid][0] <= timestamp: # L5: O(1) compare timestamp
result = entries[mid][1] # L6: O(1) record candidate
lo = mid + 1 # L7: O(1) look for larger ts
else:
hi = mid - 1 # L8: O(1) too late, go earlier
return result

Where the time goes, line by line

Variables: n = number of set() calls for the queried key.

LinePer-call costTimes executedContribution
L1 (set append)O(1)O(1) amortized1 per setO(1)O(1) per set
L2 (init bounds)O(1)O(1)1 per getO(1)O(1)
L3-L8 (binary search loop)O(1)O(1)log nO(logn)O(log n) per get ← dominates

Since timestamps for each key are strictly increasing (guaranteed by the problem), the per-key list is sorted, enabling binary search. We track result as we go right, keeping the last valid timestamp found.

Complexity

  • set: O(1)O(1).
  • get: O(logn)O(log n), driven by L3 (binary search over n sorted entries).
  • Space: O(n)O(n).

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: bisect for clean per-key binary search (optimal, idiomatic)

Use Python’s bisect with parallel timestamps and values arrays per key.

from collections import defaultdict
from bisect import bisect_right
class TimeMap:
def __init__(self):
self.timestamps = defaultdict(list)
self.values = defaultdict(list)
def set(self, key: str, value: str, timestamp: int) -> None:
self.timestamps[key].append(timestamp) # L1: O(1) amortized
self.values[key].append(value) # L2: O(1) amortized
def get(self, key: str, timestamp: int) -> str:
ts = self.timestamps[key]
i = bisect_right(ts, timestamp) - 1 # L3: O(log n) binary search
if i < 0: # L4: O(1)
return ""
return self.values[key][i] # L5: O(1) index lookup

Where the time goes, line by line

Variables: n = number of set() calls for the queried key.

LinePer-call costTimes executedContribution
L1/L2 (set appends)O(1)O(1) amortized1 per setO(1)O(1) per set
L3 (bisect_right)O(logn)O(log n)1 per getO(logn)O(log n) per get ← dominates
L4/L5 (bounds check + index)O(1)O(1)1 per getO(1)O(1)

bisect_right(ts, timestamp) returns the insertion point for timestamp in the sorted ts list. Subtracting 1 gives the rightmost entry with timestamp ≤ the query. If i < 0, no entry exists for that key at or before the query.

Complexity

  • set: O(1)O(1) amortized, driven by L1/L2 (list appends).
  • get: O(logn)O(log n), driven by L3 (bisect_right over n sorted timestamps).
  • Space: O(n)O(n).

bisect_right(ts, timestamp) - 1 returns the largest index whose timestamp is ≤ timestamp.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Swift notes

The Swift implementation stores small TimeMapEntry values inside dictionary-owned arrays. values[key, default: []].append(...) mutates the selected array through the dictionary while copy-on-write preserves value semantics. Swift has no standard-library bisect, so Approach 3 packages the same upper-bound invariant into a reusable helper.

Summary

ApproachsetgetSpace
Linear scanO(1)O(1)O(n)O(n)O(n)O(n)
Manual binary searchO(1)O(1)O(logn)O(log n)O(n)O(n)
bisect_rightO(1)O(1)O(logn)O(log n)O(n)O(n)

The bisect version is idiomatic Python; the manual binary search is what you’d write in languages without a bisect-equivalent (Java: Collections.binarySearch; C++: upper_bound).

Test cases

# Quick smoke tests - paste into a REPL or save as test_981.py and run.
# Uses the optimal Approach 3 implementation.
from collections import defaultdict
from bisect import bisect_right
class TimeMap:
def __init__(self):
self.timestamps = defaultdict(list)
self.values = defaultdict(list)
def set(self, key: str, value: str, timestamp: int) -> None:
self.timestamps[key].append(timestamp)
self.values[key].append(value)
def get(self, key: str, timestamp: int) -> str:
ts = self.timestamps[key]
i = bisect_right(ts, timestamp) - 1
if i < 0:
return ""
return self.values[key][i]
def _run_tests():
store = TimeMap()
store.set("foo", "bar", 1)
assert store.get("foo", 1) == "bar"
assert store.get("foo", 3) == "bar" # no entry at 3, returns closest before
store.set("foo", "bar2", 4)
assert store.get("foo", 4) == "bar2"
assert store.get("foo", 5) == "bar2"
assert store.get("foo", 0) == "" # before any entry
assert store.get("missing", 1) == "" # key never set
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Binary Search, monotonic search tactics for cutting a sorted or ordered search space in half until one answer remains.
  • Modified Binary Search, binary-search variants for rotated arrays, peak finding, and data where the ordering is present but disguised.