1 Trie树介绍
- trie树是一种树形结构,是一种哈希树的变种。
- 应用:典型应用于统计、排序和保存大量的字符串(但不限于字符串),经常被搜索引擎系统用于文本词频统计。
- 优点:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
2 Trie树结构体
typedef struct trie{
bool isEnd;
struct trie *next[26];
} Trie;
单词sea、sell、she的Trie树:
Trie 中一般都含有大量的空链接,因此在绘制一棵单词查找树时一般会忽略空链接,为方便理解可以画成:
2.1 Trie树的三个性质
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某个节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
这样看来,对于一个长为l的单词,无论是插入还是查询都是O(l)的时间复杂度。
2.2 对Trie树的操作
2.2.1 插入
- 插入就是将单词中的每个字母都逐一插入Trie树,插入前看这个字母对应的节点是否存在,若不存在就新建一个节点,否则就共享那个节点。
向图中插入单词set
1、获取第一个字符s,判断p->next['s'-'a']!=NULL,说明根节点存在子节点s,则共享节点s
2、p = p->next['s'-'a']
3、获取第二个字符e,判断p->next['e'-'a']!=NULL,说明节点s存在子节点e,则共享节点e
4、p=p->next['e'-'a']
5、获取第三个字符t,判断p->next['t'-'a']==NULL,则说明节点e不存在子节点t,则新建子节点t
2.2.2 查询
-
查询与插入操作类似,就是在Trie树中找到这个单词的每个字母, 若找到就继续找下去,若没有找到就可以直接退出了,因为没有找到就说明没有这个单词。
-
以查询单词sea为例:
1、查询第一个字母s,发现根节点存在子节点s,则继续查询e
2、查询第二个字母e,发现s节点存在子节点e,则继续查询a
3、查询第三个字母a,发现e节点存在子节点a,并且节点a所指向的数据域为true,表明这个单词已经结束,说明存在单词sea。
3 Trie树使用示例
请你实现 Trie 类:
- Trie() 初始化前缀树对象。
- void insert(String word) 向前缀树中插入字符串 word 。
- boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
- boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]解释
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
提示:
- 1 <= word.length, prefix.length <= 2000
- word 和 prefix 仅由小写英文字母组成
- insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次
题目来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/implement-trie-prefix-tree
代码实现:
(1)C语言:
typedef struct trie{
bool isEnd;
struct trie *next[26];
} Trie;
//新建trie节点
Trie* trieCreate() {
Trie * trie = malloc(sizeof(Trie));
//初始化数据
memset(trie->next, 0, sizeof(trie->next));
trie->isEnd = false;
return trie;
}
//插入单词
void trieInsert(Trie* obj, char * word) {
int i;
for(i=0; i<strlen(word); i++){
char ch = word[i];
if(obj->next[ch-'a'] == NULL){
obj->next[ch-'a'] = trieCreate();
}
obj = obj->next[ch-'a'];
}
obj->isEnd = true;
}
//查询单词
bool trieSearch(Trie* obj, char * word) {
int i;
for(i=0; i<strlen(word); i++){
char c = word[i];
if(obj->next[c-'a'] == NULL){
return false;
}
obj = obj->next[c-'a'];
}
return obj->isEnd;
}
//匹配前缀
bool trieStartsWith(Trie* obj, char * prefix) {
int i;
for(i=0; i<strlen(prefix); i++){
int ch = prefix[i]-'a';
if(obj->next[ch] == NULL){
return false;
}
obj = obj->next[ch];
}
return true;
}
//释放内存
void trieFree(Trie* obj) {
for (int i = 0; i < 26; ++i) {
if (obj->next[i] != NULL) {
trieFree(obj->next[i]);
}
}
free(obj);
}
(2) java语言
class Trie {
private boolean isEnd;
private Trie[] next;
public Trie() {
this.isEnd = false;
this.next = new Trie[26];
}
public void insert(String word) {
Trie trie = this;
for(int i=0; i<word.length(); i++){
int index = word.charAt(i)-'a';
if(trie.next[index] == null){
trie.next[index] = new Trie();
}
trie = trie.next[index];
}
trie.isEnd = true;
}
public boolean search(String word) {
Trie trie = this;
for(int i=0; i<word.length(); i++){
int index = word.charAt(i)-'a';
if(trie.next[index] == null){
return false;
}
trie = trie.next[index];
}
return trie.isEnd;
}
public boolean startsWith(String prefix) {
Trie trie = this;
for(int i=0; i<prefix.length(); i++){
int index = prefix.charAt(i)-'a';
if(trie.next[index] == null){
return false;
}
trie = trie.next[index];
}
return true;
}
}
4 结构体的内存释放
转载自:
C语言中free()函数释放struct结构体中的规律 - whitesad - 博客园 (cnblogs.com)
- 在使用struct来定义并声明一个变量时,将会自动划分出一个连续的储存空间(虽然根据某些对齐原则会出现内存间隙,但是大体上来说还是连续的)。这一块连续空间将会包括结构体中的其他变量所需要的内存,如上图。
- free()函数的作用是对动态分配的内存进行释放,这也就意味着当使用free函数清空一个结构体时,只会清空这个大框里的内存,而不会对a,b,c,d指向的内存进行清理。
- 所以,要是用free()函数释放结构体内存时,要先释放结构体中的指针类型变量所指向的内存,再释放动态分配的结构体的内存。