Start learning free

LESSON 345 OF 572 · BINARY SEARCH

Time Based Key-Value Store in C#: O(log n) Binary Search

Practice O(log n) Read the full explanation

A time-based key-value store keeps every value a key has ever had, each with the time it was set. You can then ask what a key's value was at any moment: the value that was set most recently at or before that moment.

Implement the class TimeMap:

  • TimeMap() creates an empty store.
  • set(key, value, timestamp) stores the string value for the string key at the time timestamp. The key's earlier values are kept.
  • get(key, timestamp) returns the value that was set for key with the largest timestamp that is still <= timestamp. If no value was set for key at or before timestamp, or key was never set at all, it returns the empty string "".

The input is a list of operations, applied in order to one new TimeMap. The output lists what the get calls return, in order; an empty string is shown as "". The starter code spells these names the way your language does.

 

Example 1:

Input:
    set("foo", "bar", 1)
    get("foo", 1)
    get("foo", 3)
    set("foo", "bar2", 4)
    get("foo", 4)
    get("foo", 5)
Output: bar bar bar2 bar2
Explanation:
At time 1, "foo" was set to "bar", so get("foo", 1) is "bar".
Nothing was set for "foo" between 1 and 3, so at time 3 the value is still "bar".
At time 4, "foo" was set to "bar2": from then on, get returns "bar2", at time 4 and at time 5.

Example 2:

Input:
    set("love", "high", 10)
    set("love", "low", 20)
    get("love", 5)
    get("love", 10)
    get("love", 15)
    get("love", 20)
    get("love", 25)
    get("hate", 25)
Output: "" high high low low ""
Explanation:
At time 5, "love" had no value yet, so the answer is the empty string.
At times 10 and 15 the latest value is "high"; at 20 and 25 it is "low".
"hate" was never set, so its answer is the empty string too.

Example 3:

Input:
    set("a", "x", 1)
    set("b", "y", 2)
    set("a", "z", 3)
    get("a", 2)
    get("b", 1)
    get("b", 2)
    get("a", 3)
Output: x "" y z
Explanation: Each key keeps its own values. At time 2, "a" still has "x": "z" comes only at time 3. "b" gets its first value at time 2, so at time 1 it has none.

 

Constraints:

  • 1 <= key.length, value.length <= 20
  • key and value consist of lowercase English letters and digits.
  • 1 <= timestamp <= 107
  • The timestamps of the set calls are strictly increasing: each set has a larger timestamp than every set before it.
  • At most 105 calls in total are made to set and get.

Note:

set should run in O(1) time and get in O(log n) time, where n is the number of values set for that key. Looking through all of a key's values in get would take O(n) time per call.

