Problem overview
You write a class TimeMap that remembers every value a key has ever had. set(key, value, timestamp) stores a string value for a string key at a given time and keeps the earlier values. get(key, timestamp) returns the value whose timestamp is the largest one that is still at most timestamp. If the key was never set, or all its values are newer than the question, it returns the empty string.
Say "love" is set to "high" at time 10 and to "low" at time 20. Then get("love", 5) is empty, because nothing was set yet. Times 10 and 15 give high, and times 20 and 25 give low. A value set exactly at the asked time counts. A key never set, such as "hate", gives the empty string.
Approach
The simplest design keeps, for each key, a list of (timestamp, value) pairs, and get walks through the whole list to find the best match. That costs O(n) per call, where n is how many values the key has.
The constraints give a lot: the timestamps of the set calls strictly increase. So each new pair belongs at the end of its key's list, and every list is automatically sorted by time with no sorting. A sorted list can be binary searched. The question "the latest timestamp at or before t" is a binary search that, instead of looking for an exact match, remembers the best candidate seen so far:
- if the middle entry's timestamp is at most
t, it is a valid answer, and anything better lies to its right, so remember its value and search the right half; - otherwise the middle entry and everything after it are too new, so search the left half.
When the range is empty, the remembered value is the answer, or the empty string if no entry was old enough. set takes O(1) and get takes O(log n).
Step by step
- One map of lists. The field
storeis aHashMap<String, MutableList<Pair<Int, String>>>: each key maps to its list of pairs. In aPair,.firstis the timestamp and.secondthe value. - Create a list on first use. In
set, if the key is not instore, store an emptymutableListOf()for it. - Append. Add
Pair(timestamp, value)tostore[key]!!. The!!says the lookup is notnull, which theifjust made sure of. The new pair has the largest timestamp so far, so the end is its place. - Unknown key. In
get, if the key is not instore, return"". - Set up the search. Name the key's list
entries(the same list, not a copy), startresultas"", and startleftat 0 andrightatentries.size - 1. Both ends are inclusive. - Loop while
left <= right. With<instead, a range of one entry would be skipped. - Take the middle.
(left + right) / 2; integer division rounds down, so it is a valid position. - Old enough: if
entries[middle].first <= timestamp, saveentries[middle].secondinresultand movelefttomiddle + 1. - Too new: otherwise move
righttomiddle - 1. - Return
result. Each saved match sits to the right of the one before it, soresultends as the value with the largest timestamp not after the question.
The solution in Kotlin
class TimeMap {
val store = HashMap<String, MutableList<Pair<Int, String>>>()
fun set(key: String, value: String, timestamp: Int) {
if (!store.containsKey(key)) {
store[key] = mutableListOf()
}
store[key]!!.add(Pair(timestamp, value))
}
fun get(key: String, timestamp: Int): String {
if (!store.containsKey(key)) {
return ""
}
val entries = store[key]!!
var result = ""
var left = 0
var right = entries.size - 1
while (left <= right) {
val middle = (left + right) / 2
if (entries[middle].first <= timestamp) {
result = entries[middle].second
left = middle + 1
} else {
right = middle - 1
}
}
return result
}
}
Kotlin details worth knowing:
store[key]is nullable. Indexing a map givesnullfor a missing key, so its type isMutableList<Pair<Int, String>>?. The!!operator converts it to the non-null type and would throw if it werenull. The code only uses it right after checking the key.- The names
setandgetare fine. They are soft keywords, meaningful only in property accessors, so the class can havefun set(...)andfun get(...)as ordinary methods called asmap.set(...)andmap.get(...). - Assigning with brackets.
store[key] = mutableListOf()calls the map'sset; Kotlin infers the list's element type fromstore's declared type. - Midpoint and overflow.
(left + right) / 2could overflow anIntonly for lists with more than a billion entries, far above the limit of 100,000 calls here.
Seeing it run
The first part of this code repeats the example above; the second stores five values at times 10, 20, 30, 40 and 50 and asks between and around them.
fun main() {
val map = TimeMap()
map.set("love", "high", 10)
map.set("love", "low", 20)
for (t in intArrayOf(5, 10, 15, 20, 25)) {
println("get(\"love\", $t) = \"${map.get("love", t)}\"")
}
println("get(\"hate\", 25) = \"${map.get("hate", 25)}\"")
val k = TimeMap()
for ((i, v) in listOf("a", "b", "c", "d", "e").withIndex()) {
k.set("k", v, (i + 1) * 10)
}
println(k.get("k", 35))
println(k.get("k", 50))
println("\"" + k.get("k", 9) + "\"")
}
It prints:
get("love", 5) = ""
get("love", 10) = "high"
get("love", 15) = "high"
get("love", 20) = "low"
get("love", 25) = "low"
get("hate", 25) = ""
c
e
""
At time 35 the values stored at 10, 20 and 30 are old enough, and the latest of them is c. At time 50 the last entry matches exactly, so the answer is e. At time 9 no entry is old enough, so result keeps its starting value, the empty string.
Complexity
set: O(1) on average: one hash lookup and a list append.get: O(log n), where n is the number of values stored for that key. Every pass halves the range, so a key with 100,000 values needs at most 17 passes. Other keys do not slow it down, because each key has its own list.- Space: O(total number of
setcalls).
Edge cases and common mistakes
- Exact match. A value set at exactly the asked time counts, so the comparison is
<=, not<. - Stopping at the first match. A normal binary search returns when it finds the target. Here a match is only a candidate, and the search must go right to look for a later one.
- Nothing old enough. If every entry is newer, the search only ever goes left and
resultstays empty. - Key never set. Check for it first; otherwise
store[key]!!throws aNullPointerException. - Scanning the list. A loop from the end of the list is correct but takes O(n) per
get. - Relying on sorted order. The list is sorted only because the timestamps of the
setcalls strictly increase. If they could arrive out of order, the append would break it.
