[数据结构学习笔记12] 前序树(Trie/Prefix tree)
前序树(Trie/Prefix tree),它的一个典型的应用场景在搜索引擎里,当你输入查询关键字的时候,会联想自动补齐你想要输入的内容。比如,你输入app,下面可能会出来联想Apple, Applied等等。
什么是Trie?
Trie(读作Try)是这样一个数据结构,它把短语或者单词分解字母,然后以一种方式去存储,让添加、删除、查找或者自动补齐短语/单词更高效。
我们看一个例子:
1. 开始我们有一个空的根节点
*
2. 我们想把apple存放进来
2.1 *
a
p
p
l
e (The end of our phase or word!)
3. 把cat,dog加进来
3.1 *
a c d
p a o
p t g
l
e
4. 把duck加进来,d作为了dog和duck公共的前序字母
4.1 *
a c d
p a o u
p t g c
l k
e
5. 把dune和monk,monkey加进来
5.1 *
a c d m
p a o u o
p t g c n n
l k e k
e e
y
上面的前序树里,我们有apple,app,cat,dog,duck,dune,monkey,monk。
查找单词:
1. 我们要查找eagle,首先我们把eagle拆解成e,a,g,l,e,我们看e是不是在根节点的孩子节点里。这里我们并没有e,所以我们可以停止搜索,告知eagle不在我们树里。
2. 我们要查找monk,过程是一样的,把monk拆解成m,o,n,k,看m在不在孩子节点,这里m是其中一个孩子节点,所以继续往下找o,同样也能找到,可以看到,我们这里查找的时候就关注在m,o这条分支上,继续往下,n,k可以都找到。
2.1 如果我们是要看完整的单词是否存在,那我们要看最后一个字母是不是单词的结尾,monk的例子上,我们看到k是被标记为结尾的,所以monk是一个完整的单词。
2.2 如果我们是看prefix是否存在,那么我们不需要看最后一个字母是不是单词结尾,monk是存在的,同样的m,mo,mon也都是存在的。
删除单词:
如果我们要删除单词,我们不能简单的把这个单词的字母删掉,因为这个单词的字母有可能是和其他单词共用了前缀。
1. 查找要删除的单词在我们前序树里存在
2. 如果单词的某个字母是和其他单词共享的,那不能删除。比如要删除duck,d是和dog,dune共享的,u是和dune共享的
3. 我们要删除的单词不是另一个单词的子集
所以我们这样删:
把我们要删除的单词解散成字母,然后反相删除。从最后一个字母开始,看这个字母是否和其他单词共享,或者是其他单词的一部分,如果都不是,则把这个字母删除,然后相同的方法,查看前一个字母,直到字母和其他单词共享则停止。
我们删除duck后的前序树是这个样子:d,u还在,因为和其他单词共用,c,k被删除了。
*
a c d m
p a o u o
p t g n n
l e k
e e
y
前序树应用场景:
前序树应用,直接的例子,对于我们上面的前序树,加入你输入d,它会快速返回给你dog,duck,dune作为你可能要查找的目标。
1. 自动补齐或者预测文字:比如在搜索引擎,message app,email client,我们常会看到自动提示,或者自动补齐的功能;
2. 拼写检查和矫正:前序树可以用于存储字典,从而帮助查找和建议正确的单词;当一个不正确的字母输入时,我们可以回退几步,给出可能正确的单词选项;
3. IP路由和网络路由表:前序树中每个节点表示ip的一部分,子节点表示剩下部分可能的值,遍历前序树,路由器可以查看下一个hop;
4. 单词游戏:Wordle。
代码实现(javascript)
我们将要实现一下功能:
1. 插入一个单词
2. 查找一个单词是否存在
3. 检查是否有单词能匹配输入前缀
4. 返回所有匹配输入前缀的单词
class TrieNode { constructor() { // Each TrieNode has a map of children nodes, // where the key is the character and the value is the child TrieNode this.children = new Map(); // Flag to indicate if the current TrieNode represents the end of a word this.isEndOfWord = false; } } class Trie { constructor() { // The root of the Trie is an empty TrieNode this.root = new TrieNode(); } insert(word) { let current = this.root; for (let i = 0; i < word.length; i++) { const char = word[i]; // if the character doesn't exist as a child node, // create a new TrieNode for it if (!current.children.get(char)) { current.children.set(char, new TrieNode()); } // Move to the next TrieNodee current = current.children.get(char); } // Mark the end of the word by setting isEndOfWord to true current.isEndOfWord = true; } search(word) { let current = this.root; for (let i = 0; i < word.length; i++) { const char = word[i]; // if the character doesn't exist as a child node, // the word doesn't exist in the Trie if (!current.children.get(char)) { return false; } // move to the next TrieNode current = current.children.get(char); } return current.isEndOfWord; } startsWith(prefix) { let current = this.root; for (let i = 0; i < prefix.length; i++) { const char = prefix[i]; if (!current.children.get(char)) { return false; } currrent = current.children.get(char); } return true; } getAllWords(prefix = '') { const words = []; // Find the node corresponding to the given prefix const current = this.#findNode(prefix); if (current) { // if the node exists, traverse the Trie starting from that node // to find all words and add them to the 'words' array this.#traverse(current, prefix, words); } return words; } delete(word) { let current = this.root; const stack = []; let index = 0; // Find the last node of the word in the Trie while (index < word.length) { const char = word[index]; if (!current.children.get(char)) { return; } stack.push({ node: current, char }); current = current.children.get(char); index++; } if (!current.isEndOfWord) { // word doesn't exist in the Trie, nothing to delete return; } // Mark the last node as not representing the end of a word current.isEndOfWord = false; // Remove nodes in reverse order until reaching a node // that has other children or is the end of another word while (stack.length > 0) { const { node, char } = stack.pop(); if (current.children.size === 0 && !current.isEndOfWord) { node.children.delete(char); current = node; } else { break; } } } #findNode(prefix) { let current = this.root; for (let i = 0; i < prefix.length; i++) { const char = prefix[i]; if (!current.children.get(char)) { return null; } current = current.children.get(char); } return current; } #traverse(node, prefix, words) { const stack = []; stack.push ({ node, prefix }); while (stack.length > 0) { const { node, prefix } = stack.pop(); // If the current node represents the end of a word, // add the word to the 'words' array if (node.isEndOfWord) { words.push(prefix); } // Push all child nodes to the stack to continue traversal for (const char of node.children.keys()) { const childNode = node.children.get(char); stack.push({ node: childNode, prefix: prefix + char }); } } } }
使用Trie
cnost trie = new Trie(); trie.insert("apple"); trie.insert("app"); trie.insert("monkey"); trie.insert("monk"); trie.insert("cat"); trie.insert("dog"); trie.insert("duck"); trie.insert("dune"); trie.search("apple"); // true tire.search("bat"); // false trie.getAllWords("ap"); // ['apple', 'app'] trie.getAllWords("b"); // [] trie.delete("monkey"); trie.getAllWords("m") // ['monk']
性能
前序树,我们底层使用了一个哈希表来实现的。性能来自两个因素:
1. 输入的单词或者前缀的长度
2. 给定一个字母,它下面所包含的子节点的数量
我们插入,查找,删除都是线性时间复杂度O(k),k是单词长度。我们底层是hashmap,一般检查一个字母是否存在是O(1)复杂度,最坏是O(n),这个发生在一个节点它有异常多的子节点。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战