STEP 1 OF 13
    Ctrl + Enter to check

    C# · BINARY SEARCH

    Time Based Key Value Store in C#, explained

    Problem overview

    You write the class TimeMap. Set(key, value, timestamp) stores a string value for a string key at a given time and keeps the key's earlier values. Get(key, timestamp) returns the value set for that key with the largest timestamp that is still less than or equal to the one asked about. If the key had no value yet at that time, or was never set, Get returns the empty string.

    The timestamps of the Set calls are strictly increasing, which makes the whole design possible. Here is the first example, with what each Get returned:

    Set("foo", "bar", 1)
    Get("foo", 1)  gives  bar
    Get("foo", 3)  gives  bar
    Set("foo", "bar2", 4)
    Get("foo", 4)  gives  bar2
    Get("foo", 5)  gives  bar2
    

    Between times 1 and 3 nothing changed, so the value stays bar. A value set exactly at the time asked about counts, which is why Get("foo", 4) already returns bar2.

    In the second example, "love" is set to "high" at 10 and to "low" at 20. Asking at times 5, 10, 15, 20 and 25 gives "", "high", "high", "low" and "low". Asking for the never-set key "hate" gives "".

    Approach: sorted lists and binary search

    Each key gets its own list of (timestamp, value) pairs. A loop through the list in Get works but costs O(n) per call, where n is the number of values set for that key.

    The lists are sorted for free. Every Set has a larger timestamp than every Set before it, so a new pair always belongs at the end of its key's list. Set is a plain Add, O(1), with no sorting.

    A sorted list can be searched by halving. Get asks for the last pair whose timestamp is at most the given one. That is a binary search with one twist: when the middle pair is old enough, it is a candidate answer, but a later pair might also qualify, so remember the middle value and keep searching to the right. When the middle pair is too new, so is every pair after it, so search to the left. Each pass halves the positions left to check, so Get takes O(log n): about 17 passes for 100,000 values.

    Here is the search over four pairs with timestamps 10, 20, 30, 40, asking for time 35:

    left=0 right=3 middle=1 ts=20 -> keep "low", go right
    left=2 right=3 middle=2 ts=30 -> keep "mid", go right
    left=3 right=3 middle=3 ts=40 -> go left
    result mid
    

    The values at those positions were "high", "low", "mid" and "end". The search keeps the best candidate found so far and ends with the pair at timestamp 30, the latest one not after 35.

    Step by step

    1. Declare store, a Dictionary<string, List<(int timestamp, string value)>> set to new(). The element type is a tuple with two named items, read as .timestamp and .value. new() creates an object of the type the field already names.
    2. In Set, create the key's list if the key is new, then add (timestamp, value) to it.
    3. In Get, return "" if the key was never set.
    4. Put the key's list in entries (the same list, not a copy) and declare result as the empty string. If no value is old enough, result stays empty and that is the answer.
    5. Set left to 0 and right to entries.Count - 1, both inclusive, and loop while left <= right. With <, the last remaining position would never be checked.
    6. Compute middle as (left + right) / 2. Integer division rounds down, so it is a valid position.
    7. If entries[middle].timestamp is <= timestamp, save entries[middle].value in result and set left to middle + 1. Otherwise set right to middle - 1.
    8. After the loop, return result.

    The solution in C#

    class TimeMap {
        Dictionary<string, List<(int timestamp, string value)>> store = new();
    
        public void Set(string key, string value, int timestamp) {
            if (!store.ContainsKey(key)) {
                store[key] = new();
            }
            store[key].Add((timestamp, value));
        }
    
        public string Get(string key, int timestamp) {
            if (!store.ContainsKey(key)) {
                return "";
            }
            List<(int timestamp, string value)> entries = store[key];
            string result = "";
            int left = 0, right = entries.Count - 1;
            while (left <= right) {
                int middle = (left + right) / 2;
                if (entries[middle].timestamp <= timestamp) {
                    result = entries[middle].value;
                    left = middle + 1;
                } else {
                    right = middle - 1;
                }
            }
            return result;
        }
    }
    

    On the C# side: a tuple such as (int timestamp, string value) is a value type with named fields, and (timestamp, value) in Add builds one. Reading store[key] for a missing key would throw a KeyNotFoundException, which is why both methods check ContainsKey first. The expression (left + right) / 2 could overflow for lists with more than a billion entries, but here a key holds at most 100,000 values.

    Complexity

    • Set: O(1), apart from the occasional growth of a list or the dictionary.
    • Get: O(log n), where n is the number of values set for that key. The dictionary lookup is O(1) and the search halves its range each pass.
    • Space: O(total number of Set calls), since every value is kept.

    Edge cases and common mistakes

    • Asking before the first value. Every pair is newer than the time asked about, the search only moves left, and result stays "". In the second example, Get("love", 5) does this.
    • A key that was never set. There is no list for it; return "" before indexing.
    • < instead of <=. A value set exactly at the time asked about must count.
    • Stopping at the first match. A match is only the latest one so far. Keep searching to the right.
    • Each key has its own list. In the third example, "a" is set to "x" at 1 and to "z" at 3, and "b" to "y" at 2. Asking "a" at time 2 gives x, "b" at time 1 gives "", "b" at time 2 gives y, and "a" at time 3 gives z.
    Keep going with C#500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free