Facebook Pixel

981. Time Based Key-Value Store

MediumDesignHash TableStringBinary Search
Leetcode Link

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:

  1. Initialize: Create an empty TimeMap object.

  2. Set operation: Store a key-value pair along with a timestamp. The method set(key, value, timestamp) takes:

    • key: a string identifier
    • value: a string value to store
    • timestamp: an integer representing when this value was stored
  3. 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

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)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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. Since chr(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, then tv[i-1] contains the largest timestamp not exceeding our query, so we return tv[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 operation
  • get: 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 Evaluator

Example 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)) using bisect_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 is O(1) and list append is O(1) amortized, resulting in overall O(1) time complexity.

  • get operation: O(log n) where n 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)

Space Complexity:

  • O(n) where n is the total number of set operations performed. Each set 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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More