198. Alien Dictionary Order
Problem
Given words sorted according to an unknown alphabet, return one valid ordering of all distinct characters. Choose the lexicographically smallest valid character ordering under ordinary Python character comparison. Return '' for inconsistent constraints, including a longer word immediately preceding its strict prefix. Empty input also returns ''.
Examples
Example 1
alien_order(["wrt","wrf","er","ett","rftt"])
=> 'wertf'The adjacent comparisons force this complete character order.
Example 2
alien_order(["abc","ab"])
=> ''A longer word cannot precede its own strict prefix.
Example 3
alien_order(["ab"])
=> 'ab'Unconstrained characters are emitted in the smallest available order.
Example 4
alien_order(["z","x","z"])
=> ''The inferred z<x and x<z constraints form a cycle.
Reveal the worked solution and reasoning
Why this mechanism works
Adjacent sorted words reveal only their first differing character: that pair imposes one necessary ordering edge. Later positions cannot add constraints once the comparison is decided. Collect all characters, deduplicate edges, detect the prefix contradiction, then topologically sort with a min-heap to resolve unconstrained choices deterministically.
State and boundaries
adj stores distinct necessary character-order edges. indegree counts unmet distinct constraints; the heap contains currently eligible characters.
Protocol
- Register every character.
- Compare adjacent words until their first mismatch.
- Add that edge once, or reject a reversed prefix.
- Heapify zero-indegree characters.
- Emit the smallest available character and release neighbors.
- Reject if not all characters were emitted.
Visualization
'wrt' before 'wrf' -> t < f
'wrf' before 'er' -> w < e
'abc' before 'ab' -> impossible prefix orderPython implementation
import heapq
def alien_order(words):
adj = {char: set() for word in words for char in word}
indegree = dict.fromkeys(adj, 0)
for first, second in zip(words, words[1:]):
for a, b in zip(first, second):
if a != b:
if b not in adj[a]:
adj[a].add(b)
indegree[b] += 1
break
else:
if len(first) > len(second):
return ''
heap = [char for char in adj if indegree[char] == 0]
heapq.heapify(heap)
output = []
while heap:
char = heapq.heappop(heap)
output.append(char)
for neighbor in adj[char]:
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
heapq.heappush(heap, neighbor)
return ''.join(output) if len(output) == len(adj) else ''
Worked execution
alien_order(["wrt","wrf","er","ett","rftt"])
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; word='wrt'; char='w'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; word='wrt'; char='r'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; word='wrt'; char='t'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; word='wrf'; char='w'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; word='wrf'; char='r'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; word='wrf'; char='f'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; word='er'; char='e'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; word='er'; char='r'
... 51 intermediate state changes omitted ...
L23 alien_order: indegree[neighbor] -= 1
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; adj={'w':{'e'}, 'r':{'t'}, 't':{'f'}, 'f':set(), 'e':{'r'}}; indegree={'w':0, 'r':0, 't':0, 'f':0, 'e':0}; first='ett'; second='rftt'; a='e'; b='r'; heap=[]; output=['w', 'e', 'r', 't']; char='t'; neighbor='f'
L25 alien_order: heapq.heappush(heap, neighbor)
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; adj={'w':{'e'}, 'r':{'t'}, 't':{'f'}, 'f':set(), 'e':{'r'}}; indegree={'w':0, 'r':0, 't':0, 'f':0, 'e':0}; first='ett'; second='rftt'; a='e'; b='r'; heap=['f']; output=['w', 'e', 'r', 't']; char='t'; neighbor='f'
L20 alien_order: char = heapq.heappop(heap)
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; adj={'w':{'e'}, 'r':{'t'}, 't':{'f'}, 'f':set(), 'e':{'r'}}; indegree={'w':0, 'r':0, 't':0, 'f':0, 'e':0}; first='ett'; second='rftt'; a='e'; b='r'; heap=[]; output=['w', 'e', 'r', 't']; char='f'; neighbor='f'
L21 alien_order: output.append(char)
words=['wrt', 'wrf', 'er', 'ett', 'rftt']; adj={'w':{'e'}, 'r':{'t'}, 't':{'f'}, 'f':set(), 'e':{'r'}}; indegree={'w':0, 'r':0, 't':0, 'f':0, 'e':0}; first='ett'; second='rftt'; a='e'; b='r'; heap=[]; output=['w', 'e', 'r', 't', 'f']; char='f'; neighbor='f'Result: 'wertf'. The adjacent comparisons force this complete character order.
alien_order(["abc","ab"])
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['abc', 'ab']; word='abc'; char='a'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['abc', 'ab']; word='abc'; char='b'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['abc', 'ab']; word='abc'; char='c'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['abc', 'ab']; word='ab'; char='a'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['abc', 'ab']; word='ab'; char='b'
L4 alien_order: adj = {char: set() for word in words for char in word}
words=['abc', 'ab']; adj={'a':set(), 'b':set(), 'c':set()}
L5 alien_order: indegree = dict.fromkeys(adj, 0)
words=['abc', 'ab']; adj={'a':set(), 'b':set(), 'c':set()}; indegree={'a':0, 'b':0, 'c':0}
L6 alien_order: for first, second in zip(words, words[1:]):
words=['abc', 'ab']; adj={'a':set(), 'b':set(), 'c':set()}; indegree={'a':0, 'b':0, 'c':0}; first='abc'; second='ab'
L7 alien_order: for a, b in zip(first, second):
words=['abc', 'ab']; adj={'a':set(), 'b':set(), 'c':set()}; indegree={'a':0, 'b':0, 'c':0}; first='abc'; second='ab'; a='a'; b='a'
L7 alien_order: for a, b in zip(first, second):
words=['abc', 'ab']; adj={'a':set(), 'b':set(), 'c':set()}; indegree={'a':0, 'b':0, 'c':0}; first='abc'; second='ab'; a='b'; b='b'Result: ''. A longer word cannot precede its own strict prefix.
Why the solution is correct
The first mismatch or prefix relation fully determines each adjacent lexical comparison. Therefore satisfying all extracted edges satisfies the given list. Kahn processing respects those edges; choosing the smallest currently feasible character is the smallest possible next character of any valid order. A cycle or reversed prefix makes an order impossible.
Time and space
O(S+E+A log A) time for S input characters, A distinct characters and E distinct inferred edges; O(A+E) auxiliary space.
Common mistakes
Inferring edges from every mismatching position invents constraints. Counting a duplicate edge twice but removing it once breaks indegrees.
Reasoning card
State: adj stores distinct necessary character-order edges. indegree counts unmet distinct constraints; the heap contains currently eligible characters.
Invariant: Every inferred edge is necessary to preserve the given word order.
Decision to rehearse: Create edges from the first differing character and topologically sort.
Finish: Reject if not all characters were emitted
Transfer exercise
Detect whether the valid alphabet order is unique by observing how many characters are eligible at each step.