前缀树概念
计算机科学中的「前缀树(Trie)」是什么,优缺点是哪些,有什么应用场景?www.zhihu.com示例1(Implement Trie (Prefix Tree))
实现一个 Trie (前缀树),包含 insert
, search
, 和 startsWith
这三个操作。
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
思路
使用 HashMap 存储各个字符。
插入操作
for 循环遍历需要插入的字符,如果当前节点不包含字符中的某个字节,则进行插入操作。
插入操作后/本来就包含对应字符,则往下走一层,直到走到最后一层,标记一个 '$',表示结束。
查询操作
for 循环遍历需要查询的字符串,从 hashTable 顶部开始往下查找。
- 若未找到对应的字符,则表示该前缀树中未包含对应的字符串。
- 若包含对应的字符,则继续往下查找,如果查找到的最后一个字符为 "$",则表示前缀树中包含对应的字符串。
JavaScript 实现
/**
* Initialize your data structure here.
*/
var Trie = function() {
this.root = {}
};
/**
* Inserts a word into the trie.
* @param {string} word
* @return {void}
*/
Trie.prototype.insert = function(word) {
let cur = this.root
for (const n of word) {
if (!cur[n]) {
cur[n] = {}
}
cur = cur[n]
}
cur['$'] = true
};
/**
* Returns if the word is in the trie.
* @param {string} word
* @return {boolean}
*/
Trie.prototype.search = function(word) {
let cur = this.root
for (const n of word) {
if (cur[n]) {
cur = cur[n]
} else {
return false
}
}
return cur['$'] === true
};
/**
* Returns if there is any word in the trie that starts with the given prefix.
* @param {string} prefix
* @return {boolean}
*/
Trie.prototype.startsWith = function(prefix) {
let cur = this.root
for (const n of prefix) {
if (cur[n]) {
cur = cur[n]
} else {
return false
}
}
return true
};
/**
* Your Trie object will be instantiated and called as such:
* var obj = new Trie()
* obj.insert(word)
* var param_2 = obj.search(word)
* var param_3 = obj.startsWith(prefix)
*/
示例2(Add and Search Word)
设计一个支持以下两种操作的数据结构:
void addWord(word)
bool search(word)
search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 .
或 a-z
。 .
可以表示任何一个字母。
addWord("bad")
addWord("dad")
addWord("mad")
search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true
思路
与示例1的差别是,查询操作多了一个可以匹配任意字符的 .
。
插入操作
与示例1保持一致
查询操作
因为多了一个可以匹配任意字符的 .
,所以需要考虑一些特殊情况。
for 循环遍历需要查询的字符串,如果循环到的字符为 .
,则转换为子前缀树问题。
我们可以通过递归来实现子问题的查询。
JavaScript 实现
/**
* Initialize your data structure here.
*/
var WordDictionary = function() {
this.hashTable = {};
};
/**
* Adds a word into the data structure.
* @param {string} word
* @return {void}
*/
WordDictionary.prototype.addWord = function(word) {
let map = this.hashTable;
for (let i = 0; i < word.length; i++) {
if (!map[word[i]]) {
map[word[i]] = {};
}
map = map[word[i]];
}
map["$"] = true;
};
/**
* Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter.
* @param {string} word
* @return {boolean}
*/
WordDictionary.prototype.search = function(word) {
return searchHelper(word, 0, this.hashTable);
};
const searchHelper = (word, index, node) => {
if (index === word.length) {
return Boolean(node["$"]);
}
const char = word[index];
if (node[char]) {
return searchHelper(word, index + 1, node[char]);
} else if (char === ".") {
for (let letter in node) {
if (searchHelper(word, index + 1, node[letter])) {
return true;
}
}
}
return false;
};
/**
* Your WordDictionary object will be instantiated and called as such:
* var obj = new WordDictionary()
* obj.addWord(word)
* var param_2 = obj.search(word)
*/
示例3(Word Search II)
给定一个二维网格 board 和一个字典中的单词列表 words,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
输入:
board = [
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
]
words = ["oath","eea","eat","rain"]
输出: ["eat","oath"]
说明: 你可以假设所有输入都由小写字母a-z 组成。
思路
给定的 words 是一个数组,如果我们通过遍历 words 数组把它转化「给定一个二维网格和一个单词,找出该单词是否存在于网格中」的问题的话,时间复杂度会很高。我们可以利用前缀树来减少时间复杂度。
构建前缀树
以 ["oath","eea","eat","rain"]
为例,我们构建对应的前缀树。
dfs 查找
遍历 board,从第一个字符开始进行上/下/左/右的查找。
如果形成的单词不在前缀树中,则直接停止查找,因为我们不可能找到对应的单词。
如果上/下/左/右是边界,则停止查找该方向。
走过的路进行标记,防止重复查找。
找到对应的值时保存该路径的字符。
JavaScript 代码示例
/**
* @param {character[][]} board
* @param {string[]} words
* @return {string[]}
*/
var findWords = function(board, words) {
if (!words || words.length === 0 || board.length === 0) {
return [];
}
const m = board.length;
const n = board[0].length;
const trie = new Trie();
for (let i = 0; i < words.length; i++) {
trie.insert(words[i]);
}
const res = [];
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
searchWord(trie.hashMap, i, j, board, res);
}
}
return res;
};
function searchWord(trie, i, j, board, res) {
if (trie["$"]) {
res.push(trie["$"]);
trie["$"] = null;
}
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length) return;
if (board[i][j] === "#" || !trie[board[i][j]]) return;
const c = board[i][j];
board[i][j] = "#";
// down
searchWord(trie[c], i + 1, j, board, res);
// up
searchWord(trie[c], i - 1, j, board, res);
// right
searchWord(trie[c], i, j + 1, board, res);
//left
searchWord(trie[c], i, j - 1, board, res);
board[i][j] = c;
}
class Trie {
constructor() {
this.hashMap = {};
}
insert(word) {
let map = this.hashMap;
for (let i = 0; i < word.length; i++) {
if (!map[word[i]]) {
map[word[i]] = {};
}
map = map[word[i]];
}
map["$"] = word;
}
}
王天笑:15个示例帮你解决常见前缀树(Trie)问题 part-2zhuanlan.zhihu.com