Problem overview
The function 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 in the magazine can be used only once, and the order of the letters doesn't matter.
With ransomNote = "aa" and magazine = "abb" the answer is false: the note needs two as, and the magazine has one. With magazine = "aab" the answer is true. The statement asks for O(n + m) time and extra space that depends only on the alphabet size, not on the string lengths.
Approach: compare letter counts
The order of letters never matters, only how many of each letter are available and how many are needed. So the note can be built exactly when, for every letter, the magazine has at least as many copies as the note.
Checking this by searching the magazine for each note letter and crossing it out would take O(n * m) time. Counting is faster. Build one frequency map for the note and one for the magazine, each from letter to count, in a single pass over each string. Then compare the two counts letter by letter. Since each pass is linear and the comparison covers a fixed alphabet, the total is O(n + m).
For "aa" and "abb" the note map is {a=2} and the magazine map is {a=1, b=2}. The note needs 2 as, the magazine has 1, so the answer is false. For "abba" and "aaabbb" the maps are {a=2, b=2} and {a=3, b=3}. Every count in the note is at most the magazine's count, so the answer is true, and the leftover letters are simply unused.
Step by step
- Create the two maps.
ransomFreqandmagazineFreqare each aHashMap<Char, Int>. They'revals because the references never change, though the contents do. - Loop over the note.
for (c in ransomNote)visits each character in turn. - Count each letter. Store
getOrDefault(c, 0) + 1underc. The default makes a letter that is seen for the first time start from 0. - Loop over the magazine the same way. Reusing the name
cis fine, since each loop variable exists only inside its loop. - Count the magazine's letters into
magazineFreq. - Walk the alphabet. Declare a new
var c = 'a'and loop withwhile (c <= 'z'). This variable is separate from the earlier loop variables. - Compare counts. If the note's count for
cis greater than the magazine's, returnfalse. Both lookups usegetOrDefault(c, 0), because a letter that never occurred is simply missing from the map. - Move to the next letter with
c++. Without it the loop would never end. - Return
trueafter the loop. Every letter had enough copies.
The solution in Kotlin
class Solution {
fun canConstruct(ransomNote: String, magazine: String): Boolean {
val ransomFreq = HashMap<Char, Int>()
val magazineFreq = HashMap<Char, Int>()
for (c in ransomNote) {
ransomFreq[c] = ransomFreq.getOrDefault(c, 0) + 1
}
for (c in magazine) {
magazineFreq[c] = magazineFreq.getOrDefault(c, 0) + 1
}
var c = 'a'
while (c <= 'z') {
if (ransomFreq.getOrDefault(c, 0) > magazineFreq.getOrDefault(c, 0)) {
return false
}
c++
}
return true
}
}
Kotlin details worth knowing:
- Iterating a string.
for (c in text)gives each character as aChar. ACharis written with single quotes,'a', and aStringwith double quotes. - Reading a map gives a nullable.
ransomFreq[c]has typeInt?and isnullfor a missing key, so it can't be used in arithmetic.getOrDefault(c, 0)returns a plainInt. Charsupports comparison and++.c++moves to the next character code, so from'a'it reaches'b', and after'z'comes'{', which ends thewhileloop since it is greater than'z'.- Shadowing. The variable
cinvar c = 'a'is declared after bothforloops have ended, so it doesn't clash with their loop variables. valon a map. It stops the name from being reassigned, butransomFreq[c] = ...still changes the map.
Complexity
Let n be the length of ransomNote and m the length of magazine.
Time: O(n + m). Each counting loop is linear in its string, and each map update is O(1) on average. The alphabet loop runs 26 times, a constant.
Space: O(Σ), where Σ is the alphabet size. Each map holds at most one entry per distinct letter, so with lowercase letters that is at most 26 entries per map, whatever the string lengths.
Edge cases and common mistakes
- Empty note. An empty
ransomNotecan always be built, so the answer istrue, even from an empty magazine. - Magazine too short. If the note is longer than the magazine, some letter must be short, and the answer is
false. - Counting only presence. Checking that each letter occurs, instead of how many times, wrongly accepts
"aa"with"ab". - Extra letters. Letters in the magazine that the note doesn't use are fine.
- Order.
"ab"from"ba"istrue. - Letters outside
'a'to'z'. This solution compares only lowercase letters, so it assumes the inputs hold only those. Given"A"and"a", it would returntrue, because uppercase letters and digits are never compared. To handle any characters, loop over the note's own map entries instead of the alphabet. - Not defaulting. Using
ransomFreq[c] + 1without a default doesn't compile.
Testing
ransomNote = "aa",magazine = "abb"givesfalse.ransomNote = "aa",magazine = "aab"givestrue.ransomNote = "ab",magazine = "ba"givestrue.ransomNote = "",magazine = ""givestrue.