Start learning free

LESSON 475 OF 535 · GRAPHS

Alien Dictionary in C++: Topological Sort with Kahn's Algorithm

Practice O(C) Read the full explanation

An alien language uses the lowercase English letters, but in an order you don't know. You are given a vector of strings words from the dictionary of this language: the words are sorted in the alien alphabetical order.

Two words are compared letter by letter, as in any dictionary: at the first position where they differ, the word with the earlier letter comes first. If no position differs because one word is the beginning (a prefix) of the other, the shorter word comes first, so "ab" always comes before "abc".

Return a string with every letter that appears in words, each once, in the order of the alien alphabet. If the words contradict themselves, so that no order of the letters makes them sorted, return "" (an empty string). If several orders work, return the one that comes first in the usual alphabetical order: that makes the answer unique.

 

Example 1:

Input: words = ["wrt", "wrf", "er", "ett", "rftt"]
Output: "wertf"
Explanation: Each pair of neighbouring words tells us one thing, from the first letters where they differ:
    "wrt" before "wrf":  t comes before f
    "wrf" before "er":   w comes before e
    "er" before "ett":   r comes before t
    "ett" before "rftt": e comes before r
Chained together: w → e → r → t → f. These are all the letters of the words, and this is the only order that works.

Example 2:

Input: words = ["z", "x", "z"]
Output: ""
Explanation: "z" before "x" says z comes before x, but "x" before "z" says x comes before z.
The two facts form a cycle, so no order of the letters can make this list sorted.

Example 3:

Input: words = ["cb", "ca"]
Output: "bac"
Explanation: The words differ first at their second letters, so b comes before a. Nothing tells us where c goes.
The orders "bac", "bca" and "cba" all work; "bac" comes first alphabetically.

 

Constraints:

  • 1 <= words.length <= 104
  • 1 <= words[i].length <= 20
  • words[i] consists of lowercase English letters. The same word can appear more than once.

Note:

Turn the facts into arrows between letters: an order that agrees with every arrow is a topological order, which Kahn's algorithm from Course Schedule finds; taking the alphabetically smallest ready letter each time gives the answer above. Neighbouring words are enough, so the time is O(C), where C is the total number of letters in words; comparing every pair of words would take O(n2) comparisons for n words.

