前缀树(Prefix Tree)
1、背景
- 节点所有的后代都与该节点相关的字符串有着共同的前缀。这就是前缀树名称的由来。
- 对于一个字符串数据,我们要从查找某个字符串是否出现过,或者其中以“hell”开头 ,或者以"ive"结尾的字符是否出现以及出现的个数等等操作。我们只需要在定义前缀树的时候加上相应得数据项就可以了。
2、定义
前缀树是N叉树的一种特殊形式。通常来说,一个前缀树是用来存储字符串的。前缀树的每一个节点代表一个字符串(前缀)。每一个节点会有多个子节点,通往不同子节点的路径上有着不同的字符。子节点代表的字符串是由节点本身的原始字符串,以及通往该子节点路径上所有的字符组成的。
- 在上图示例中,我们在节点中标记的值是该节点对应表示的字符串。例如,我们从根节点开始,选择第二条路径 ‘b’,然后选择它的第一个子节点
‘a’,接下来继续选择子节点 ‘d’,我们最终会到达叶节点 “bad”。节点的值是由从根节点开始,与其经过的路径中的字符按顺序形成的。 - 以节点 “b” 为根的子树中的节点表示的字符串,都具有共同的前缀 “b”。反之亦然,具有公共前缀 “b” 的字符串,全部位于以 "b"为根的子树中,并且具有不同前缀的字符串来自不同的分支。
3、特点
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。(字母用边表示,不要塞到节点里)
- 每个节点的所有子节点包含的字符都不相同。
- 根节点表示空字符串
4、构造
用数组存储子节点:
- 如果我们只存储含有字母 a 到 z 的字符串,我们可以在每个节点中声明一个大小为26的数组来存储其子节点。对于特定字符 c,我们可以使用c - ‘a’ 作为索引来查找数组中相应的子节点。
4.1、实现 Trie (前缀树)——力扣 208
208. 实现 Trie (前缀树)
实现一个 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
你可以假设所有的输入都是由小写字母 a-z 构成的。
保证所有输入均为非空字符串。
包含三个单词"sea",“sells”,"she"的 Trie 会长啥样呢?
步骤如下:
- 1、定义类 Trie
class Trie {
private:
bool isEnd;//表示它是一个单词的末尾。
//字母映射表,每个位置对应一个Trie指针,不是字母
//因为只有26个小写字母,所以构造26个,
//索引利用ASC码辨识
Trie* next[26];
public:
//方法将在下文实现...
};
- 2、插入
描述:向 Trie 中插入一个单词 word
实现:这个操作和构建链表很像。首先从根结点的子结点开始与 word 第一个字符进行匹配,一直匹配到前缀链上没有对应的字符,这时开始不断开辟新的结点,直到插入完 word 的最后一个字符,同时还要将最后一个结点isEnd = true;,表示它是一个单词的末尾。
void insert(string word) {
Trie* node = this;
for (char c : word) {
if (node->next[c-'a'] == NULL) {
node->next[c-'a'] = new Trie();
}
node = node->next[c-'a'];
}
node->isEnd = true;
}
- 3、查找
描述:查找 Trie 中是否存在单词 word
实现:从根结点的子结点开始,一直向下匹配即可,如果出现结点值为空就返回false,如果匹配到了最后一个字符,那我们只需判断node->isEnd即可。
bool search(string word) {
Trie* node = this;
for (char c : word) {
node = node->next[c - 'a'];
if (node == NULL) {
return false;
}
}
return node->isEnd;
}
- 4、前缀匹配
描述:判断 Trie 中是或有以 prefix 为前缀的单词
实现:和 search 操作类似,只是不需要判断最后一个字符结点的isEnd,因为既然能匹配到最后一个字符,那后面一定有单词是以它为前缀的
bool startsWith(string prefix) {
Trie* node = this;
for (char c : prefix) {
node = node->next[c-'a'];
if (node == NULL) {
return false;
}
}
return true;
}
综合
class Trie {
private:
bool isEnd;//表示它是一个单词的末尾。
//字母映射表,每个位置对应一个Trie指针,不是字母
//因为只有26个小写字母,所以构造26个,
//索引利用ASC码辨识
Trie* TrieNode[26];
public:
/** Initialize your data structure here. */
Trie() {
isEnd = false;
memset(TrieNode,0,sizeof(TrieNode));//指针赋0就相当于将该指针置为NULL。
}
/** Inserts a word into the trie. */
void insert(string word) {
if(word.size() < 1)
return ;
Trie* Node = this;
for(char c:word)
{
if(Node->TrieNode[c-'a'] == NULL)//c-'a'得到索引,对应TrieNode表对应的位置被记录
Node->TrieNode[c-'a'] = new Trie;
Node = Node->TrieNode[c-'a'];//c字母对应的位置被标记以后,需要进行c之后的字母
}
Node->isEnd = true;
}
/** Returns if the word is in the trie. */
bool search(string word) {
if(word.size() < 1)
return true;
Trie* Node = this;
for(char c:word)
{
if(Node->TrieNode[c-'a'] == NULL)
return false;
Node = Node->TrieNode[c-'a'];
}
return Node->isEnd;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
if(prefix.size() < 1)
return true;
Trie* Node = this;
for(char c:prefix)
{
if(Node->TrieNode[c-'a'] == NULL)
return false;
Node = Node->TrieNode[c-'a'];
}
return true;
}
};
4.2、Trie ——文本词频统计
相比较力扣 208,这里需要增加新的功能,
- 1、统计前缀字符串出现的次数
- 2、节点删除
前缀树如下4个操作: - 1.插入字符串:
void insert(string str)
遍历字符串,沿途经过的pass++,如果出现某个字符从未出现时则新建一个。遍历到最后一个字符时,其结点的end++; - 2.删除字符串:
void delete(string str)
- 遍历字符串,每个字符串的pass–,如果遍历到最后一个,end也–;
- 如果沿途发现某个结点的pass(自减之前)值为1,则直接删除该结点。
- 3.在前缀树中查询字符串出现的次数:
int search(string str)
遍历字符串,返回最后一个字符对应结点的end值。 - 4.在前缀树中查询以str字符串为前缀的个数:
int prefixNumber(string str)
遍历字符串,返回最后一个字符对应的结点的pass值。
//前缀树结点
class Trienode{
public:
int pass;//为经过该结点的次数
int end;//以该结点结尾的次数
Trienode* nexts[26];//同上
};
class Trie{
public:
Trienode* root;
Trie(){//构造函数
root=new Trienode;//头部空节点
}
//插入操作
void insert(string word)
{
if(word.size()==0)
return;
Trienode* cur=root;
for(char c:word)
{
int index = c-'a';
//节点从没有插入过,需要新建
if(cur->nexts[index]==NULL)
cur->nexts[index]=new Trienode;
//指向新建节点,或者原来已建立的位置
cur=cur->nexts[index];
cur->pass++;// 划过当前节点的字符串数+1
}
cur->end++;// 遍历结束了,记录下以该字母结束的字符串数+1
}
// 在trie树中查找word字符串出现的次数
int search(string word)
{
if(word.size()==0)
return 0;
Trienode* cur=root;
for(char c:word)
{
int index = c-'a';
if(cur->nexts[index]==NULL)
return 0;//说明此单词不存在
// 到达了该字母记录的节点路径,继续往下走
cur=cur->nexts[index];
}
return cur->end;//返回word出现的次数
}
// 删除一个字符串
void deletenode(string word)
{
// 删除之前,先判断有没有
if(search(word) == 0){
return;
}
Trienode* cur=root;
for(char c:word)
{
int index = c-'a';
// 如果遍历到某个节点时,将其index处passNum减1后等于0,则说明没有其他字符串经过它了
if(--cur->nexts[index]->pass == 0)
{
delete cur->nexts[index];
return;
}
cur=cur->nexts[index];
}
// 遍历完了,删除了整个单词,则将以该单词最后一个字符结尾的字符串的数目减1
cur->end--;
}
// 返回有多少单词以pre为前缀的
int prefixnum(string word)
{
if(word.size()==0)
return 0;
Trienode* cur=root;
for(char c:word)
{
int index = c-'a';
if(cur->nexts[index]==NULL)
return 0; // 不存在
cur=cur->nexts[index];
}
return cur->pass;// 找到word最后一个字符的pass值
}
};
参考
1、https://www.cnblogs.com/vincent1997/p/11237389.html
2、https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/trie-tree-de-shi-xian-gua-he-chu-xue-zhe-by-huwt/
3、https://blog.youkuaiyun.com/y1054765649/article/details/88700590