Time Based Key-Value Store is a classic interview problem that hides a binary search inside an innocent-looking data structure. It teaches you to spot when a list is secretly sorted and to reach for the largest-value-not-exceeding search.
Problem. Design a store with two operations. set(key, value, timestamp) saves a value under a key
at a given time. get(key, timestamp) returns the value that was set for key with the largest
stored timestamp ≤ the queried timestamp. If there is none, return "". Timestamps for a key are
always set in strictly increasing order.
Example: set(k, 'a', 1), set(k, 'b', 5), set(k, 'c', 10), set(k, 'd', 14), set(k, 'e', 20).
Then get(k, 13) → 'c' (timestamp 10 is the largest one ≤ 13).
The slow way first
The obvious idea: for each key keep a list of (timestamp, value) pairs, and on get scan the whole list to find the largest timestamp that is still ≤ the query. That works, but every get is O(n) in the number of pairs for that key. With many gets on a long history, that is far too slow.
The question to ask: is there any structure I can exploit? Yes — the problem promises timestamps are set in increasing order, so the list is already sorted by timestamp. A sorted list is an invitation to binary search.
The idea: binary search for the best timestamp
For each key store an append-only list of (timestamp, value) pairs. Because timestamps only ever increase, that list stays sorted. On get(key, t), binary-search the timestamps for the largest one ≤ t.
The trick is the comparison. When timestamps[mid] ≤ t, mid is a valid candidate, so we record it and push lo to the right hoping for an even closer match. When timestamps[mid] > t, mid is too big, so we move hi left. Whatever we last recorded is the answer.
The key insight: we never give up the moment we find a fit. We save it and keep searching right, because a later timestamp might be even closer to t while still not exceeding it.
Walk through it
Step through the animation for get(key, 13). The pointers lo, hi, and mid bracket the sorted timestamp row. At mid = 10 we have a hit (10 ≤ 13), so we record value 'c' and go right. At mid = 14 we overshoot (14 > 13), so we go left, the window crosses, and the loop ends — leaving the recorded 'c' as the answer.
Pseudocode
on get(key, t):
look up the sorted (timestamp, value) list for key
lo, hi = 0, len(list) - 1
ans = "" # default if nothing fits
while lo <= hi:
mid = (lo + hi) // 2
if timestamp[mid] <= t:
ans = value[mid] # candidate; try for something closer
lo = mid + 1 # search the right half
else:
hi = mid - 1 # too big, search the left half
return ansThe Python solution
def get(stamps, values, t):
lo, hi = 0, len(stamps) - 1
ans = ""
while lo <= hi:
mid = (lo + hi) // 2
if stamps[mid] <= t:
ans = values[mid]
lo = mid + 1
else:
hi = mid - 1
return ansstampsandvaluesare the sorted, parallel lists stored for one key (in a real store, a dict maps each key to them).ansstarts as""— the answer when no timestamp is small enough.mid = (lo + hi) // 2is the usual binary-search midpoint.- Lines 6-8 are the heart: when
stamps[mid] <= t, mid fits, so we record its value and moveloright to chase a closer timestamp. - When
stamps[mid] > t, mid overshoots, so we drop the right half by movinghileft. - When the loop ends,
ansholds the value for the largest qualifying timestamp.
Complexity
| Case | Time | Notes |
|---|---|---|
| Linear scan per get | O(n) (moderate) | checks every pair for the key |
| Binary search (this solution) | O(log n) (fast) | halves the range each step |
O(n) (moderate)set is O(1) amortized — just append, since timestamps already arrive sorted. get drops from O(n) to O(log n) by binary-searching that sorted list. Storage is O(n) across all pairs.
When this pattern shows up
Whenever a problem asks for the largest value ≤ x (or smallest ≥ x) in a sorted collection, reach for binary search with a recorded candidate. The move — find a fit, save it, then keep narrowing toward a closer one — is the same in floor/ceiling lookups, version control, and rate-limited logs.
Watch the comparison direction. We want the largest timestamp not exceeding t, so the hit case is
<= (not <), and on a hit we go right while still recording the value. Flip either and you return
the wrong neighbor or skip an exact match.
Practice
For timestamps [1, 5, 10, 14, 20], what does get(key, 13) return and why?
1. Why can we binary-search the timestamps instead of scanning them?
2. When stamps[mid] <= t, what do we do?
3. What is the time complexity of get with this approach?
4. What does get return when every stored timestamp is larger than t?