| 
 | 1 | +### 题目描述  | 
 | 2 | + | 
 | 3 | +这是 LeetCode 上的 **[648. 单词替换](https://leetcode.cn/problems/replace-words/solution/by-ac_oier-jecf/)** ,难度为 **中等**。  | 
 | 4 | + | 
 | 5 | +Tag : 「字典树」  | 
 | 6 | + | 
 | 7 | + | 
 | 8 | + | 
 | 9 | +在英语中,我们有一个叫做 词根(`root`) 的概念,可以词根后面添加其他一些词组成另一个较长的单词——我们称这个词为 继承词(`successor`)。例如,词根`an`,跟随着单词 `other`(其他),可以形成新的单词 `another`(另一个)。  | 
 | 10 | + | 
 | 11 | +现在,给定一个由许多词根组成的词典 `dictionary` 和一个用空格分隔单词形成的句子 `sentence`。你需要将句子中的所有继承词用词根替换掉。如果继承词有许多可以形成它的词根,则用最短的词根替换它。  | 
 | 12 | + | 
 | 13 | +你需要输出替换之后的句子。  | 
 | 14 | + | 
 | 15 | +示例 1:  | 
 | 16 | +```  | 
 | 17 | +输入:dictionary = ["cat","bat","rat"], sentence = "the cattle was rattled by the battery"  | 
 | 18 | + | 
 | 19 | +输出:"the cat was rat by the bat"  | 
 | 20 | +```  | 
 | 21 | +示例 2:  | 
 | 22 | +```  | 
 | 23 | +输入:dictionary = ["a","b","c"], sentence = "aadsfasf absbs bbab cadsfafs"  | 
 | 24 | + | 
 | 25 | +输出:"a a b c"  | 
 | 26 | +```  | 
 | 27 | + | 
 | 28 | +提示:  | 
 | 29 | +* 1ドル <= dictionary.length <= 1000$  | 
 | 30 | +* 1ドル <= dictionary[i].length <= 100$  | 
 | 31 | +* `dictionary[i]` 仅由小写字母组成。  | 
 | 32 | +* 1ドル <= sentence.length <= 10^6$  | 
 | 33 | +* `sentence` 仅由小写字母和空格组成。  | 
 | 34 | +* `sentence` 中单词的总量在范围 $[1, 1000]$ 内。  | 
 | 35 | +* `sentence` 中每个单词的长度在范围 $[1, 1000]$ 内。  | 
 | 36 | +* `sentence` 中单词之间由一个空格隔开。  | 
 | 37 | +* `sentence` 没有前导或尾随空格。  | 
 | 38 | + | 
 | 39 | +---  | 
 | 40 | + | 
 | 41 | +### 基本分析  | 
 | 42 | + | 
 | 43 | +这是一道 `Trie` 的模板题,还不了解 `Trie` 的同学可以先看前置 🧀:[【设计数据结构】实现 Trie (前缀树)  | 
 | 44 | +](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089)  | 
 | 45 | + | 
 | 46 | +前置 🧀 通过图解形式讲解了 `Trie` 的结构与原理,以及提供了两种实现 `Trie` 的方式。  | 
 | 47 | + | 
 | 48 | +回到本题,为了方便,我们令 `ds` 为 `dictionary`,令 `s` 为 `sentence`。  | 
 | 49 | + | 
 | 50 | +---  | 
 | 51 | + | 
 | 52 | +### 二维数组   | 
 | 53 | + | 
 | 54 | +一个比较习惯的做法,是使用「二维数组」来实现 `Trie`,配合 `static` 优化,可以有效控制 `new` 的次数,耗时相对稳定。  | 
 | 55 | + | 
 | 56 | +考虑两个 `Trie` 的基本操作:  | 
 | 57 | + | 
 | 58 | +* `add` 操作:变量入参字符串 `s`,将字符串中的每位字符映射到 $[0, 25],ドル同时为了能够方便查询某个字符串(而不只是某个前缀)是否曾经存入过 `Trie` 中,额外使用一个布尔数组 `isEnd` 记录某个位置是否为单词结尾。  | 
 | 59 | +* `query` 操作:  | 
 | 60 | + | 
 | 61 | +至于二维数组的大小估算,可以直接开成 $N \times C,ドル其中 $N$ 为要插入到 `Trie` 中的字符总数,$C$ 为对应的字符集大小。在 $N \times C$ 没有 `MLE` 风险时,可以直接开这么多;而当 $N \times C$ 较大(超过 1ドルe7,ドル甚至 1ドルe8$ 时),可以适当将 $N \times C$ 中的 $N$ 减少,使得总空间在 1ドルe7$ 左右,因为实际上由于二维数组中的某些行中会存储一个字符以上,实际上我们用不到这么多行。  | 
 | 62 | + | 
 | 63 | +代码(不使用 `static` 优化,耗时增加十倍):  | 
 | 64 | +```Java  | 
 | 65 | +class Solution {  | 
 | 66 | + static int N = 100000, M = 26;  | 
 | 67 | + static int[][] tr = new int[N][M];  | 
 | 68 | + static boolean[] isEnd = new boolean[N * M];  | 
 | 69 | + static int idx;  | 
 | 70 | + void add(String s) {  | 
 | 71 | + int p = 0;  | 
 | 72 | + for (int i = 0; i < s.length(); i++) {  | 
 | 73 | + int u = s.charAt(i) - 'a';  | 
 | 74 | + if (tr[p][u] == 0) tr[p][u] = ++idx;  | 
 | 75 | + p = tr[p][u];  | 
 | 76 | + }  | 
 | 77 | + isEnd[p] = true;  | 
 | 78 | + }  | 
 | 79 | + String query(String s) {  | 
 | 80 | + for (int i = 0, p = 0; i < s.length(); i++) {  | 
 | 81 | + int u = s.charAt(i) - 'a';  | 
 | 82 | + if (tr[p][u] == 0) break;  | 
 | 83 | + if (isEnd[tr[p][u]]) return s.substring(0, i + 1);  | 
 | 84 | + p = tr[p][u];  | 
 | 85 | + }  | 
 | 86 | + return s;  | 
 | 87 | + }  | 
 | 88 | + public String replaceWords(List<String> ds, String s) {  | 
 | 89 | + for (int i = 0; i <= idx; i++) {  | 
 | 90 | + Arrays.fill(tr[i], 0);  | 
 | 91 | + isEnd[i] = false;  | 
 | 92 | + }  | 
 | 93 | + for (String d : ds) add(d);  | 
 | 94 | + StringBuilder sb = new StringBuilder();  | 
 | 95 | + for (String str : s.split(" ")) sb.append(query(str)).append(" ");  | 
 | 96 | + return sb.substring(0, sb.length() - 1);  | 
 | 97 | + }  | 
 | 98 | +}  | 
 | 99 | +```  | 
 | 100 | +* 时间复杂度:令 $n = \sum_{i = 0}^{ds.length - 1} ds[i].length,ドル$m$ 为 `s` 长度,复杂度为 $O(n + m)$  | 
 | 101 | +* 空间复杂度:$O(n \times C),ドル其中 $C = 26$ 为字符集大小  | 
 | 102 | + | 
 | 103 | +---  | 
 | 104 | + | 
 | 105 | +### TrieNode  | 
 | 106 | + | 
 | 107 | +另外一个能够有效避免「估数组大小」操作的方式,是使用 `TrieNode` 的方式实现 `Trie`:每次使用到新的格子再触发 `new` 操作。  | 
 | 108 | + | 
 | 109 | +至于为什么有了 `TrieNode` 的方式,我还是会采用「二维数组」优先的做法,在 [知乎](https://zhuanlan.zhihu.com/p/531180364) 上有同学问过我类似的问题,只不过原问题是「为什么有了动态开点线段树,直接 `build` 出 4ドルn$ 空间的做法仍有意义」,这对应到本题使用「二维数组」还是「TrieNode」是一样的道理:  | 
 | 110 | + | 
 | 111 | +除非某些语言在启动时,采用虚拟机的方式,并且预先分配了足够的内存,否则所有的 `new` 操作都需要反映到 os 上,而在 `linux` 分配时需要遍历红黑树,因此即使是总空间一样,一次性的 `new` 要比多次小空间的 `new` 更省时间,同时集中性的 `new` 也比分散性的 `new` 操作要更快,这也就是为什么我们不无脑使用 `TrieNode` 的原因。  | 
 | 112 | + | 
 | 113 | +代码:  | 
 | 114 | +```Java  | 
 | 115 | +class Solution {  | 
 | 116 | + class Node {  | 
 | 117 | + boolean isEnd;  | 
 | 118 | + Node[] tns = new Node[26];  | 
 | 119 | + }  | 
 | 120 | + Node root = new Node();  | 
 | 121 | + void add(String s) {  | 
 | 122 | + Node p = root;  | 
 | 123 | + for (int i = 0; i < s.length(); i++) {  | 
 | 124 | + int u = s.charAt(i) - 'a';  | 
 | 125 | + if (p.tns[u] == null) p.tns[u] = new Node();  | 
 | 126 | + p = p.tns[u];  | 
 | 127 | + }  | 
 | 128 | + p.isEnd = true;  | 
 | 129 | + }  | 
 | 130 | + String query(String s) {  | 
 | 131 | + Node p = root;  | 
 | 132 | + for (int i = 0; i < s.length(); i++) {  | 
 | 133 | + int u = s.charAt(i) - 'a';  | 
 | 134 | + if (p.tns[u] == null) break;  | 
 | 135 | + if (p.tns[u].isEnd) return s.substring(0, i + 1);  | 
 | 136 | + p = p.tns[u];  | 
 | 137 | + }  | 
 | 138 | + return s;  | 
 | 139 | + }  | 
 | 140 | + public String replaceWords(List<String> ds, String s) {  | 
 | 141 | + for (String str : ds) add(str);  | 
 | 142 | + StringBuilder sb = new StringBuilder();  | 
 | 143 | + for (String str : s.split(" ")) sb.append(query(str)).append(" ");  | 
 | 144 | + return sb.substring(0, sb.length() - 1);  | 
 | 145 | + }  | 
 | 146 | +}  | 
 | 147 | +```  | 
 | 148 | +* 时间复杂度:令 $n = \sum_{i = 0}^{ds.length - 1} ds[i].length,ドル$m$ 为 `s` 长度,复杂度为 $O(n + m)$  | 
 | 149 | +* 空间复杂度:$O(n \times C),ドル其中 $C = 26$ 为字符集大小  | 
 | 150 | + | 
 | 151 | +---  | 
 | 152 | + | 
 | 153 | +### 加餐  | 
 | 154 | + | 
 | 155 | +**今天额外增加一道更贴合笔试面试的加餐题 : [结合 DFS 的 Trie 运用题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492188&idx=1&sn=a1436d1ffe2b8200a36c3196ca1c7ed1) 🎉🎉**  | 
 | 156 | + | 
 | 157 | +---  | 
 | 158 | + | 
 | 159 | +### 最后  | 
 | 160 | + | 
 | 161 | +这是我们「刷穿 LeetCode」系列文章的第 `No.648` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。  | 
 | 162 | + | 
 | 163 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。  | 
 | 164 | + | 
 | 165 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。  | 
 | 166 | + | 
 | 167 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。  | 
 | 168 | + | 
0 commit comments