Problem overview
An alien language uses the lowercase English letters in an unknown order. The function alienOrder gets words, a slice of words sorted in that alien alphabet, and returns a string holding every letter that appears in the words, once each, in the alien order. If the words contradict themselves, so that no order of the letters could make them sorted, it returns "". If several orders work, it returns the one that comes first in the usual alphabetical order, which makes the answer unique.
Words are compared as in any dictionary: at the first position where they differ, the word with the earlier letter comes first. If one word is a prefix of the other, the shorter one comes first, so "ab" always precedes "abc".
For ["wrt", "wrf", "er", "ett", "rftt"] the answer is "wertf". Each pair of neighbouring words gives one fact: t before f, w before e, r before t, and e before r. Chained, that is w, e, r, t, f. For ["z", "x", "z"] the answer is "", because the pairs say both "z before x" and "x before z". For ["cb", "ca"] only "b before a" is known, so "bac", "bca" and "cba" all work, and "bac" is the alphabetically first.
The cost is O(C), where C is the total number of letters in words.
Approach: facts as arrows, then Kahn's algorithm
Turn each fact into an arrow between two letters. An order that agrees with every arrow is a topological order of the resulting graph, and Kahn's algorithm finds one: repeatedly place a letter that has no unplaced letter before it. Taking the alphabetically smallest such letter each time gives the unique answer the problem asks for.
Neighbouring words are enough. If every word is in order with the next one, the whole list is sorted, so other pairs add no new facts. Comparing every pair of words would cost O(n²) comparisons for n words.
Each neighbouring pair gives at most one arrow, from the first position where the words differ. The letters after that position say nothing. If no position differs because one word runs out, there is no arrow, unless the first word is the longer one: then the second is a prefix of the first and should have come first in any alphabet, so the list contradicts itself.
Step by step
- Mark the letters that appear. Create
waiting, a slice of 26 bools.waiting[k]istruewhile letter numberkappears in the words but isn't in the answer yet. With only 26 possible letters, a slice indexed by letter is simpler than a map. - Count the different letters in
letters, starting at 0. The answer is complete when its length reaches this number. - Loop over each word, then over each character
cof the word. - Test
!waiting[c - 'a']. The lettersatozare consecutive, soc - 'a'is 0 fora, 1 forb, up to 25 forz. - Mark the letter and count it. Set
waiting[c - 'a'] = trueand add 1 toletters. Theifmakes sure each letter is counted once. - Build the graph:
graph := make([][]int, 26).graph[a]holds the letters that must come after lettera. The 26 inner slices start asnil, andappendworks on anilslice. - Create
inDegree, a slice of 26 ints.inDegree[b]counts the arrows pointing intobfrom letters not placed yet. A letter can be placed as soon as its count is 0. - Loop over neighbouring pairs:
ifrom 0 whilei < len(words) - 1. Storewords[i]asfirstandwords[i + 1]assecond. - Find the first difference. Start
jat 0 and advance it while it is inside both words andfirst[j] == second[j]. The checks run left to right, so the letters are read only whilejis inside both. - If
jis still inside both words, the letters atjare the first that differ. Sincefirstcomes beforesecond,first[j]comes beforesecond[j]in the alien alphabet. - Add the arrow: append
int(second[j] - 'a')tograph[first[j] - 'a'], storing the result back, and add 1 toinDegree[second[j] - 'a']. Two pairs may give the same arrow. Each copy is counted here and removed once later, so that is fine. - Otherwise, with
} else if len(first) > len(second), return"": the shorter word is a prefix of the longer one but comes after it. - Start Kahn's algorithm. Create
order, an empty string, and loop whilelen(order) < letters. Each round places one letter. - Find the smallest ready letter. Start
letterat 0 and advance it whileletter < 26and the letter is not waiting or still has arrows coming in (!waiting[letter] || inDegree[letter] > 0). A ready letter appears in the words, isn't placed, and has every earlier letter placed. Scanning fromatozfinds the smallest one. - If
letter == 26, return"". Letters are still waiting, but each waits for another waiting letter: they form a cycle. - Place the letter. Set
waiting[letter] = false, addstring(rune('a' + letter))toorder, and loop overgraph[letter], subtracting 1 frominDegree[nextLetter]for each. When a count reaches 0, the next round can pick that letter. - Return
orderonce every letter has been placed.
The solution in Go
func alienOrder(words []string) string {
waiting := make([]bool, 26)
letters := 0
for _, word := range words {
for _, c := range word {
if !waiting[c - 'a'] {
waiting[c - 'a'] = true
letters++
}
}
}
graph := make([][]int, 26)
inDegree := make([]int, 26)
for i := 0; i < len(words) - 1; i++ {
first := words[i]
second := words[i + 1]
j := 0
for j < len(first) && j < len(second) && first[j] == second[j] {
j++
}
if j < len(first) && j < len(second) {
graph[first[j] - 'a'] = append(graph[first[j] - 'a'], int(second[j] - 'a'))
inDegree[second[j] - 'a']++
} else if len(first) > len(second) {
return ""
}
}
order := ""
for len(order) < letters {
letter := 0
for letter < 26 && (!waiting[letter] || inDegree[letter] > 0) {
letter++
}
if letter == 26 {
return ""
}
waiting[letter] = false
order += string(rune('a' + letter))
for _, nextLetter := range graph[letter] {
inDegree[nextLetter]--
}
}
return order
}
The Go details worth a closer look:
- Runes and bytes.
for _, c := range wordgivescas arune(anint32), whilefirst[j]indexes the string and gives abyte(uint8). Both subtract'a'to give a number from 0 to 25 that can index a slice. - The
int(...)conversion.graphholdsints, so the byte differencesecond[j] - 'a'must be converted beforeappendtakes it. appendreturns the grown slice, so it has to be assigned back tograph[first[j] - 'a'].- Back to a letter.
rune('a' + letter)turns the number into a character, andstring(...)turns that character into a string that+=can add.string(rune(100))is"d". - Short-circuit order. In the
jloop,&&keepsfirst[j]andsecond[j]from being read past the end of either word.
Complexity
Time: O(C). Collecting the letters reads every character once. Comparing neighbouring words reads each word at most twice, once per pair it belongs to. The graph has at most one arrow per pair, so at most n - 1 arrows. Kahn's loop runs at most 26 rounds, each scanning 26 letters and following the arrows of one letter, which is constant work plus the arrows, so it takes O(26 · 26 + n).
Space: O(1) beyond the arrows. The slices have 26 entries each, and the arrows number at most n - 1. The result has at most 26 letters.
Edge cases and common mistakes
- A prefix in the wrong order.
["abc", "ab"]returns"", while["ab", "abc"]returns"abc". - Letters with no arrows. They still belong in the answer. In
["cb", "ca"]the letterchas no arrow, and the answer is"bac", withclast:bis the smallest ready letter at the start, and once it is placed,abecomes ready and is smaller thanc. - Repeated words.
["z", "z"]gives"z": equal neighbours add no arrow, and the letter is counted once.["ab", "ab", "ac"]gives"abc". - One word.
["a"]gives"a". The loop over pairs doesn't run. - A cycle.
["z", "x", "z"]and["x", "y", "x"]both give"", because the arrows go both ways between two letters. - Counting only the letters in arrows. The answer must have every letter that appears, not just those in arrows.
- Forgetting the smallest-first rule. Taking any ready letter gives a valid order but not necessarily the alphabetically first one.
Testing
For ["wrt", "wrf", "er", "ett", "rftt"] the arrows found, in order, are:
t -> f
w -> e
r -> t
e -> r
The ready letters at the start are those with no incoming arrow: only w. Then e, r, t and f become ready one at a time, giving "wertf". More inputs, with the results the function returns:
["z", "x", "z"]gives"".["cb", "ca"]gives"bac".["abc", "ab"]gives"".["ba", "bc", "ac", "cab"]gives"bac".["z", "z"]gives"z".["a"]gives"a".
