当前位置: > > > Java编程题 - 外星文字典详解(拓扑排序+深度优先搜索)

Java编程题 - 外星文字典详解(拓扑排序+深度优先搜索)

1,题目描述

(1)现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。
(2)给定一个字符串列表 words ,作为这门语言的词典,words 中的字符串已经按这门新语言的字母顺序进行了排序 。
(3)请你根据该词典还原出此语言中已知的字母顺序,并按字母递增顺序排列。若不存在合法字母顺序,返回 "" 。若存在多种可能的合法字母顺序,返回其中任意一种顺序即可。
(4)字符串 s 字典顺序小于 字符串 t 有两种情况:
  • 在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t
  • 如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t

2,解题思路

(1)根据描述可以得知外星文字母排序并不是正常的 abcd....,而是需要根据我们给定的字符串列表来推算出字母顺序,比如:
  • 输入["z","x"] 输出"zx"
  • 输入["wrt","wrf","er","ett","rftt"] 输出"wertf"

(2)但有以下两种情况不存在合法字母顺序:
  • 字母之间的顺序关系存在由至少 22 个字母组成的环,例如 words=["a","b","a"]
  • 相邻两个单词满足后面的单词是前面的单词的前缀,且后面的单词的长度小于前面的单词的长度,例如 words=["ab",“a"]

(3)这道题是拓扑排序问题。将外星文字典中的每个字母看成一个节点,将字母之间的顺序关系看成有向边。对于外星文字典中的两个相邻单词,同时从左到右遍历,当遇到第一个不相同的字母时,该位置的两个字母之间即存在顺序关系。比如通过字符串列表 ["we","wr","wt","wf","er","eq","cc","ct"] 可以构建出如下拓扑图:

(4)拓扑排序可以使用深度优先搜索或广度优先搜索实现,这里我们使用广度优先搜索。具体做法是,使用队列存储可以加入拓扑排序的节点,初始时将所有入度为 0 的节点入队列。每次将一个节点出队列并加入拓扑排序中,然后将该节点的所有相邻节点的入度都减 1,如果一个相邻节点的入度变成 0,则将该相邻节点入队列。重复上述操作,直到队列为空时,广度优先搜索结束。
提示:广度优先搜索结束时,判断拓扑排序的长度是否等于字典中的字母个数,即可判断有向图中是否有环。
  • 如果拓扑排序的长度等于字典中的字母个数,则拓扑排序包含字典中的所有字母,返回拓扑排序;
  • 如果拓扑排序的长度小于字典中的字母个数,则有向图中有环,不存在拓扑排序。

3,解题代码

(1)代码具体的实现逻辑如下:
  • 记录所有出现字母的入度和图的映射关系
  • 不合法情况(前面的长后面的短,短的部分两者相同,abcab)这种情况直接返回
  • 入度为 0 的点入队
  • 逐个出队,减少到达点的入度,入度为 0 的入队
  • 最终需要确定是否所有已有字母都完成了排序,如果没有,则说明字典构建失败,需要返回空
public class AlienOrder {
  // 通过输入的字典,返回字符顺序(拓扑排序 + 广度优先搜索)
  public static String getOrder(String[] words) {
    // 存储图(当前节点字符对应下一个节点字符,下一个节点可能有多个)
    Map<Character,List<Character>> graph = new HashMap<>();
    // 存储每个字符节点的入度
    Map<Character, Integer> indegrees = new HashMap<Character, Integer>();

    // 初始化图(将所有的字符作为key存入graph)
    for(String word:words){
      for(int j = 0; j < word.length(); j++){
        graph.putIfAbsent(word.charAt(j),new ArrayList<>());
      }
    }

    // 两两单词比较构建图和入度数据
    for(int i = 0; i < words.length-1; i++){
      String w1 = words[i];
      String w2 = words[i+1];
      int minLength = Math.min(w1.length(),w2.length());
      int j = 0;
      while (j < minLength) {
        // 逐个比较字符直到不相同的时候
        if(w1.charAt(j) != w2.charAt(j)){
          char c1 = w1.charAt(j);
          char c2 = w2.charAt(j);
          // 增加边联系(c1->c2)
          graph.get(c1).add(c2);
          // c2入度+1
          indegrees.put(c2, indegrees.getOrDefault(c2, 0) + 1);
          break;
        }
        j++;
      }
      //存在不合法的情况 前一个字符串长度大于当前字符串长度 但前minLength均相等
      if(j == minLength && w1.length()>w2.length()) return "";
    }

    // 存储顺序字符串
    StringBuilder sb = new StringBuilder();
    // 初始时将所有入度为 0 的节点入队列
    Queue<Character> queue = new LinkedList<>();
    for (Character ch : graph.keySet()) {
      if (!indegrees.containsKey(ch)) {
        queue.offer(ch);
      }
    }

    // 队列中是否存在入度为0的点
    while (!queue.isEmpty()){
      // 有的话则输出
      Character ch = queue.poll();
      sb.append(ch);
      // 判断当前输出的节点下一个相联系的节点
      if(graph.containsKey(ch)) {
        for(Character next: graph.get(ch)){
          //下一个相联系的节点入度-1
          indegrees.put(next, indegrees.get(next) - 1);
          //如果-1后如果变为0则加入队列
          if(indegrees.get(next)==0){
            queue.offer(next);
          }
        }
      }
    }

    // 返回最终结果(判断是否所有的点都加入了结果集,如果不是则存在环,返回空字符串表示不存在合法字母顺序)
    return sb.length() < graph.size()? "" : sb.toString();
  }

  public static void main(String[] args) {
    String[] ws1 = {"z","x","a"};
    System.out.println("输入:" + Arrays.toString(ws1));
    System.out.println("输出:" + getOrder(ws1));

    String[] ws2 = {"cd","bnc","ca"};
    System.out.println("输入:" + Arrays.toString(ws2));
    System.out.println("输出:" + getOrder(ws2));

    String[] ws3 = {"wrt","wrf","er","ett", "ec"};
    System.out.println("输入:" + Arrays.toString(ws3));
    System.out.println("输出:" + getOrder(ws3));

    String[] ws4 = {"we","wr","wt","wf","er","eq","cc","ct"};
    System.out.println("输入:" + Arrays.toString(ws4));
    System.out.println("输出:" + getOrder(ws4));
  }
}

(2)运行结果如下:
评论0