Start learning free

LESSON 480 OF 540 · GRAPHS

Alien Dictionary in Kotlin: Topological Sort in O(C)

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 an array 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

    KOTLIN · GRAPHS

    Alien Dictionary in Kotlin, explained

    Problem overview

    An alien language uses the lowercase English letters in an order you don't know. The function alienOrder gets an Array<String> named words, sorted in that alien alphabetical order, and 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 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" is always before "abc". If the words contradict themselves and no letter order could make them sorted, return "". If several orders work, return 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". For ["z", "x", "z"] it is "", because "z" before "x" and "x" before "z" can't both hold. For ["cb", "ca"] it is "bac": the words only say that b comes before a, and of the orders that respect this, "bac" is first alphabetically.

    Approach

    A sorted list of words tells you facts about letters. Compare two neighbouring words, find the first position where they differ, and the letter in the first word comes before the letter in the second. Letters after that position tell you nothing. Neighbouring pairs are enough: if each word is in order with the next, the whole list is sorted, so other pairs add no new facts.

    Draw each fact as an arrow from one letter to another. An order that agrees with every arrow is a topological order of this graph, and Kahn's algorithm finds one: keep an in-degree for each letter (how many letters must come before it that aren't placed yet), and place a letter once its count reaches 0. Placing a letter lowers the count of the letters it points to.

    Two extra rules are specific to this problem:

    • Prefix contradiction. If a longer word comes right before its own prefix ("abc" then "ab"), the list can't be sorted in any alphabet. The function returns "".
    • Cycles. If, at some point, letters are still waiting but none has an in-degree of 0, they wait for each other. That is a cycle, and the function returns "".

    To get the alphabetically first answer, each round takes the smallest ready letter instead of any ready letter. With at most 26 letters, scanning from a to z each round is cheap. Building the graph looks at each letter of the input a bounded number of times, so the time is O(C), where C is the total number of letters in words. Comparing every pair of words would take O(n²) comparisons for n words.

    Step by step

    1. Find the letters that appear. waiting is a BooleanArray(26), one entry per letter, all false at first. waiting[k] becomes true when letter number k appears in the words, and goes back to false when the letter is placed in the answer. An array indexed by letter is simpler than a map for 26 letters.
    2. Count them. var letters = 0 counts the different letters, so the answer is complete when its length reaches letters.
    3. Visit every character. An outer loop over words and an inner loop over the characters c of each word.
    4. Mark each letter once. if (!waiting[c - 'a']) is true the first time a letter appears. Subtracting two Char values gives an Int, so c - 'a' is 0 for a through 25 for z. Inside, set the entry to true and do letters++.
    5. Create the graph. Array(26) { mutableListOf<Int>() } makes one list per letter, with its own list each time because the lambda runs once per index. graph[a] holds the letters that must come after a.
    6. Create the in-degrees. IntArray(26) starts at zeros. inDegree[b] counts the arrows pointing at b.
    7. Loop over neighbouring pairs. for (i in 0 until words.size - 1), with first = words[i] and second = words[i + 1].
    8. Find the first difference. var j = 0 and a while loop that advances while j is inside both words and first[j] == second[j]. The checks run left to right, so the characters are read only while j is valid.
    9. Add the arrow. If j is still inside both words, first[j] and second[j] differ: add second[j] - 'a' to graph[first[j] - 'a'] and do inDegree[second[j] - 'a']++. If two pairs give the same arrow, it is counted twice here and removed twice later, which stays consistent.
    10. Catch the prefix case. Otherwise one word ran out. If first.length > second.length, the second is a prefix of the first and comes after it: return "". An equal or shorter first word gives no fact.
    11. Run Kahn's algorithm. var order = "" collects the answer, and while (order.length < letters) places one letter per round.
    12. Find the smallest ready letter. Start var letter = 0 and advance while letter < 26 && (!waiting[letter] || inDegree[letter] > 0). A letter is ready when it appears in the words, isn't placed yet, and every letter before it is placed.
    13. Detect a cycle. If letter == 26, nothing is ready: return "".
    14. Place the letter. Set waiting[letter] = false and append 'a' + letter to order.
    15. Release the letters after it. For each nextLetter in graph[letter], do inDegree[nextLetter]--.
    16. Return the order. Every letter was placed after all the letters that must precede it.

    The solution in Kotlin

    class Solution {
        fun alienOrder(words: Array<String>): String {
            val waiting = BooleanArray(26)
            var letters = 0
            for (word in words) {
                for (c in word) {
                    if (!waiting[c - 'a']) {
                        waiting[c - 'a'] = true
                        letters++
                    }
                }
            }
            val graph = Array(26) { mutableListOf<Int>() }
            val inDegree = IntArray(26)
            for (i in 0 until words.size - 1) {
                val first = words[i]
                val second = words[i + 1]
                var 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 ""
                }
            }
            var order = ""
            while (order.length < letters) {
                var letter = 0
                while (letter < 26 && (!waiting[letter] || inDegree[letter] > 0)) {
                    letter++
                }
                if (letter == 26) {
                    return ""
                }
                waiting[letter] = false
                order += 'a' + letter
                for (nextLetter in graph[letter]) {
                    inDegree[nextLetter]--
                }
            }
            return order
        }
    }
    

    Kotlin details worth knowing:

    • Char arithmetic. Char - Char is an Int, and Char + Int is a Char. So 'c' - 'a' is 2 and 'a' + 2 is 'c'. That is how the code moves between letters and indexes.
    • String plus Char. order += 'a' + letter builds the Char first and then appends it to the string. The expression "w" + ('a' + 2) gives "wc".
    • for (c in word) iterates over the characters of a string directly.
    • Array(26) { mutableListOf<Int>() } creates 26 separate lists. Filling an array with one shared list would make every letter point at the same list.
    • x++ on an array element, as in inDegree[b]++, works on a val array because only its contents change.

    Complexity

    The first pass reads every letter once. Comparing neighbouring words reads each word at most twice, so that part is O(C) too. Kahn's algorithm places at most 26 letters, scanning at most 26 entries each time, and walks each arrow once, so that part is bounded by a constant plus the number of pairs. The total is O(C) time, treating the 26-letter alphabet as a constant. The extra space is O(1) for the fixed-size arrays, plus at most one arrow per neighbouring pair in the lists.

    Edge cases and common mistakes

    • A longer word before its prefix. ["abc", "ab"] returns "", while ["ab", "abc"] returns "abc".
    • Cycles. ["z", "x", "z"] returns "", as does ["a", "b", "a"].
    • Letters without any arrow. They still belong in the answer. ["abc"] gives "abc", and in ["cb", "ca"] the letter c has no arrow but is placed in alphabetical position.
    • Several valid orders. Always take the smallest ready letter. ["zy", "zx"] gives "yxz", because y must come before x and z is free but alphabetically last.
    • Repeated words. ["x", "x", "x"] gives "x": equal neighbours give no arrow and no contradiction.
    • Using a cycle-blind approach. A DFS that doesn't track "in progress" nodes may loop or return a wrong order for contradictory input.
    • Reading past a word's end. Compare j with both lengths before reading first[j] and second[j].
    • Counting letters from the arrows only. A letter that appears in no differing position would be dropped. That's why the first pass collects letters from all the words.

    Testing

    Results from running the solution:

    • ["wrt", "wrf", "er", "ett", "rftt"]: "wertf"
    • ["z", "x", "z"]: ""
    • ["cb", "ca"]: "bac"
    • ["abc", "ab"]: ""
    • ["ab", "abc"]: "abc"
    • ["z", "z"]: "z"
    • ["zy", "zx"]: "yxz"
    • ["ba", "bc", "ac", "cab"]: "bac"
    • ["ab", "adc"]: "abcd"
    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free