1 什么是Trie树
Trie树,即前缀树,又称单词查找树,字典树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
Trie树的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。 它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
它有3个基本性质:
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
2 树的构建
一个字符串加的过程中总是从头结点开始,依次看有没有沿途的路,如果有则复用,如果没有则新建。
对于字符串“abc”、“bce"、"bef"、"abd"
这里的思想主要是将字母放在边上,在节点中并不存放字母。
扩存功能:
- 实现判断某个前缀的字符串有多少个
- 判断某个字符串出现了多少次
这样就需要对节点进行填写成员,记录在划过该节点的次数,记录字符串在该节点结束的次数。
3 Trie树的应用
除了本文引言处所述的问题能应用Trie树解决之外,Trie树还能解决下述问题:
1、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
2、1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现?
3、 一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
4、寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
4 C++实现Trie树
主要注意,在删除字符串的时候,若该字符串只出现了一次,对一次的节点进行删除。将TrieNode节点进行删除。运用递归循环
向上删除。若直接删除当前节点会丢失下一个节点。
#pragma once
#include<vector>
#include<string>
using std::string;
using std::vector;
struct TrieNode
{
int path;
int end;
TrieNode* nexts[26];//将字符串限定在26个字母中,可以用vector为了避免也可以用map或者hashmap
TrieNode() {
path = 0;
end = 0;
for (int i = 0; i < 26; i++)
{
nexts[i] = NULL;
}
};
};
class Tried
{
public:
Tried();
~Tried();
//在前缀树中插入这个字符串
void insert(string str);
//在前缀树中删除这个字符串
bool Delete(string str);
//删除某个节点开始的所有节点
void destory(TrieNode* root,const char* chs);
//在前缀树中查找以该字符串为前缀的字符串有几个
int search_pre(string str);
//这个字符出现过几次
int search_end(string str);
private:
TrieNode* root;
};
Tried::Tried()
{
root = new TrieNode();
}
Tried::~Tried()
{
delete root;
}
void Tried::insert(string str) {
if (str.empty())
{
return;
}
const char *chs=str.c_str();//将string转换为C字符串
int index;
TrieNode* node = root;
while (*chs!=NULL)//这里是c字符串。c字符串结尾是一个以null结尾的字符串。不能判断是chs!=NULL
{
index = *chs - 'a';
if (node->nexts[index] == NULL) {
node->nexts[index] = new TrieNode();
}
node = node->nexts[index];
node->path++;
chs++;
}
node->end++;
}
inline bool Tried::Delete(string str)
{
if (search_end(str) != 0)
{
TrieNode* node = root;//每次都需要回到根节点
int index = 0;
const char* chs = str.c_str();
while (*chs != NULL) {
index = *chs - 'a';
if (--node->nexts[index]->path == 0) {//若该节点的path为0那么后面的都需要删除
destory(node->nexts[index],++chs);//递归删除节点
return true;
}
//判断的时候已经相减,不需要再次相减
node = node->nexts[index];
chs++;
}
}
return false;
}
inline int Tried::search_pre(string str)
{
if (str.empty())
{
return 0;
}
const char* chs = str.c_str();
int index;
TrieNode* node = root;
while (*chs != NULL) {
index = *chs - 'a';
if (node->nexts[index] == NULL) {
return 0;
}
node = node->nexts[index];
chs++;
}
return node->path;
}
inline int Tried::search_end(string str)
{
if (str.empty())
{
return 0;
}
const char* chs = str.c_str();
int index;
TrieNode* node = root;
while (*chs != NULL) {
index = *chs - 'a';
if (node->nexts[index]==NULL) {
return 0;
}
node = node->nexts[index];
chs++;
}
return node->end;
}
//使用递归来实现先删除后面的节点,再删除前面的节点
void Tried::destory(TrieNode* root,const char* chs) {
if (root == NULL||chs==NULL) {
return;
}
destory(root->nexts[*chs-'a'],++chs);
delete root;
}