Problem overview
The method findTheDifference receives two strings. The string t is s shuffled with one extra letter inserted, and the method returns that letter as a Character.
For s = "abcd" and t = "abcde" the answer is "e". For s = "" and t = "y" it is "y".
This lesson asks for O(n) time and O(1) extra space, where n is the length of t. A dictionary of counts is linear in time as well, but this lesson's goal is to get rid of the dictionary and keep a single number.
Approach: XOR cancels pairs
The XOR operation, written ^ in Swift, combines two integers bit by bit: a result bit is 1 when exactly one of the two input bits is 1. It has three properties that matter here:
x ^ x = 0: a number XORed with itself cancels out.x ^ 0 = x: XOR with 0 changes nothing.- The order doesn't matter, so the numbers can be combined in any sequence.
Every letter has a numeric code. Start with 0 and XOR in the code of every letter of s, then the code of every letter of t. Each letter of s has an equal partner in t, and the two cancel to 0, wherever they sit and however many other letters lie between them. The extra letter has no partner. What remains is its code.
For example, with s = "ae" and t = "aea", the codes are 97 for a and 101 for e. The XOR of 97, 101, 97, 101, 97 is 97, the code of a. Two as cancel, the two es cancel, and one a is left.
This works even when the extra letter duplicates a letter from s, because cancelling happens per copy: three as in total leave one a after one pair cancels. The solution keeps one small number, so the extra space is constant.
Step by step
- Declare an accumulator.
var result: UInt8 = 0holds one byte. A byte is enough for a lowercase letter's code, andUInt8supports^. - Loop over the bytes of
s.s.utf8is a view of the string as its UTF-8 bytes, sofor c in s.utf8givesUInt8values, notCharacters. - XOR each byte into
resultwithresult ^= c. - Loop over the bytes of
tin the same way. - XOR those bytes in too. After this loop, every letter that occurs in both strings has been combined twice and cancelled, and only the extra letter is left in
result. - Turn the byte back into a character.
UnicodeScalar(result)makes a Unicode scalar from the byte, andCharacter(...)wraps it as the return type requires.
The solution in Swift
class Solution {
func findTheDifference(_ s: String, _ t: String) -> Character {
var result: UInt8 = 0
for c in s.utf8 {
result ^= c
}
for c in t.utf8 {
result ^= c
}
return Character(UnicodeScalar(result))
}
}
The Swift details worth a closer look:
utf8is a view, not a copy. It walks the string's bytes without building an array. For the lowercase letters in this lesson each character is exactly one byte, equal to its ASCII code:Array("abc".utf8)is[97, 98, 99].- Non-ASCII text is different. A character such as
étakes two bytes, andArray("é".utf8)is[195, 169]. The bytes would still cancel in pairs, but the one-byte result at the end could no longer name the character. The lesson's input is limited to lowercase English letters, so this is safe here. ^=is XOR-and-assign.result ^= cmeansresult = result ^ c. Both sides areUInt8, so no conversion is needed.- The
UInt8initializer can't fail. Every byte value is a valid Unicode scalar, soUnicodeScalar(result)returns a plain value. The version that takes anIntreturns an optional instead, because not every integer is a valid scalar:UnicodeScalar(Int(97))givesOptional("a"). That is why the accumulator is aUInt8and not anInt. - No fallback return is needed. Unlike the loop-and-return solutions, this method always reaches its last line.
- No overflow can happen. XOR never produces a value outside the range of its operands' type, so there is no arithmetic trap to worry about.
Complexity
Time: O(n). The first loop runs n - 1 times and the second runs n times, with one XOR in each pass.
Space: O(1). The only state is the byte result and the loop variables. The utf8 views don't copy the strings.
Edge cases and common mistakes
- Empty
s. The first loop does nothing, and the second XORs the single byte oftinto 0. Sincex ^ 0 = x, the result is that byte, so"y"comes back fort = "y". - The extra letter repeats a letter of
s.s = "aab"andt = "abaa"gives"a": threeas and twobs are XORed, the pairs cancel, and onearemains. - Using
+instead of^. Addition doesn't cancel anything. Subtracting would work in principle, butUInt8arithmetic traps when it overflows, so XOR is the safer tool here. - Accumulating in an
Int. That works for the XOR itself, but the last step then needs anInt-to-scalar conversion that returns an optional. - Looping over
sandtas strings. A loop over the string givesCharactervalues, and^isn't defined for them. Loop overutf8.
Testing
s = "abcd",t = "abcde"returns"e".s = "",t = "y"returns"y".s = "aab",t = "abaa"returns"a".s = "ae",t = "aea"returns"a".
