981. Time Based Key Value Store - Explanation

Problem Link

Description

Implement a time-based key-value data structure that supports:

  • Storing multiple values for the same key at specified time stamps
  • Retrieving the key's value at a specified timestamp

Implement the TimeMap class:

  • TimeMap() Initializes the object.
  • void set(String key, String value, int timestamp) Stores the key key with the value value at the given time timestamp.
  • String get(String key, int timestamp) Returns the most recent value of key if set was previously called on it and the most recent timestamp for that key prev_timestamp is less than or equal to the given timestamp (prev_timestamp <= timestamp). If there are no values, it returns "".

Note: For all calls to set, the timestamps are in strictly increasing order.

Example 1:

Input:
["TimeMap", "set", ["alice", "happy", 1], "get", ["alice", 1], "get", ["alice", 2], "set", ["alice", "sad", 3], "get", ["alice", 3]]

Output:
[null, null, "happy", "happy", null, "sad"]

Explanation:
TimeMap timeMap = new TimeMap();
timeMap.set("alice", "happy", 1);  // store the key "alice" and value "happy" along with timestamp = 1.
timeMap.get("alice", 1);           // return "happy"
timeMap.get("alice", 2);           // return "happy", there is no value stored for timestamp 2, thus we return the value at timestamp 1.
timeMap.set("alice", "sad", 3);    // store the key "alice" and value "sad" along with timestamp = 3.
timeMap.get("alice", 3);           // return "sad"

Constraints:

  • 1 <= key.length, value.length <= 100
  • key and value only include lowercase English letters and digits.
  • 1 <= timestamp <= 1000


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(1) time for set(), O(logn) time for get(), and O(m * n) space, where n is the total number of values associated with a key, and m is the total number of keys.


Hint 1

Can you think of a data structure that is useful for storing key-value pairs? Perhaps a hash-based data structure where we not only store unique elements but also associate additional information with each element?


Hint 2

We store key-value pairs in a hash map. In this case, we store the keys as usual, but instead of a single value, we store a list of values, each paired with its corresponding timestamp. This allows us to implement the set() method in O(1). How can you leverage this hash map to implement the get() method?


Hint 3

A brute force solution would involve linearly going through every value associated with the key and returning the most recent value with a timestamp less than or equal to the given timestamp. This approach has a time complexity of O(n) for each get() call. Can you think of a better way? Since the timestamps in the value list are sorted in ascending order by default, maybe an efficient searching algorithm could help.


Hint 4

We can use binary search because the timestamps in the values list are sorted in ascending order. This makes it straightforward to find the value with the most recent timestamp that is less than or equal to the given timestamp.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Hash maps/dictionaries - Storing key-value pairs with efficient lookup
  • Binary search - Finding the largest timestamp less than or equal to the query in O(log n)
  • Sorted data structures - Maintaining timestamps in sorted order for efficient searching

1. Brute Force

Intuition

We want to store values for a key along with timestamps, and when someone asks for a value at a given time, we must return the latest value set at or before that timestamp.

The brute-force idea is:
store everything as-is, and while retrieving, look through all timestamps for that key and pick the best match.
It's easy to implement, but slow because we scan all timestamps every time we call get().

Algorithm

  1. Use a dictionary to map each key to another dictionary of timestamps → list of values.
  2. When setting a value:
    • Insert the value under the corresponding timestamp.
  3. When getting a value:
    • If the key does not exist, return an empty string.
    • Otherwise, loop through all timestamps for that key.
    • Track the largest timestamp ≤ given timestamp.
    • Return the value stored at that timestamp.
  4. If no such timestamp exists, return an empty string.
class TimeMap:

    def __init__(self):
        self.keyStore = {}

    def set(self, key: str, value: str, timestamp: int) -> None:
        if key not in self.keyStore:
            self.keyStore[key] = {}
        if timestamp not in self.keyStore[key]:
            self.keyStore[key][timestamp] = []
        self.keyStore[key][timestamp].append(value)

    def get(self, key: str, timestamp: int) -> str:
        if key not in self.keyStore:
            return ""
        seen = 0

        for time in self.keyStore[key]:
            if time <= timestamp:
                seen = max(seen, time)
        return "" if seen == 0 else self.keyStore[key][seen][-1]

Time & Space Complexity

  • Time complexity: O(1)O(1) for set()set() and O(n)O(n) for get()get().
  • Space complexity: O(mn)O(m * n)

Where nn is the total number of unique timestamps associated with a key and mm is the total number of keys.


2. Binary Search (Sorted Map)

Intuition

For each key, we store all its (timestamp, value) pairs in sorted order by timestamp.

