Problem overview
A TimeMap remembers every value a key has ever had, together with the time it was set. set(key, value, timestamp) stores a value. get(key, timestamp) returns the value that was set for that key with the largest timestamp that is still less than or equal to the one asked about. If the key has no value at or before that time, or was never set, get returns the empty string.
The timestamps of the set calls strictly increase: each set has a larger timestamp than every set before it. The statement wants set in O(1) and get in O(log n), where n is the number of values set for that key.
Take set("foo", "bar", 1). Then get("foo", 1) is "bar", and so is get("foo", 3), because nothing else was set in between. After set("foo", "bar2", 4), both get("foo", 4) and get("foo", 5) return "bar2". The value set exactly at the asked time counts. A call such as get("foo", 0) returns an empty string: nothing existed yet.
Approach: sorted lists and a binary search
Each key keeps a list of (timestamp, value) pairs. A get could scan the list from the end until it finds a timestamp that is not after the asked one. That is correct, but it costs O(n) per call when the answer is near the start.
The list has a property worth using: it is sorted by timestamp. Because the set timestamps only go up, every new pair belongs at the end, so appending keeps the list sorted with no sorting and set takes O(1). A sorted list can be searched by halving, and that makes get take O(log n).
This is a binary search for the last position whose timestamp is at most the asked one. At each step look at the middle pair:
- if its timestamp is at most the asked one, it is a valid answer, and so far the best. Every position before it is older, so only a later position could beat it. Save its value and search the right half;
- otherwise it was set too late, and so was everything after it. Search the left half.
When the range is empty, the saved value is the answer. If no middle pair ever qualified, the saved value is still the empty string, which is the right answer. For a store with 100,000 values for one key, a search takes about 17 passes through the loop.
Step by step
- Declare
storeas avarof type[String: [(timestamp: Int, value: String)]], set to[:]. The tuple has named parts, so a pair reads as.timestampand.value. - In
set, append(timestamp: timestamp, value: value)tostore[key, default: []]. The default gives a new key an empty array, so the first value needs no special case. - In
get, return""ifstore[key]isnil: a key that was never set has no value at any time. - Store the key's array in
entrieswithstore[key]!. The check above makes the!safe. - Declare
resultas avarset to"". It holds the best value found so far. - Set the search bounds:
leftis 0 andrightisentries.count - 1, both inclusive. - Loop while
left <= right. Use<=so that a range with one position left is still checked. - Compute
middleas(left + right) / 2. Integer division rounds down, so it is a valid position. - If
entries[middle].timestamp <= timestamp, saveentries[middle].valueinresult. - Still inside, set
left = middle + 1to look for a later qualifying value. - Otherwise set
right = middle - 1, since the middle pair and everything after it is too new. - Return
result.
The solution in Swift
class TimeMap {
var store: [String: [(timestamp: Int, value: String)]] = [:]
func set(_ key: String, _ value: String, _ timestamp: Int) {
store[key, default: []].append((timestamp: timestamp, value: value))
}
func get(_ key: String, _ timestamp: Int) -> String {
if store[key] == nil {
return ""
}
let entries = store[key]!
var result = ""
var left = 0, right = entries.count - 1
while left <= right {
let middle = (left + right) / 2
if entries[middle].timestamp <= timestamp {
result = entries[middle].value
left = middle + 1
} else {
right = middle - 1
}
}
return result
}
}
The Swift details worth a closer look:
- Dictionary lookups return optionals.
store[key]has the type[(timestamp: Int, value: String)]?. Comparing it withnilworks even though the tuples aren'tEquatable. The!instore[key]!would crash on a missing key, and the check just above rules that out. default:inset.store[key, default: []].append(...)changes the array inside the dictionary directly. For a missing key it starts from an empty array and stores the result.- A copy of the array.
let entries = store[key]!gives a constant view of the array. Swift arrays are value types, and the copy is cheap because the storage is shared until something changes it. Nothing does here. - Several variables in one
var.var left = 0, right = entries.count - 1declares both at once. For an empty array,rightis -1 and the loop doesn't run. - The middle.
(left + right) / 2is safe from overflow here, since positions are at most 100,000 andIntis 64 bits. - The method named
setandget. They are ordinary method names in Swift. They are only special inside a property declaration, so a class can have methods with these names.
Complexity
Time. set appends to an array and updates a dictionary: O(1) on average. get does one dictionary lookup and then a binary search that halves the range each pass, so it takes O(log n) for the n values of that key.
Space. O(N) for all the N pairs ever set, which is the data the class has to remember. get uses a few extra integers.
Edge cases and common mistakes
- A key that was never set. With "love" set at times 10 and 20,
get("hate", 25)returns an empty string. - A time before the first value.
get("love", 5)returns an empty string, as no pair qualified. Times 10 and 15 return"high", and times 20 and 25 return"low". - A time exactly equal to a set time. Use
<=, not<. A value set at the asked time counts. - The loop condition
left < right. It skips the last remaining position. With one pair,set("foo", "bar", 1), the callget("foo", 1)would return an empty string, and with two pairs a value at the last position can be missed. - Returning at the first match. Several values can be at or before the asked time, and the lesson wants the latest. Stopping at the first qualifying middle can return an older value, so the search keeps going right.
- Keys are independent. After
set("a", "x", 1),set("b", "y", 2)andset("a", "z", 3),get("a", 2)is"x",get("b", 1)is an empty string, andget("b", 2)is"y". - Scanning the list. A loop over all the pairs is correct but
O(n)per call.
Testing
set("foo", "bar", 1), thenget("foo", 1)andget("foo", 3)givebarandbar. Afterset("foo", "bar2", 4),get("foo", 4)andget("foo", 5)givebar2andbar2.- With
set("love", "high", 10)andset("love", "low", 20), the callsget("love", 5),get("love", 10),get("love", 15),get("love", 20),get("love", 25)give an empty string,high,high,low,low. - With 100,000 values
v1tov100000set at times 10, 20, ..., 1,000,000,get("k", 5)is an empty string,get("k", 10)isv1,get("k", 555)isv55andget("k", 5000000)isv100000.
