LC-472. 连接词(字典树+DFS)

472. 连接词

难度困难277

给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词

连接词 定义为:一个完全由给定数组中的至少两个较短单词组成的字符串。

示例 1:

输入:words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]
输出:["catsdogcats","dogcatsdog","ratcatdogcat"]
解释:"catsdogcats" 由 "cats", "dog" 和 "cats" 组成; 
     "dogcatsdog" 由 "dog", "cats" 和 "dog" 组成; 
     "ratcatdogcat" 由 "rat", "cat", "dog" 和 "cat" 组成。

示例 2:

输入:words = ["cat","dog","catdog"]
输出:["catdog"]

提示:

  • 1 <= words.length <= 104
  • 0 <= words[i].length <= 30
  • words[i] 仅由小写字母组成
  • 0 <= sum(words[i].length) <= 105

字典树+DFS

假设我们使用暴力来求解,应该怎么解决呢?

我们可以遍历每一个单词,然后尝试把它按每一个位置进行分割,看分割出来的单词在数组中是不是存在。

比如,假设给定的单词数组为:[“abcd”, “ab”, “cd”],当我们遍历到单词 “abcd” 时,按索引从小到大:

  • 看单词 “a” 是否存在于数组中,不存在;
  • 看单词 “ab” 是否存在于数组中,存在,尝试从这里分割出单词 “ab”;
  • 看单词 “c” 是否存在于数组中,不存在;
  • 看单词 “cd” 是否存在于数组中,存在,结束,返回 true。

但是,请看提示中的数据量为:1 <= words.length <= 1 0 4 10^4 104 且 0 <= words[i].length <= 1000 且 0 <= sum(words[i].length) <=$ 10^5$,按上述的过程,计算量可能会达到 1 0 4 10^4 104 * 1000 * 1 0 5 10^5 105 = 1 0 12 10^{12} 1012,肯定超时,所以,我们要想办法提高效率。

分析上述过程,我们可以使用 字典树 来提升查询单词的速度,这样对于,对于每一个单词,只需要最多 1000 的计算量就可以查询到,而不用遍历整个数组,再一个字符一个字符地去对比,最终我们的计算量可以降低到 1 0 4 10^4 104 * 1000 = 1 0 7 10^{7} 107,是可以通过的。

另外,题目约定了不存在重复的单词,所以,我们可以把单词数组按长度从小到排个序,这样的话,我们可以边遍历边检查是否是连接词,如果不是连接词再把它加入到字典树中。

class Solution {
    public List<String> findAllConcatenatedWordsInADict(String[] words) {
        // 字典树 + DFS
        // 先使用字典树把所有单词放进去
        // 然后遍历每个单词是否可以在字典树中拆成多个单词
        // 为了方便,我们可以先把words按长度排个序
        // 这样,我们先遍历长度短的再遍历长度长的,可以边遍历边从字典树中查找边往字典树中放
        Arrays.sort(words,(a, b)-> a.length() - b.length());
        Trie trie = new Trie();
        List<String> res = new ArrayList<>();
        for(String word : words){
            // 可以分割成多个单词,放入结果集中
            if(trie.dfs(word,0)){
                res.add(word);
            }else{
                // 是连接词的单词不用插入到字典树中
                // 因为一个单词是连接词,说明字典树中存在多个更短的单词
                // 即使一个更长的连接词由上述的连接词构成,它也可以拆成更多个更短的单词构成
                // 比如,"abcd" 由 "ab" + "cd" 构成,同时存在另一个短单词 "ef"
                // 那么,"abcdef" 可以由 "ab" + "cd" + "ef" 构成,不需要把 "abcd" 放入字典树
                trie.insert(word);
            }
        }
        return res;
    }
}

class Trie {
    class TrieNode{//字典树的结点数据结构
		boolean end;//是否是单词末尾的标识
		TrieNode[] child; //26个小写字母的拖尾
		public TrieNode(){
			end = false;
			child = new TrieNode[26];
		}
	}

	TrieNode root;//字典树的根节点。

    public Trie() {
        root = new TrieNode();
    }

    public void insert(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
			//若当前结点下没有找到要的字母,则新开结点继续插入
            if (p.child[u] == null) p.child[u] = new TrieNode();
            p = p.child[u]; 
        }
        p.end = true;
    }

    /**
     * 递归遍历单词,在字典树中寻找是否存在,遇到结束符则尝试分割单词
     * @param word 单词
     * @param i 单词中字符的索引
     * @return 可以分割为多个单词,返回 true,否则返回 false
     */
    public boolean dfs(String word,int i){
        // 因为不存在重复的单词,所以,不会出现只包含一个单词的连接词
        if(i == word.length()){
            return true;
        }
        TrieNode p = root;
        while(i < word.length()){
            // 如果不存在,返回false
            if(p.child[word.charAt(i)-'a'] == null){
                return false;
            }
            p = p.child[word.charAt(i)-'a'];

            // 如果形成了一个完整的单词,深入下一层
            if(p.end && dfs(word,i+1)){
                return true;
            }
            i++;
        }
        return false;
    }
}
  • 时间复杂度为: O ( n l o g n + n ∗ m 2 ) O(nlogn + n * m^2) O(nlogn+nm2),n 为单词数组的长度,m 为单词平均长度,排序的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),这里是按长度排序,不用比较每一个字符,所以,跟单词长度 m 没有关系,检查一个单词是不是连接词的最坏时间复杂度为 O ( m 2 ) O(m^2) O(m2),所以,总的时间复杂度为 O ( n l o g n + n ∗ m 2 ) O(nlogn + n * m^2) O(nlogn+nm2)
  • 空间复杂度为: O ( n ∗ m ∗ C ) O(n * m * C) O(nmC),C 固定为 26,最坏情况是所有单词加入到字典树中。

你可能感兴趣的:(算法刷题记录,深度优先,算法,数据结构)