Problem overview
An alien language uses the lowercase English letters in an order you don't know. The function alien_order receives words, a vector of strings sorted in that alien alphabetical order. It returns a string with every letter that appears in the words, each once, in the alien order.
Words are compared as in any dictionary: at the first position where they differ, the word with the earlier letter comes first. When one word is a prefix of the other, the shorter one comes first, so "ab" comes before "abc". If the words contradict themselves, so that no order of the letters makes them sorted, the function returns "". When several orders work, it returns the one that comes first in the usual alphabet, which makes the answer unique.
For ["wrt", "wrf", "er", "ett", "rftt"] the answer is "wertf". For ["z", "x", "z"] it is "", because the list says z comes before x and also x before z. For ["cb", "ca"] it is "bac": the words only tell us that b comes before a, and of the orders "bac", "bca" and "cba", the first is alphabetically smallest.
Approach: turn neighbouring words into arrows
Each pair of neighbouring words gives at most one fact, found at the first position where they differ: the letter in the first word comes before the letter in the second. Letters after that position tell us nothing. If every word is in order with the next one, the whole list is sorted, so neighbours are enough. Comparing every pair would take far more work for no new facts.
For the first example, the four pairs give:
wrt before wrf: t -> f
wrf before er: w -> e
er before ett: r -> t
ett before rftt: e -> r
Chained together, that is w, e, r, t, f.
These arrows form a graph on the letters, and an order that respects every arrow is a topological order. Kahn's algorithm finds one: repeatedly place a letter that has no unplaced letter required before it, then remove its arrows. To get the alphabetically first answer, each round takes the smallest letter that is ready. If at some point letters remain but none is ready, they wait for each other in a cycle, and the answer is "".
There is one more contradiction that arrows can't show: a word that comes before its own prefix, such as ["abc", "ab"]. There is no differing letter, but the longer word is first, so no alphabet can sort the list.
Running the pairs of ["cb", "ca"] gives the single arrow b to a, and the letters are placed as b, then a, then c. The letter c is placed last because a is smaller and becomes ready first.
Step by step
- Mark the letters that appear.
waitingis a vector of 26falsevalues, one per letter. An array indexed by letter is simpler than a dictionary when there are only 26 possibilities.waiting[k]will be true while letter numberkappears in the words and isn't in the answer yet. - Count the different letters in
letters, starting at 0. The answer is complete when its length reaches this number. - Loop over each word and then over each of its bytes
c. The expressionc - b'a'is 0 for a, 1 for b and so on to 25 for z, andas usizemakes it an index. - Count each letter once: when
!waiting[...]is true, set the entry to true and add 1 toletters. - Create the graph as a
Vec<Vec<usize>>of 26 empty vectors withvec![Vec::new(); 26].graph[a]holds the letters that must come after lettera. - Create
in_degree, 26 zeros.in_degree[b]counts the letters that must come beforeband aren't placed yet. - Loop over the neighbouring pairs with
iin0..words.len() - 1, and take the bytes ofwords[i]andwords[i + 1]withas_bytes(), since aStringcan't be indexed by position. - Find the first difference: start
jat 0 and advance it while it is inside both words and the bytes are equal. The checks run left to right and stop at the first one that fails, so the bytes are read only whilejis in range for both. - If
jis still inside both words, the two bytes there are the first letters that differ. Push the second letter onto the first letter's list ingraph, and add 1 to the second letter'sin_degree. Two pairs may give the same arrow. Each copy is counted here and removed once later, so that is fine. - Otherwise, if the first word is longer (
} else if first.len() > second.len()), the second word is a prefix of the first and should have come first. ReturnString::new(). A first word that is shorter or equal gives no fact. - Create
orderas an emptyStringand loop whileorder.len() < letters. Each round places one letter. - Find the smallest ready letter. Start
letterat 0 and advance it whileletter < 26and the letter isn't waiting or still has arrows coming in. A ready letter appears in the words, isn't placed yet, and has all the letters before it placed. - Detect a cycle. If
letter == 26, no letter is ready, so the remaining ones wait on each other. ReturnString::new(). - Place the letter. Set
waiting[letter]to false and push(b'a' + letter as u8) as charontoorder. The index becomes a byte so it can be added tob'a', andas charturns the byte into a character. - Release the letters after it. For each
next_letterin&graph[letter], subtract 1 from itsin_degree. - Return
orderwithout a semicolon.
The solution in Rust
impl Solution {
fn alien_order(words: Vec<String>) -> String {
let mut waiting = vec![false; 26];
let mut letters = 0;
for word in &words {
for c in word.bytes() {
if !waiting[(c - b'a') as usize] {
waiting[(c - b'a') as usize] = true;
letters += 1;
}
}
}
let mut graph: Vec<Vec<usize>> = vec![Vec::new(); 26];
let mut in_degree = vec![0; 26];
for i in 0..words.len() - 1 {
let first = words[i].as_bytes();
let second = words[i + 1].as_bytes();
let mut j = 0;
while j < first.len() && j < second.len() && first[j] == second[j] {
j += 1;
}
if j < first.len() && j < second.len() {
graph[(first[j] - b'a') as usize].push((second[j] - b'a') as usize);
in_degree[(second[j] - b'a') as usize] += 1;
} else if first.len() > second.len() {
return String::new();
}
}
let mut order = String::new();
while order.len() < letters {
let mut letter = 0;
while letter < 26 && (!waiting[letter] || in_degree[letter] > 0) {
letter += 1;
}
if letter == 26 {
return String::new();
}
waiting[letter] = false;
order.push((b'a' + letter as u8) as char);
for &next_letter in &graph[letter] {
in_degree[next_letter] -= 1;
}
}
order
}
}
The Rust details worth a closer look:
- Bytes as letters.
word.bytes()yieldsu8values, andb'a'is theu8code of the letter a. Subtracting gives au8, andas usizemakes it an index. The subtraction can't underflow, because the words hold only lowercase letters. - Inferred types.
lettersandin_degreename no integer type. Both default toi32, except thatlettersis compared withorder.len(), which makes it ausize. Andletteris ausize, because it indexes vectors. vec![Vec::new(); 26]. The macro clones the value for every entry, so each letter gets its own empty vector, not one shared vector. The type ongraphtells the compiler what the vectors hold.&wordsand&graph[letter]. The&borrows instead of moving. The pattern&next_letterin the last loop copies eachusizeout of the reference.(b'a' + letter as u8) as char. The castletter as u8binds tighter than+. Au8can be cast tochardirectly, andpushappends a character to theString.String::len()counts bytes. For the lowercase letters here, bytes and characters are the same, soorder.len()is the number of letters placed.words.len() - 1. The length is ausize, so for an empty vector the subtraction would underflow. The constraints promise at least one word. With exactly one word the range0..0is empty and the loop doesn't run.
Complexity
Time: O(C), where C is the total number of letters in words. Reading every letter takes O(C). Each neighbouring pair is compared up to the length of the shorter word, which is at most C in total. The placing loop runs at most 26 rounds, each scanning 26 letters and releasing the arrows of one letter. The number of different arrows is bounded by the pairs, so the placing loop is constant work for the fixed alphabet.
Space: O(1) beyond the input, plus the arrows. The vectors have 26 entries each, and the arrows are at most one per neighbouring pair, so the graph holds at most n - 1 entries for n words.
Edge cases and common mistakes
- A word before its own prefix.
["abc", "ab"]returns"". Without theelse ifbranch, the pair would add no arrow and the function would return"abc". - A prefix after its word.
["ab", "abc"]is fine and returns"abc", since the shorter word comes first. - A cycle.
["z", "x", "z"]gives the arrows z to x and x to z. Neither letter ever hasin_degree0, soletterreaches 26 and the function returns"". The list["a", "b", "a"]does the same. - Letters with no arrows. In
["cb", "ca"], the letter c is in no arrow. It is still in the answer, and it is placed when it is the smallest ready letter. - Repeated words.
["z", "z"]gives no arrow, because the words are equal, and returns"z". - Counting letters that don't appear. Only letters found in the words are placed. The
waitingvector keeps unused letters out of the answer. - Taking any ready letter. The answer must be the alphabetically first order, so each round scans from a upward and takes the first ready letter. A stack or queue of ready letters may give a different valid order.
- Comparing non-neighbours. Looking only at neighbouring words is enough, because the order is transitive.
Testing
["wrt", "wrf", "er", "ett", "rftt"]gives"wertf".["z", "x", "z"]gives"".["cb", "ca"]gives"bac".["abc", "ab"]gives"".["ab", "abc"]gives"abc".["z"]gives"z".["z", "z"]gives"z".["za", "zb", "ab", "eb", "ec"]gives"zabce".["ba", "bc", "ac", "cb"]gives"bac".["ab", "ac", "bc", "bd"]gives"abcd".
