携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第22天,点击查看活动详情
题目描述
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()初始化前缀树对象。void insert(String word)向前缀树中插入字符串word。boolean search(String word)如果字符串word在前缀树中,返回true(即,在检索之前已经插入);否则,返回false。boolean startsWith(String prefix)如果之前已经插入的字符串word的前缀之一为prefix,返回true;否则,返回false。
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
什么是前缀树?
题目中提到“前缀树是一种树形数据结构,用于 高效地存储和检索 字符串数据集中的键”。
为啥前缀树它高效呢?接下来跟我来一探究竟。
我们先来看看广义的 Trie 树长什么样。
假设有字符串数组 ['cat', 'watermelon', 'catwatermelon'],生成的 Trie 树长这样:
图1. 字符串数组生成的最终Trie树
创建 Trie 树 & 插入
我们需要记住的是,因为这边都是小写字母,所以前缀树的每个节点都要存储26个字母对应的信息,js 中可以用对象或者数组进行存储。
如果是用数组进行存储,那么 数组中的每个元素要么为空,表示没有任何字符串包含该下标表示的字母,要么为一个 Trie 节点。Trie 节点包含有存储26个字母的数据结构及一个判断字符串是否结束的变量 isEnd。这里还需要注意的是,如果是用数组进行存储的话,字母的ASCII-字母a的ASCII-1 得到的值就是对应字母在数组中的下标。
这里演示一下生成一颗前缀树的过程:
- 首先创建一个
Trie节点作为根节点。
var Trie = function() {
this.children = new Array(26);
this.isEnd = false;
};
- 遍历字符串数组,对于字符串
cat,我们对其进行遍历。 - 首先是字母
c,我们去根节点的数组中找其对应的下标是否为空,如果为空, 则在该位置创建一个Trie节点,表示当前路径中字母c是存在的;如果不为空,则说明之前已经存储过前缀为c的字符串了,我们应该跳到字母c对应下标的Trie节点的数组去遍历是否存在cat的下一个字母a(有点嵌套的意思,数组中的数组)。 - 这里我们是第一次创建,肯定为空的,所以我们应该在根节点数组里字母
c对应下标的位置创建一个Trie节点,然后从这个新Trie节点的数组中去查找字母a对应下标是否为空,这里肯定为空,所以我们在对应下标创建一个新的Trie节点,然后再去该新的Trie节点的数组里查看字母t对应下标是否为空,这里肯定为空,所以创建一个新的Trie节点,同时字符串cat遍历结束,所以最后这个Trie节点的isEnd变量应该置为true。
经过这么一番操作下来,生成的前缀树长这样:
同理,字符串数组中剩余的字符串执行 2-4 步骤后,最终的 Trie 数就如 图1 所示。
查找 Trie 树
接下来我们演示查找一个 Trie 树的过程。
假设我们已经有了 ['cat', 'watermelon', 'catwatermelon'] 对应的 Trie 树。
查找的过程就是一个层层查询的过程。
拿查找字符串 catwatermelon 来说,我们对该字符串进行遍历。
Trie.prototype.search = function(word) {
let children = this.children;
for(let w of prefix) {
const index = w.charCodeAt()-'a'.charCodeAt();
if(children[index] == void 0) {
return false;
}
children = children[index].children;
}
if(children && children.isEnd) {
return true;
}
return false;
};
可以看到,如果为空,则查找失败,返回 false,否则去该字符对应下标的数组中去查找下一个字符。只有当被查找字符串正常遍历结束且 isEnd变量为 true 时才说明当前 Trie中存在该字符串。
为什么要强调 isEnd 遍历要为 true 呢?
因为同个前缀的单词有很多,单词 ending 存在的 Trie 树,我们查 end 也是可以找到的,但是 end 严格来说并不是我们存的单词,所以需要一个 isEnd 标记当前是否为单词结尾。
插入
Trie.prototype.insert = function(word) {
let children = this.children;
for(let w of word) {
const index = w.charCodeAt()-'a'.charCodeAt();
if(children[index] == void 0) {
children[index] = new Trie();
}
children = children[index].children;
}
children.isEnd = true;
};
题解
var Trie = function() {
this.data = {};
};
/**
* @param {string} word
* @return {void}
*/
Trie.prototype.insert = function(word) {
let r = this.data;
for (const w of word) {
if (r[w] === void 0) {
r[w] = {};
}
r = r[w];
}
r.isEnd = true;
};
/**
* @param {string} word
* @return {boolean}
*/
Trie.prototype.search = function(word) {
const t = this.startsWith(word);
if(t && t.isEnd) {
return true;
}
return false;
};
/**
* @param {string} prefix
* @return {boolean}
*/
Trie.prototype.startsWith = function(prefix) {
let r = this.data;
for(let w of prefix) {
if(r[w] == void 0) {
return false;
}
r = r[w];
}
return r;
};