题目
剑指 Offer II 114. 外星文字典
现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。
给定一个字符串列表 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] 仅由小写英文字母组成
解法
同269题
注意题意中这两点的描述
字符串 s 字典顺序小于 字符串 t 有两种情况:
- 在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。
- 如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。
实际举例:
以下两种情况不存在合法字母顺序:
-
字母之间的顺序关系存在由至少 22 个字母组成的环,例如words=[“a",“b",“a"];
-
相邻两个单词满足后面的单词是前面的单词的前缀,且后面的单词的长度小于前面的单词的长度,例如 words=[“ab",“a"]。
方法1:拓扑排序+BFS
Map<Character, List<Character>> edges = new HashMap<>();//字符间边的关系
Map<Character, Integer> indegrees = new HashMap<>();//统计某个字符的入度
boolean valid = true;//判断是否需要提前退出
public String alienOrder(String[] words) {
//建图,完成拓扑排序的准备工作
int n = words.length;
for (String word : words) {
for (char c : word.toCharArray()) {
edges.putIfAbsent(c, new ArrayList<>());//给每个字符添加一个相邻边
}
}
for (int i = 1; i < n && valid; i++) {
addEdge(words[i - 1], words[i]);
}
if (!valid) return "";
//bfs
Queue<Character> q = new LinkedList<>();
for (char u : edges.keySet()) {//将入度为0的字符加入到队列中
if (!indegrees.containsKey(u)) {
q.offer(u);
}
}
StringBuilder sb = new StringBuilder();//记录弹出的顺序
while (!q.isEmpty()) {
char u = q.poll();
sb.append(u);
for (char v : edges.get(u)) {//遍历u的邻居
indegrees.put(v, indegrees.get(v) - 1);
if (indegrees.get(v) == 0) {//入度为0后,该节点转入队列中
q.offer(v);
}
}
}
//["z","x","a","zb","zx"]
//对于前四个字符串 排序是zxab 但是来了zx后 x又得排序到b之后,但是b之前已经出现了x,是矛盾的
//这个case范围的是"" 如果不加上判断,返回的是"b", "b"的邻居"x"在入度减一后并没有立马减少为0,bfs提前结束
return sb.length() == edges.size() ? sb.toString() : "";
}
public void addEdge(String prev, String cur) {
int m = prev.length(), n = cur.length();
int len = Math.min(m, n);
int i = 0;
for (; i < len; i++) {
char u = prev.charAt(i), v = cur.charAt(i);
if (u != v) {
if (u == 'b') {
System.out.println();
}
edges.get(u).add(v);
indegrees.put(v, indegrees.getOrDefault(v, 0) + 1);
break;
}
}
//如果"abc" "ab"的这种case,提前返回""
if (i == len && m > n) {
valid = false;
}
}
方法2:拓扑排序+DFS
官方dfs拓扑排序的思路:
由于拓扑排序的顺序和搜索完成的顺序相反,因此需要使用一个栈存储所有已经搜索完成的节点。深度优先搜索的过程中需要维护每个节点的状态,每个节点的状态可能有三种情况:「未访问」、「访问中」和「已访问」。初始时,所有节点的状态都是「未访问」。
每一轮搜索时,任意选取一个「未访问」的节点 u,从节点 u 开始深度优先搜索。将节点 u的状态更新为「访问中」,对于每个与节点 u 相邻的节点 v,判断节点 v 的状态,执行如下操作:
-
如果节点 v的状态是「未访问」,则继续搜索节点 v;
-
如果节点 v 的状态是「访问中」,则找到有向图中的环,因此不存在拓扑排序;
-
如果节点 v 的状态是「已访问」,则节点 v 已经搜索完成并入栈,节点 u 尚未入栈,因此节点 u 的拓扑顺序一定在节点 v 的前面,不需要执行任何操作。
static final int VISITING = 1, VISITED = 2;
Map<Character, List<Character>> edges = new HashMap<>();//字符间边的关系
Map<Character, Integer> states = new HashMap<>();//统计某个字符的状态
boolean valid = true;
char[] paths;
int index;
public String alienOrder(String[] words) {
//建图,完成拓扑排序的准备工作
int n = words.length;
for (String word : words) {
for (char c : word.toCharArray()) {
edges.putIfAbsent(c, new ArrayList<>());//给每个字符添加一个相邻边
}
}
for (int i = 1; i < n && valid; i++) {
addEdge(words[i - 1], words[i]);
}
if (!valid) return "";
//dfs
paths = new char[edges.size()];
index = edges.size() - 1;
for (char u : edges.keySet()) {
if (!states.containsKey(u)) {
dfs(u);
}
}
if (!valid) return "";
return String.valueOf(paths);
}
private void dfs(char u) {
states.put(u, VISITING);//当前节点标记为「访问中」
for (char v : edges.get(u)) {
if (!states.containsKey(v)) {//节点v没有被访问
dfs(v);//继续遍历v
if (!valid) return;//如果发现有不符合条件的 ,提前结束
} else if (states.get(v) == VISITING) //第二次进入v,说明有环
{
valid = false;
return;
}
}
states.put(u, VISITED);//u这个节点是安全的,标记为「已访问」
paths[index--] = u;//记录u在栈的位置
}
public void addEdge(String prev, String cur) {
int m = prev.length(), n = cur.length();
int len = Math.min(m, n);
int i = 0;
for (; i < len; i++) {
char u = prev.charAt(i), v = cur.charAt(i);
if (u != v) {
edges.get(u).add(v);
break;
}
}
if (i == len && m > n) {
valid = false;
}
}