Problem overview
The string t is the string s shuffled, plus one extra lowercase letter. The method findTheDifference(s: String, t: String): Char returns that letter. For s = "abcd" and t = "abcde" it returns 'e', and for s = "" and t = "y" it returns 'y'.
This lesson asks for O(n) time, where n is the length of t. The first lesson on this problem counted a letter in both strings again and again, which costs O(n²). Here the counts are computed once and stored.
Approach: count s once, then spend the counts on t
Think of the letters of s as tokens. Walk through s and, for each letter, put one more token in that letter's pile. A mutable.Map[Char, Int] is the set of piles: the key is the letter, the value is how many tokens it has.
Now walk through t and spend one token for each letter. Because t contains every letter of s plus one more, there are enough tokens for all letters of t except the extra one. The first time a letter of t finds its pile empty, or finds no pile at all, that letter is the added one.
Take s = "aab" and t = "abaa". After counting s the map holds a -> 2 and b -> 1. Walking through t: a uses one token (a: 1), b uses one (b: 0), a uses one (a: 0), and the last a finds a count of 0. It is the extra letter, so the method returns 'a'. Note that the duplicate letter is detected even though a is in s: what matters is that its tokens ran out.
Each lookup and update in a hash map takes O(1) on average, and each string is walked once, so the total is O(n).
Step by step
- Create the map.
counteris avalholding a mutableMap[Char, Int]. The reference to the map never changes, but its contents do, sovalis fine. - Loop over
s. Each character is bound toc. - Add a token. Set
counter(c)to its current count plus one.getOrElse(c, 0)supplies 0 the first time a letter is seen. - Loop over
t. Again each character is bound toc. - Check for an empty pile. If
counter.getOrElse(c, 0) == 0, the letter is missing from the map or its count is already used up. - Return it. That letter has more occurrences in
tthan ins. - Spend a token. If the pile was not empty, subtract one from
counter(c). This line is only reached when the letter still has a copy left ins. - Add a fallback. After the second loop the method returns a space. The compiler requires a
Charon every path, and valid input never gets there.
The solution in Scala
class Solution {
def findTheDifference(s: String, t: String): Char = {
val counter = scala.collection.mutable.Map[Char, Int]()
for (c <- s) {
counter(c) = counter.getOrElse(c, 0) + 1
}
for (c <- t) {
if (counter.getOrElse(c, 0) == 0) {
return c
}
counter(c) = counter.getOrElse(c, 0) - 1
}
return ' '
}
}
The Scala details worth a closer look:
- Mutable versus immutable maps. A plain
Mapin Scala is immutable, andcounter(c) = ...only works onscala.collection.mutable.Map. That is why the full package name appears in the code.counter(c) = vis shorthand forcounter.update(c, v). getOrElseversusapply.counter(c)on a missing key throws aNoSuchElementException, andcounter.get(c)returns anOption[Int].getOrElse(c, 0)returns the count, or 0 when the key is missing, which is exactly the "no tokens" case. Without it, the very firstcounter(c) = counter(c) + 1would fail.- Type arguments.
Map[Char, Int]()creates an empty map; the type arguments are needed because there is nothing to infer them from. returnin a loop. Thereturn cinside theforleaves the whole method. Scala 3.5 compiles it, with a warning that non-local returns are no longer supported.- Characters as keys.
for (c <- s)givesCharvalues, matching the map's key type. Characters compare and hash by value, so equal letters always land on the same entry.
Complexity
Time: O(n). The first loop runs n - 1 times and the second at most n times. Each iteration does a constant number of map operations, which are O(1) on average.
Space: O(1). The strings hold only lowercase letters, so the map never has more than 26 entries, regardless of the length of the input.
Edge cases and common mistakes
- Empty
s. The map stays empty. The first character oftfinds no entry,getOrElsegives 0, and the method returns that character. - The extra letter repeats a letter of
s. Fors = "aab"andt = "abaa"the answer is'a'. Testing only "is this letter in the map?" would miss it; the check must be about the count reaching zero. - Forgetting the decrement. Without the second update, a letter that appears twice in both strings would never run out, and a duplicate extra letter would go unnoticed.
- Indexing a missing key. Writing
counter(c) + 1instead ofcounter.getOrElse(c, 0) + 1throws at run time whencis new. - Using the default
Map. Importing nothing and writingMap[Char, Int]()gives an immutable map, and assigning withcounter(c) = ...then doesn't compile:value update is not a member of Map[Char, Int].
Testing
s = "abcd",t = "dbeca"returns'e'.s = "",t = "y"returns'y'.s = "aab",t = "abaa"returns'a'.s = "z",t = "zz"returns'z'.
