Problem overview
The method alienOrder(words: Array[String]): String receives words from an alien dictionary. The alien language uses the lowercase English letters, but in an order you do not know, and the words are sorted in that order. Words compare as in any dictionary: at the first position where they differ, the word with the earlier letter comes first, and a word that is a prefix of another comes first, so "ab" always precedes "abc".
The method returns every letter that appears in words, each once, in alien order. If the words contradict themselves, so that no order makes them sorted, it returns "". When several orders work, it returns the one that comes first in the usual alphabet, which makes the answer unique.
For Array("wrt", "wrf", "er", "ett", "rftt") the answer is "wertf". For Array("z", "x", "z") it is "": "z" before "x" says z comes first, and "x" before "z" says x comes first. For Array("cb", "ca") the answer is "bac": the words only tell us that b comes before a, the orders "bac", "bca" and "cba" all work, and "bac" is the first alphabetically.
Approach: turn neighbouring words into arrows
Each pair of neighbouring words gives at most one fact: at the first position where they differ, the letter of the first word comes before the letter of the second. Letters after that position say nothing. Neighbouring pairs are enough, because if every word is in order with the next one, the whole list is sorted.
For the first example the four pairs give:
wrt wrf: t before f
wrf er: w before e
er ett: r before t
ett rftt: e before r
Chained, these facts say w, e, r, t, f. For "cb" and "ca" the only fact is b before a.
Each fact is an arrow between two letters. An order that agrees with every arrow is a topological order, and Kahn's algorithm finds one: keep an in-degree for every letter, the number of unplaced letters that must come before it, and repeatedly place a letter whose in-degree is 0. Taking the alphabetically smallest ready letter each time gives the answer the statement asks for. If at some point no letter is ready but some are still waiting, they wait on one another: a cycle, so the answer is "".
One more contradiction does not show up as an arrow: if the first word is longer than the second and the second is a prefix of it, no alphabet can sort them. Array("abc", "ab") returns "".
The work is proportional to the total number of letters in the words, O(C), since neighbouring words are compared once. Comparing every pair of words would take O(n²) comparisons.
Step by step
- Find the letters that appear. Create
waitingasArray.fill(26)(false), one flag per letter.waiting(k)istruewhile letter numberkappears in the words and is not yet in the answer. With only 26 letters, an array indexed by letter is simpler than a map. - Count the different letters in a
var lettersstarting at 0. The answer is complete when its length reaches this count. - Loop over each word and each character
c. If!waiting(c - 'a'), set it totrueand add 1 toletters. Subtracting twoChars gives anInt, soc - 'a'is 0 for a and 25 for z. Theifcounts each letter the first time it appears. - Build the graph as
Array.fill(26)(mutable.ArrayBuffer[Int]()).graph(a)holds the letters that must come after lettera.Array.fillevaluates its second argument once per element, so each letter gets its own buffer. - Create
inDegreeasArray.fill(26)(0).inDegree(b)counts the unplaced letters that must come beforeb. - Loop over neighbouring pairs with
i <- 0 until words.length - 1, namingwords(i)asfirstandwords(i + 1)assecond. - Find the first difference. A
var jstarts at 0 and awhileloop advances it whilejis inside both words andfirst(j) == second(j). The checks run left to right and stop at the first false one, so the characters are read only whilejis valid. - If
jis still inside both words, the letters atjare the first that differ. Appendsecond(j) - 'a'tograph(first(j) - 'a')with+=, and add 1 toinDegree(second(j) - 'a'). Two pairs can give the same arrow; each copy is counted here and removed once later. - Otherwise, if
first.length > second.length, return"": the second word is a prefix of the first and should come first in any alphabet. A shorter or equal first word gives no fact. - Collect the answer in a
var orderthat starts as an empty string, and loop whileorder.length < letters. Each round places one letter. - Find the smallest ready letter. Start a
var letterat 0 and advance it whileletter < 26and the letter is not waiting or still has arrows coming in:!waiting(letter) || inDegree(letter) > 0. The first letter that stops the loop is the alphabetically smallest ready one. - If
letter == 26, no letter is ready although some are waiting: return"". - Place the letter. Set
waiting(letter)tofalseand append('a' + letter).toChartoorder.'a' + letteris anInt, andtoCharturns it back into a letter, so+=adds a letter and not a number. - Release its neighbours. For each
nextLetteringraph(letter), subtract 1 frominDegree(nextLetter). When a count reaches 0, the next round finds that letter ready. - Return
orderonce every letter is placed.
The solution in Scala
class Solution {
def alienOrder(words: Array[String]): String = {
val waiting = Array.fill(26)(false)
var letters = 0
for (word <- words) {
for (c <- word) {
if (!waiting(c - 'a')) {
waiting(c - 'a') = true
letters += 1
}
}
}
val graph = Array.fill(26)(mutable.ArrayBuffer[Int]())
val inDegree = Array.fill(26)(0)
for (i <- 0 until words.length - 1) {
val first = words(i)
val second = words(i + 1)
var j = 0
while (j < first.length && j < second.length && first(j) == second(j)) {
j += 1
}
if (j < first.length && j < second.length) {
graph(first(j) - 'a') += second(j) - 'a'
inDegree(second(j) - 'a') += 1
} else if (first.length > second.length) {
return ""
}
}
var order = ""
while (order.length < letters) {
var letter = 0
while (letter < 26 && (!waiting(letter) || inDegree(letter) > 0)) {
letter += 1
}
if (letter == 26) {
return ""
}
waiting(letter) = false
order += ('a' + letter).toChar
for (nextLetter <- graph(letter)) {
inDegree(nextLetter) -= 1
}
}
return order
}
}
The Scala details worth a closer look:
Chararithmetic.'d' - 'a'is theInt3.'a' + 3is theInt100, and('a' + 3).toCharis'd'. Adding theIntdirectly to aStringwould append digits instead of a letter.mutable.ArrayBufferis a growable list, and+=appends to it. The lesson providesmutable; in a file of your own, addimport scala.collection.mutable.Array.fill(26)(mutable.ArrayBuffer[Int]())creates 26 separate buffers, because the second argument is evaluated for every element. Appending tograph(0)leavesgraph(1)empty.for (c <- word)walks the characters of aStringdirectly.- String building.
order += ...on avarof typeStringmakes a new string each time. With at most 26 letters that cost does not matter. returninside loops. Early exits usereturn "". Those inside aforbody are non-local returns, which Scala 3.5 accepts but warns about. The returns in thewhileloops are ordinary.
Complexity
Time: O(C), where C is the total number of letters in words. Counting letters touches each letter once, and comparing neighbouring words touches each letter at most twice. The placing loop does at most 26 rounds, each scanning 26 letters and releasing the arrows of one letter, so it costs a constant plus the number of arrows, which is at most the number of word pairs.
Space: O(n) in the number of words n, at worst. The arrays have 26 entries each. The buffers together hold one entry per recorded arrow, and each neighbouring pair of words records at most one.
Edge cases and common mistakes
- A word that is a prefix after its extension.
Array("abc", "ab")gives"", and so doesArray("ab", "a"). In the other order,Array("ab", "abc")gives no fact and is fine. - A cycle.
Array("z", "x", "z")andArray("a", "b", "a")give"": no letter is ever ready. - A repeated word.
Array("z", "z")gives"z". Equal neighbours say nothing. - One word.
Array("a")gives"a": no pairs, one letter, placed in the first round. - Letters with no facts. In
Array("c", "a", "b")the facts are c before a and a before b, so the answer is"cab". InArray("ab", "adc")the single fact is b before d, and the answer is"abcd", with each unconstrained letter in its alphabetical place. - Forgetting letters that never appear in a fact. Every letter of every word must be in the answer. Counting letters first, and not only those in arrows, is what guarantees it.
- Not taking the smallest ready letter. Any ready letter gives a valid order, but only the smallest gives the order the statement asks for.
- Sharing one buffer. Filling the graph with one buffer repeated would make every letter's neighbours the same list.
Testing
Array("wrt", "wrf", "er", "ett", "rftt")gives"wertf".Array("z", "x", "z")gives"".Array("cb", "ca")gives"bac".Array("abc", "ab")gives"".Array("z", "z")gives"z".Array("a")gives"a".Array("ab", "adc")gives"abcd".Array("ba", "bc", "ac", "cab")gives"bac".Array("abc", "abd", "b")gives"abcd".Array("x", "y", "z")gives"xyz".Array("c", "a", "b")gives"cab".
