981. Time Based Key-Value Store
Problem Description
You need to design a time-based key-value data structure that can store values with timestamps and retrieve them based on timestamp queries.
The data structure should support the following operations:
-
Initialize: Create an empty
TimeMap
object. -
Set operation: Store a key-value pair along with a timestamp. The method
set(key, value, timestamp)
takes:key
: a string identifiervalue
: a string value to storetimestamp
: an integer representing when this value was stored
-
Get operation: Retrieve a value for a given key at or before a specific timestamp. The method
get(key, timestamp)
should:- Find the value that was set with the largest timestamp less than or equal to the given
timestamp
- Return that value if it exists
- Return an empty string
""
if no such value exists
- Find the value that was set with the largest timestamp less than or equal to the given
Key characteristics:
- Multiple values can be stored for the same key at different timestamps
- When querying, you want the most recent value that doesn't exceed the query timestamp
- If the query timestamp is earlier than any stored timestamp for that key, return
""
- The timestamps for each key are guaranteed to be set in strictly increasing order (this is implied by the solution's use of binary search)
Example scenario:
If you set ("foo", "bar1", 1)
and ("foo", "bar2", 4)
, then:
get("foo", 3)
returns"bar1"
(timestamp 1 ≤ 3, and it's the latest available)get("foo", 5)
returns"bar2"
(timestamp 4 ≤ 5, and it's the latest available)get("foo", 0)
returns""
(no timestamp ≤ 0)
Intuition
The core challenge is efficiently finding the most recent value that doesn't exceed a given timestamp. This naturally suggests we need to maintain some ordering of timestamps for each key.
Let's think about what happens when we store values:
- Each key can have multiple values at different timestamps
- We need to keep track of both the timestamp and value together
- The timestamps are inserted in increasing order (a critical observation)
For retrieval, we need to find the largest timestamp that is ≤
the query timestamp. This is essentially asking: "What's the most recent value before or at this time?"
This problem pattern - finding the largest element not exceeding a target value in a sorted collection - is a classic binary search scenario. Since timestamps come in increasing order, we can maintain a sorted list of (timestamp, value)
pairs for each key.
The key insight is using a hash table where:
- Keys map to lists of
(timestamp, value)
tuples - These lists remain sorted by timestamp naturally (since timestamps are inserted in increasing order)
For the get
operation, we can use binary search to find the right position. Specifically, we want to find where our query timestamp would fit in the sorted list, then look at the element just before that position. This gives us the largest timestamp ≤
our query.
The solution uses bisect_right
with a clever trick: searching for (timestamp, chr(127))
. Since chr(127)
is the highest ASCII character, this tuple will be greater than any (timestamp, actual_value)
pair with the same timestamp. bisect_right
returns the insertion point after all equal elements, so we get the position right after the last valid timestamp. Subtracting 1 gives us the desired element.
If the returned index is 0, it means no timestamp is ≤
our query timestamp, so we return an empty string.
Learn more about Binary Search patterns.
Solution Approach
The solution uses a hash table combined with binary search to efficiently store and retrieve time-based key-value pairs.
Data Structure Design
We use a defaultdict(list)
called ktv
(key-timestamp-value) where:
- Each key maps to a list of tuples
(timestamp, value)
- The list for each key remains sorted by timestamp since insertions happen in chronological order
Implementation Details
1. Initialization (__init__
)
self.ktv = defaultdict(list)
Creates an empty hash table that automatically initializes new keys with empty lists.
2. Set Operation (set
)
self.ktv[key].append((timestamp, value))
Simply appends the (timestamp, value)
tuple to the list for the given key. Since timestamps are guaranteed to be in increasing order, the list remains sorted without any additional work.
3. Get Operation (get
)
The retrieval uses binary search to find the appropriate value:
if key not in self.ktv:
return ''
tv = self.ktv[key]
i = bisect_right(tv, (timestamp, chr(127)))
return tv[i - 1][1] if i else ''
Let's break down the binary search logic:
- First, check if the key exists; return empty string if not
bisect_right(tv, (timestamp, chr(127)))
finds the insertion point for our search tuple- Why
chr(127)
? When comparing tuples, Python compares element by element. Sincechr(127)
is the highest ASCII character,(timestamp, chr(127))
will be greater than any(timestamp, actual_value)
pair with the same timestamp bisect_right
returns the index where we'd insert to maintain sorted order - this is the position after all elements with timestamp≤
our query timestamp- If
i > 0
, thentv[i-1]
contains the largest timestamp not exceeding our query, so we returntv[i-1][1]
(the value part) - If
i == 0
, no timestamp is small enough, so we return empty string
Time Complexity
set
: O(1) - simple append operationget
: O(log n) where n is the number of timestamps for the key - binary search- Space: O(m × n) where m is the number of unique keys and n is the average number of timestamps per key
The solution elegantly leverages the fact that timestamps arrive in order, eliminating the need for sorting while maintaining the ability to perform efficient binary searches.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the TimeMap works:
Step 1: Initialize and Set Values
tm = TimeMap()
tm.set("user1", "active", 1)
tm.set("user1", "away", 3)
tm.set("user1", "offline", 7)
After these operations, our internal data structure looks like:
ktv = { "user1": [(1, "active"), (3, "away"), (7, "offline")] }
Step 2: Query at timestamp 5
result = tm.get("user1", 5)
Let's trace through the binary search:
- We have the list:
[(1, "active"), (3, "away"), (7, "offline")]
- We search for
(5, chr(127))
usingbisect_right
- Binary search compares:
(5, chr(127))
vs(3, "away")
: Since 5 > 3, look right(5, chr(127))
vs(7, "offline")
: Since 5 < 7, we found our insertion point
bisect_right
returns index 2 (where we'd insert to keep sorted order)- We return
tv[2-1][1]
=tv[1][1]
="away"
Step 3: Query at timestamp 10
result = tm.get("user1", 10)
- Search for
(10, chr(127))
in[(1, "active"), (3, "away"), (7, "offline")]
- Since 10 is greater than all timestamps,
bisect_right
returns index 3 - We return
tv[3-1][1]
=tv[2][1]
="offline"
Step 4: Query at timestamp 0
result = tm.get("user1", 0)
- Search for
(0, chr(127))
in[(1, "active"), (3, "away"), (7, "offline")]
- Since 0 is less than all timestamps,
bisect_right
returns index 0 - Since index is 0, we return
""
(no valid value exists)
Step 5: Add another key and query
tm.set("user2", "online", 2)
result = tm.get("user2", 5)
Now our structure is:
ktv = { "user1": [(1, "active"), (3, "away"), (7, "offline")], "user2": [(2, "online")] }
- For
get("user2", 5)
: - Search for
(5, chr(127))
in[(2, "online")]
bisect_right
returns index 1 (after the single element)- We return
tv[1-1][1]
=tv[0][1]
="online"
This example demonstrates how the binary search efficiently finds the most recent value not exceeding the query timestamp, handling edge cases like queries before the first timestamp or after the last timestamp.
Solution Implementation
1from collections import defaultdict
2from bisect import bisect_right
3
4class TimeMap:
5 def __init__(self):
6 # Dictionary to store key -> list of (timestamp, value) tuples
7 # Each key maps to a list of timestamp-value pairs
8 self.key_to_time_value = defaultdict(list)
9
10 def set(self, key: str, value: str, timestamp: int) -> None:
11 """
12 Stores the key with the value at the given timestamp.
13 Timestamps are strictly increasing for each key.
14
15 Args:
16 key: The key to store
17 value: The value associated with the key
18 timestamp: The timestamp when this key-value pair is set
19 """
20 self.key_to_time_value[key].append((timestamp, value))
21
22 def get(self, key: str, timestamp: int) -> str:
23 """
24 Returns the value associated with the key at the largest timestamp <= given timestamp.
25 If no such timestamp exists, returns empty string.
26
27 Args:
28 key: The key to search for
29 timestamp: The timestamp to search at or before
30
31 Returns:
32 The value at the largest timestamp <= given timestamp, or empty string if not found
33 """
34 # If key doesn't exist, return empty string
35 if key not in self.key_to_time_value:
36 return ''
37
38 # Get the list of (timestamp, value) pairs for this key
39 time_value_list = self.key_to_time_value[key]
40
41 # Use binary search to find the rightmost position where we could insert (timestamp, chr(127))
42 # chr(127) is used as a sentinel value that's lexicographically greater than any normal string
43 # This ensures we get the rightmost occurrence if there are duplicate timestamps
44 insertion_index = bisect_right(time_value_list, (timestamp, chr(127)))
45
46 # If insertion_index is 0, no timestamp <= given timestamp exists
47 # Otherwise, the element at insertion_index - 1 is the largest timestamp <= given timestamp
48 return time_value_list[insertion_index - 1][1] if insertion_index else ''
49
50
51# Your TimeMap object will be instantiated and called as such:
52# obj = TimeMap()
53# obj.set(key,value,timestamp)
54# param_2 = obj.get(key,timestamp)
55
1/**
2 * Time-based Key-Value Store implementation.
3 * Stores key-value pairs with timestamps and retrieves values based on timestamp queries.
4 */
5class TimeMap {
6 // Map structure: key -> (timestamp -> value)
7 // TreeMap is used for efficient timestamp-based lookups
8 private Map<String, TreeMap<Integer, String>> keyToTimestampValueMap;
9
10 /**
11 * Initializes the TimeMap data structure.
12 */
13 public TimeMap() {
14 this.keyToTimestampValueMap = new HashMap<>();
15 }
16
17 /**
18 * Stores the key-value pair with the given timestamp.
19 *
20 * @param key the key to store
21 * @param value the value associated with the key
22 * @param timestamp the timestamp when this key-value pair is stored
23 */
24 public void set(String key, String value, int timestamp) {
25 // Create a new TreeMap for the key if it doesn't exist, then add the timestamp-value pair
26 keyToTimestampValueMap
27 .computeIfAbsent(key, k -> new TreeMap<>())
28 .put(timestamp, value);
29 }
30
31 /**
32 * Retrieves the value associated with the key at the largest timestamp <= given timestamp.
33 *
34 * @param key the key to look up
35 * @param timestamp the query timestamp
36 * @return the value at the largest timestamp <= given timestamp, or "" if not found
37 */
38 public String get(String key, int timestamp) {
39 // Check if the key exists in the map
40 if (!keyToTimestampValueMap.containsKey(key)) {
41 return "";
42 }
43
44 // Get the TreeMap of timestamp-value pairs for this key
45 TreeMap<Integer, String> timestampValueMap = keyToTimestampValueMap.get(key);
46
47 // Find the largest timestamp that is <= the query timestamp
48 Integer floorTimestamp = timestampValueMap.floorKey(timestamp);
49
50 // Return the corresponding value if found, otherwise return empty string
51 return floorTimestamp == null ? "" : timestampValueMap.get(floorTimestamp);
52 }
53}
54
55/**
56 * Your TimeMap object will be instantiated and called as such:
57 * TimeMap obj = new TimeMap();
58 * obj.set(key,value,timestamp);
59 * String param_2 = obj.get(key,timestamp);
60 */
61
1class TimeMap {
2public:
3 TimeMap() {
4 // Constructor - no initialization needed as unordered_map is default constructed
5 }
6
7 void set(string key, string value, int timestamp) {
8 // Add the timestamp-value pair to the list associated with the key
9 // Timestamps are guaranteed to be strictly increasing for each key
10 keyToTimestampValues[key].emplace_back(timestamp, value);
11 }
12
13 string get(string key, int timestamp) {
14 // Get reference to the list of timestamp-value pairs for this key
15 auto& timestampValuePairs = keyToTimestampValues[key];
16
17 // Create a dummy pair with the target timestamp and a maximum string value
18 // Using ASCII 127 ensures this pair is greater than any real value at the same timestamp
19 pair<int, string> targetPair = {timestamp, string({127})};
20
21 // Find the first element greater than targetPair
22 // This binary search works because the pairs are sorted by timestamp (guaranteed by problem)
23 auto upperBoundIter = upper_bound(timestampValuePairs.begin(),
24 timestampValuePairs.end(),
25 targetPair);
26
27 // If upper_bound returns begin(), no timestamp <= target timestamp exists
28 // Otherwise, return the value of the previous element (largest timestamp <= target)
29 return upperBoundIter == timestampValuePairs.begin() ? "" : (upperBoundIter - 1)->second;
30 }
31
32private:
33 // Maps each key to a vector of (timestamp, value) pairs
34 // The vector is sorted by timestamp in ascending order
35 unordered_map<string, vector<pair<int, string>>> keyToTimestampValues;
36};
37
38/**
39 * Your TimeMap object will be instantiated and called as such:
40 * TimeMap* obj = new TimeMap();
41 * obj->set(key,value,timestamp);
42 * string param_2 = obj->get(key,timestamp);
43 */
44
1// Maps each key to an array of [timestamp, value] pairs
2// The array is sorted by timestamp in ascending order
3const keyToTimestampValues: Map<string, Array<[number, string]>> = new Map();
4
5function set(key: string, value: string, timestamp: number): void {
6 // Add the timestamp-value pair to the array associated with the key
7 // Timestamps are guaranteed to be strictly increasing for each key
8 if (!keyToTimestampValues.has(key)) {
9 keyToTimestampValues.set(key, []);
10 }
11 keyToTimestampValues.get(key)!.push([timestamp, value]);
12}
13
14function get(key: string, timestamp: number): string {
15 // Get the array of timestamp-value pairs for this key
16 const timestampValuePairs = keyToTimestampValues.get(key);
17
18 // If key doesn't exist, return empty string
19 if (!timestampValuePairs) {
20 return "";
21 }
22
23 // Binary search to find the largest timestamp <= target timestamp
24 let left = 0;
25 let right = timestampValuePairs.length - 1;
26 let result = "";
27
28 while (left <= right) {
29 const mid = Math.floor((left + right) / 2);
30 const [midTimestamp, midValue] = timestampValuePairs[mid];
31
32 if (midTimestamp <= timestamp) {
33 // This timestamp is valid, store its value and search for a larger valid timestamp
34 result = midValue;
35 left = mid + 1;
36 } else {
37 // This timestamp is too large, search in the left half
38 right = mid - 1;
39 }
40 }
41
42 return result;
43}
44
45/**
46 * Your functions will be called as such:
47 * set(key, value, timestamp);
48 * const param2 = get(key, timestamp);
49 */
50
Time and Space Complexity
Time Complexity:
-
set
operation:O(1)
- The operation appends a tuple to a list stored in a hash table. Hash table access isO(1)
and list append isO(1)
amortized, resulting in overallO(1)
time complexity. -
get
operation:O(log n)
wheren
is the number of timestamps stored for a given key. The operation involves:- Hash table lookup:
O(1)
- Binary search using
bisect_right
:O(log n)
- Overall:
O(log n)
- Hash table lookup:
Space Complexity:
O(n)
wheren
is the total number ofset
operations performed. Eachset
operation stores a new(timestamp, value)
tuple in the hash table, and all these entries are maintained in memory. The hash table itself and the lists within it collectively require space proportional to the number of stored entries.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Using bisect_left
Instead of bisect_right
The Issue:
A common mistake is using bisect_left
instead of bisect_right
. This can lead to incorrect results when dealing with exact timestamp matches.
Why it's wrong:
# Incorrect approach
insertion_index = bisect_left(time_value_list, (timestamp, ''))
return time_value_list[insertion_index][1] if insertion_index < len(time_value_list) and time_value_list[insertion_index][0] <= timestamp else ''
This fails because:
bisect_left
returns the leftmost insertion point- You'd need additional logic to check if the found element's timestamp is actually <= the query timestamp
- The code becomes more complex and error-prone
The correct solution uses bisect_right
:
insertion_index = bisect_right(time_value_list, (timestamp, chr(127)))
return time_value_list[insertion_index - 1][1] if insertion_index else ''
Pitfall 2: Incorrect Tuple Comparison in Binary Search
The Issue: Using just the timestamp for binary search without considering tuple comparison rules:
# Incorrect - might not handle edge cases properly insertion_index = bisect_right([t for t, v in time_value_list], timestamp)
Why it's wrong:
- This creates an unnecessary list comprehension
- Loses the direct mapping between index and value
- Less efficient due to list creation
The solution:
Use a tuple with a sentinel value chr(127)
to ensure proper comparison. When Python compares tuples, it compares element by element, so (timestamp, chr(127))
will always be greater than any (timestamp, actual_value)
with the same timestamp.
Pitfall 3: Not Handling Empty Results Correctly
The Issue: Forgetting to check if the binary search returns index 0:
# Incorrect - will cause index error or return wrong value
insertion_index = bisect_right(time_value_list, (timestamp, chr(127)))
return time_value_list[insertion_index - 1][1] # Fails when insertion_index is 0
Why it's wrong:
- When
insertion_index
is 0, it means all stored timestamps are greater than the query timestamp - Accessing
time_value_list[-1]
would incorrectly return the last element
The solution:
Always check if insertion_index
is 0 before accessing the previous element:
return time_value_list[insertion_index - 1][1] if insertion_index else ''
Pitfall 4: Not Leveraging the Sorted Nature of Timestamps
The Issue: Attempting to sort the list on each insertion or retrieval:
# Inefficient approach
def set(self, key, value, timestamp):
self.ktv[key].append((timestamp, value))
self.ktv[key].sort() # Unnecessary!
Why it's wrong:
- The problem guarantees timestamps are inserted in strictly increasing order
- Sorting adds O(n log n) complexity to each insertion
- Wastes computational resources
The solution: Trust the problem constraints and simply append to the list. The list naturally maintains sorted order due to the timestamp guarantee.
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!