Problem overview
The method canConstruct receives two strings, ransomNote and magazine. It returns true if the note can be built from letters of the magazine, and false otherwise. Each letter of the magazine can be used only once, but the letters can be taken in any order.
With ransomNote = "aa" and magazine = "abb", the note needs two a's and the magazine has only one, so the answer is false. With magazine = "aab" there are two a's, so the answer is true. The unused b doesn't matter.
The statement asks for O(n + m) time, where n and m are the lengths of the two strings, and extra space proportional to the alphabet size, not to the string lengths.
Approach: compare letter counts
Since order doesn't matter, the question is only about quantities. The note can be built exactly when, for every letter, the magazine has at least as many copies as the note needs. The note "abba", for example, needs two a's and two b's, and both "aaabbb" and "ababab" satisfy that.
So count how often each letter occurs in the note, count the same for the magazine, then check each letter of the alphabet: if the note needs more than the magazine has, return false. If every letter passes, return true.
Searching the magazine for each letter of the note would take O(n * m) time. Counting reads each string once, which gives O(n + m). The alphabet loop runs 26 times whatever the input, so it adds only a constant.
Step by step
- Create two count tables.
ransomFreqandmagazineFreqare empty objects typed{[key: string]: number}. That type says any string key maps to a number, so a character can be used as the key. - Count the note. Loop over
ransomNotewithfor...of, which visits one character at a time asc. - Increment. Set
ransomFreq[c]to(ransomFreq[c] || 0) + 1. The first time a character appears its entry is missing, which reads asundefined.|| 0turns that into 0. - Count the magazine with the same kind of loop. The variable
cexists only inside its own loop, so reusing the name is fine. - Update
magazineFreq[c]in the same way. - Loop over the alphabet. Use a numeric
charCodethat goes from'a'.charCodeAt(0)to'z'.charCodeAt(0), inclusive. - Turn the code into a letter.
String.fromCharCode(charCode)gives the one-character stringcto use as a key. - Compare. If
(ransomFreq[c] || 0)is greater than(magazineFreq[c] || 0), the magazine can't supply that letter, so returnfalse. - Return
trueafter the loop. It is reached only if no letter failed.
The solution in TypeScript
class Solution {
canConstruct(ransomNote: string, magazine: string): boolean {
let ransomFreq: {[key: string]: number} = {};
let magazineFreq: {[key: string]: number} = {};
for (let c of ransomNote) {
ransomFreq[c] = (ransomFreq[c] || 0) + 1;
}
for (let c of magazine) {
magazineFreq[c] = (magazineFreq[c] || 0) + 1;
}
for (let charCode = 'a'.charCodeAt(0); charCode <= 'z'.charCodeAt(0); charCode++) {
let c = String.fromCharCode(charCode);
if ((ransomFreq[c] || 0) > (magazineFreq[c] || 0)) {
return false;
}
}
return true;
}
}
The TypeScript details worth a closer look:
- Index signatures. The type
{[key: string]: number}describes an object used as a dictionary. It is only a compile-time description. At run time the object is empty until you add keys. - Missing keys read as
undefined. The type saysnumber, but an absent key givesundefinedat run time. Types are erased when TypeScript compiles to JavaScript, so nothing in the compiled code fills in a default. The|| 0does that job. - Why the final comparison needs
|| 0too. Any comparison withundefinedis false. Without the defaults, a letter that is in the note but missing from the magazine would give1 > undefined, which is false, and the method would wrongly returntrue. for...ofon a string iterates over characters, not over indices. Use it when you only need the characters.- Character codes.
charCodeAt(0)returns the numeric code of the first character: 97 for'a'and 122 for'z'. The letters in between have consecutive codes, which is what makes the alphabet loop work. - The loop covers only
atoz. The check looks at lowercase English letters only. The method is correct for inputs made of those letters. Characters outside that range are counted but never compared.
Complexity
Time: O(n + m). Each string is read once, with constant work per character, and the alphabet loop runs exactly 26 times.
Space: O(Σ), where Σ is the alphabet size. Each table holds at most one entry per distinct character, which is at most 26 for lowercase letters. The space does not grow with the length of the strings.
Edge cases and common mistakes
- An empty note.
canConstruct("", "abc")istrue: nothing needs to be built, and no letter exceeds its count. - An empty magazine.
canConstruct("a", "")isfalse, because the note needs a letter the magazine doesn't have. - Reordered letters.
"abba"from"aabb"istrue. Only the counts matter. - A magazine with extra letters. Extra letters are fine. Only the letters the note needs are compared.
- Checking only whether a letter appears. The note
"aa"needs two a's, so "the magazine contains an a" is not enough. - Forgetting
|| 0. Without it, the first increment computesundefined + 1, which isNaN, and every later check fails or passes for the wrong reason. - Using
<instead of>. The method must reject a letter when the note needs more copies than the magazine has.
Testing
ransomNote = "aa",magazine = "abb"givesfalse.ransomNote = "aa",magazine = "aab"givestrue.ransomNote = "abba",magazine = "aabb"givestrue.ransomNote = "",magazine = "abc"givestrue.ransomNote = "a",magazine = ""givesfalse.