Problem overview
The method alienOrder receives words, a list sorted according to an unknown alphabet. 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 comes first, so "ab" always precedes "abc".
The method returns a string with every letter that appears in words, once each, in the order of the alien alphabet. If the list contradicts itself, so that no order of letters could make it sorted, it returns "". When several orders are possible, it returns the one that comes first alphabetically, which makes the answer unique.
For ["wrt", "wrf", "er", "ett", "rftt"] the answer is "wertf". For ["z", "x", "z"] it is "": the first pair says z comes before x, the second says x comes before z. For ["cb", "ca"] it is "bac": the words differ at the second letter, so b comes before a, and nothing says where c goes. The orders "bac", "bca" and "cba" all work, and "bac" is first alphabetically.
Approach: letters as a graph, then a topological order
Each pair of neighbouring words gives at most one fact. Find the first position where the two words differ; the letter in the first word comes before the letter in the second. Everything after that position says nothing. For the first example:
wrt wrf: t before f
wrf er: w before e
er ett: r before t
ett rftt: e before r
Only neighbours are needed. If every word is in order with the next one, the whole list is sorted, so other pairs add nothing. That keeps the work proportional to the total number of letters, O(C), instead of comparing all pairs of words.
Draw each fact as an arrow from one letter to the other. An alphabet that agrees with every arrow is a topological order of this graph. Kahn's algorithm builds one: count for each letter how many arrows point at it (its in-degree), place a letter whose count is 0, then lower the counts of the letters it points to, and repeat. If at some point letters are left but none has count 0, they wait on each other in a cycle and no order exists.
To get the alphabetically first order, each round places the smallest letter that is ready. The first letter in a to z that appears in the words, isn't placed, and has no unplaced letter before it is the one to take.
One case gives "" without any arrow: when a word is followed by a shorter word that is its own prefix, as in ["abc", "ab"], the list can't be sorted in any alphabet.
Step by step
- Create
waiting: 26falsevalues, one per letter.waiting[k]will betruewhile letter numberkappears in the words and isn't in the answer yet. With only 26 letters, an array indexed by letter is simpler than an object. - Create
letterswithlet, starting at 0. It counts the different letters, so the answer is complete when its length reaches this number. - Loop over each
wordwithfor...of. - Loop over each character
cof the word. - Test
!waiting[c.charCodeAt(0) - 97].charCodeAt(0)gives the character's code, and a to z have the codes 97 to 122, so subtracting 97 gives an index from 0 to 25. - Mark the letter by setting that entry to
true. - Add 1 to
letters. Theifensures each letter is counted once, the first time it is met. - Create
graphwithArray.from({length: 26}, () => []). The arrow function runs once per letter, so each letter gets its own empty array.graph[a]holds the letters that must come aftera. - Create
inDegree: 26 zeros.inDegree[b]counts the unplaced letters that must come beforeb. - Loop
iover neighbouring pairs whilei < words.length - 1. - Name the pair
firstandsecond. - Start
jat 0 to look for the first difference. - Advance while
jis inside both words andfirst[j] === second[j]. The checks run left to right, so the characters are read only whilejis inside both words. - Increase
jinside the loop. When it stops, either the letters atjdiffer or one word has run out. - If
jis inside both words, thenfirst[j]andsecond[j]are the first letters that differ. - Add the arrow: push the index of
second[j]ontographat the index offirst[j]. - Increase
inDegreeat the index ofsecond[j]. Two pairs can give the same arrow; each copy is counted here and removed once later, so that is fine. - Otherwise, if
firstis longer thansecond, return"":secondis a prefix offirst, so it should come first in any alphabet. A first word that is shorter or equal gives no fact. - Start
orderas an empty string. - Loop while
order.length < letters. Each round places one letter. - Start
letterat 0. It walks from a to z to find the first ready letter. - Skip letters that aren't ready: while
letter < 26and!waiting[letter] || inDegree[letter] > 0, a letter is skipped. A letter is ready when it appears in the words, isn't placed, and every letter that must precede it is placed. - Increase
letter. The first ready letter from a to z is the smallest, which makes the answer alphabetically first. - If
letter === 26, no letter is ready and some are still waiting: they form a cycle. Return"". - Place the letter by setting
waiting[letter]tofalse. - Append it to
orderwithString.fromCharCode(97 + letter), which turns the index back into a character. - Loop over
graph[letter]withfor...of, naming eachnextLetter. - Decrease
inDegree[nextLetter]. One of the letters it waited for is placed; at 0 the next round finds it ready. - Return
orderafter the loop.
The solution in TypeScript
class Solution {
alienOrder(words: string[]): string {
const waiting: boolean[] = new Array(26).fill(false);
let letters = 0;
for (const word of words) {
for (const c of word) {
if (!waiting[c.charCodeAt(0) - 97]) {
waiting[c.charCodeAt(0) - 97] = true;
letters++;
}
}
}
const graph: number[][] = Array.from({length: 26}, () => []);
const inDegree: number[] = new Array(26).fill(0);
for (let i = 0; i < words.length - 1; i++) {
const first: string = words[i];
const second: string = words[i + 1];
let j = 0;
while (j < first.length && j < second.length && first[j] === second[j]) {
j++;
}
if (j < first.length && j < second.length) {
graph[first.charCodeAt(j) - 97].push(second.charCodeAt(j) - 97);
inDegree[second.charCodeAt(j) - 97]++;
} else if (first.length > second.length) {
return "";
}
}
let order: string = "";
while (order.length < letters) {
let letter = 0;
while (letter < 26 && (!waiting[letter] || inDegree[letter] > 0)) {
letter++;
}
if (letter === 26) {
return "";
}
waiting[letter] = false;
order += String.fromCharCode(97 + letter);
for (const nextLetter of graph[letter]) {
inDegree[nextLetter]--;
}
}
return order;
}
}
Some TypeScript details:
- Letters as indexes.
"x".charCodeAt(0) - 97is 23. The code is a number, so the subtraction works on numbers, andString.fromCharCode(97 + 22)gives"w". for (const c of word)iterates over the characters of a string. For lowercase English letters, each is one character, socharCodeAt(0)gives its full code.Array.from({length: 26}, () => [])creates 26 separate arrays.new Array(26).fill([])would fill every slot with the same array, so an arrow pushed for one letter would show up for all of them.- Strings can be indexed.
first[j]is a one-character string, compared with===by value. Reading past the end givesundefined, not an error, which is why thejbounds come first in the loop condition. order += ...builds a new string each time, since strings are immutable. For at most 26 letters that cost doesn't matter.- Annotations are erased.
boolean[],number[][]and: stringonly help the compiler.
Complexity
Time: O(C), where C is the total number of letters in words. Counting the letters reads each once. Each neighbouring pair is compared up to the length of the shorter word, which adds up to at most C overall. The ordering phase places at most 26 letters, and each round scans at most 26 candidates plus that letter's arrows, so it costs a constant amount of work plus the number of arrows (at most one per pair).
Space: O(1) plus the arrows. The arrays waiting, graph and inDegree have 26 entries. The lists inside graph hold at most one arrow per neighbouring pair of words.
Edge cases and common mistakes
- A prefix in the wrong order.
["abc", "ab"]returns"", while["ab", "abc"]returns"abc". - Contradictions.
["z", "x", "z"]returns"", and so does["x", "y", "x"]: the arrows form a cycle, so no letter is ever ready. - Repeated words.
["z", "z"]returns"z". Equal neighbours give no fact, and the letter is still counted once. - One word.
["a"]returns"a": there are no pairs, and the only letter is placed in the first round. - Letters with no constraints. In
["cb", "ca"],chas no arrows. Taking the smallest ready letter each round gives"bac"rather than another valid order. - Forgetting letters that appear in no arrow. They still belong in the answer. That is why the letters are collected before the graph is built, and why
lettersis counted. - Comparing non-neighbours. It is unnecessary: neighbours already imply the rest. It is also slower.
- Taking any ready letter. Any ready letter gives a valid order, but only the smallest ready one gives the alphabetically first order the statement asks for.
Testing
["wrt", "wrf", "er", "ett", "rftt"]gives"wertf".["z", "x", "z"]gives"".["cb", "ca"]gives"bac".["abc", "ab"]gives"".["ab", "abc"]gives"abc".["z", "z"]gives"z".["a"]gives"a".["x", "y", "x"]gives"".
