Alien Dictionary

hard

Given a list of strings words from an alien language dictionary, where the strings are sorted lexicographically by the rules of this new language, derive the order of characters in the alien alphabet.

If a valid ordering exists, return a string of the unique characters in the alien language sorted according to the derived order. If no valid ordering exists (the input is contradictory), return an empty string "". If there are multiple valid orderings, return any one of them.

The ordering is derived by comparing adjacent words. If two adjacent words share a common prefix but differ at some character position, the character in the first word comes before the character in the second word in the alien alphabet. If a shorter word appears after a longer word that starts with the shorter word, the input is invalid.

Examples

Example 1

  • Input: words = ["wrt","wrf","er","ett","rftt"]
  • Output: "wertf"
  • Explanation: From "wrt" and "wrf" we get t < f. From "wrf" and "er" we get w < e. From "er" and "ett" we get r < t. From "ett" and "rftt" we get e < r. Combining these: w < e < r < t < f.

Example 2

  • Input: words = ["z","x"]
  • Output: "zx"
  • Explanation: From "z" and "x" we derive z < x.

Example 3

  • Input: words = ["z","x","z"]
  • Output: ""
  • Explanation: The ordering is contradictory because z < x and x < z cannot both be true.

Constraints

  • 1 <= words.length <= 100
  • 1 <= words[i].length <= 100
  • words[i] consists of only lowercase English letters.

Approaches

2 worked solutions ranked from straightforward to optimal — each with intuition, algorithm, complexity, and verified code in C++, Java, Python, and Go.

Approach 1

