Problem overview
The task is to build a collection of integers that starts empty and supports three operations: insert(value) adds one occurrence of a number, remove(value) takes one occurrence away, and count(value) returns how many occurrences of that number the collection currently holds. The same number can be inserted several times, so this is a multiset, not a set.
In the example, insert(5), insert(2) and insert(5) leave two 5s and one 2. The first count(5) returns 2. After remove(5) there is one 5, so the second count(5) returns 1. Every operation must run in O(1) time, with O(n) total space.
Approach: store a frequency for each value
A list of all inserted numbers would make count and remove scan the list, which is O(n). The collection doesn't need to remember the numbers one by one. It only needs to know how many of each there are.
So keep a hash map from each value to its number of occurrences, a frequency table. Looking up, updating and removing a key in a hash map are O(1) on average. Each operation becomes a small change to one entry:
count(value)reads the entry, and a value that was never inserted counts as 0.insert(value)adds 1 to the entry.remove(value)subtracts 1, but only if the count is positive.
The table has at most one entry per distinct value inserted, so the space is O(n).
Step by step
This lesson is a class, Solution, with a property and three methods. The state has to live in the class so that all the methods share it between calls.
- Declare the table as a property.
var freq: HashMap<Int, Int>goes at the top of the class, above theinitblock. It has no initial value yet, which Kotlin allows only if every constructor path assigns it before use. - Assign it in
init. Theinitblock runs when aSolutionis created, andfreq = HashMap()gives the collection its empty starting state. Kotlin infers the type arguments from the declared type offreq. - Write
count. Returnfreq.getOrDefault(value, 0). A plainfreq[value]has typeInt?, which can't be returned from a function declared to returnInt.getOrDefaultreturns a plainIntand gives 0 for an unknown value. - Write
insert. Storecount(value) + 1back withfreq[value] = .... The bracket assignment adds the key if it is new, and overwrites it otherwise. Reusingcountmeans the first insertion starts from 0. - Guard
remove. Wrap the update inif (count(value) > 0). Removing something that isn't there must do nothing. Without the check,removeof an unknown value would store -1. - Subtract one inside the
if:freq[value] = count(value) - 1.
The solution in Kotlin
class Solution {
var freq: HashMap<Int, Int>
init {
freq = HashMap()
}
fun count(value: Int): Int {
return freq.getOrDefault(value, 0)
}
fun insert(value: Int) {
freq[value] = count(value) + 1
}
fun remove(value: Int) {
if (count(value) > 0) {
freq[value] = count(value) - 1
}
}
}
Kotlin details worth knowing:
- Properties and
init. A property declared without an initializer must be assigned in aninitblock before the class can be used. The shorter formval freq = HashMap<Int, Int>()does both jobs in one line; the lesson's two-part form shows howinitworks. freq[value]is nullable. The bracket read returnsInt?, and Kotlin won't let you add 1 to it or return it asInt. The compiler reports that the type isInt?whereIntis expected. This is whycountis the one place that reads the table.map[key] = visput. It is the Kotlin shorthand forput(key, v).- Methods call each other.
insertandremovecallcounton the same object, with no prefix needed. - Functions without a return type return
Unit, soinsertandremoveneed noreturn.
Complexity
Time: O(1) per operation on average. Each method does a constant number of hash-map reads and writes.
Space: O(n). The table holds one entry per distinct value that was ever inserted, which is at most the number of inserts. An entry whose count has dropped to 0 stays in the map. That doesn't affect correctness, since a count of 0 is what a missing entry means too.
Edge cases and common mistakes
- Counting a value never inserted.
count(9)returns 0 and doesn't add an entry. - Removing a value never inserted.
remove(9)does nothing, andcount(9)is still 0, not -1. - Removing more times than inserted. After
insert(2), callingremove(2)twice leaves the count at 0, because the second call fails the guard. - Removing every copy at once.
removetakes away one occurrence only. After twoinsert(5)calls and oneremove(5), the count is 1. - Negative numbers and zero. They work as keys like any other integer.
- Forgetting the default. Using
freq[value]directly incountdoesn't compile, because of the nullable type.
Testing
Starting from an empty collection:
insert(5),insert(2),insert(5), thencount(5)gives2.- Then
remove(5)andcount(5)gives1. remove(9)andcount(9)gives0.remove(2)twice andcount(2)gives0.