前缀树
前缀树(trie树),即字典树,是一种树形结构典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
以树的形式存放字符串
下面的前缀树中存放了abc,abcd,abd,b,bcd,efg,hii这几个字符串。
特点
- 每一个节点都包含一个字符,除根节点外。
- 从根节点到每个标红的节点的路径代表一个字符串。
- 每个节点的子节点所存值各不相同,且子节点个数也不同
- 红色表示一个字符串的结束
它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。前缀树的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。它的缺点就是内存消耗非常大。
代码
先给出代码,运行了解一下前缀树的用处:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct Tree
{
int count;
struct Tree *child[26];
}Node,*Trie_node;
Node *CreateTrie()
{
Node *node = (Node*)malloc(sizeof(Node));
memset(node, 0, sizeof(Node));
return node;
}
void insert_node(Trie_node root,char *str)
{
if(root == NULL||*str == '\0')
return;
Node *t = root;
char *p = str;
while(*p!='\0')
{
if(t->child[*p-'a']==NULL)
{
Node *tmp = CreateTrie();
t->child[*p-'a'] = tmp;
}
t = t->child[*p-'a'];
p++;
}
t->count++;
}
void search_str(Trie_node root,char *str)
{
if(NULL==root || *str=='\0')
{
printf("trie is empty or str is null\n");
return;
}
Node *t = root;
char *p = str;
while(*p!='\0')
{
if(t->child[*p-'a'] != NULL)
{
t = t->child[*p-'a'];
p++;
}
else
{
break;
}
}
if(*p == '\0')
{
if(t->count == 0)
{
printf("该字符串不在trie树中,但该串是某个单词的前缀\n");
}
else
{
printf("该字符串在该trie树中\n");
}
}
else
{
printf("该字符串不在trie树中\n");
}
}
void del(Trie_node root) //释放整个字典树占的堆空间
{
int i;
for(i=0;i<26;i++)
{
if(root->child[i]!=NULL)
del(root->child[i]);
}
free(root);
}
int main()
{
int i, n, h;
char str[20];
printf("请输入要创建数的大小:\n");
scanf("%d",&n);
printf("请输入要存的单词:\n");
Trie_node root=NULL;
root=CreateTrie();
if(root==NULL)
printf("创建trie树失败\n");
for(i=0;i<n;i++)
{
scanf("%s",str);
insert_node(root,str);
}
printf("tree建立完成\n");
printf("请输入要查询的单词的个数:\n");
scanf("%d",&h);
i = 0;
while(i<h)
{
printf("请输入要查询的单词:\n");
scanf("%s",str);
search_str(root,str);
i++;
printf("\n");
}
del(root);
return 0;
}
构建前缀树
我认为前缀树就像链表一样,每一个节点存一个字母和一个用来判断该字母是否是单词的最后一个字母的东西(可以是int count,count等于不同时表示不同的情况),显然这个链表不是单链表,那就意味着一个节点可能需要被多个节点连接,那我们在一个节点里就需要多个连接下一个节点的指针,而且需要判断下一个字母去哪一个分支节点,要解决上面两个问题我们可以用以下方式定义结构体:
typedef struct Tree
{
int count;
struct Tree *child[26];
}Node,*Trie_node;
可以看到我们在结构体里面定义了一个struct Tree *child[26] 这个数组存的是指针,child[0],child[1],…,child[25]都是指针,它们可以指向下一个节点,那么如何让它知道下一个要判断的字母在哪一个节点里或者不在呢,这就是定义数组大小为26的目的了,我们用下标找出下一个字母所在的节点。这里我们创建的前缀树每个节点只能存26个字母,无法存其他字符,这里可以更具所面对的情况进行扩展。
创建节点
Node *CreateTrie()
{
Node *node = (Node*)malloc(sizeof(Node));
memset(node, 0, sizeof(Node));
return node;
}
memset(node, 0, sizeof(Node));这行代码将节点所分配到的所有内存一次性初始化为0。
你可能会问那字母存在哪儿呢?下面告诉你。
将单词放入节点
void insert_node(Trie_node root,char *str)
{
if(root == NULL||*str == '\0')
return;
Node *t = root;
char *p = str;
while(*p!='\0')
{
if(t->child[*p-'a']==NULL)
{
Node *tmp = CreateTrie();
t->child[*p-'a'] = tmp;
}
t = t->child[*p-'a'];
p++;
}
t->count++;
}
child[p-‘a’]是为了更好找到指向不同字母所在的节点的指针。比如,a就在child[0]所指的节点,b就在child[1]所指的节点。其实我们并没有死板的存字母,而是用p-'a’的方式存字母。所以child数组不仅存了字母还用来指向下一个节点。
查询
void search_str(Trie_node root,char *str)
{
if(NULL==root || *str=='\0')
{
printf("trie is empty or str is null\n");
return;
}
Node *t = root;
char *p = str;
while(*p!='\0')
{
if(t->child[*p-'a'] != NULL)
{
t = t->child[*p-'a'];
p++;
}
else
{
break;
}
}
if(*p == '\0')
{
if(t->count == 0)
{
printf("该字符串不在trie树中,但该串是某个单词的前缀\n");
}
else
{
printf("该字符串在该trie树中\n");
}
}
else
{
printf("该字符串不在trie树中\n");
}
}
释放内存
void del(Trie_node root)
{
int i;
for(i=0;i<26;i++)
{
if(root->child[i]!=NULL)
del(root->child[i]);
}
free(root);
}
主函数
int main()
{
int i, n, h;
char str[20];
printf("请输入要创建数的大小:\n");
scanf("%d",&n);
printf("请输入要存的单词:\n");
Trie_node root=NULL;
root=CreateTrie();
if(root==NULL)
printf("创建trie树失败\n");
for(i=0;i<n;i++)
{
scanf("%s",str);
insert_node(root,str);
}
printf("tree建立完成\n");
printf("请输入要查询的单词的个数:\n");
scanf("%d",&h);
i = 0;
while(i<h)
{
printf("请输入要查询的单词:\n");
scanf("%s",str);
search_str(root,str);
i++;
printf("\n");
}
del(root);
return 0;
}
扩展
在上面的代码中我们使用了26个指针构成的指针数组,这样其实有很多的浪费,有时会有很多的指针并未被用到,在C++中你可以使用hashmap来替换指针数组。
typedef struct node{
int count;
HashMap<int, Node> paths = new HashMap<>();
}Node
应用
因为将字符串按字母存入,相当于进行了排序,所以放入前缀树的字符串都是排好序的,它用于统计和排序大量的字符串(但不仅限于字符串),被搜索引擎系统用于文本词频统计最大限度地减少无谓的字符串比较,查询效率比哈希表高。同时还可以用于前缀匹配和一些其他数据结构的辅助结构,比如后缀树、AC自动机、hamt等。