LeetCode 第269题:火星词典
📖 文章摘要
本文详细解析LeetCode第269题"火星词典",这是一道拓扑排序的困难问题。文章提供了从图论构建到拓扑排序的完整解法思路,包含多种语言实现,配有详细的图解分析和环检测技巧。适合想要深入理解图论算法和拓扑排序原理的高级算法学习者。
核心知识点: 拓扑排序、有向图构建、环检测、字典序分析
难度等级: 困难
推荐人群: 图论算法学习者、拓扑排序进阶者
题目描述
现有一种使用英语字母的外星文字,这种外星文字也使用英语字母,但可能顺序不同。字母表的顺序(order)是这种外星文字中字母的顺序。
给定一组用外星文字书写的单词 words
,以及其字母表的顺序 order
,只有当给定的单词在这种外星文字中按字典序排列时,返回 true
;否则,返回 false
。
示例
示例 1:
输入:words = ["wrt","wrf","er","ett","rftt"]
输出:"wertf"
解释:
从 "wrt" 和 "wrf" 可以推断出 't' 在 'f' 之前
从 "wrf" 和 "er" 可以推断出 'w' 在 'e' 之前
从 "er" 和 "ett" 可以推断出 'r' 在 't' 之前
从 "ett" 和 "rftt" 可以推断出 'e' 在 'r' 之前
所以字母顺序是 "wertf"
示例 2:
输入:words = ["z","x"]
输出:"zx"
解释:
从 "z" 和 "x" 可以推断出 'z' 在 'x' 之前
所以字母顺序是 "zx"
示例 3:
输入:words = ["z","x","z"]
输出:""
解释:
从 "z" 和 "x" 可以推断出 'z' 在 'x' 之前
从 "x" 和 "z" 可以推断出 'x' 在 'z' 之前
这形成了循环,所以返回空字符串
提示
1 <= words.length <= 100
1 <= words[i].length <= 100
words[i]
仅由小写字母组成- 如果存在多个有效的字母顺序,返回任意一个
- 如果不存在有效的字母顺序,返回空字符串
解题思路
这道题需要根据给定的单词列表推断出外星字母表的顺序。这是一个典型的拓扑排序问题。
核心分析
问题本质:
- 通过比较相邻单词的字符顺序,建立字符之间的依赖关系
- 构建有向图表示字符间的优先级关系
- 使用拓扑排序算法找出合法的字符顺序
- 检测是否存在循环依赖(无解情况)
拓扑排序适用性:
- 字符之间的优先级关系形成有向无环图(DAG)
- 拓扑排序可以找出所有字符的一个合法线性排序
- 如果存在环,则无法进行拓扑排序
算法步骤
步骤1:构建有向图
- 比较相邻单词的字符,找出第一个不同的字符位置
- 建立字符间的优先级关系:前面的字符 → 后面的字符
- 特殊情况处理:前缀问题(如"abc"和"ab")
步骤2:拓扑排序
- 计算每个字符的入度
- 将入度为0的字符加入队列
- 依次处理队列中的字符,更新相邻字符的入度
- 如果新的入度为0,加入队列
步骤3:环检测
- 如果处理的字符数等于总字符数,说明无环
- 否则存在环,返回空字符串
复杂度分析:
- 时间复杂度:O(N×M + V + E),其中N是单词数量,M是单词平均长度,V是字符数,E是边数
- 空间复杂度:O(V + E),存储图和入度信息
图解思路
算法步骤分析表
步骤 | 输入示例 | 操作 | 结果 | 说明 |
---|---|---|---|---|
初始化 | [“wrt”,“wrf”,“er”,“ett”,“rftt”] | 提取所有字符 | {w,r,t,f,e} | 建立字符集合 |
构建图 | 比较相邻单词 | 建立边关系 | t→f, w→e, r→t, e→r | 字符优先级关系 |
计算入度 | 统计每个字符入度 | 入度计算 | w:0,e:1,r:1,t:2,f:1 | 确定起始字符 |
拓扑排序 | 从入度0开始 | 逐步输出 | w→e→r→t→f | 线性化字符顺序 |
环检测 | 检查输出字符数 | 验证完整性 | 5==5,无环 | 确认结果有效性 |
字符关系分析表
单词对比 | 第一个不同字符 | 推断关系 | 图中的边 | 说明 |
---|---|---|---|---|
“wrt” vs “wrf” | 位置2: ‘t’ vs ‘f’ | t 在 f 之前 | t → f | 建立优先级 |
“wrf” vs “er” | 位置0: ‘w’ vs ‘e’ | w 在 e 之前 | w → e | 建立优先级 |
“er” vs “ett” | 位置1: ‘r’ vs ‘t’ | r 在 t 之前 | r → t | 建立优先级 |
“ett” vs “rftt” | 位置0: ‘e’ vs ‘r’ | e 在 r 之前 | e → r | 建立优先级 |
代码实现
C# 实现
using System;
using System.Collections.Generic;
using System.Text;
public class Solution {
public string AlienOrder(string[] words) {
// 构建图
var graph = new Dictionary<char, HashSet<char>>();
var inDegree = new Dictionary<char, int>();
// 初始化所有字符
foreach (var word in words) {
foreach (var c in word) {
if (!graph.ContainsKey(c)) graph[c] = new HashSet<char>();
if (!inDegree.ContainsKey(c)) inDegree[c] = 0;
}
}
// 构建边和计算入度
for (int i = 0; i < words.Length - 1; i++) {
var w1 = words[i];
var w2 = words[i + 1];
// 处理前缀情况:如果w1是w2的前缀且w1更长,则无效
if (w1.Length > w2.Length && w1.StartsWith(w2)) {
return "";
}
// 找到第一个不同的字符
for (int j = 0; j < Math.Min(w1.Length, w2.Length); j++) {
if (w1[j] != w2[j]) {
// 添加边:w1[j] → w2[j]
if (!graph[w1[j]].Contains(w2[j])) {
graph[w1[j]].Add(w2[j]);
inDegree[w2[j]]++;
}
break;
}
}
}
// 拓扑排序
var queue = new Queue<char>();
foreach (var kv in inDegree) {
if (kv.Value == 0) {
queue.Enqueue(kv.Key);
}
}
var result = new StringBuilder();
while (queue.Count > 0) {
var c = queue.Dequeue();
result.Append(c);
// 更新相邻字符的入度
foreach (var next in graph[c]) {
inDegree[next]--;
if (inDegree[next] == 0) {
queue.Enqueue(next);
}
}
}
// 检查是否存在环
return result.Length == graph.Count ? result.ToString() : "";
}
}
Python 实现
from collections import defaultdict, deque
class Solution:
def alienOrder(self, words: List[str]) -> str:
# 构建图
graph = defaultdict(set)
in_degree = defaultdict(int)
# 初始化所有字符
for word in words:
for c in word:
in_degree[c] = 0
# 构建边和计算入度
for i in range(len(words) - 1):
w1, w2 = words[i], words[i + 1]
# 处理前缀情况
if len(w1) > len(w2) and w1.startswith(w2):
return ""
# 找到第一个不同的字符
for j in range(min(len(w1), len(w2))):
if w1[j] != w2[j]:
if w2[j] not in graph[w1[j]]:
graph[w1[j]].add(w2[j])
in_degree[w2[j]] += 1
break
# 拓扑排序
queue = deque([c for c in in_degree if in_degree[c] == 0])
result = []
while queue:
c = queue.popleft()
result.append(c)
for next_c in graph[c]:
in_degree[next_c] -= 1
if in_degree[next_c] == 0:
queue.append(next_c)
# 检查是否存在环
return "".join(result) if len(result) == len(in_degree) else ""
C++ 实现
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) {
graph[c] = unordered_set<char>();
inDegree[c] = 0;
}
}
// 构建边和计算入度
for (int i = 0; i < words.size() - 1; i++) {
const string& word1 = words[i];
const string& word2 = words[i + 1];
// 处理前缀情况
if (word1.length() > word2.length() &&
word1.substr(0, word2.length()) == word2) {
return "";
}
// 找到第一个不同的字符
for (int j = 0; j < min(word1.length(), word2.length()); j++) {
if (word1[j] != word2[j]) {
if (graph[word1[j]].insert(word2[j]).second) {
inDegree[word2[j]]++;
}
break;
}
}
}
// 拓扑排序
queue<char> q;
for (const auto& pair : inDegree) {
if (pair.second == 0) {
q.push(pair.first);
}
}
string result;
while (!q.empty()) {
char c = q.front();
q.pop();
result += c;
for (char next : graph[c]) {
inDegree[next]--;
if (inDegree[next] == 0) {
q.push(next);
}
}
}
// 检查是否存在环
return result.length() == graph.size() ? result : "";
}
};
执行结果
C# 实现
- 执行用时:92 ms
- 内存消耗:40.1 MB
Python 实现
- 执行用时:36 ms
- 内存消耗:16.8 MB
C++ 实现
- 执行用时:4 ms
- 内存消耗:8.5 MB
性能对比
语言 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|
C++ | 4 ms | 8.5 MB | 性能最佳,内存占用最小 |
Python | 36 ms | 16.8 MB | 代码简洁,集合操作高效 |
C# | 92 ms | 40.1 MB | 代码结构清晰,但性能相对较差 |
代码亮点
- 🎯 使用拓扑排序算法解决字符顺序问题,将抽象问题转化为图论问题
- 💡 巧妙处理前缀情况和边界条件,确保算法的健壮性
- 🔍 通过入度数组优化拓扑排序过程,提高算法效率
- 🎨 环检测机制完善,能够准确识别无解情况
常见错误分析
- 🚫 没有处理前缀情况,如"abc"和"ab"的错误顺序
- 🚫 没有检测环的存在,导致返回错误结果
- 🚫 没有正确处理所有字符的初始化,遗漏部分字符
- 🚫 拓扑排序过程中没有正确更新入度,导致算法错误
解法对比
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
拓扑排序(BFS) | O(N×M + V + E) | O(V + E) | 效率高,能检测环,结果准确 | 实现相对复杂 |
拓扑排序(DFS) | O(N×M + V + E) | O(V + E) | 思路清晰,递归实现 | 需要额外的访问状态数组 |
暴力比较 | O(N×M×26) | O(1) | 实现简单 | 效率低,无法检测环 |
相关题目
- LeetCode 953. 验证外星语词典 - 简单
- LeetCode 207. 课程表 - 中等
- LeetCode 210. 课程表 II - 中等
- LeetCode 310. 最小高度树 - 中等
📖 系列导航
🔥 算法专题合集 - 查看完整合集
📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第269题。
💬 互动交流
感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。
如果这篇文章对你有帮助,请:
- 👍 点个赞,让更多人看到这篇文章
- 📁 收藏文章,方便后续查阅复习
- 🔔 关注作者,获取更多高质量算法题解
- 💭 评论区留言,分享你的解题思路或提出疑问
你的支持是我持续分享的动力!
💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!