Problem overview
An alien language uses the lowercase English letters in an unknown order. The method alienOrder receives words, a list of words from its dictionary that is sorted in the alien alphabet. Words are compared like in any dictionary: at the first position where two words differ, the word with the earlier letter comes first, and when one word is a prefix of the other the shorter word comes first. The method returns a string with every letter that appears in words, once each, in alien order. If the words contradict themselves, it returns an empty string. If several orders work, it returns the one that comes first in ordinary alphabetical order, which makes the answer unique.
For ["wrt", "wrf", "er", "ett", "rftt"] the result is "wertf". Neighbouring words give facts: "wrt" before "wrf" means t comes before f, "wrf" before "er" means w before e, "er" before "ett" means r before t, and "ett" before "rftt" means e before r. Chained, that is w, e, r, t, f. For ["z", "x", "z"] the result is "", because z before x and x before z contradict each other. For ["cb", "ca"] the only fact is b before a, and c is unconstrained, so "bac", "bca" and "cba" all work. The alphabetically first of them, "bac", is the answer.
The statement describes an O(C) approach, where C is the total number of letters in words. There can be up to 10,000 words of up to 20 letters.
Approach: letters as a graph, then a topological sort
Turn each fact into an arrow between letters: an arrow from a to b means a must come before b. An order that respects every arrow is a topological order of this graph. Kahn's algorithm builds one by repeatedly placing a letter that has no unplaced letters pointing at it, which means its in-degree is 0.
The facts come only from neighbouring words. If every word is in order with the next one, the whole list is sorted, so comparing other pairs adds nothing, and comparing all pairs would cost O(n²) comparisons for n words. For two neighbours, find the first position where they differ. If it exists, the letter in the first word comes before the letter in the second word, and the letters after that position say nothing. If it doesn't exist, one word is a prefix of the other. That is fine when the first word is the shorter (or equal) one. When the first word is longer, the list is impossible in any alphabet, so the answer is "".
To make the answer the alphabetically smallest order, each round of Kahn's algorithm scans from a to z and takes the first letter that is ready. A letter is ready when it appears in the words, hasn't been placed, and its in-degree is 0. If a round finds no ready letter but some letters are still waiting, they wait for each other in a cycle, and the answer is "".
For ["cb", "ca"] there is one arrow, b to a, so a has in-degree 1. The first round skips a and takes b, which lowers the in-degree of a to 0. The next round takes a, and the last takes c. The order is "bac".
Step by step
- Create
waiting, abooleanarray of size 26. Entrykistruewhile letter numberkappears in the words but isn't in the answer yet. An array indexed by letter is simpler than a map for 26 letters. - Create
letters, anintthat starts at 0 and counts the different letters. - Loop over each word with a for-each loop.
- Loop over each
char cofword.toCharArray(). - Check
!waiting[c - 'a']: the letter hasn't been marked yet. Since a to z are consecutive,c - 'a'is 0 for a, 1 for b, up to 25 for z. - Mark the letter by setting
waiting[c - 'a']totrue. - Count it with
letters++. Theifensures each letter is counted once. - Create
graphas aList<List<Integer>>holding an emptyArrayList.graph.get(a)will list the letters that must come after lettera. - Loop
kfrom 0 to 25, once per letter. - Add a new empty
ArrayListtographin each pass, since Java has no one-line way to build a list of separate lists. - Create
inDegree, anintarray of size 26.inDegree[b]counts the letters that must come beforeband aren't placed yet. - Loop over neighbouring pairs with
i < words.length - 1. - Name the pair:
firstiswords[i]andsecondiswords[i + 1]. - Start
jat 0 to find the first position where they differ. - Advance
jwhile it is inside both words and the characters are equal. The checks run left to right, so the characters are read only whilejis valid. - Increment
jinside that loop. - If
jis inside both words, the characters atjare the first ones that differ. - Add the arrow: append the index of
second.charAt(j)to the list at the index offirst.charAt(j). - Increase
inDegreeat the index ofsecond.charAt(j). Two pairs can give the same arrow, and each copy is counted here and removed once later. - Otherwise, if
firstis longer thansecond, thensecondis a proper prefix offirstyet comes after it: return"". - Create
orderas an empty string to collect the answer. - Loop while
order.length() < letters. Each round places one letter. - Start
letterat 0. - Skip letters that aren't ready: while
letter < 26and the letter isn't waiting or still has incoming arrows. - Increment
letterinside that loop. The first ready letter from a to z is the smallest one. - If
letter == 26, nothing is ready: return"". - Place the letter by setting
waiting[letter]tofalse. - Append it to
orderwith(char) ('a' + letter). - Loop over the letters after it,
graph.get(letter). - Decrease
inDegree[nextLetter]. When a count reaches 0, the next round can pick that letter. - Return
order.
The solution in Java
class Solution {
public String alienOrder(String[] words) {
boolean[] waiting = new boolean[26];
int letters = 0;
for (String word : words) {
for (char c : word.toCharArray()) {
if (!waiting[c - 'a']) {
waiting[c - 'a'] = true;
letters++;
}
}
}
List<List<Integer>> graph = new ArrayList<>();
for (int k = 0; k < 26; k++) {
graph.add(new ArrayList<>());
}
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.charAt(j) == second.charAt(j)) {
j++;
}
if (j < first.length() && j < second.length()) {
graph.get(first.charAt(j) - 'a').add(second.charAt(j) - 'a');
inDegree[second.charAt(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);
for (int nextLetter : graph.get(letter)) {
inDegree[nextLetter]--;
}
}
return order;
}
}
The Java details worth a closer look:
- Characters are numbers. A
charis a 16-bit unsigned number, soc - 'a'is anintand works as an array index. This relies on the input being lowercase letters, as the constraints promise. - The cast on
'a' + letter. Adding anintto achargives anint. Without(char),order += 'a' + letterwould append a number such as98instead of the letterb. - Unboxing in the for-each.
graph.get(letter)is aList<Integer>, andint nextLetterunboxes each element automatically. - Generics with the diamond.
new ArrayList<>()lets the compiler infer the element type from the declaration. The lesson's editor already providesjava.util. In a file of your own, addimport java.util.*;. - String concatenation.
order += ...creates a new string each time. With at most 26 letters that cost is negligible. - Short-circuit evaluation. In
letter < 26 && (...), the array reads happen only whileletteris a valid index. Without that order,waiting[26]would throw anArrayIndexOutOfBoundsException.
Complexity
Reading every letter once to find the letters present, and comparing each pair of neighbours up to their first difference, takes O(C), where C is the total number of letters. There are at most 26 letters and each pair of words adds at most one arrow, so the graph work is bounded by the number of words. The placing loop runs at most 26 rounds, each scanning at most 26 letters, which is a constant. The total is O(C). The extra space is constant apart from the arrows: the arrays have 26 entries and the graph holds at most one arrow per neighbouring pair.
Edge cases and common mistakes
- A word followed by its own prefix.
["abc", "ab"]returns"", since the shorter word must come first.["ab", "abc"]is fine and returns"abc". - Letters with no constraints. They still belong in the answer. For
["ab", "adc"]the only fact is b before d, and the result is"abcd", with a and c placed by alphabetical order. - A single word. The loop over pairs doesn't run, and
["abc"]returns"abc", the letters in alphabetical order. - Duplicate words.
["z", "z"]gives no fact and returns"z". Equal neighbours are legal. - Cycles.
["z", "x", "z"]and["a", "b", "a"]both return"", because the arrows point both ways. - Repeated facts. Only the first differing position of a pair matters, and the same fact can appear twice. In
["ba", "bc", "ac", "cab"]the facts are a before c, b before a and a before c, and the answer is"bac". - Forgetting the alphabetical rule. Taking any ready letter would still give a valid order, but not necessarily the one the lesson's tests expect.
Testing
These results come from running the solution:
words | Result |
|---|---|
["wrt", "wrf", "er", "ett", "rftt"] | "wertf" |
["z", "x", "z"] | "" |
["cb", "ca"] | "bac" |
["abc", "ab"] | "" |
["ab", "abc"] | "abc" |
["abc"] | "abc" |
["z", "z"] | "z" |
["z", "x"] | "zx" |
["ab", "adc"] | "abcd" |
["ba", "bc", "ac", "cab"] | "bac" |
