持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第21天,点击查看活动详情
现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。
给定一个字符串列表 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"]
输出:""
解释:不存在合法字母顺序,因此返回"" 。
拓扑排序 + 深度优先搜索
使用深度优先搜索实现拓扑排序的总体思想是:对于一个特定节点,如果该节点的所有相邻节点都已经搜索完成,则该节点也会变成已经搜索完成的节点,在拓扑排序中,该节点位于其所有相邻节点的前面。一个节点的相邻节点指的是从该节点出发通过一条有向边可以到达的节点。
由于拓扑排序的顺序和搜索完成的顺序相反,因此需要使用一个栈存储所有已经搜索完成的节点。深度优先搜索的过程中需要维护每个节点的状态,每个节点的状态可能有三种情况:「未访问」、「访问中」和「已访问」。初始时,所有节点的状态都是「未访问」。
每一轮搜索时,任意选取一个「未访问」的节点 u,从节点 u 开始深度优先搜索。将节点 u 的状态更新为「访问中」,对于每个与节点 u 相邻的节点 v,判断节点 v 的状态,执行如下操作:
- 果节点 v 的状态是「未访问」,则继续搜索节点 v;
- 如果节点 v 的状态是「访问中」,则找到有向图中的环,因此不存在拓扑排序;
- 如果节点 v 的状态是「已访问」,则节点 v 已经搜索完成并入栈,节点 u 尚未入栈,因此节点 u 的拓扑顺序一定在节点 v 的前面,不需要执行任何操作。
当节点 u 的所有相邻节点的状态都是「已访问」时,将节点 u 的状态更新为「已访问」,并将节点 u 入栈。
当所有节点都访问结束之后,如果没有找到有向图中的环,则存在拓扑排序,所有节点从栈顶到栈底的顺序即为拓扑排序。
实现方面,由于每个节点是一个字母,因此可以使用字符数组代替栈,当节点入栈时,在字符数组中按照从后往前的顺序依次填入每个字母。当所有节点都访问结束之后,将字符数组转成字符串,即为字典顺序。
var alienOrder = function(words) {
const VISITING = 1, VISITED = 2;
let valid = true;
const edges = new Map();
const states = new Map();
const length = words.length;
for (const word of words) {
const wordLength = word.length;
for (let j = 0; j < wordLength; j++) {
const c = word[j];
if (!edges.has(c)) {
edges.set(c, []);
}
}
}
const addEdge = (before, after) => {
const length1 = before.length, length2 = after.length;
const length = Math.min(length1, length2);
let index = 0;
while (index < length) {
const c1 = before[index], c2 = after[index];
if (c1 !== c2) {
edges.get(c1).push(c2);
break;
}
index++;
}
if (index === length && length1 > length2) {
valid = false;
}
}
const dfs = (u) => {
states.set(u, VISITING);
const adjacent = edges.get(u);
for (const v of adjacent) {
if (!states.has(v)) {
dfs(v);
if (!valid) {
return;
}
} else if (states.get(v) === VISITING) {
valid = false;
return;
}
}
states.set(u, VISITED);
order[index] = u;
index--;
}
for (let i = 1; i < length && valid; i++) {
addEdge(words[i - 1], words[i]);
}
const order = new Array(edges.size).fill(0);
let index = edges.size - 1;
const letterSet = edges.keys();
for (const u of letterSet) {
if (!states.has(u)) {
dfs(u);
}
}
if (!valid) {
return "";
}
return order.join('');
};