转:trie树--详解

前几天学习了并查集和trie树,这里总结一下trie。

    本文讨论一棵最简单的trie树,基于英文26个字母组成的字符串,讨论插入字符串、判断前缀是否存在、查找字符串等基本操作;至于trie树的删除单个节点实在是少见,故在此不做详解。

  • Trie原理

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

  • Trie性质

好多人说trie的根节点不包含任何字符信息,我所习惯的trie根节点却是包含信息的,而且认为这样也方便,下面说一下它的性质 (基于本文所讨论的简单trie树)

1.    字符的种数决定每个节点的出度,即branch数组(空间换时间思想)

2.    branch数组的下标代表字符相对于a的相对位置

3.    采用标记的方法确定是否为字符串。

4.    插入、查找的复杂度均为O(len),len为字符串长度

  • Trie的示意图

如图所示,该trie树存有abc、d、da、dda四个字符串,如果是字符串会在节点的尾部进行标记。没有后续字符的branch分支指向NULL

 

  • Trie的优点举例

已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个串为另一个串的前缀子串。下面对比3种方法:

1.    最容易想到的:即从字符串集中从头往后搜,看每个字符串是否为字符串集中某个字符串的前缀,复杂度为O(n^2)。

2.    使用hash:我们用hash存下所有字符串的所有的前缀子串。建立存有子串hash的复杂度为O(n*len)。查询的复杂度为O(n)* O(1)= O(n)。

3.    使用trie:因为当查询如字符串abc是否为某个字符串的前缀时,显然以b,c,d....等不是以a开头的字符串就不用查找了。所以建立trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度只是O(len)。

解释一下hash为什么不能将建立与查询同时执行,例如有串:911,911456输入,如果要同时执行建立与查询,过程就是查询911,没有,然后存入9、91、911,查询911456,没有然后存入9114、91145、911456,而程序没有记忆功能,并不知道911在输入数据中出现过。所以用hash必须先存入所有子串,然后for循环查询。

而trie树便可以,存入911后,已经记录911为出现的字符串,在存入911456的过程中就能发现而输出答案;倒过来亦可以,先存入911456,在存入911时,当指针指向最后一个1时,程序会发现这个1已经存在,说明911必定是某个字符串的前缀,该思想是我在做pku上的3630中发现的,详见本文配套的“入门练习”。

PKU 3630

Description

Given a list of phone numbers, determine if it is consistent in the sense that no number is the prefix of another. Let's say the phone catalogue listed these numbers:

  • Emergency 911
  • Alice 97 625 999
  • Bob 91 12 54 26

In this case, it's not possible to call Bob, because the central would direct your call to the emergency line as soon as you had dialled the first three digits of Bob's phone number. So this list would not be consistent.

Input

The first line of input gives a single integer, 1 ≤ t ≤ 40, the number of test cases. Each test case starts with n, the number of phone numbers, on a separate line, 1 ≤ n ≤ 10000. Then follows n lines with one unique phone number on each line. A phone number is a sequence of at most ten digits.

Output

For each test case, output "YES" if the list is consistent, or "NO" otherwise.

Sample Input
2
3
911
97625999
91125426
5
113
12340
123440
12345
98346

Sample Output
NO
YES

方法一:trie

有了上面学习的思考与总结,3630trie树本以为可以水过,可是学习和做题终究是两回事,我很快写出trie树,然后提交,超时了。

后来受discuss提示,我大致计算了一下本题trie树的复杂度,号码个数10000,长度10,树的宽度大概有10000,所以总的节点数大概就有100,000级,即要进行十万次new的操作,确实时间耗费很多,估计这样题目的用时要有1秒到2秒左右的样子。

于是为了纯粹的做题,我将new操作省掉,改为提前申请一个buffer空间,就ac了,时间变为125ms了,不过这样确实挺耗空间的,没办法,为了做题只能用空间换时间。

代码如下:

