LeetCode第269题_火星词典

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] 仅由小写字母组成
  • 如果存在多个有效的字母顺序,返回任意一个
  • 如果不存在有效的字母顺序,返回空字符串

解题思路

这道题需要根据给定的单词列表推断出外星字母表的顺序。这是一个典型的拓扑排序问题。

核心分析

问题本质

  1. 通过比较相邻单词的字符顺序,建立字符之间的依赖关系
  2. 构建有向图表示字符间的优先级关系
  3. 使用拓扑排序算法找出合法的字符顺序
  4. 检测是否存在循环依赖(无解情况)

拓扑排序适用性

  • 字符之间的优先级关系形成有向无环图(DAG)
  • 拓扑排序可以找出所有字符的一个合法线性排序
  • 如果存在环,则无法进行拓扑排序

算法步骤

步骤1:构建有向图

  1. 比较相邻单词的字符,找出第一个不同的字符位置
  2. 建立字符间的优先级关系:前面的字符 → 后面的字符
  3. 特殊情况处理:前缀问题(如"abc"和"ab")

步骤2:拓扑排序

  1. 计算每个字符的入度
  2. 将入度为0的字符加入队列
  3. 依次处理队列中的字符,更新相邻字符的入度
  4. 如果新的入度为0,加入队列

步骤3:环检测

  1. 如果处理的字符数等于总字符数,说明无环
  2. 否则存在环,返回空字符串

复杂度分析

  • 时间复杂度: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 ms8.5 MB性能最佳,内存占用最小
Python36 ms16.8 MB代码简洁,集合操作高效
C#92 ms40.1 MB代码结构清晰,但性能相对较差

代码亮点

  1. 🎯 使用拓扑排序算法解决字符顺序问题,将抽象问题转化为图论问题
  2. 💡 巧妙处理前缀情况和边界条件,确保算法的健壮性
  3. 🔍 通过入度数组优化拓扑排序过程,提高算法效率
  4. 🎨 环检测机制完善,能够准确识别无解情况

常见错误分析

  1. 🚫 没有处理前缀情况,如"abc"和"ab"的错误顺序
  2. 🚫 没有检测环的存在,导致返回错误结果
  3. 🚫 没有正确处理所有字符的初始化,遗漏部分字符
  4. 🚫 拓扑排序过程中没有正确更新入度,导致算法错误

解法对比

解法时间复杂度空间复杂度优点缺点
拓扑排序(BFS)O(N×M + V + E)O(V + E)效率高,能检测环,结果准确实现相对复杂
拓扑排序(DFS)O(N×M + V + E)O(V + E)思路清晰,递归实现需要额外的访问状态数组
暴力比较O(N×M×26)O(1)实现简单效率低,无法检测环

相关题目


📖 系列导航

🔥 算法专题合集 - 查看完整合集

📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第269题。


💬 互动交流

感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。

如果这篇文章对你有帮助,请:

  • 👍 点个赞,让更多人看到这篇文章
  • 📁 收藏文章,方便后续查阅复习
  • 🔔 关注作者,获取更多高质量算法题解
  • 💭 评论区留言,分享你的解题思路或提出疑问

你的支持是我持续分享的动力!

💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值