STEP 1 OF 29
    Ctrl + Enter to check

    C++ · GRAPHS

    Alien Dictionary in C++, explained

    Problem overview

    The function alienOrder receives a vector<string> called words, which is sorted in an alien alphabet. Words compare 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" always comes before "abc".

    The function returns a string with every letter that appears in words, each once, in the order of the alien alphabet. If the words contradict themselves, so that no order of the letters makes them sorted, it returns the empty string. If 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 from the first letters where they differ: t before f, w before e, r before t, e before r. Chained, that is w, e, r, t, f. For {"z", "x", "z"} the answer is "", because z before x and x before z form a cycle. For {"cb", "ca"} the only fact is b before a. The orders "bac", "bca" and "cba" all work, and "bac" comes first alphabetically.

    Approach: turn the facts into arrows

    Every fact is an arrow between two letters: the first points to the second, meaning it must come earlier. An order that agrees with every arrow is a topological order of this graph, and Kahn's algorithm finds one. It tracks for each letter its in-degree, the number of letters that must come before it and aren't placed yet. A letter with in-degree 0 can be placed at once. Placing it removes its arrows, which may free other letters.

    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 no new facts. For a pair, find the first position where the words differ. If there is one, the letters there give one arrow, and the letters after that position say nothing. If there is none, one word is a prefix of the other. When the earlier word is the longer one, the list is impossible in any alphabet, so the answer is "".

    To get the alphabetically first order, each round of Kahn's algorithm takes the smallest letter that is ready, scanning from a to z. A letter is ready when it appears in the words, isn't placed yet, and has in-degree 0. If letters remain but none is ready, each waits for another waiting letter. That is a cycle, so the answer is "".

    The cost depends on the total number of letters, C: one pass over the words finds the letters, and one comparison per neighbouring pair stops at the first difference. Comparing every pair of words would take O(n²) comparisons for n words.

    Step by step

    1. Mark the letters that appear. A vector<bool> named waiting of 26 false values, one per letter. waiting[k] is true while letter k appears in the words and isn't in the answer yet.
    2. Count them in an int named letters. The answer is complete when its length reaches this number.
    3. Loop over each word and each character. When !waiting[c - 'a'], set it to true and increase letters. Because a to z are consecutive, c - 'a' is 0 for a up to 25 for z, and the test makes each letter count once.
    4. Create the graph: a vector<vector<int>> named graph with 26 empty vectors. graph[a] holds the letters that must come after a.
    5. Create inDegree, 26 zeros.
    6. Loop over neighbouring pairs, words[i] and words[i + 1], copied into strings first and second.
    7. Find the first difference. Start j at 0 and advance it while it is inside both words and first[j] == second[j].
    8. Record the arrow when j is still inside both words: push the index of second[j] onto graph at the index of first[j] and increase the in-degree of second[j]. Two pairs may give the same arrow. Each copy is counted here and removed once later, so that is fine.
    9. Catch the prefix case with else if (first.size() > second.size()), and return "".
    10. Create order, an empty string.
    11. Loop while order.size() < letters. Each round places one letter.
    12. Find the smallest ready letter. Start letter at 0 and advance it while letter < 26 and the letter isn't waiting or still has arrows coming in.
    13. Detect a cycle. If letter == 26, nothing is ready: return "".
    14. Place the letter. Set waiting[letter] to false and append (char) ('a' + letter) to order.
    15. Release its successors. For every nextLetter in graph[letter], decrease inDegree[nextLetter].
    16. Return order.

    The solution in C++

    class Solution {
    public:
        string alienOrder(vector<string>& words) {
            vector<bool> waiting(26, false);
            int letters = 0;
            for (string& word : words) {
                for (char c : word) {
                    if (!waiting[c - 'a']) {
                        waiting[c - 'a'] = true;
                        letters++;
                    }
                }
            }
            vector<vector<int>> graph(26);
            vector<int> inDegree(26, 0);
            for (int i = 0; i < words.size() - 1; i++) {
                string first = words[i];
                string second = words[i + 1];
                int j = 0;
                while (j < first.size() && j < second.size() && first[j] == second[j]) {
                    j++;
                }
                if (j < first.size() && j < second.size()) {
                    graph[first[j] - 'a'].push_back(second[j] - 'a');
                    inDegree[second[j] - 'a']++;
                } else if (first.size() > second.size()) {
                    return "";
                }
            }
            string order = "";
            while (order.size() < 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[letter]) {
                    inDegree[nextLetter]--;
                }
            }
            return order;
        }
    };
    

    The C++ details worth a closer look:

    • Characters are numbers. c - 'a' is an int, which indexes the vectors. Going back, 'a' + letter is also an int, so (char) turns it into a character before it is appended. Without the cast, order += 'a' + letter would still compile, because the sum is converted back to char when appended, but the cast makes the intention clear.
    • words.size() - 1 is unsigned. For a vector with one word it is 0, and the loop doesn't run. For an empty vector it would wrap to a huge number, but the statement guarantees at least one word. The comparison i < words.size() - 1 mixes an int with an unsigned value, which is harmless while i is not negative.
    • vector<vector<int>> graph(26) creates 26 empty inner vectors. vector<bool> waiting(26, false) takes a size and then the value of every element.
    • Short-circuit order. In the while over j, the two size checks come before first[j] == second[j], so the characters are read only while j is inside both words. In the scan for a ready letter, letter < 26 is checked first, so index 26 is never read.
    • string first = words[i] copies the word. The words are at most 20 characters, so the copy is cheap, and a const string& would also do.

    Complexity

    Time: O(C), where C is the total number of letters in words. Collecting the letters visits each character once. Comparing neighbouring pairs costs at most the length of the shorter word each. Kahn's algorithm runs at most 26 rounds, each scanning 26 letters, plus one decrease per arrow, and there is at most one arrow per neighbouring pair.

    Space: O(n) for n words. The vectors of size 26 are constant. The only part that grows is the list of arrows, at most one per neighbouring pair of words.

    Edge cases and common mistakes

    • A prefix in the wrong order. {"abc", "ab"} gives "". The longer word comes first, which no alphabet allows.
    • A prefix in the right order. {"ab", "abc"} gives no arrow, and the letters come out as "abc".
    • Equal words. {"ab", "ab"} gives "ab": no letter differs, and the first word is not longer.
    • A single word. {"a"} gives "a": there are no pairs and one letter to place.
    • Unconstrained letters. They are still part of the answer. With {"cb", "ca"} the letter c has no arrow, yet it appears in "bac".
    • A contradiction. {"z", "x", "z"} gives "": both z and x wait for each other.
    • Comparing beyond the first difference. Only the first differing letters say anything. The letters after them are unconstrained.
    • Skipping the smallest-first rule. Another queue order would still be a valid topological order, but the lesson asks for the alphabetically first one. The scan from a to z guarantees it.

    Testing

    • {"wrt", "wrf", "er", "ett", "rftt"} gives "wertf".
    • {"z", "x", "z"} gives "".
    • {"cb", "ca"} gives "bac".
    • {"abc", "ab"} gives "".
    • {"a"} gives "a".
    • {"ab", "ab"} gives "ab".
    • {"b", "a", "c"} gives "bac".
    • {"ba", "bc", "ac", "cab"} gives "bac".
    Keep going with C++500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free