Time Based Key-Value Store — HashMap Plus Binary Search for Temporal Queries
Advertisement
Problem Statement
Design a time-based key-value data structure that can store multiple values for the same key at different timestamps and retrieve the value at a given timestamp.
Implement the TimeMap class:
TimeMap()— initialises the data structure.void set(String key, String value, int timestamp)— storeskeywithvalueattimestamp.String get(String key, int timestamp)— returns the value wheresetwas called previously withtimestamp_prev <= timestamp. If multiple values qualify, returns the one with the largesttimestamp_prev. If none qualifies, returns"".
Constraints:
1 <= key.length, value.length <= 100keyandvalueconsist of lowercase English letters and digits1 <= timestamp <= 10^7- All calls to
setuse strictly increasing timestamps - At most
2 × 10^5calls tosetandget
TimeMap timeMap = new TimeMap();
timeMap.set("foo", "bar", 1);
timeMap.get("foo", 1); // returns "bar"
timeMap.get("foo", 3); // returns "bar" (t=1 is latest timestamp <= 3)
timeMap.set("foo", "bar2", 4);
timeMap.get("foo", 4); // returns "bar2"
timeMap.get("foo", 5); // returns "bar2" (t=4 is latest <= 5)Why This Problem Matters
Time Based Key-Value Store (LC 981) is a condensed version of a fundamental real-world system: temporal databases, version control, and event-sourcing. It appears in interviews at Google, Amazon, and Facebook because it combines three important skills: hash map design, binary search, and understanding of temporal queries.
The "largest timestamp <= query timestamp" requirement maps directly to bisect_right in Python, upper_bound in C++, and a right-boundary binary search in other languages. Candidates who reach for linear search O(n) when binary search O(log n) is available immediately signal a gap in their repertoire.
At Google, this problem probes candidates' understanding of time-series data structures. The pattern it teaches — hash map of sorted lists queried with binary search — is the backbone of many time-series databases like InfluxDB and Prometheus, and Apache Cassandra's version columns.
The strictly increasing timestamp guarantee is a critical constraint that justifies appending to the list (no sorting needed) and enables binary search. Interviewers sometimes remove this guarantee to see if candidates notice the change in complexity implications.
The Core Insight
Data structure: A hash map where each key maps to a sorted list of (timestamp, value) pairs. Since set is called with strictly increasing timestamps, appending maintains sorted order automatically — no sorting step needed.
For get(key, timestamp): Find the rightmost entry where timestamp_entry <= timestamp. This is a classic right-boundary binary search: find the last element not greater than the query timestamp.
In Python, bisect.bisect_right(list, (timestamp, chr(127))) - 1 gives the index of the largest timestamp not exceeding the query. If this index is -1 (all timestamps are larger), return "".
set: O(1) amortised — just append.get: O(log n) — binary search over the stored list for this key.
Visual Dry Run
After set("foo","bar",1) and set("foo","bar2",4):
Store: {"foo": [(1,"bar"), (4,"bar2")]}
| Query | Binary search on timestamps [1,4] | Result |
|---|---|---|
| get("foo", 1) | bisect_right finds position 1, idx 0 | "bar" |
| get("foo", 3) | bisect_right finds position 1, idx 0 | "bar" |
| get("foo", 4) | bisect_right finds position 2, idx 1 | "bar2" |
| get("foo", 5) | bisect_right finds position 2, idx 1 | "bar2" |
| get("foo", 0) | bisect_right finds position 0, idx -1 | "" |
Solution (Optimal)
from collections import defaultdict
import bisect
class TimeMap:
def __init__(self):
# key -> list of (timestamp, value), maintained in sorted order
self.store = defaultdict(list)
def set(self, key: str, value: str, timestamp: int) -> None:
# Strictly increasing timestamps: appending keeps the list sorted
self.store[key].append((timestamp, value))
def get(self, key: str, timestamp: int) -> str:
if key not in self.store:
return ""
entries = self.store[key]
# bisect_right with chr(127) finds rightmost timestamp <= query
idx = bisect.bisect_right(entries, (timestamp, chr(127))) - 1
if idx >= 0:
return entries[idx][1]
return ""class TimeMap {
constructor() {
this.store = new Map();
}
set(key, value, timestamp) {
if (!this.store.has(key)) {
this.store.set(key, []);
}
this.store.get(key).push({ timestamp, value });
}
get(key, timestamp) {
if (!this.store.has(key)) return "";
const entries = this.store.get(key);
// Binary search for the largest timestamp <= query
let lo = 0, hi = entries.length - 1, result = -1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (entries[mid].timestamp <= timestamp) {
result = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return result === -1 ? "" : entries[result].value;
}
}Time: set O(1). get O(log n) where n = number of set calls for this key.
Space: O(n) total across all keys and timestamps.
Common Mistakes
- Using
bisect_leftinstead ofbisect_right:bisect_leftfinds the leftmost insert position; after subtracting 1, it may miss an exact timestamp match. Usebisect_rightfor "largest timestamp not exceeding query." - Not handling idx = -1: If the query timestamp is smaller than all stored timestamps,
bisect_rightreturns 0, and0 - 1 = -1. Always checkidx >= 0. - Not handling a missing key: Check for key existence before accessing the store. Return
""for missing keys. - Storing timestamps and values in separate parallel lists: Works but is error-prone. Paired tuples or objects are cleaner and less prone to index-mismatch bugs.
- Assuming timestamps might not be increasing: The problem guarantees strictly increasing timestamps for
set, which justifies appending. If this guarantee were lifted, you would needinsort— O(n) per insert.
Interview Tips
- Point out that the strictly increasing timestamp constraint is what makes
setO(1) — it eliminates the need to sort. - Explain
bisect_rightwith a concrete example before using it. - Note the
chr(127)trick for tuple comparison: it ensures the search finds the rightmost entry at exactly the query timestamp. - Draw the timeline diagram to show what "largest timestamp not exceeding query" means visually.
Follow-up Questions
- What if timestamps are not strictly increasing? Use
bisect.insortto maintain sorted order on insert — O(n) per insert, or use a balanced BST. - How would you add a
delete(key, timestamp)operation? Use tombstone entries — mark deletions and filter duringget. - How would you implement TTL (time-to-live) for entries? Store an expiry timestamp alongside each entry; during
get, skip expired entries. - How would you design this for billions of keys across distributed machines? Consistent hashing for key distribution, replicated sorted lists per node, distributed binary search within each node.
- What if you want exact timestamp matches only? Change the binary search condition to
==and return""if no exact match exists.
Key Takeaways
- Time Based Key-Value Store (LC 981) combines a hash map of sorted lists with binary search for O(1) set and O(log n) get.
- The strictly increasing timestamp guarantee makes
setan O(1) append — no sorting needed. - The query asks for the "largest timestamp not exceeding the query" — this is the right-boundary binary search (
bisect_rightminus 1). - Always check for missing keys and for
idx = -1(query timestamp smaller than all stored timestamps). - The
bisect_right(entries, (timestamp, chr(127)))pattern handles exact timestamp matches correctly. - This design pattern (hash map of sorted lists + binary search) underlies time-series databases like InfluxDB, Prometheus, and Cassandra's version columns.
- For distributed systems, the same design extends to consistent hashing for key placement and per-node binary search for time queries.
Advertisement