Problem overview
The method alienOrder(words) receives an array of words that are sorted according to an unknown alphabet, which uses the lowercase English letters in some other order. It returns a string with every letter that appears in the words, each once, in the order of that alien alphabet.
Words are compared as in any dictionary: at the first position where two words 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". If the words contradict themselves, so that no letter order 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.
For ["wrt", "wrf", "er", "ett", "rftt"] the answer is "wertf". Each pair of neighbouring words gives one fact: "wrt" before "wrf" says t comes before f; "wrf" before "er" says w before e; "er" before "ett" says r before t; "ett" before "rftt" says e before r. Chained together they give w, e, r, t, f. For ["z", "x", "z"] the answer is "", since z must come before x and x before z. For ["cb", "ca"] only b before a is known, and of the orders that work ("bac", "bca", "cba") the alphabetically first is "bac".
Approach: letters as a graph, then a topological order
Turn each fact into an arrow between two letters: an arrow from a to b means a must come before b. An order of the letters that agrees with every arrow is a topological order, and Kahn's algorithm finds one. It tracks an in-degree for each letter, the number of arrows pointing into it from letters not yet placed. A letter with in-degree 0 is ready: everything that must come before it is already in the answer. Place a ready letter, then subtract 1 from the in-degree of every letter it points to. Repeat until all letters are placed.
Two parts of the problem need care:
- Where the arrows come from. Only neighbouring words need to be compared. If every word is in order with the next one, the whole list is sorted, so other pairs add nothing. For a pair, find the first position where the letters differ; that gives one arrow. The letters after that position say nothing. If no position differs and the first word is longer, the second is a prefix of the first and should have come first in any alphabet: the list is contradictory.
- Which ready letter to take. Always take the smallest one in the usual alphabet. That is what makes the answer unique.
If at some point letters are still waiting but none has in-degree 0, each of them waits for another waiting letter. That is a cycle, so the method returns "".
Comparing neighbouring words takes time proportional to the total number of letters, O(C). Comparing every pair of the n words would take O(n²) comparisons.
Step by step
- Mark the letters that appear.
waitingis an array of 26 booleans, one per letter.waiting[k]istruewhile letter numberkappears in the words but is not in the answer yet. An array indexed by letter is simpler than an object when there are only 26 letters. - 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 charactercwithfor...of. - Convert the letter to an index.
c.charCodeAt(0) - 97gives 0 fora, 1 forb, up to 25 forz. - Mark the letter only once. If it isn't marked yet, set it to
trueand add 1 toletters. - Build the graph.
graphis 26 empty arrays, made withArray.from({length: 26}, () => [])so that each letter gets its own array.graph[a]lists the letters that must come aftera. - Create
inDegree, 26 zeros. - Loop over neighbouring pairs:
ifrom 0 whilei < words.length - 1, withfirst = words[i]andsecond = words[i + 1]. - Find the first difference. Start
jat 0 and advance it whilejis inside both words andfirst[j] === second[j]. The checks run left to right and stop at the first false one, so the letters are read only whilejis inside both words. - If
jis inside both words, the letters atjare the first that differ. Push the index ofsecond[j]ontographat the index offirst[j], and add 1 toinDegreeforsecond[j]. Two pairs may give the same arrow; each copy is counted here and removed once later. - Otherwise, if
first.length > second.length, the second word is a prefix of the first: return"". A shorter or equal first word gives no fact. - Collect the answer in
order, an empty string, with awhileloop that runs whileorder.length < 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. - If
letter === 26, nothing is ready: return"". - Place the letter. Set
waiting[letter]tofalseand appendString.fromCharCode(97 + letter)toorder, which turns the index back into a character. - Release the letters that follow it. For each
nextLetteringraph[letter], subtract 1 frominDegree[nextLetter]. - Return
order.
The solution in JavaScript
class Solution {
alienOrder(words) {
const waiting = 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 = Array.from({length: 26}, () => []);
const inDegree = new Array(26).fill(0);
for (let i = 0; i < words.length - 1; i++) {
const first = words[i];
const second = 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 = "";
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;
}
}
JavaScript details worth knowing:
- Character codes.
"a".charCodeAt(0)is 97 and"z".charCodeAt(0)is 122. Subtracting 97 maps a letter to 0 to 25.String.fromCharCode(97 + 3)gives"d", the reverse. - Looping over a string.
for (const c of word)gives one character at a time, as one-character strings.word[j]reads the character at an index, and past the end it givesundefinedinstead of an error, but the loop never gets there because of the bounds checks. Array.from({length: 26}, () => []). The function runs once per index, so each entry is a different array.new Array(26).fill([])would put the same array in all 26 entries, so pushing to one letter would change all.- The inner
whileas a search. Scanning from 0 to 26 each round costs 26 steps at most. That is why the loop picks the smallest ready letter without any sorting or priority queue. - Appending to a string.
order += ...builds a new string each time, which is fine for at most 26 letters.
Complexity
Time: O(C), where C is the total number of letters in words. Marking the letters visits each one once. Comparing neighbouring words visits at most the letters of the shorter word in each pair. Placing the letters takes at most 26 rounds of at most 26 steps plus the arrows, and the number of arrows is at most the number of pairs, which is below the number of words. That part is bounded by a constant.
Space: O(1) for the letters (arrays of 26) plus up to one arrow per neighbouring pair stored in graph.
Edge cases and common mistakes
- Prefix in the wrong order.
["abc", "ab"]returns"", because"ab"should come first.["ab", "abc"]is fine and returns"abc". - Letters without any arrow.
["z"]returns"z", and["z", "z"]also returns"z": equal words give no fact, and each letter appears once in the answer. - Letters that appear only in the rest of a word.
["ab", "adc"]returns"abcd". The only arrow is b before d, and the letters a and c have no constraints, so they are placed alphabetically when ready. - Cycles that come from non-neighbours.
["a", "b", "a"]returns"": the arrows a before b and b before a form a cycle. - Taking any ready letter. The answer must be the alphabetically first valid order. For
["za", "zb", "ca", "cb"]the arrows are z before c and a before b, and the result is"abzc". Taking letters in another order, such as z first, would give a different valid order that the lesson's tests reject. - Forgetting to count letters. A letter that never appears in an arrow still has to be in the answer.
Testing
["wrt", "wrf", "er", "ett", "rftt"]gives"wertf".["z", "x", "z"]gives"".["cb", "ca"]gives"bac".["abc", "ab"]gives"".["ab", "abc"]gives"abc".["ab", "adc"]gives"abcd".["za", "zb", "ca", "cb"]gives"abzc".
