981. Time Based Key-Value Store

981. Time Based Key-Value Store

Problem Solving - Day 6

ยท

8 min read

Hello, reader ๐Ÿ‘‹๐Ÿฝ ! Welcome to day 6 of the series on Problem Solving. Through this series, I aim to pick up at least one question everyday and share my approach for solving it.

Today, I will be picking up LeetCode's daily challenge problem: Time Based Key-Value Store.


๐Ÿค” Problem Statement

Design a time-based key-value data structure that can store multiple values for the same key at different time stamps and retrieve the key's value at a certain timestamp.

We have to implement the TimeMap class as follows:

  • TimeMap() Initialises the object of the data structure.
  • 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 a value such that set was called previously, with timestamp_prev <= timestamp. If there are multiple such values, it returns the value associated with the largest timestamp_prev. If there are no values, it returns "".

Link to the question: Time Based Key-Value Store.


  • According to the problem, we need to design a data structure that stores items in key => [timestamp => value] pairs.
  • Here a single key could have multiple mappings of timestamp => value.
  • Intuition says that we map every value to the given time stamp, which can be done using a normal hash map, and also map every key to multiple timestamp => value pairs.
  • Hence, we could use something like a 2D hash map that allows two-level access. First to the string key that is mapped to timestamp -> value and second to integer timestamp that is mapped to string value.
  • Something like this:

timestamp-key-value-pairs.png

  • Now, this is the initial set up. But what about the set and get operations?

Set Operation

  • The set operation is straightforward. Given a key, timestamp, and value, We check if the key exists:

    1. Yes ? Then we get the timestamp => value list associated to the given key and add the new timestamp => value.
    2. No? Then we create add a new key to the store and then for that key, we associate a new pair of timestamp => value.

    set_operation.png

Get Operation

  • The get operation is not straightforward as it looks like. There are some things to consider:

    1. What if the given key is not a part of the store?
      • Then we need to return "".
    2. What if the given key is present, but the given timestamp is not present?
      • Then we need to return a value whose time (let's denote this by timestamp') where timestamp' < timestamp for the given string key.
        • What if a timestamp' is not present at all?
          • Then we need to return "". This would be the case when the the given timestamp is smaller than all the timestamps for the given string key.
    3. What if the given key and timestamp are both present?
      • Then we simply return the value associated with the given timestamp and key.

    get_operation.png

To find a timestamp' < timestamp we will have to iterate through the list of timestamp => value pairs for the given key and select the appropriate value.

Now let's code this out!

๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป Solution 1

  • Below is the code for the above approach:
class TimeMap {
    // store by key => [timestamp => value]
    Map<String, Map<Integer, String>> store;

    public TimeMap() {
        store = new HashMap<>();
    }

    public void set(String key, String value, int timestamp) {
        // If the store already contains the key
        if(store.containsKey(key)) {
            store.get(key).put(timestamp, value);
        }
        // If not, add a mew key and [timestamp => value] pair for it
        else {
            Map<Integer, String> keyValue = new HashMap<>();
            keyValue.put(timestamp, value);
            store.put(key, keyValue);
        }
    }

    public String get(String key, int timestamp) {
        int maxTimeStamp = -1;

        // if the key is present
        if(store.containsKey(key)) {
            // Then find a key such that it's the largest stamp 
            // smaller than given time stamp
            for(int k: store.get(key).keySet()) {
                if(timestamp == k) {
                    return store.get(key).get(timestamp);
                }
                maxTimeStamp = k < timestamp ? Math.max(maxTimeStamp, k) : maxTimeStamp;
            }
        }

        // If no timestamp found
        if(maxTimeStamp > timestamp || maxTimeStamp == -1) {
            return "";
        }

        return store.get(key).get(maxTimeStamp);
    }
}

/**
 * Your TimeMap object will be instantiated and called as such:
 * TimeMap obj = new TimeMap();
 * obj.set(key,value,timestamp);
 * String param_2 = obj.get(key,timestamp);
 */
Time Complexity: 
    - Set Operation: O(1)
       Constant look up for value, constant time for insertion
    - Get Operation: O(n) 
        n => number of `timestamp => value` pairs. In the worst case, all the pairs are associated with the same key, and we will have to traverse the entire list to find the value mapped to timestamp.