BFS Topological Sort (Kahn's Algorithm)

Intuition

The key insight is that comparing adjacent words reveals ordering constraints between characters. If two adjacent words share a prefix but differ at position k, then the character at position k in the first word comes before the character at position k in the second word. We build a directed graph of these character orderings and perform a topological sort. If the sort includes all characters, we have a valid ordering. If not, there is a contradiction (cycle). We use BFS (Kahn's algorithm) for the topological sort: start with characters that have no incoming edges and process layer by layer.

Algorithm

  1. Collect all unique characters from all words. Initialize in-degree to 0 for each.
  2. Compare each pair of adjacent words. Find the first position where characters differ -- this gives an edge from the first character to the second. If a longer word appears before its prefix, return "" (invalid).
  3. Build an adjacency list and compute in-degrees from these edges.
  4. Add all characters with in-degree 0 to a queue.
  5. Process the queue: dequeue a character, append to result, and decrement in-degrees of its neighbors. Enqueue neighbors that reach in-degree 0.
  6. If the result length equals the number of unique characters, return it. Otherwise, return "" (cycle detected).
TimeO(C) where C is the total length of all words combinedSpaceO(1) since the alphabet size is bounded (at most 26 characters)

Code (C++ · Java · Python · Go)

C++17
class Solution {
public:
    string alienOrder(vector<string>& words) {
        unordered_map<char, unordered_set<char>> graph;
        unordered_map<char, int> indegree;

        for (const string& word : words) {
            for (char c : word) {
                indegree[c] = 0;
            }
        }

        for (int i = 0; i + 1 < (int)words.size(); i++) {
            string& w1 = words[i];
            string& w2 = words[i + 1];
            int minLen = min(w1.size(), w2.size());

            if (w1.size() > w2.size() && w1.substr(0, minLen) == w2.substr(0, minLen)) {
                return "";
            }

            for (int j = 0; j < minLen; j++) {
                if (w1[j] != w2[j]) {
                    if (!graph[w1[j]].count(w2[j])) {
                        graph[w1[j]].insert(w2[j]);
                        indegree[w2[j]]++;
                    }
                    break;
                }
            }
        }

        queue<char> q;
        for (auto& [c, deg] : indegree) {
            if (deg == 0) q.push(c);
        }

        string result;
        while (!q.empty()) {
            char c = q.front();
            q.pop();
            result += c;

            for (char neighbor : graph[c]) {
                indegree[neighbor]--;
                if (indegree[neighbor] == 0) {
                    q.push(neighbor);
                }
            }
        }

        return result.size() == indegree.size() ? result : "";
    }
};
Java 21
class Solution {
    public String alienOrder(String[] words) {
        Map<Character, Set<Character>> graph = new HashMap<>();
        Map<Character, Integer> indegree = new HashMap<>();

        for (String word : words) {
            for (char c : word.toCharArray()) {
                indegree.put(c, 0);
            }
        }

        for (int i = 0; i < words.length - 1; i++) {
            String w1 = words[i], w2 = words[i + 1];
            int minLen = Math.min(w1.length(), w2.length());

            if (w1.length() > w2.length() && w1.substring(0, minLen).equals(w2.substring(0, minLen))) {
                return "";
            }

            for (int j = 0; j < minLen; j++) {
                if (w1.charAt(j) != w2.charAt(j)) {
                    graph.putIfAbsent(w1.charAt(j), new HashSet<>());
                    if (graph.get(w1.charAt(j)).add(w2.charAt(j))) {
                        indegree.put(w2.charAt(j), indegree.get(w2.charAt(j)) + 1);
                    }
                    break;
                }
            }
        }

        Queue<Character> queue = new LinkedList<>();
        for (Map.Entry<Character, Integer> entry : indegree.entrySet()) {
            if (entry.getValue() == 0) queue.add(entry.getKey());
        }

        StringBuilder result = new StringBuilder();
        while (!queue.isEmpty()) {
            char c = queue.poll();
            result.append(c);

            if (graph.containsKey(c)) {
                for (char neighbor : graph.get(c)) {
                    indegree.put(neighbor, indegree.get(neighbor) - 1);
                    if (indegree.get(neighbor) == 0) {
                        queue.add(neighbor);
                    }
                }
            }
        }

        return result.length() == indegree.size() ? result.toString() : "";
    }
}
Python 3.12
from typing import List
from collections import deque, defaultdict


class Solution:
    def alienOrder(self, words: List[str]) -> str:
        graph = defaultdict(set)
        indegree = {c: 0 for word in words for c in word}

        for i in range(len(words) - 1):
            w1, w2 = words[i], words[i + 1]
            min_len = min(len(w1), len(w2))

            if len(w1) > len(w2) and w1[:min_len] == w2[:min_len]:
                return ""

            for j in range(min_len):
                if w1[j] != w2[j]:
                    if w2[j] not in graph[w1[j]]:
                        graph[w1[j]].add(w2[j])
                        indegree[w2[j]] += 1
                    break

        queue = deque(c for c in indegree if indegree[c] == 0)
        result = []

        while queue:
            c = queue.popleft()
            result.append(c)

            for neighbor in graph[c]:
                indegree[neighbor] -= 1
                if indegree[neighbor] == 0:
                    queue.append(neighbor)

        return "".join(result) if len(result) == len(indegree) else ""
Go 1.26
func alienOrder(words []string) string {
    graph := make(map[byte]map[byte]bool)
    indegree := make(map[byte]int)

    for _, word := range words {
        for i := 0; i < len(word); i++ {
            indegree[word[i]] = indegree[word[i]]
        }
    }

    for i := 0; i < len(words)-1; i++ {
        w1, w2 := words[i], words[i+1]
        minLen := len(w1)
        if len(w2) < minLen {
            minLen = len(w2)
        }

        if len(w1) > len(w2) && w1[:minLen] == w2[:minLen] {
            return ""
        }

        for j := 0; j < minLen; j++ {
            if w1[j] != w2[j] {
                if graph[w1[j]] == nil {
                    graph[w1[j]] = make(map[byte]bool)
                }
                if !graph[w1[j]][w2[j]] {
                    graph[w1[j]][w2[j]] = true
                    indegree[w2[j]]++
                }
                break
            }
        }
    }

    queue := []byte{}
    for c, deg := range indegree {
        if deg == 0 {
            queue = append(queue, c)
        }
    }

    result := []byte{}
    for len(queue) > 0 {
        c := queue[0]
        queue = queue[1:]
        result = append(result, c)

        for neighbor := range graph[c] {
            indegree[neighbor]--
            if indegree[neighbor] == 0 {
                queue = append(queue, neighbor)
            }
        }
    }

    if len(result) != len(indegree) {
        return ""
    }
    return string(result)
}
Approach 2

DFS Topological Sort

Intuition

We can also derive the character ordering using DFS-based topological sort. After building the same directed graph from adjacent word comparisons, we perform DFS from each unvisited character. We use a three-state system to detect cycles: unvisited, visiting (on the current DFS path), and visited (fully processed). When DFS finishes for a node (all descendants explored), we prepend it to the result. If we detect a cycle (encounter a "visiting" node), the ordering is invalid. The final result, built in reverse post-order, gives a valid topological ordering.

Algorithm

  1. Collect all unique characters and build the adjacency list from adjacent word comparisons (same as BFS approach).
  2. Initialize all characters as "unvisited".
  3. For each unvisited character, start DFS.
  4. Mark the current character as "visiting". For each neighbor, if it is "visiting", a cycle exists -- return "". If "unvisited", recurse.
  5. After all neighbors are processed, mark the character as "visited" and prepend it to the result.
  6. Return the result string if all characters were processed successfully.
TimeO(C) where C is the total length of all words combinedSpaceO(1) since the alphabet size is bounded (at most 26 characters)

Code (C++ · Java · Python · Go)

C++17
class Solution {
    unordered_map<char, unordered_set<char>> graph;
    unordered_map<char, int> state; // 0=unvisited, 1=visiting, 2=visited
    string result;
    bool hasCycle;

    void dfs(char c) {
        state[c] = 1;
        for (char neighbor : graph[c]) {
            if (state[neighbor] == 1) { hasCycle = true; return; }
            if (state[neighbor] == 0) dfs(neighbor);
            if (hasCycle) return;
        }
        state[c] = 2;
        result = c + result;
    }

public:
    string alienOrder(vector<string>& words) {
        graph.clear();
        state.clear();
        result.clear();
        hasCycle = false;

        for (const string& word : words) {
            for (char c : word) {
                state[c] = 0;
            }
        }

        for (int i = 0; i + 1 < (int)words.size(); i++) {
            string& w1 = words[i];
            string& w2 = words[i + 1];
            int minLen = min(w1.size(), w2.size());

            if (w1.size() > w2.size() && w1.substr(0, minLen) == w2.substr(0, minLen)) {
                return "";
            }

            for (int j = 0; j < minLen; j++) {
                if (w1[j] != w2[j]) {
                    graph[w1[j]].insert(w2[j]);
                    break;
                }
            }
        }

        for (auto& [c, s] : state) {
            if (s == 0) dfs(c);
            if (hasCycle) return "";
        }

        return result;
    }
};
Java 21
class Solution {
    private Map<Character, Set<Character>> graph;
    private Map<Character, Integer> state;
    private StringBuilder result;
    private boolean hasCycle;

    public String alienOrder(String[] words) {
        graph = new HashMap<>();
        state = new HashMap<>();
        result = new StringBuilder();
        hasCycle = false;

        for (String word : words) {
            for (char c : word.toCharArray()) {
                state.put(c, 0);
            }
        }

        for (int i = 0; i < words.length - 1; i++) {
            String w1 = words[i], w2 = words[i + 1];
            int minLen = Math.min(w1.length(), w2.length());

            if (w1.length() > w2.length() && w1.substring(0, minLen).equals(w2.substring(0, minLen))) {
                return "";
            }

            for (int j = 0; j < minLen; j++) {
                if (w1.charAt(j) != w2.charAt(j)) {
                    graph.putIfAbsent(w1.charAt(j), new HashSet<>());
                    graph.get(w1.charAt(j)).add(w2.charAt(j));
                    break;
                }
            }
        }

        for (char c : state.keySet()) {
            if (state.get(c) == 0) dfs(c);
            if (hasCycle) return "";
        }

        return result.toString();
    }

    private void dfs(char c) {
        state.put(c, 1);

        if (graph.containsKey(c)) {
            for (char neighbor : graph.get(c)) {
                if (state.get(neighbor) == 1) { hasCycle = true; return; }
                if (state.get(neighbor) == 0) dfs(neighbor);
                if (hasCycle) return;
            }
        }

        state.put(c, 2);
        result.insert(0, c);
    }
}
Python 3.12
from typing import List
from collections import defaultdict


class Solution:
    def alienOrder(self, words: List[str]) -> str:
        graph = defaultdict(set)
        state = {c: 0 for word in words for c in word}

        for i in range(len(words) - 1):
            w1, w2 = words[i], words[i + 1]
            min_len = min(len(w1), len(w2))

            if len(w1) > len(w2) and w1[:min_len] == w2[:min_len]:
                return ""

            for j in range(min_len):
                if w1[j] != w2[j]:
                    graph[w1[j]].add(w2[j])
                    break

        result = []
        has_cycle = False

        def dfs(c):
            nonlocal has_cycle
            state[c] = 1

            for neighbor in graph[c]:
                if state[neighbor] == 1:
                    has_cycle = True
                    return
                if state[neighbor] == 0:
                    dfs(neighbor)
                if has_cycle:
                    return

            state[c] = 2
            result.append(c)

        for c in state:
            if state[c] == 0:
                dfs(c)
            if has_cycle:
                return ""

        return "".join(reversed(result))
Go 1.26
func alienOrder(words []string) string {
    graph := make(map[byte]map[byte]bool)
    state := make(map[byte]int) // 0=unvisited, 1=visiting, 2=visited

    for _, word := range words {
        for i := 0; i < len(word); i++ {
            state[word[i]] = 0
        }
    }

    for i := 0; i < len(words)-1; i++ {
        w1, w2 := words[i], words[i+1]
        minLen := len(w1)
        if len(w2) < minLen {
            minLen = len(w2)
        }

        if len(w1) > len(w2) && w1[:minLen] == w2[:minLen] {
            return ""
        }

        for j := 0; j < minLen; j++ {
            if w1[j] != w2[j] {
                if graph[w1[j]] == nil {
                    graph[w1[j]] = make(map[byte]bool)
                }
                graph[w1[j]][w2[j]] = true
                break
            }
        }
    }

    result := []byte{}
    hasCycle := false

    var dfs func(c byte)
    dfs = func(c byte) {
        state[c] = 1
        for neighbor := range graph[c] {
            if state[neighbor] == 1 {
                hasCycle = true
                return
            }
            if state[neighbor] == 0 {
                dfs(neighbor)
            }
            if hasCycle {
                return
            }
        }
        state[c] = 2
        result = append(result, c)
    }

    for c := range state {
        if state[c] == 0 {
            dfs(c)
        }
        if hasCycle {
            return ""
        }
    }

    // Reverse the result
    for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
        result[i], result[j] = result[j], result[i]
    }

    return string(result)
}

Frequently asked questions

What is the optimal time complexity of Alien Dictionary?

The most efficient approach in this guide ("DFS Topological Sort") runs in O(C) where C is the total length of all words combined time and uses O(1) since the alphabet size is bounded (at most 26 characters) extra space. Earlier brute-force approaches are slower; see all 2 approaches above for the full progression.

What pattern does Alien Dictionary use?

Alien Dictionary is a hard-level Graphs problem involving Graph, Topological Sort, String. Recognizing the pattern is half the work — see the intuition section above for how to spot it.

Can Alien Dictionary be solved without extra space?

The most space-efficient approach in this guide uses O(1) since the alphabet size is bounded (at most 26 characters) extra space (excluding the input). If you're aiming for in-place — see the trade-off in the algorithm section above — the brute-force approach uses O(1) since the alphabet size is bounded (at most 26 characters), while the optimized version uses O(1) since the alphabet size is bounded (at most 26 characters).

Which programming languages does this Ratta solution support?

Every approach above ships with verified, runnable solutions in C++, Java, Python, and Go. The starter templates in the workspace match the same four languages so you can practice in your interview language of choice.

Related Problems

Loading editor...
words=
["wrt","wrf","er","ett","rftt"]
"wertf"