漫画:什么是前缀树(Trie树)?

2023年 9月 27日 31.8k 0

————— 第二天 —————

如上图所示,我们在百度输入框输入ap两个字母,下拉菜单就会自动列举出包含该前缀的所有单词,比如api、app、apple等等。

小灰的想法,是要建立一个很大的哈希表,哈希表中的key,是所有单词包含的前缀。

举个例子,有两个单词app和apple,它们的前缀包括a、ap、app、appl、apple,把这些前缀都作为key存储到哈希表中,每一个key对应的value,就是具有这个前缀的单词:

比如app和apple都具有前缀a,那么a所对应的value就是app,apple;再比如appl是apple的前缀,那么appl这个key对应的value就是apple。

————————————

我们利用apple,app,api,banana,bus这5个单词来举例子,构建出一颗前缀树,这颗前缀树就像下图这样:

在这棵树中,每一个节点最多可以拥有26个孩子节点,对应着从a到z这26个英文字母。

在我们给的例子中,apple,app,api这三个单词是字母a开头,banana,bus这两个单词是字母b开头,所以根节点拥有两个孩子,分别对应着以字母a开头的单词和以字母b开头的单词。

apple,app,api这三个单词,第二个字母都是p,所以a孩子节点只拥有一个孩子节点,对应着第二个字母是p的单词。

banana,bus这两个单词,第二个字母分别是a和u,所以b孩子节点拥有两个孩子节点,分别对应着第二个字母是a的单词(banana),以及第二个字母是u的单词(bus)。

以此类推,所有单词的所有字母,共同构成了这个前缀树的所有节点。

假如我们输入查询关键字“ap”,进行前缀查询,前缀树将会如何工作呢?

首先,前缀树会根据关键字中的第一个字母“a”,检查根节点是否有a对应的孩子节点,发现存在该孩子节点:

接下来,根据关键字中的第二个字母“p”,检查a孩子节点是否拥有对应字母p的孩子节点,发现存在该孩子节点:

这样一来,前缀树就判断出当前字典中存在以“ap”为前缀的单词。

假如我们输入查询关键字“bus”,进行精确查询,前缀树将会如何工作呢?

首先,前缀树会根据关键字中的第一个字母“b”,检查根节点是否有b对应的孩子节点,发现存在该孩子节点:

接下来,根据关键字中的第二个字母“u”,检查b孩子节点是否拥有对应字母u的孩子节点,发现存在该孩子节点:

左后,根据关键字中的第三个字母“s”,检查u孩子节点是否拥有对应字母s的孩子节点,发现存在该孩子节点,并且该节点的结束标志位为真:

这样一来,前缀树就判断出当前字典中存在精确匹配“bus”的单词。

假如我们要插入一个新单词“buy”,前缀树如何工作呢?

首先,前缀树会根据关键字中的第一个字母“b”,检查根节点是否有b对应的孩子节点,发现存在该孩子节点:

接下来,根据关键字中的第二个字母“u”,检查b孩子节点是否拥有对应字母u的孩子节点,发现存在该孩子节点:

然后,根据关键字中的第三个字母“y”,检查u孩子节点是否拥有对应字母y的孩子节点,发现并没有这个孩子节点:

最后,创建字母y对应的新孩子节点。由于字母y是单词buy当中的最后一个字母,所以该节点的结束标志位置为“真”:

这样一来,前缀树成功插入了“buy”这个新单词。

class TrieNode {
// 存储当前节点的子节点
private TrieNode[] children;
// 标记该节点是否是一个单词的结束
private boolean isEndOfWord;
public TrieNode() {
// 初始化子节点数组,通常为26个字母
children = new TrieNode[26];
isEndOfWord = false;
}
// 添加一个单词到Trie树中
public void insert(String word) {
TrieNode current = this;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a';
if (current.children[index] == null) {
current.children[index] = new TrieNode();
}
current = current.children[index];
}
current.isEndOfWord = true;
}
// 从Trie树删除一个单词
public boolean delete(String word) {
return delete(this, word, 0);
}
private boolean delete(TrieNode current, String word, int index) {
if (index == word.length()) {
if (!current.isEndOfWord) {
return false; // 单词不存在于树中
}
current.isEndOfWord = false;
return canDelete(current); // 返回是否可以删除该节点
}
char ch = word.charAt(index);
int childIndex = ch - 'a';
TrieNode child = current.children[childIndex];
if (child == null) {
return false; // 单词不存在于树中
}
boolean canDeleteChild = delete(child, word, index + 1);
if (canDeleteChild) {
current.children[childIndex] = null;
return canDelete(current);
}
return false;
}
// 判断节点是否能被删除
private boolean canDelete(TrieNode node) {
if(node.isEndOfWord){
return false;
}
for (TrieNode child : node.children) {
if (child != null) {
return false;
}
}
return true;
}
// 检查一个单词是否存在于Trie树中
public boolean search(String word) {
TrieNode current = this;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a';
if (current.children[index] == null) {
return false;
}
current = current.children[index];
}
return current.isEndOfWord;
}
// 检查是否有以给定前缀开头的单词存在
public boolean startsWith(String prefix) {
TrieNode current = this;
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
int index = ch - 'a';
if (current.children[index] == null) {
return false;
}
current = current.children[index];
}
return true;
}
}
public class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
// 插入一个单词到Trie树中
public void insert(String word) {
root.insert(word);
}
// 从Trie树删除一个单词
public boolean delete(String word) {
return root.delete(word);
}
// 检查一个单词是否存在于Trie树中
public boolean search(String word) {
return root.search(word);
}
// 检查是否有以给定前缀开头的单词存在
public boolean startsWith(String prefix) {
return root.startsWith(prefix);
}
public static void main(String[] args) {
Trie trie = new Trie();
trie.insert("apple");
System.out.println(trie.search("apple")); // 输出 true
System.out.println(trie.search("app")); // 输出 false
System.out.println(trie.startsWith("app")); // 输出 true
trie.insert("app");
System.out.println(trie.search("app")); // 输出 true
trie.delete("apple");
System.out.println(trie.search("apple")); // 输出 false
}
}

在该代码中,TrieNode为前缀树的节点类,Trie类负责前缀树的整体操作。

至于前缀树的删除操作,比插入和查找操作要复杂一些,涉及到递归,大家可以阅读代码中的delete方法进行理解。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论