【Trie】模板(动态指针,静态数组)

摘自hackbuteer1

Trie树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。

Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

Trie 的强大之处就在于它的时间复杂度。它的插入和查询时间复杂度都为 O(k) ,其中 k 为 key 的长度,与 Trie 中保存了多少个元素无关。Hash 表号称是 O(1) 的,但在计算 hash 的时候就肯定会是 O(k) ,而且还有碰撞之类的问题;Trie 的缺点是空间消耗很高。

基本性质
(1)根节点不包含字符,除根节点意外每个节点只包含一个字符。
(2)从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
(3)每个节点的所有子节点包含的字符串不相同。


特性
1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3)每个节点的所有子节点包含的字符都不相同。
4)如果字符的种数为n,则每个结点的出度为n,这也是空间换时间的体现,浪费了很多的空间。
5)插入查找的复杂度为O(n),n为字符串长度。


基本思想(以字母树为例):
1、插入过程
     对于一个单词,从根开始,沿着单词的各个字母所对应的树中的节点分支向下走,直到单词遍历完,将最后的节点做标记,表示该单词已插入Trie树。
2、查询过程
     从根开始按照单词的字母顺序向下遍历trie树,一旦发现某个节点标记不存在或者单词遍历完成而最后的节点未做标记,则表示该单词不存在,若最后的节点有标记,表示该单词存在。

复杂度:

  建立Trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程。所以总的复杂度为O(n*len),实际查询的复杂度只是O(len)。

操作:

在Trie树中主要有3个操作,插入、查找和删除。一般情况下Trie树中很少存在删除单独某个结点的情况,因此只考虑删除整棵树。
1、插入

  假设存在字符串str,Trie树的根结点为root。i=0,p=root。
  1)取str[i],判断p->next[str[i]-97]是否为空,若为空,则建立结点temp,并将p->next[str[i]-97]指向temp,然后p指向temp;
  若不为空,则p=p->next[str[i]-97];
  2)i++,继续取str[i],循环1)中的操作,直到遇到结束符'',此时将当前结点p中的 exist置为true。

2、查找

  假设要查找的字符串为str,Trie树的根结点为root,i=0,p=root
  1)取str[i],判断判断p->next[str[i]-97]是否为空,若为空,则返回false;若不为空,则p=p->next[str[i]-97],继续取字符。
  2)重复1)中的操作直到遇到结束符'',若当前结点p不为空并且 exist 为true,则返回true,否则返回false。

3、删除

  删除可以以递归的形式进行删除。

模板

(静态数组)

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstdlib>
 4 #include<cstring>
 5 using namespace std;
 6 const int MAX_N = 5000005;
 7 typedef struct TrieNode
 8 {
 9     bool is_leaf; //标记到字典树从根到当前结点所构成的字符串是否为一个(颜色)单词
10     int id;          //当前字符串的编号
11     struct TrieNode* next[26];
12 }TrieNode;
13 TrieNode Node;
14 TrieNode Root[MAX_N];
15 int node_cnt;
16 int idn;
17 
18 int Insert(char *word)
19 {
20     TrieNode *p = &Node;
21     while(*word)
22     {
23         int ch = *word - 'a';
24         if(p->next[ch] == NULL)
25         {
26             Root[node_cnt].is_leaf = false;
27             Root[node_cnt].id = 0;
28             p->next[ch] = &Root[node_cnt++];
29         }
30         p = p->next[ch];
31         word++;
32     }
33     if(p->is_leaf)
34         return p->id;
35     p->is_leaf = true;
36     p->id = ++idn;
37     return p->id;
38 }
39 bool Search(char *word)
40 {
41     TrieNode *p = &Node;
42     while(*word && p)
43     {
44         p = p->next[*word-'a'];
45         word++;
46     }
47     return(p != NULL && p->is_leaf);
48 }
View Code

(动态指针)

 1 #include <iostream>
 2 #include <cstring>
 3 #include <cstdlib>
 4 #include <cstdio>
 5 using namespace std;
 6 const int branchNum = 26; //声明常量
 7 int i;
 8 
 9 struct Trie_node
10 {
11    bool isStr; //记录此处是否构成一个串。
12    Trie_node *next[branchNum];//指向各个子树的指针,下标0-25代表26字符
13    Trie_node()
14    {
15        isStr = false;
16        memset(next,NULL,sizeof(next));
17    }
18 };
19 
20 class Trie
21 {
22 public:
23    Trie();
24    void insert(const char* word);
25    bool search(char* word);
26    void deleteTrie(Trie_node *root);
27 private:
28    Trie_node* root;
29 };
30 
31 Trie::Trie()
32 {
33    root = new Trie_node();
34 }
35 
36 void Trie::insert(const char* word)
37 {
38    Trie_node *location = root;
39    while(*word)
40    {
41        if(location->next[*word-'a'] == NULL)//不存在则建立
42        {
43            Trie_node *tmp = new Trie_node();
44            location->next[*word-'a'] = tmp;
45        }
46        location = location->next[*word-'a']; //每插入一步,相当于有一个新串经过,指针要向下移动
47        word++;
48    }
49    location->isStr = true; //到达尾部,标记一个串
50 }
51 
52 bool Trie::search(char *word)
53 {
54    Trie_node *location = root;
55    while(*word && location)
56    {
57        location = location->next[*word-'a'];
58        word++;
59    }
60    return(location!=NULL && location->isStr);
61 }
62 
63 void Trie::deleteTrie(Trie_node *root)
64 {
65    for(i = 0; i < branchNum; i++)
66    {
67        if(root->next[i] != NULL)
68        {
69            deleteTrie(root->next[i]);
70        }
71    }
72    delete root;
73 }
74 
75 int main() //简单测试
76 {
77    Trie t;
78    t.insert("a");
79    t.insert("abandon");
80    char* c = "abandoned";
81    t.insert(c);
82    t.insert("abashed");
83    if(t.search("abashed"))
84        printf("true
");
85 }
View Code

静态建树与动态建树的主要区别在于插入和删除操作。

插入操作:前者每次插入一个新节点当不存在相应字符时就利用实现已经创建好的数组存放,后者则动态申请一个节点。

删除操作:前者直接将根节点的next全部置为NULL即可,后者要释放所有动态申请的节点空间。

查询操作基本上一样。

然而动态分配内存和静态分配内存性能上存在显著不同!

静态分配会高效很多,但用了一些全局变量,不熟悉的情况下容易出错。熟悉了就没问题了。

动态分配,对于有多个测试实例,如果不释放动态分配的内存,可能导致MLE!

原文地址:https://www.cnblogs.com/LLGemini/p/4736027.html