Space Complexity: O(n)
        n => number of `timestamp => value` pairs.
  • The issue with the previous approach is in the get operation which is O(n).
  • Because we know that the timestamp for every value is strictly increasing, the pairs associated with every value will be sorted.
  • We can use binary search to improve time complexity from O(n) to O(log n)`.
  • But to use binary search, we need to convert the pair of timestamp, value to an array or list.
  • Since the size of the list of pairs is dynamic, we can use the dynamic list supported by languages (ArrayList in Java).
  • We can store every <timestamp, value> as a pair. (Here implemented using the Pair class in Java).

๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป Solution 2

  • Below is the code with binary search implemented in get operation:
class TimeMap {
    HashMap<String, ArrayList<Pair<Integer, String>>> store;
    public TimeMap() {
        store = new HashMap();
    }

    // Every <timestamp, value> pair will be in sorted order
    // as timestamp is strictly increasing
    public void set(String key, String value, int timestamp) {
        if(!store.containsKey(key)) {
            store.put(key, new ArrayList());
        }
        store.get(key).add(new Pair(timestamp, value));
    }

    public String get(String key, int timestamp) {
        // When there is no key, return ""
        if(!store.containsKey(key)) {
            return "";
        }

        ArrayList<Pair<Integer, String>> innerList = store.get(key);
        int left = 0;
        int right = innerList.size()-1;

        // If the first key is lesser than the timestamp value, we return ""
        if(timestamp < innerList.get(left).getKey()) {
            return "";
        }

        // If the last value is less than or equal to the timestamp, we return ""
        if(timestamp >= innerList.get(right).getKey()) {
            return innerList.get(right).getValue();
        }

        // search for the timestamp within bounds [0, size-1]        
        while(left <= right) {
            int mid = (left+right)/2;

            if(innerList.get(mid).getKey() == timestamp) {
                return innerList.get(mid).getValue();
            }
            if(innerList.get(mid).getKey() < timestamp) {
                left = mid + 1;
            }
            else {
                right = mid - 1;
            }
        }

        return innerList.get(right).getValue();
    }
}

/**
 * Your TimeMap object will be instantiated and called as such:
 * TimeMap obj = new TimeMap();
 * obj.set(key,value,timestamp);
 * String param_2 = obj.get(key,timestamp);
 */
Time Complexity: 
    - Set Operation: O(1)
       Constant look up for value, constant time for insertion
    - Get Operation: O(log n) 
        n => number of `timestamp => value` pairs. In the worst case, all the pairs are associated with the same key, and we will have to perform binary search on the list to find the value mapped to timestamp.
Space Complexity: O(n)
        n => number of `timestamp => value` pairs.

๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป Alternative Solution 2 (Java)

  • Instead of using a dynamic list, we can also use the following data structure HashMap<String, TreeMap<Integer, String>>.
  • The TreeMap class stores key value pairs in the sorted order.
  • It also provides a method floorKey(int key) that searches and returns a value <= key.
  • Hence this method can be used instead of converting the inner map to a dynamic list and performing binary search on it.
  • Below is the code using TreeMap:
class TimeMap {
    Map<String, TreeMap<Integer, String>> store;
    public TimeMap() {
        store = new HashMap<>();
    }

    public void set(String key, String value, int timestamp) {
        if(!store.containsKey(key)) {
            TreeMap<Integer, String> innerMap = new TreeMap<>();
            store.put(key, innerMap);
        }
        store.get(key).put(timestamp, value);
    }

    public String get(String key, int timestamp) {
        if(!store.containsKey(key)) {
            return "";
        }

        int maxTimestamp = -1;

        TreeMap<Integer, String> innerMap = store.get(key);

        int firstKey = innerMap.firstKey();
        int lastKey = innerMap.lastKey();

        // If the given timestamp is less than the smallest key => then you return empty string
        if(firstKey > timestamp) {
            return "";
        }

        // If the time stamp is present, return the value mapped to it
        if(innerMap.containsKey(timestamp)) {
            return innerMap.get(timestamp);
        }

        // If the last key is smaller than time stamp, then you just return it.
        if(lastKey < timestamp) {
            return innerMap.get(lastKey);
        }

        // else return the greatest value smaller than timestamp
        Integer timestamp_prev = innerMap.floorKey(timestamp);

        return timestamp_prev == null ? "" : innerMap.get(timestamp_prev);
    }
}

/**
 * Your TimeMap object will be instantiated and called as such:
 * TimeMap obj = new TimeMap();
 * obj.set(key,value,timestamp);
 * String param_2 = obj.get(key,timestamp);
 */
Time Complexity: 
    - Set Operation: O(1)
       Constant look up for value, constant time for insertion
    - Get Operation: O(log n) 
        n => number of `timestamp => value` pairs. In the worst case, all the pairs are associated with the same key, and we will have to perform binary search on the list to find the value mapped to timestamp.
Space Complexity: O(n)
        n => number of `timestamp => value` pairs.

Link to the GitHub repo for solution.

Conclusion

That's a wrap for today's problem. If you liked my explanation then please do drop a like/ comment. Also, please correct me if I've made any mistakes or if you want me to improve something!

Thank you for reading!