Problem overview
An alien language uses the lowercase English letters in an unknown order. The method alienOrder receives a list words that is sorted in that alien order, and returns a string holding every letter that appears in words, once each, in the alien alphabet's order.
Words are compared as in any dictionary: at the first position where two words differ, the one with the earlier letter comes first. If one word is a prefix of the other, the shorter one comes first, so "ab" always comes before "abc". If the words contradict themselves, so that no order of letters can make them sorted, the method returns "". When several orders work, it returns the one that comes first in the usual alphabetical order, which makes the answer unique.
Take ["wrt", "wrf", "er", "ett", "rftt"]. Each pair of neighbouring words gives one fact, from the first letters where they differ:
wrt before wrf: t before f
wrf before er: w before e
er before ett: r before t
ett before rftt: e before r
Chained together, w → e → r → t → f, so the answer is "wertf". For ["z", "x", "z"] the facts "z before x" and "x before z" contradict each other, so the answer is "". For ["cb", "ca"] the only fact is "b before a". The letter c is unconstrained, and of the working orders "bac", "bca" and "cba", the first alphabetically is "bac".
Approach: turn the facts into arrows
Each fact is an arrow from one letter to another. An order that agrees with every arrow is a topological order of this graph of letters. It exists only if the graph has no cycle, and Kahn's algorithm finds it: keep, for every letter, the number of arrows pointing at it that aren't placed yet (its in-degree), repeatedly place a letter whose count is 0, and lower the counts of the letters it points to. Taking the alphabetically smallest ready letter each round gives the unique answer the statement asks for. If at some point letters are still waiting but none is ready, they wait for each other in a cycle, and the answer is "".
Comparing neighbouring words is enough. If every word is in order with the next one, the whole list is sorted, so other pairs add no new facts. This keeps the work proportional to the total number of letters. Comparing every pair would take a quadratic number of comparisons in the number of words.
Two special cases come from the prefix rule. If the words differ at some position, they give one arrow, and the letters after that position say nothing. If one word runs out first and it is the second of the pair, such as "abc" followed by "ab", no alphabet can sort them, so the answer is "". If the first one is the shorter, or the two are equal, there is no fact at all.
Step by step
- Create
waitingas 26Falsevalues, one per letter.waiting[k]will beTruewhile letterkappears in the words but isn't in the answer yet. With only 26 letters, a list indexed by letter is simpler than a dict. - Start
lettersat 0. It counts the distinct letters, so the answer is complete when its length reaches this number. - Loop over each
wordand, inside it, over each characterc. - Test
not waiting[ord(c) - ord('a')].ordgives a character's code, andatozhave consecutive codes, so the expression is 0 foraup to 25 forz. - Mark the letter as
Trueand add 1 toletters. Theifensures each letter is counted once, at its first appearance. - Build
graphas 26 empty lists with a list comprehension.graph[a]holds the letters that must come after lettera. - Create
inDegreeas 26 zeros.inDegree[b]counts the unplaced letters that must come beforeb. - Loop
ioverrange(len(words) - 1). The pair isfirst = words[i]andsecond = words[i + 1]. - Find the first difference. Start
jat 0 and, in awhileloop, add 1 whilejis inside both words andfirst[j] == second[j]. The checks run left to right, so the characters are read only whilejis valid. - If
jis still inside both words, the letters atjare the first that differ. Append the index ofsecond[j]tographat the index offirst[j], and add 1 toinDegreeat the index ofsecond[j]. Two pairs can give the same arrow. Each copy is counted here and removed once later, so that is fine. elif len(first) > len(second): the second word is a prefix of the first, which no alphabet allows. Return"".- Start
orderas an empty string. - Loop while
len(order) < letters. Each round places one letter. - Scan for the smallest ready letter. Start
letterat 0 and add 1 whileletter < 26and the letter isn't waiting or still has arrows coming in. The first letter the scan stops at is the smallest ready one. - If
letter == 26, nothing is ready: return"". - Place the letter. Set
waiting[letter]toFalseand appendchr(ord('a') + letter)toorder.chris the opposite oford. - Release the letters after it. For each
nextLetteringraph[letter], subtract 1 frominDegree[nextLetter]. - Return
orderafter the loop.
The solution in Python
class Solution:
def alienOrder(self, words: List[str]) -> str:
waiting = [False] * 26
letters = 0
for word in words:
for c in word:
if not waiting[ord(c) - ord('a')]:
waiting[ord(c) - ord('a')] = True
letters += 1
graph = [[] for _ in range(26)]
inDegree = [0] * 26
for i in range(len(words) - 1):
first = words[i]
second = words[i + 1]
j = 0
while j < len(first) and j < len(second) and first[j] == second[j]:
j += 1
if j < len(first) and j < len(second):
graph[ord(first[j]) - ord('a')].append(ord(second[j]) - ord('a'))
inDegree[ord(second[j]) - ord('a')] += 1
elif len(first) > len(second):
return ""
order = ""
while len(order) < letters:
letter = 0
while letter < 26 and (not waiting[letter] or inDegree[letter] > 0):
letter += 1
if letter == 26:
return ""
waiting[letter] = False
order += chr(ord('a') + letter)
for nextLetter in graph[letter]:
inDegree[nextLetter] -= 1
return order
Python details worth a closer look:
- A list comprehension for
graph.[[] for _ in range(26)]creates 26 separate lists.[[]] * 26repeats one list object 26 times: afterg = [[]] * 3andg[0].append(1),gis[[1], [1], [1]]. With the comprehension, the same two lines leave[[1], [], []]. ordandchr.ord('c') - ord('a')is 2, andchr(ord('a') + 2)is'c'. They convert between a letter and its index in the lists.- Short-circuit
and. Inj < len(first) and j < len(second) and first[j] == second[j], the comparison is evaluated only whenjis a valid index in both words, so there is noIndexError. - Building a string with
+=. Strings are immutable, so eachorder += ...creates a new string. With at most 26 letters that cost doesn't matter. - The parenthesised condition.
letter < 26comes first so thatwaiting[letter]is never read at index 26.
Complexity
Time: O(C), where C is the total number of letters in words. Collecting the letters reads each character once. Comparing neighbouring words takes at most the length of the shorter word per pair, so also O(C). The ordering loop places at most 26 letters, and each round scans at most 26 positions, which is a constant.
Space: O(1) beyond the input. The lists have 26 entries each, and the arrows are at most one per pair of neighbouring words.
Edge cases and common mistakes
- A prefix in the wrong order.
["abc", "ab"]returns"", because the longer word can't come first.["ab", "abc"]is fine and returns"abc". - Cycles.
["z", "x", "z"]returns"". - A single word or equal words.
["a"]returns"a"and["z", "z"]returns"z", because no facts are produced and every letter is free. - Letters without any arrow. They still belong in the answer. The count
lettersmakes sure they are placed, and the scan picks them in alphabetical order. - Not taking the smallest ready letter. Any ready letter gives a valid order, but only the smallest each round gives the alphabetically first one. For
["za", "zb", "ca", "cb"]the answer is"abzc". - Comparing only the first letters. Words can share a long prefix. The fact comes from the first position where they differ.
- Using more than the first difference. Letters after the first differing position say nothing about the order.
Testing
words = ["wrt", "wrf", "er", "ett", "rftt"]gives"wertf".words = ["z", "x", "z"]gives"".words = ["cb", "ca"]gives"bac".words = ["abc", "ab"]gives"".words = ["ab", "abc"]gives"abc".words = ["a"]gives"a".words = ["ba", "bc", "ac", "cab"]gives"bac".words = ["za", "zb", "ca", "cb"]gives"abzc".