#include<iostream>
 2using namespace std;
 3int cases, count;
 4int nodenum;
 5
 6struct node
 7{
 8    bool isExist;
 9    node * branch[10];
10}Node[100000];
11
12class Trie
13{
14private:
15    node root;
16public:
17    Trie(){root = Node[0];}
18    bool insert(char num[])
19    {
20        node *location = &root;
21        int i = 0;
22        int len = strlen(num);
23        while(num[i])
24        {
25            if(i==len-1 && location->branch[num[i]-'0'] != NULL) //解决没有按照长度排序而存在的问题
26            {
27                return false;
28            }
29            if(location->branch[num[i]-'0']==NULL)//没有建立
30            {
31                location->branch[num[i]-'0'] = &Node[nodenum];
32                Node[nodenum].isExist = false;
33                memset(Node[nodenum].branch,NULL,sizeof(Node[nodenum].branch));
34                nodenum++;
35            }
36            if(location->branch[num[i]-'0']->isExist == true)
37            {
38                return false;
39            }
40            location = location->branch[num[i]-'0'];
41            i++;
42        }
43        location->isExist = true;
44        return true;
45    }
46};
47
48int main()
49{
50    scanf("%d",&cases);
51    while(cases--)
52    {
53        nodenum = 1;
54        bool flag = true;
55        scanf("%d",&count);
56        char tel[11];
57        Trie t;
58        while(count--)
59        {
60            scanf("%s",tel);
61            if(!t.insert(tel))
62            {
63                flag = false;
64            }
65        }
66        if(flag)
67        {
68            printf("YES
");
69        }
70        else
71        {
72            printf("NO
");
73        }
74    }
75    return 0;
76}
77

我写的如下:运行ok,但是超时。

#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;

int cases, count;
int nodenum;

struct  Node
{
     
    bool terminal;
    Node *branch[10];
    
    Node(){ 
         
        terminal=false;
        for(int i=0;i<10;i++)
            branch[i]=NULL;
    
    }
};
class Trie
{
public:

    Node *root;
    Trie()
    {
        root=new Node();
    }
    bool insert(char *str)
    {
        Node* p=root;
        for(int i=0;i<strlen(str);i++)
        {
            if(p->branch[str[i]-'0']==NULL)
            {
                p->branch[str[i]-'0']=new Node();
                nodenum++;
            }
            if(p->branch[str[i]-'0']->terminal==true)
                return false;

            p=p->branch[str[i]-'0'];
        }
        p->terminal=true;
        return true;
    }
};
int main()
{
    scanf("%d",&cases);
    while(cases--)
    {
        nodenum=1;
        bool flag=true;
        scanf("%d",&count);
        char tel[11];
        Trie *root=new Trie();
        while(count--)
        {
            scanf("%s",tel);
            if(root->insert(tel)==false)
                flag=false;
        }
        if(flag)
        {
            cout<<"Yes"<<endl;
        }
        else
        {
            cout<<"No"<<endl;
        }
    }
}

 

方法二:

    转成数字存储比较,这样的话用long整形就可以,然用除法+取余的方法核对是否是某个数字的前缀,但是这种方法的复杂度显然是O(n^2)呀,所以就不尝试了。

方法三:

受大雄提示,可以使用字符串排序比较来做,因为通过排序,前缀子串肯定是与父串挨着的,嘿嘿,这样做,思路简单、代码量少,易理解啊,所以很快ac,下面分析一下复杂度。

 

    理论上使用trie的平均复杂度应该是n*len;其中,len是号码的平均长度,n是号码的个数。使用数组进行字符比较,理论上的复杂度有n*len+logn,排序为logn,然后查询是否存在前缀子串是n*len。所以后者应该时间稍微多一点,提交后果然,耗时188ms

另外值得一提的是使用数组比较的方法有个好处,那就是地址都是连续的,cpu在寻址时会非常快,而用链式结构(即指针),包括使用数组型的trie树则是跳来跳去的,故会有一些开销吧。

呵呵,我所崇拜的排序又一次派上用场了。

代码如下:

#include<iostream>
 2using namespace std;
 3
 4int cases, count;
 5char tel[10005][11];
 6int i, j;
 7
 8int cmp(const void *a, const void *b)
 9{
10    return strcmp( (char*)a,(char*)b );
11}
12
13int main()
14{
15    scanf("%d",&cases);
16    while(cases--)
17    {
18        bool flag = true;
19        scanf("%d",&count);
20        for(i = 0; i < count; i++)
21        {
22            scanf("%s",tel[i]);
23        }
24        qsort(tel,count,sizeof(char)*11,cmp);
25        int len1, len2;
26        for(i = 1; i < count; i++)
27        {
28            len1 = strlen(tel[i-1]);
29            len2 = strlen(tel[i]);
30            j = 0;
31            if(len1 <= len2)
32            {
33                while(tel[i-1][j] == tel[i][j] && j < len1)
34                {
35                    j++;
36                }
37                if(j == len1)
38                {
39                    flag = false;
40                }
41            }
42            if(!flag)
43            {
44                break;
45            }
46        }
47        if(flag)
48        {
49            printf("YES
");
50        }
51        else
52        {
53            printf("NO
");
54        }
55    }
56    return 0;
57}

参考:http://www.cnblogs.com/cherish_yimi/archive/2009/10/12/1581795.html

 http://www.ahathinking.com/archives/14.html

原文地址:https://www.cnblogs.com/youxin/p/3302189.html