Problem overview
AlienOrder receives words, a list of words written with the lowercase letters but sorted in an unknown alien alphabet. It returns a string with every letter that appears in the words, once each, in the alien order. If the words contradict themselves, so that no order can make them sorted, it returns "". When several orders work, it returns the one that comes first in the usual alphabet.
Words are compared like in a 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". For {"z", "x", "z"} it is "". For {"cb", "ca"} it is "bac": the words only say that b comes before a, and "bac" is the alphabetically first of the valid orders.
Approach: turn word pairs into arrows
Each pair of neighbouring words tells you at most one fact. Find the first position where they differ; the letter in the first word comes before the letter in the second. In the first example, "wrt" before "wrf" gives t before f, and "wrf" before "er" gives w before e. Neighbours are enough: if each word is in order with the next, the whole list is sorted.
The facts become arrows between letters, and an order that respects every arrow is a topological order. Kahn's algorithm builds one: keep, for every letter, an in-degree, the number of arrows pointing at it from letters not yet placed. A letter with in-degree 0 is ready. Place a ready letter, then reduce the in-degree of the letters it points to.
To get the alphabetically first answer, take the smallest ready letter in each round. If some letters remain but none is ready, they wait for each other in a cycle, and the answer is "". A second source of contradiction is a longer word before its own prefix, such as "abc" before "ab".
Comparing every pair of words would take O(n^2) comparisons; neighbours alone give time proportional to the total number of letters.
Step by step
- Mark the letters that appear. Create
bool[] waitingof size 26 (one entry per letter) and anint lettersthat starts at 0.waiting[k]istruewhile letter numberkappears in the words and is not in the answer yet. - Scan every character with two nested
foreachloops. For a lettercthat is not marked, setwaiting[c - 'a']totrueand add 1 toletters. Acharis a number, andatozare consecutive, soc - 'a'is 0 foraup to 25 forz. - Build the graph. Create
List<List<int>> graphand add 26 new emptyList<int>in a loop.graph[a]holds the letters that must come after lettera. - Create
int[] inDegreeof size 26. - Go through neighbouring pairs with
ifrom 0 whilei < words.Length - 1. Name themfirstandsecond. - Find the first difference. Start
jat 0 and advance it while it is inside both words andfirst[j] == second[j]. - If
jis inside both words, the letters atjare the first that differ. Addsecond[j] - 'a'tograph[first[j] - 'a']and increaseinDegree[second[j] - 'a']. Two pairs can give the same arrow; each copy is counted once here and removed once later. - Otherwise, if
first.Length > second.Length,secondis a prefix offirstyet comes after it. Return"". - Run Kahn's algorithm. Create
string order = ""and loop whileorder.Length < letters. - Pick the smallest ready letter. Start
letterat 0 and move it up whileletter < 26and the letter is not waiting or still has arrows coming in. - Detect a cycle. If
letter == 26, no letter is ready: return"". - Place it. Set
waiting[letter]tofalseand append(char) ('a' + letter)toorder. - Release its followers. For each
nextLetteringraph[letter], decreaseinDegree[nextLetter]. - Return
orderwhen every letter has been placed.
The solution in C#
class Solution {
public string AlienOrder(string[] words) {
bool[] waiting = new bool[26];
int letters = 0;
foreach (string word in words) {
foreach (char c in word) {
if (!waiting[c - 'a']) {
waiting[c - 'a'] = true;
letters++;
}
}
}
List<List<int>> graph = new List<List<int>>();
for (int k = 0; k < 26; k++) {
graph.Add(new List<int>());
}
int[] inDegree = new int[26];
for (int i = 0; i < words.Length - 1; i++) {
string first = words[i];
string second = words[i + 1];
int j = 0;
while (j < first.Length && j < second.Length && first[j] == second[j]) {
j++;
}
if (j < first.Length && j < second.Length) {
graph[first[j] - 'a'].Add(second[j] - 'a');
inDegree[second[j] - 'a']++;
} else if (first.Length > second.Length) {
return "";
}
}
string order = "";
while (order.Length < letters) {
int letter = 0;
while (letter < 26 && (!waiting[letter] || inDegree[letter] > 0)) {
letter++;
}
if (letter == 26) {
return "";
}
waiting[letter] = false;
order += (char) ('a' + letter);
foreach (int nextLetter in graph[letter]) {
inDegree[nextLetter]--;
}
}
return order;
}
}
The C# details worth knowing:
'a' + letteris anint, because arithmetic on acharpromotes it. Writingorder += 'a' + letterwould append the number as text (forletter = 3, the text100). The cast(char)turns it back into the letter.- A
bool[]orint[]starts withfalseor 0 in every slot. AList<List<int>>starts empty, and the inner lists do not exist until they are added, which is why the loop adds 26 of them. foreach (char c in word)reads a string onecharat a time.- The loop condition
j < first.Length && j < second.Length && first[j] == second[j]relies on&&stopping at the firstfalse, so the indexing is safe. - Building
orderwith+=creates a new string each time, since strings are immutable. With at most 26 letters that cost does not matter.
Complexity
Time: O(C), where C is the total number of letters in words. Reading the letters and comparing neighbours is O(C). Kahn's algorithm works on at most 26 letters and at most words.Length - 1 arrows, and each round scans 26 letters at most, which is a constant.
Space: O(n) for n words, at most. The arrays have 26 entries, and the arrow lists hold at most one entry per neighbouring pair of words.
Edge cases and common mistakes
- Cycle.
{"z", "x", "z"}givesz before xandx before z, so the result is"". - Prefix after the longer word.
{"abc", "ab"}returns"". The reverse,{"ab", "abc"}, is valid and gives"abc". - Letters with no arrows. In
{"cb", "ca"}the lettercis never compared, but it must still appear in the answer. That is why the first pass records every letter, not only those in arrows. - Equal words.
{"z", "z"}gives"z": identical neighbours say nothing. - Not stopping at the first difference. Only the first differing position of a pair counts. Later letters tell nothing.
- Counting an arrow twice in the graph but once in the in-degree, or the other way round, would leave a letter that never becomes ready.
- Lowest letter first. Taking any ready letter gives a valid order but not necessarily the alphabetically first one.
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", "b", "c"}gives"abc".
