目录
1. 问题描述
现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。
给定一个字符串列表 words
,作为这门语言的词典,words
中的字符串已经 按这门新语言的字母顺序进行了排序 。
请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 ""
。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。
字符串 s
字典顺序小于 字符串 t
有两种情况:
- 在第一个不同字母处,如果
s
中的字母在这门外星语言的字母顺序中位于t
中字母之前,那么s
的字典顺序小于t
。 - 如果前面
min(s.length, t.length)
字母都相同,那么s.length < t.length
时,s
的字典顺序也小于t
。
示例 1:
输入:words = ["wrt","wrf","er","ett","rftt"] 输出:"wertf"
示例 2:
输入:words = ["z","x"] 输出:"zx"
示例 3:
输入:words = ["z","x","z"] 输出:"" 解释:不存在合法字母顺序,因此返回 "" 。 提示:
1 <= words.length <= 100
1 <= words[i].length <= 100
words[i]
仅由小写英文字母组成
2. 思路与算法
2.1 示例解释
示例1解释。words = ["wrt","wrf","er","ett","rftt"]
看各单词的第一个字母,可以知道(这里用>表示排位顺序,左边的表示排在前面。以下同)。又对比“er”和“ett”可知,对比"wrt"和“wrf”,可知。因此可以得到合法字母顺序为。但是,由于题目并不保证一定存在合法字母顺序,因此还需要验证其它单词是否有违背的情况。
以下题解为摘抄学习官解(对拓扑排序不熟悉,惭愧)。
2.2 拓扑排序
这道题是拓扑排序问题。外星文字典中的字母和字母顺序可以看成有向图,字典顺序即为所有字母的一种排列,满足每一条有向边的起点字母和终点字母的顺序都和这两个字母在排列中的顺序相同,该排列即为有向图的拓扑排序。
只有当有向图中无环时,才有拓扑排序,且拓扑排序可能不止一种。如果有向图中有环,则环内的字母不存在符合要求的排列,因此没有拓扑排序。
使用拓扑排序求解时,将外星文字典中的每个字母看成一个节点,将字母之间的顺序关系看成有向边。对于外星文字典中的两个相邻单词,同时从左到右遍历,当遇到第一个不相同的字母时,该位置的两个字母之间即存在顺序关系。
以下两种情况不存在合法字母顺序:
- 字母之间的顺序关系存在由至少 2 个字母组成的环,例如 ;
- 相邻两个单词满足后面的单词是前面的单词的前缀,且后面的单词的长度小于前面的单词的长度,例如 ;
其余情况下都存在合法字母顺序(但是如何证明这一点呢?),可以使用拓扑排序得到字典顺序。拓扑排序可以使用深度优先搜索或广度优先搜索实现,以下分别介绍两种实现方法。
2.3 拓扑排序+广度优先搜索
使用广度优先搜索实现拓扑排序,可以得到正向的拓扑排序。
首先计算每个节点的入度,只有入度为 0 的节点可能是拓扑排序中最前面的节点。当一个节点加入拓扑排序之后,该节点的所有相邻节点的入度都减 1,表示相邻节点少了一条入边。当一个节点的入度变成 0,则该节点前面的节点都已经加入拓扑排序,该节点也可以加入拓扑排序。
具体做法是,使用队列存储可以加入拓扑排序的节点,初始时将所有入度为 0 的节点入队列。每次将一个节点出队列并加入拓扑排序中,然后将该节点的所有相邻节点的入度都减 1,如果一个相邻节点的入度变成 0,则将该相邻节点入队列。重复上述操作,直到队列为空时,广度优先搜索结束。
如果有向图中无环,则每个节点都将加入拓扑排序,因此拓扑排序的长度等于字典中的字母个数。如果有向图中有环,则环中的节点不会加入拓扑排序,因此拓扑排序的长度小于字典中的字母个数。广度优先搜索结束时,判断拓扑排序的长度是否等于字典中的字母个数,即可判断有向图中是否有环。
- 如果拓扑排序的长度等于字典中的字母个数,则拓扑排序包含字典中的所有字母,返回拓扑排序;
- 如果拓扑排序的长度小于字典中的字母个数,则有向图中有环,不存在拓扑排序。
拓扑排序也可以用深度优先搜索的方式来解决,但是比广度优先搜索要麻烦,此处略过。
3. 代码实现
from typing import List
from collections import defaultdict
# from itertools import pairwise # Introduced in python 3.10
class Solution:
def alienOrder(self, words: List[str]) -> str:
# 首先,遍历words构建有向图并统计各节点的入度
g = defaultdict(list)
inDeg = {c: 0 for c in words[0]}
#for s, t in pairwise(words):
for k in range(len(words)-1):
s,t = words[k], words[k+1]
for c in t:
inDeg.setdefault(c, 0)
for u, v in zip(s, t):
if u != v:
# u不等于v则表明u的字典序一定在v之前,建立一条u->v的有向边,且v的入度数加一
g[u].append(v)
inDeg[v] += 1
break
else:
# 如果发现排在后面的单词是前面的单词的前缀则肯定违反字典序,直接返回空列表
if len(s) > len(t):
return ""
# 基于有向图进行广度优先搜索
# 将入度为0的节点加入队列,这里用简单的列表来实现队列,是因为节点个数有限,不需要考虑存储开销
# 一般情况下推荐使用collections.deque
q = [u for u, d in inDeg.items() if d == 0]
for u in q:
# 顺序遍历u,模拟逐个从队列头部中取出各节点并移除的操作
for v in g[u]:
# 遍历u的各邻节点,由于u被移除,所以v的入度相应减一。
# 如果减一后v的入度也变为0了就将v也加入队列(添加到表的尾部)
inDeg[v] -= 1
if inDeg[v] == 0:
q.append(v)
return ''.join(q) if len(q) == len(inDeg) else ""
if __name__ == "__main__":
sln = Solution()
words = ["wrt","wrf","er","ett","rftt"]
print(sln.alienOrder(words))
words = ["z","x"]
print(sln.alienOrder(words))
words = ["z","x","z"]
print(sln.alienOrder(words))
执行用时:32 ms, 在所有 Python3 提交中击败了96.80%的用户
内存消耗:15.2 MB, 在所有 Python3 提交中击败了10.80%的用户
回到总目录:Leetcode每日一题总目录(动态更新。。。)