1. 问题背景与题目解析
这道题目来自LeetCode的LCR114题,被称作"火星词典"问题。初次看到这个标题时,我脑海中浮现的是科幻电影中外星人使用的奇特文字符号。但实际上,这是一道典型的拓扑排序应用题目,考察我们对有向无环图(DAG)的理解和算法实现能力。
题目描述大致是这样的:假设我们有一本来自火星的词典,其中的单词是按照某种我们不知道的字母顺序排列的。现在给定一组按照这个顺序排列的单词,我们需要推断出火星字母的可能顺序。换句话说,我们需要根据单词的排列顺序,重建字母的拓扑顺序。
举个例子,如果输入是["wrt","wrf","er","ett","rftt"],那么正确的字母顺序可能是"wertf"。因为:
- 从"wrt"和"wrf"可以推断t在f前
- 从"wrf"和"er"可以推断w在e前
- 以此类推...
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路与算法选择
2.1 问题转化为图论模型
这道题的核心在于将字母顺序问题转化为图论中的拓扑排序问题。我们需要:
- 比较相邻单词,找到第一个不同的字母对,建立有向边
- 构建完整的有向图
- 对这个图进行拓扑排序
这个转化过程是解题的关键。我刚开始做这道题时,花了很长时间才想明白如何从单词列表构建字母的顺序关系图。
2.2 拓扑排序的实现方式
拓扑排序有两种经典实现方式:
-
Kahn算法(基于入度):
- 统计每个节点的入度
- 将入度为0的节点加入队列
- 依次处理队列中的节点,更新相关节点的入度
- 如果最终所有节点都被处理,则得到拓扑序
-
DFS后序遍历:
- 对图进行深度优先搜索
- 在回溯时将节点加入结果列表
- 最后将列表反转得到拓扑序
对于这道题,我推荐使用Kahn算法,因为它更直观,也更容易处理可能的环的情况(即无效输入)。
3. 详细实现步骤
3.1 构建图的邻接表和入度统计
首先我们需要构建图的邻接表表示,并统计每个字母的入度:
python复制from collections import defaultdict, deque
def alienOrder(words):
# 初始化邻接表和入度字典
adj = defaultdict(set)
in_degree = {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 adj[w1[j]]: # 避免重复添加边
adj[w1[j]].add(w2[j])
in_degree[w2[j]] += 1
break
这里有几个关键点需要注意:
- 我们需要处理无效输入情况,比如["abc", "ab"]这种明显违反字典序的情况
- 要避免重复添加相同的边,否则会影响入度统计
- 只比较到第一个不同字母即可,后面的字母顺序无法确定
3.2 执行拓扑排序
接下来实现拓扑排序的核心部分:
python复制 # 初始化队列:所有入度为0的节点
queue = deque([c for c in in_degree if in_degree[c] == 0])
result = []
while queue:
c = queue.popleft()
result.append(c)
for neighbor in adj[c]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
# 检查是否有环
if len(result) != len(in_degree):
return ""
return "".join(result)
拓扑排序完成后,我们需要检查结果的长度是否与所有字母的数量一致。如果不一致,说明图中存在环,无法确定字母顺序。
4. 边界情况与测试用例
这道题有许多容易忽略的边界情况,我在实际解题过程中踩过不少坑:
4.1 特殊输入处理
-
空输入或单单词输入:
- 输入:[] → 应该返回""
- 输入:["a"] → 应该返回"a"
-
所有单词相同:
- 输入:["abc", "abc", "abc"] → 应该返回"abc"的任意排列(题目通常保证唯一解)
-
无效输入:
- 输入:["abc", "ab"] → 应该返回""(违反字典序)
4.2 复杂情况测试
python复制# 测试用例1:基本示例
assert alienOrder(["wrt","wrf","er","ett","rftt"]) == "wertf"
# 测试用例2:有环的情况
assert alienOrder(["a","b","a"]) == ""
# 测试用例3:单字母
assert alienOrder(["z","z"]) == "z"
# 测试用例4:多个解的情况(题目通常保证唯一解)
assert alienOrder(["ab","adc"]) in ["abcd", "acbd", "acdb"] # 实际题目会保证唯一解
5. 算法复杂度分析
让我们分析一下这个算法的时间和空间复杂度:
-
时间复杂度:
- 构建图:O(C),其中C是所有单词中字母的总数
- 拓扑排序:O(V + E),其中V是字母数量,E是边数量
- 总体:O(C)
-
空间复杂度:
- 存储图:O(V + E)
- 存储入度:O(V)
- 总体:O(V + E)
在实际面试中,能够清楚地分析算法复杂度是很重要的加分项。
6. 实际编码中的优化技巧
在实现过程中,我发现了几处可以优化的地方:
-
使用defaultdict和set:这样可以方便地处理不存在的键,同时避免重复边
-
提前终止比较:一旦找到第一个不同的字母对,就可以停止当前单词对的比较
-
入度初始化:需要包含所有出现的字母,即使它们的入度为0
-
无效输入检查:在构建图的过程中就可以检查是否存在明显违反字典序的情况
7. 类似题目与扩展思考
这道题属于拓扑排序的经典应用,类似的题目还有:
- 课程表问题(LeetCode 207/210):判断课程安排是否合理
- 任务调度:给定任务间的依赖关系,确定执行顺序
- 包依赖解析:如npm或pip中的包依赖关系处理
对于想进一步巩固拓扑排序概念的同学,我建议按以下顺序练习:
- 先做课程表问题(更基础)
- 然后尝试这道火星词典
- 最后挑战更复杂的依赖解析问题
8. 个人解题心得
这道题我前后做了三次才完全掌握。第一次做时,我忽略了无效输入的处理,导致部分测试用例失败。第二次我尝试用DFS实现拓扑排序,但在处理环检测时遇到了困难。直到第三次使用Kahn算法,才真正理解了这道题的精髓。
几个重要的教训:
- 一定要先处理边界情况,特别是那些可能导致算法崩溃的特殊输入
- 对于拓扑排序问题,Kahn算法通常比DFS实现更直观易懂
- 在构建图的时候就要考虑可能的环,而不是等到排序时才处理
对于准备面试的同学,我的建议是:
- 熟练掌握图的两种表示方法(邻接表和邻接矩阵)
- 理解拓扑排序的两种实现方式及其适用场景
- 多做类似题目,培养将实际问题转化为图论模型的思维能力
