Problem overview
The lesson asks for a class that keeps a collection of integers, initially empty, and supports three operations:
insert(val)adds one occurrence ofval;remove(val)removes one occurrence ofval;count(val)returns how many occurrences ofvalthe collection holds.
The same number can be inserted several times. In the example, the calls are insert(5), insert(2), insert(5), count(5), remove(5) and count(5). The first count(5) returns 2, because 5 was inserted twice. After one remove(5), the second returns 1.
Every operation must run in O(1) time, and the collection may use O(n) space, where n is the number of distinct values stored.
Approach: store a frequency per value
A list of all inserted numbers would make count and remove scan the whole list, which is O(n). But the collection never needs to remember the order of the numbers or which copy is which. It only needs to know how many copies of each value it holds.
So keep a hash map from each value to its number of occurrences. All three operations then become one lookup or one update:
count(val)reads the stored count, or gives 0 when the value was never stored.insert(val)adds 1 to the count.remove(val)subtracts 1, but only when the count is positive, so a value that isn't in the collection stays at 0.
Hash map reads and writes take O(1) time on average, which gives the required running time.
Step by step
- Declare the property. A TypeScript class must declare its properties, so the first line of the class body is
freq: Record<number, number>;, a map from numbers to numbers. In the constructor,this.freq = {}starts it empty. - Write
count. Returnthis.freq[val] || 0. A missing key reads asundefined, which is falsy, so||turns it into 0. A stored 0 also gives 0, which is correct. - Write
insert. Setthis.freq[val]tothis.count(val) + 1. Going throughcounthandles the first insertion of a value, when nothing is stored yet. - Guard
remove. Wrap the update inif (this.count(val) > 0). Removing a value that isn't there then changes nothing. - Decrease the count. Inside the
if, subtract 1 fromthis.freq[val]. The entry is known to exist and to be positive, so updating it in place is safe.
The solution in TypeScript
class Solution {
freq: Record<number, number>;
constructor() {
this.freq = {};
}
count(val: number): number {
return this.freq[val] || 0;
}
insert(val: number): void {
this.freq[val] = this.count(val) + 1;
}
remove(val: number): void {
if (this.count(val) > 0) {
this.freq[val] -= 1;
}
}
}
The TypeScript details worth a closer look:
Record<number, number>is the type of an object whose keys are numbers and whose values are numbers. Types are erased when the code is compiled to JavaScript, so at run timefreqis just an ordinary object.- Number keys become strings. JavaScript objects store keys as strings, so
freq[5]is stored under"5". Lookups with the same number always find it, and negative numbers work too:-3is stored as"-3". - Why the property declaration is needed. Without the line
freq: Record<number, number>;, the compiler reports thatfreqdoes not exist on the class. A property that is only assigned inside the constructor still has to be declared in the class body. - Missing keys give
undefined. TypeScript typesthis.freq[val]as anumber, but when the key is absent the value at run time isundefined. The type doesn't know that, which is why|| 0matters. - Why
insertusescount.undefined + 1isNaN, andNaNwould then be stored for that key. Starting fromthis.count(val)always begins at a real number. - Counts are not deleted. After the last copy is removed, the key stays with the value 0. That is harmless, because
countreturns 0 either way. ||versus??.||replaces every falsy value,??onlyundefinedandnull. Here both work, because replacing a 0 with 0 changes nothing.
Complexity
Time: O(1) per operation. Each method does a fixed number of object reads and writes, which take constant time on average.
Space: O(n). The object holds one entry per distinct value ever inserted, so it grows with the number of distinct values, not with the number of calls.
Edge cases and common mistakes
- Counting a value never inserted.
count(7)returns 0, thanks to the|| 0. - Removing a value that isn't there. With the guard,
remove(7)does nothing. Without it, an in-place subtraction on the missing entry storesNaN, and removing a value more often than it was inserted pushes its count below 0. - Removing more times than inserting. After
insert(-3)and tworemove(-3)calls,count(-3)is still 0. The second removal sees a count of 0 and skips the update. - Incrementing directly.
this.freq[val] += 1on a missing key givesNaN. This is whyinsertgoes throughcount. - Forgetting that
removeremoves one copy. It lowers the count by 1. It doesn't delete every occurrence of the value. - Using an array of all inserted numbers. Searching it makes
countandremovetakeO(n)time, which breaks the limit.
Testing
- The example calls give counts
2and then1for the value 5. - On a new collection,
count(7)returns0. - On a new collection,
remove(7)followed bycount(7)returns0. insert(-3),remove(-3),remove(-3),count(-3)returns0.