携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情
每日刷题 2022.08.10
- leetcode原题链接:leetcode.cn/problems/Jf…
- 难度:困难
- 方法:建图+拓扑
题目
- 现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。
- 给定一个字符串列表 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 <= 1001 <= words[i].length <= 100words[i]仅由小写英文字母组成
解题思路
- 根据题意可知:
words中的字符串已经按照特殊的字母顺序进行了排序,现在需要返回这种特定的字母顺序,递增返回;如果不存在合法的字母顺序,则返回""。模拟的方式,就是将words中的字符串进行两两比较,将字典序小的排在字典序大的前面。 - 那么此时我们可以想象字典序小的和字典序大的之间存在一条边,这样就可以创建一张图。
- 那么根据字典序的大小创建了一张图之后,怎么才能知道图中字典序最小的,依次将整个输出呢?这时就可以想到,字典序最小的,那么它前面一定没有节点指向它,也就是入度为
0的节点。那么每次都可以使用拓扑排序的方式来找到当前的字典序最小的,这样依次输出即可。
具体的实现方式
- 首先使用
set集合将words中的所有的单词都记录下来,为什么使用set集合?因为重复的字符,只需要记录一次。 - 创建图,将
words中的字符串两两进行比较,当发现包含不同的字符的时候,就需要将字典序小和字典序大的创建一条边记录下来。数据结构:[[2,3],[4]]表示:节点0连接着2、3,节点1连接着4- 此时还需要考虑,如果
words中的所有的字符串都遍历完了,但是还是没有找到不同的字符并且前一个字符串比后一个字符串长,那么就是遇到不合法的数据了,直接返回""(空字符串)。 - 因为题目中提到:当
s和t前面的字符完全相等,并且s.length < t.length的时候,表示s的字典顺序小于t,但是这里的显然不符合。
- 此时还需要考虑,如果
- 创建图的过程中,可以顺便将每个节点的入度记录一下,记为:
edgs。后续直接进行拓扑排序,先将edgs中结果为0,即入度为0的节点全部加入队列中(完成初始化)。然后不断的将queue中的节点弹出(记为:cur),依次的去查找cur相连的节点,将其相连的节点的入度-1,也就是删除cur与其相连的节点的这条边。再接着将入度为0的节点放入到队列中,知道队列为空,结束。 - 最终判断当前拓扑排序后的节点个数是否等于总的节点数(也就是
set中存储的节点数),这一步的操作是判断图中是否存在环,如果存在环,也是无法输出合法的字母顺序,直接返回""(空字符串) - 如果也不存在环,那么拓扑排序输出的:就是我们想要的按照特定的字母顺序排序后的结果了。
坑点
- 题目中虽然说了words 中的字符串已经 按这门新语言的字母顺序进行了排序,但是在样例中还是会存在没有排序的,需要特判。如下:
["za","z","a"]、["abc","ab"],输出结果:""
- 还有一个是自己没有想到的样例
["wrt","wrtkj"], 输出结果:"jkrtw"
AC代码
/**
* @param {string[]} words
* @return {string}
*/
var alienOrder = function(words) {
// 还是需要自己将其关系找出来,然后变成一个图,随后使用拓扑排序,将其形成一个字符串
// 因为其一定会有一个合理的顺序,不会存在并列的情况
// 也就是建图,还需要记录每一个节点的入度和出度
// 需要记录入度,因为要查找入度为0的节点
let edgs = new Array(26).fill(0);
let n = words.length, set = new Set();
// 还需要创建一个记录连接边的数组,变长的二维数组
let grap = new Array(26).fill(0).map(() => new Array()), base = 'a'.charCodeAt();
// console.log(grap)
words.forEach(val => {
let arr = val.split('');
for(const one of arr) {
let o = one.charCodeAt() - base;
set.add(o);
}
});
for(let i = 0; i < n - 1; i++) {
// 两两进行比较
let pre = words[i].length, next = words[i + 1].length,j = 0, z = 0;
// 两个单词的方向是不能转换的,因为其代表着固定的字母顺序
let strP = words[i], strN = words[i + 1], flag = true;
// 将出现过的存储在set中
while(j < pre && z < next) {
// 比较两个字符串
if(strP[j] !== strN[z]) {
// 两个字母不想等的时候就需要处理
// 并且还需要标记
flag = false;
let codeP = strP[j].charCodeAt() - base, codeN = strN[z].charCodeAt() - base;
// 创建边
grap[codeP].push(codeN);
// 添加入度
edgs[codeN]++;
// 已经找到了不想等的了,因此就不用再往下找了
break;
}
j++;
z++;
}
if(flag && pre > next) {
return '';
// 表示前面的都是想等的,现在不需要操作
}
}
// console.log(grap, edgs)
// console.log(set)
// 已经建好了图,使用拓扑排序
let queue = [], e = edgs.length;
for(let i = 0; i < e; i++) {
let cur = edgs[i];
if(cur === 0 && set.has(i)) {
// 当前的入度为0
queue.push(i);
}
}
let res = [...queue];
// console.log(res)
while(queue.length != 0) {
let node = queue.pop();
let next = grap[node], nn = next.length;
while(nn > 0) {
let ne = next[nn - 1];
edgs[ne]--;
if(edgs[ne] === 0) {
queue.push(ne);
res.push(ne);
}
nn--;
}
}
// 需要将code转换为字母
let r = res.length,s = set.size;
// console.log(set, res)
if(r != s) return '';
for(let i = 0; i < r; i++) {
let sumCode = base + res[i];
let str = String.fromCharCode(sumCode);
res[i] = str;
}
return res.join('');
};