Problem overview
The class Solution models a collection of integers that starts empty and supports three operations:
insert(value)adds one occurrence ofvalue.remove(value)removes one occurrence ofvalue.count(value)returns how many occurrences ofvalueare in the collection.
In the example, the calls insert(5), insert(2), insert(5), count(5), remove(5), count(5) produce 2 and then 1: there are two 5s at first, and one after the removal.
The statement asks for O(1) time per operation and O(n) space.
Approach: store a count for each value
Keeping a list of all the numbers would make count and remove scan it, which is O(n). What matters is only how many times each value occurs, and its order never matters. A hash map from a value to its number of occurrences, a frequency table, answers all three operations in constant time on average:
insertadds 1 to the value's count.removesubtracts 1, but only when the count is positive.countreads the count, with 0 for a value never seen.
The map has one entry per distinct value that has been inserted, so the space is linear in the number of values.
This problem is also different from most in this chapter: it is not a single function but a small class. The three methods are called one at a time, in any order, so they need a place to keep the state between calls. In Scala that place is a field of the class, created once for each Solution object and shared by all of its methods.
Other structures give the same answers more slowly. A sorted list could find a value in O(log n) time but still has to shift elements to insert or remove one. The hash map is the simplest choice that reaches O(1) for all three operations.
Step by step
- Create the table in the class body.
val freqis an emptyscala.collection.mutable.Map[Int, Int]. Statements in a class body run when an object is created, so no constructor is needed, and every method shares the same table. Thevalfixes which mapfreqrefers to; its contents can still change. - Write
count. Its body isfreq.getOrElse(value, 0). A value that was never inserted reports 0. This is the last expression of the method, so it is the return value. - Write
insert. Assigncount(value) + 1tofreq(value). Usingcountkeeps the "missing means 0" rule in one place. - Write
remove. Wrap the update inif (count(value) > 0). Removing a value that isn't there then does nothing. - Decrement inside the
if. Assigncount(value) - 1tofreq(value).
The solution in Scala
class Solution {
val freq = scala.collection.mutable.Map[Int, Int]()
def count(value: Int): Int = {
freq.getOrElse(value, 0)
}
def insert(value: Int): Unit = {
freq(value) = count(value) + 1
}
def remove(value: Int): Unit = {
if (count(value) > 0) {
freq(value) = count(value) - 1
}
}
}
Following the example through, freq goes from empty to 5 -> 1, then 5 -> 1, 2 -> 1, then 5 -> 2, 2 -> 1. Now count(5) returns 2. After remove(5) the entry is 5 -> 1, and count(5) returns 1.
The Scala details worth knowing:
- Reading a missing key.
freq(value)throws aNoSuchElementExceptionwith the messagekey not found: 99when the key is absent.getOrElse(value, 0)takes a fallback instead, which is whycountuses it. - Index assignment.
freq(value) = xis shorthand forfreq.update(value, x). It adds the key or replaces the old value. - The result type.
insertandremovereturnUnit, Scala's "nothing useful" result, because they work by changing the table. - Each object has its own table. The map is created per
Solutionobject. A newSolutionstarts empty, andcount(5)on it returns 0.
Complexity
Time: O(1) per operation on average. Each method does a constant number of hash-map lookups and updates. The hash map gives constant time on average, rather than in the worst case.
Space: O(n). The map holds at most one entry per distinct value inserted.
Edge cases and common mistakes
- Counting a value never inserted.
count(7)returns 0 because ofgetOrElse. - Removing a value that isn't there.
remove(7)does nothing, andcount(7)still returns 0. Without theif, the table would store -1 for it, and the nextinsertwould produce 0 instead of 1. - Removing more times than inserted. After
insert(5)and two calls toremove(5), the count is 0, not -1. - Entries stay at zero. Removing the last occurrence leaves the key in the map with count 0, for example
HashMap(2 -> 1, 5 -> 0). The counts stay correct. If you want the map to shrink, callfreq.remove(value)when the count reaches 0. - Using a list. A list or array makes
countandremovelinear, which breaks theO(1)requirement. - Making
freqa local variable. Each method would get its own table, and nothing would be remembered between calls.
Testing
- The example sequence returns 2, then 1 for the two
count(5)calls. - On a fresh object,
count(5)returns 0. remove(7)followed bycount(7)returns 0.insert(5)thenremove(5)twice, thencount(5), returns 0.