When we call get(key, timestamp), we don’t want to scan everything.
Instead, we want to quickly find the largest timestamp ≤ given timestamp for that key.

Because the timestamps are sorted, we can use binary search to find this position in O(log n) time:

  • If we find an exact match, return its value.
  • Otherwise, return the value at the closest smaller timestamp.
  • If there is no smaller or equal timestamp, return "".

So the idea is:

  • Per key → keep timestamps sorted.
  • On get → binary search over those timestamps.

Algorithm

  1. Maintain a map:
    key -> sorted list of (timestamp, value) (or two parallel arrays: one for timestamps, one for values).
  2. set(key, value, timestamp):
    • Insert (timestamp, value) into the list for that key, keeping timestamps in sorted order.
    • (If timestamps are always added in increasing order, you can just append.)
  3. get(key, timestamp):
    • If key does not exist, return "".
    • Let times be the sorted list of timestamps for this key.
    • Use binary search on times to find the rightmost index i such that times[i] ≤ timestamp.
    • If such an index exists:
      • Return the value associated with times[i].
    • Otherwise:
      • Return "" (no value was set at or before that time).
from sortedcontainers import SortedDict

class TimeMap:
    def __init__(self):
        self.m = defaultdict(SortedDict)

    def set(self, key: str, value: str, timestamp: int) -> None:
        self.m[key][timestamp] = value

    def get(self, key: str, timestamp: int) -> str:
        if key not in self.m:
            return ""

        timestamps = self.m[key]
        idx = timestamps.bisect_right(timestamp) - 1

        if idx >= 0:
            closest_time = timestamps.iloc[idx]
            return timestamps[closest_time]
        return ""

Time & Space Complexity

  • Time complexity: O(n)O(n) or O(logn)O(\log n) for set()set() depending on the language and O(logn)O(\log n) for get()get().
  • Space complexity: O(mn)O(m * n)

Where nn is the total number of values associated with a key and mm is the total number of keys.


3. Binary Search (Array)

Intuition

Each key stores its values in the order they were inserted, and timestamps are guaranteed to be increasing for each key.
This means we can keep a simple list of (value, timestamp) pairs for every key.

To answer a get(key, timestamp) query, we only need to find the latest timestamp that is ≤ the given timestamp.
Because timestamps are sorted, we can use binary search to quickly find this position instead of scanning everything.

This gives an efficient and clean approach:
store values in arrays, then binary-search timestamps when retrieving.

Algorithm

  1. Use a dictionary:
    • key → list of [value, timestamp]
    • Timestamps for each key are stored in sorted order (because they arrive in increasing order).
  2. set(key, value, timestamp):
    • Append [value, timestamp] to the key’s list.
  3. get(key, timestamp):
    • If the key does not exist, return "".
    • Let arr be the list of [value, timestamp] pairs.
    • Perform binary search on timestamps to find the rightmost timestamp t ≤ timestamp.
    • If found, return the corresponding value.
    • If not found, return "".
class TimeMap:

    def __init__(self):
        self.keyStore = {}  # key : list of [val, timestamp]

    def set(self, key: str, value: str, timestamp: int) -> None:
        if key not in self.keyStore:
            self.keyStore[key] = []
        self.keyStore[key].append([value, timestamp])

    def get(self, key: str, timestamp: int) -> str:
        res, values = "", self.keyStore.get(key, [])
        l, r = 0, len(values) - 1
        while l <= r:
            m = (l + r) // 2
            if values[m][1] <= timestamp:
                res = values[m][0]
                l = m + 1
            else:
                r = m - 1
        return res

Time & Space Complexity

  • Time complexity: O(1)O(1) for set()set() and O(logn)O(\log n) for get()get().
  • Space complexity: O(mn)O(m * n)

Where nn is the total number of values associated with a key and mm is the total number of keys.


Common Pitfalls

A common mistake is searching for an exact timestamp match instead of finding the largest timestamp less than or equal to the query. Binary search should find the rightmost value satisfying timestamp <= query, not an exact match. If no exact match exists but earlier timestamps do, returning an empty string is incorrect.

Binary search boundaries are tricky. Using bisect_left instead of bisect_right, or not adjusting the index after the search, leads to returning values from timestamps greater than the query. Always verify your binary search returns the correct floor value by testing edge cases like querying before any set operation.

Returning Empty String When Key Exists But Timestamp Is Too Early

When a key exists but all stored timestamps are greater than the query timestamp, the correct behavior is to return an empty string. Some implementations incorrectly return the earliest stored value instead. Always check that your found index is valid (non-negative) before accessing the value.