題目地址:
https://www.lintcode.com/problem/alien-dictionary/description
給定一個字符串數組,在某種字典序下這個數組是有序的,要求求這個字典序並以字符串的形式返回。如果不存在這樣的字典序則返回空串。如果存在多個合法解,則返回字典序最小的那個(這裏的字典序指的是英文字母的自然的字典序)。
本質是求拓撲排序。首先要排除幾個特殊情況,如果數組裏只有一個字符串,那麼只需要將其字符排序後返回即可。如果數組中有兩個字符串,後面的那個是前面的那個的子串,但是後面的那個長度更短,這時候也是不存在合法解的,因爲字符串的比較中若同位置的字符都相等,那麼短的應該在前面。排除完特殊情況後,通過字符串的順序建圖,如果某個字符順序在另一個之前,則在圖中從到連一條有向邊。最後對圖做拓撲排序即可。
但是,由於題目要求返回所有拓撲序中字典序最小的那個,這個時候DFS是不管用的,DFS不能對頂點分層。而BFS則可以,BFS做拓撲排序需要算一下每個點的入度,並且要用隊列。這裏需要用優先隊列,將字典序最小的那個入度爲的點入隊。代碼如下:
import java.util.*;
public class Solution {
/**
* @param words: a list of words
* @return: a string which is correct order
*/
public String alienOrder(String[] words) {
// Write your code here
if (words == null || words.length == 0) {
return "";
}
// 嚴格來說,在數組長度爲1的時候要先對其排序後再返回
if (words.length == 1) {
char[] res = words[0].toCharArray();
Arrays.sort(res);
return new String(res);
}
// 對words進行建圖
Map<Character, Set<Character>> graph = buildGraph(words);
// 如果返回了空圖說明不存在合法字典序,返回空串
if (graph == null) {
return "";
}
// 得到所有頂點的入度
Map<Character, Integer> indegrees = getIndegrees(graph);
// 開一個優先隊列,將所有入度爲0的頂點入隊
PriorityQueue<Character> pq = new PriorityQueue<>();
for (Map.Entry<Character, Integer> entry : indegrees.entrySet()) {
if (entry.getValue() == 0) {
pq.offer(entry.getKey());
}
}
StringBuilder sb = new StringBuilder();
while (!pq.isEmpty()) {
char cur = pq.poll();
sb.append(cur);
for (char next : graph.get(cur)) {
indegrees.put(next, indegrees.get(next) - 1);
if (indegrees.get(next) == 0) {
pq.offer(next);
}
}
}
// 最後字典序的長度不足字符個數,則說明存在環,返回空串
if (sb.length() != graph.size()) {
return "";
}
return sb.toString();
}
private Map<Character, Integer> getIndegrees(Map<Character, Set<Character>> graph) {
Map<Character, Integer> indegrees = new HashMap<>();
for (Map.Entry<Character, Set<Character>> entry : graph.entrySet()) {
indegrees.putIfAbsent(entry.getKey(), 0);
for (char ch : entry.getValue()) {
indegrees.put(ch, indegrees.getOrDefault(ch, 0) + 1);
}
}
return indegrees;
}
private Map<Character, Set<Character>> buildGraph(String[] words) {
Map<Character, Set<Character>> graph = new HashMap<>();
// 把所有字符(也就是頂點)都先加入圖中
for (int i = 0; i < words.length; i++) {
for (int j = 0; j < words[i].length(); j++) {
graph.putIfAbsent(words[i].charAt(j), new HashSet<>());
}
}
for (int i = 0; i < words.length - 1; i++) {
String w1 = words[i], w2 = words[i + 1];
int idx = 0;
while (idx < w1.length() && idx < w2.length()) {
char c1 = w1.charAt(idx), c2 = w2.charAt(idx);
if (c1 != c2) {
graph.get(c1).add(c2);
break;
}
idx++;
}
// 這個對應的情況是兩個字符串對應位置字符都相等,但排在後面的字符串更短,
// 這時是不存在合法字典序的,直接返回空圖
if (idx == w2.length() && w2.length() < w1.length()) {
return null;
}
}
return graph;
}
}
時間複雜度,空間。
算法正確性證明:
算法得出的序列是拓撲排序,這一點是沒問題的。至於爲什麼字典序最小,可以用數學歸納法來證明。當第一個點出隊後,這個點顯然是排在第一位的,之後將其所有鄰居的入度減一,也就是將原圖中的這個點和其所有鄰邊都刪掉,這樣問題規模就變小了,由歸納假設,剩餘圖的字典序最小的拓撲排序就得到了,由數學歸納法知道算法正確。