昨天睡前刷题,正好看到这道新题,大致阅读一下题干发现可以复习一下 Trie 树,就开始写了。没想到写了一个小时……
首先假如已经有 Trie 的定义,写出了主干:
class Trie {
...
}
var suggestedProducts = function(products, searchWord) {
const trie = new Trie();
products.forEach(element => {
trie.insert(element);
});
let searchPrefix= '';
return searchWord.split('').map(c => {
searchPrefix += c;
return trie.findSuggestWordsForPrefix(searchPrefix)
});
};
对于 Trie 树的节点,一般结构是
class TrieNode {
constructor() {
this.isWord = false;
this.alphabets = new Array(26);
}
}
对于 JavaScript 我个人感觉不适合使用 C 风格数组,因为字符间不能直接相减求值,所以改用对象。另外由于后面需要检索某个单词节点的单词,所以改成
class TrieNode {
constructor() {
this.word = null;
this.alphabets = {};
}
}
class Trie {
constructor() {
this.root = new TrieNode();
}
insert(word) {
let node = this.root;
for(const c of word) {
if (!node.alphabets[c]) {
node.alphabets[c] = new TrieNode();
}
node = node.alphabets[c];
}
node.word = word;
}
}
还差一个findSuggestWordsForPrefix
实现。先不管,第一次 Wrong Answer 了,看了一下数据,是因为输入允许出现重复单词,并且输出时也要体现出来。所以修改了一下
const TrieNode = function() {
this.alphabets = {};
this.word = null;
this.repeat = 0; // 记录单词重复次数
}
class Trie {
constructor() {
this.root = new Node();
}
insert(word) {
let node = this.root;
for(const c of word) {
if (!node.alphabets[c]) {
node.alphabets[c] = new Node();
}
node = node.alphabets[c];
}
node.word = word;
node.repeat++;
}
findSuggestWordsForPrefix(prefix) {
let node = this.root;
for (const c of prefix) {
if(!node.alphabets[c]) return [];
node = node.alphabets[c];
}
const stack = [node], words = [];
while(stack.length && words.length < 3) { // 用栈实现 dfs
const currentNode = stack.pop();
if (currentNode.word) {
let n = currentNode.repeat;
while (n > 0 && words.length < 3) { // 处理重复单词
words.push(currentNode.word);
n--;
}
}
const keysInOrder = Object.keys(currentNode.alphabets).sort().reverse();
for (const c of keysInOrder) {
stack.push(currentNode.alphabets[c])
}
}
return words;
}
}
到这里就可以提交通过了。我看了一下提示,说可以用 Binary Search,没想明白。我提交的时候,Leetcode 上还看不到运行时间排名。就睡了。中间还错过一次,因为求倒序 alphabets 写的是:
const keysInOrder = Object.keys(currentNode.alphabets).sort((x, y) => x < y);
这个纯属基础知识不牢了。sort 的排序函数返回值是{-1, 0, 1}的,用x < y
肯定会错。
假设单词数 N,单词平均长度 K,查询单词长度 M,复杂度应该是:构建 Trie,O(KN);dfs:树的节点应该是 O(logN) 级别的,所以 dfs 复杂度 O(MlogN),合起来是 O(KN + MlogN)
今天看了一下排名,很慢。(附近的提交都是 Trie 树实现)看了快速的解法:原来做麻烦了!有一个优化点没有考虑:因为查询单词是一个字母一个字母出现的,因此后面的的联想结果一定是前面的子集。拿我的findSuggestWordsForPrefix
来说,就是不需要每次都从 Trie 树的根节点开始,而是用上次的根节点开始就好了。况且其实根本不劳烦用 Trie 树。单纯排序就已经很快了!
于是代码就这么短了……
var suggestedProducts = function(products, searchWord) {
products.sort(); // 初始排好序
return searchWord.split('').map((c, idx) => {
products = products.filter(word => word[idx] === c); //每一轮循环都是在前一轮的基础上筛出来特定前缀的单词
return products.slice(0, 3);
});
};
分析复杂度的话,这个大概是 O(KNlogN + MN)。
假如 K 和 M 的数量级相近,且都小于 N,那按说排序的方法是要比 Trie 慢的,但是数据并非如此。是我的复杂度分析有错误?还是其他什么原因?欢迎大家讨论。