[建图+拓扑排序] 剑指 Offer II 114. 外星文字典

110 阅读1分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

每日刷题 2022.08.10

题目

  • 现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。
  • 给定一个字符串列表 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] 仅由小写英文字母组成

解题思路

  • 根据题意可知:words中的字符串已经按照特殊的字母顺序进行了排序,现在需要返回这种特定的字母顺序,递增返回;如果不存在合法的字母顺序,则返回""。模拟的方式,就是将words中的字符串进行两两比较,将字典序小的排在字典序大的前面。
  • 那么此时我们可以想象字典序小的和字典序大的之间存在一条边,这样就可以创建一张图。
  • 那么根据字典序的大小创建了一张图之后,怎么才能知道图中字典序最小的,依次将整个输出呢?这时就可以想到,字典序最小的,那么它前面一定没有节点指向它,也就是入度为0的节点。那么每次都可以使用拓扑排序的方式来找到当前的字典序最小的,这样依次输出即可。

具体的实现方式

  • 首先使用set集合将words中的所有的单词都记录下来,为什么使用set集合?因为重复的字符,只需要记录一次。
  • 创建图,将words中的字符串两两进行比较,当发现包含不同的字符的时候,就需要将字典序小和字典序大的创建一条边记录下来。数据结构:[[2,3],[4]]表示:节点0连接着2、3,节点1连接着4
    • 此时还需要考虑,如果words中的所有的字符串都遍历完了,但是还是没有找到不同的字符并且前一个字符串比后一个字符串长,那么就是遇到不合法的数据了,直接返回""(空字符串)。
    • 因为题目中提到:当st前面的字符完全相等,并且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('');
};