高级数据结构的学习与实现之 Trie树,字典树

第一次自学一种新的数据结构,感觉学会利用资源很重要,参考别人写的博客,谷歌搜索一下关于这中数据结构的一些问题,解决等,学会搜索资料,学习并掌握一门数据结构并不是很难。

要知道相关的基本知识,主要应用与哪一方面,还有它的具体实现是如何的。

在编码的时候还是会遇到各种问题,需要认真解决。总体来说学习得还是比较成功得,也掌握了它的使用。


一、分析题目要求

(一)程序设计的任务:

学习一种课外的数据结构,并实现它。包括:1)初始化2)插入元素3)删除元素4)查找元素5)相关应用

本程序实现了以上5个要求,实验报告是根据Trie树的学习与实现过程而写的。

(二)函数规定

·Trie CreateTrieNode()

1) 创建Tire树 

参数       :无     

返回值     :Trie 已初始化的TrieNode节点指针

2) 输入的形式 :无

输入值的范围:无

3) 输出的形式 :无

4) 所能达到的功能:

用于创建Trie节点,初始化count0existfalse,子节点为NULL 

 

·void InsertTrieNode(Trie root , char *word)

1) 插入单词元素

参数       :Trie 指定节点

word 指定单词字符串   

返回值     :无

2) 输入的形式 :字符串类型

输入值的范围:字符串范围

3) 输出的形式 :无

4) 所能达到的功能:

用于向指定节点插入指定字符串节点

 

·bool SearchString(Trie root , char* word)

1) 查找字符串 

参数       :Trie 指定节点

word 指定单词字符串     

返回值     :bool 是否存在指定字符串

2) 输入的形式 :字符串类型

输入值的范围:字符串范围

3) 输出的形式 :无

4) 所能达到的功能:

用于在指定根节点中查找字符串是否存在

 

·int SearchTrieNode(Trie root , char* word)

1) 查找单词前缀出现的次数 

参数       :Trie 指定节点

word 指定单词字符串     

返回值     :int 单词前缀出现的次数

2) 输入的形式 :字符串类型

输入值的范围:字符串范围

3) 输出的形式 :无

4) 所能达到的功能:

用于在指定根节点中查找单词前缀次数,类似SearchString

 

·bool DelectString(Trie root , char* word)

1) 删除指定单词 

参数       :Trie 指定节点

word 指定单词字符串  

返回值     :bool 删除成功与否

2) 输入的形式 :字符串类型

输入值的范围:字符串范围

3) 输出的形式 :无

4) 所能达到的功能:

用于在指定根节点中删除指定单词

 

·void DelectTrie(Trie root)

1) 删除整棵树

参数       :Trie 指定字典树

返回值     :无

2) 输入的形式 :Trie结构指针

输入值的范围:无

3) 输出的形式 :无

4) 所能达到的功能:

因为现实中删除某个元素的情况较少见,而删除整棵树较为常见,

        因此提供删除整棵树的方法,用于释放整个字典树占的堆空间

 

·void TraverTrie(Trie root)

1) 遍历整个字典树

参数       :Trie 指定字典树

返回值     :无

2) 输入的形式 :Trie结构指针

输入值的范围:无

3) 输出的形式 :字符串类型

4) 所能达到的功能:

输出字典树中的每一个单词

 

·void introdution()

1) 遍历整个字典树

参数       :无

返回值     :无

2) 输入的形式 :无

输入值的范围:无

3) 输出的形式 :字符串类型

4) 所能达到的功能:

这是一个输出顶部介绍的函数,因为不想在这里打太多代码,

所以使用了宏定义,具体代码包含在head.h的头文件中。

 

·void memu()

1) 输出菜单的函数

参数       :无

返回值     :无

2) 输入的形式 :无

输入值的范围:无

3) 输出的形式 :字符串类型

4) 所能达到的功能:

使用了宏定义,包含在head.h

 

·void working()

1) 主要工作的函数

参数       :无

返回值     :无

2) 输入的形式 :int整型,char字符类型,string字符串类型

输入值的范围:int 2147483647~-2147483648char -128~127

3) 输出的形式 :字符串类型

4) 所能达到的功能:

主函数的运行主要在此,但在主函数里面调用此函数即可,

        用于完成增删查改

 

·int main()

1) 主函数

参数       :无

返回值     :int 

2) 输入的形式 :无

输入值的范围:无

3) 输出的形式 :字符串类型

4) 所能达到的功能:

程序的入口,分别调用其他函数以实现其功能

 

二、解题思路

Trie树的数据结构:

typedef struct TrieNode

{

    int count;                  //用来统计单词前缀出现的次数

    struct TrieNode* next[26];  //指向各个子树的指针

    bool exist;                 //标记该节点处是否构成单词

    char trans[11];             //当前节点对应的单词

 

}TrieNode ,*Trie;

 

1)初始化:

在内存申请一块Trie树的指针空间,把初始值设为0

2)插入元素:

对于一个单词,从根开始,沿着单词的各个字母所对应的树中的节点分支向下走, 直到单词遍历完,将最后的节点的exist标记为true,表示该单词已插入Trie树。

 while(字符串未结束)

    {

        if(当前位置为空)

        {

            位当前元素申请空间;

        }

        指针指向当前元素;

        为当前元素赋值

        字符串向下移

}

3)删除元素:

对于一个单词,判断是否存在字典树中,不存在即返回;

存在分是否为字符串的前缀,以递归的形式进行删除

1不是->遍历整个单词,释放内存空间

2->遍历,把count1,知道末尾把exist设为false

while(字符串未结束)

    {

        if(当前节点为空)

            return false

        else if(非前缀)

        {

            free(当前空间)

        }

        else

        {

           count--

           free(最后一个结点);

        }

        字符串指针向下移

}

 

4)查找元素:

从根开始按照单词的字母顺序向下遍历trie树,一旦发现某个节点标记不存在或者单词遍历完成而最后的节点的existfalse,则表示该单词不存在,若最后的节点标记为true,表示该单词存在。

while(字符串未结束)

    {

        f(当前节点为空)

            return 不存在;

        指向下一个节点

        字符串指针向下移

    }

    return (节点的->exist);

 

5)相关应用:

前缀查询的典型应用

很多单词(只有小写字母组成,不会有重复的单词出现),现在要统计出以某个字符串为前缀的单词数量

开始,初始化一棵字典树,然后不断输入单词,即想树插入元素。然后即可调用

int SearchTrieNode(Trie root , char* word) 函数获取前缀个数。

 

三、调试分析

(一)问题讨论与分析

在此过程中,遇到

1) codeblocks 编译器在编译的时候并不会认出单词拼错这种错误,不像java的编译器那么智能,所以有一个单词拼错了,然后我找了好久的错误才发现,记得之前也遇到过这种情况,也是找了好久才找出错误。解决:细心打代码,或者换一个编译器。

2) 在实现删除的时候,发现删除不了,有一个问题是字符串用错个,只因拷贝时没改,还有一个是free函数问题,指针的内存空间释放后并不会指向NULL,而是指向任一不确定位置,所以我再查找时还是发现找得到它。解决:使用free函数后,在把指针指向NULL

3) 遍历整棵字典树时,不知道要遍历每一个字符还是字符串,最后考虑到基本元素为字符串后,就通过递归方式遍历整棵树。

(二)测试(多组)

 *****************************************************************************

                                ●新建一棵字典树

                                ●插入新单词

                                ●查找单词

                                ●删除单词

                                ●删除整个字典树

                                ●查询前缀个数

                                ●查看所有单词

                                ●退出

 *****************************************************************************

 

测试样例一:

输入

1

2

aa

2

a

3

a

4

aa

7

6

aa

6

a

0

输出

Before Init...

Init Success!

插入成功!

插入成功!

字符串【a】存在字典树中

字符串【aa】删除成功

--------------a

前缀【aa】不存在字典树中

前缀【a】存在字典树中出现[1]

 

测试样例二:

输入

1

2

abc

2

aaa

2

guo

2

gui

2

nan

7

6

a

3

ggg

3

guo

4

abc

7

5

确定

Y

0

 

输出:

Before Init...

Init Success!

插入成功!

插入成功!

插入成功!

插入成功!

插入成功!

--------------aaa

--------------abc

--------------gui

--------------guo

--------------nan

前缀【a】存在字典树中出现[2]

字符串【ggg】不存在字典树中

字符串【guo】存在字典树中

--------------aaa

--------------gui

--------------guo

--------------nan

字典树删除成功

 

测试样例三:

输入

1

2

data

2

struct

2

trie

3

trie

3

tire

4

trie

6

da

7

0

 

输出:

Before Init...

Init Success!

插入成功!

插入成功!

插入成功!

字符串【trie】存在字典树中

字符串【tire】不存在字典树中

字符串【trie】删除成功

前缀【da】存在字典树中出现[1]

--------------data

--------------struct

(三)算法的效率分析和改进设想

 高级数据结构的学习与实现之 Trie树,字典树_第1张图片

Trie树的根结点不包含任何信息,第一个字符串为"abc",第一个字母为'a',因此根结点中数组next下标为'a'-97的值不为NULL,其他同理,构建的Trie树如图所示,红色结点表示在该处可以构成一个单词。

很显然,如果要查找单词"abc"是否存在,查找长度则为O(len)len为要查找的字符串的长度。而若采用一般的逐个匹配查找,则查找长度为O(len*n)n为字符串的个数。显然基于Trie树的查找效率要高很多。 

如上图中:Trie树中存在的就是abcabbddda四个单词。在实际的问题中可以将标记颜色的标志位改为数量count等其他符合题目要求的变量。 已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个串为另一个串的前缀子串。下面对比3 方法: 1、 最容易想到的:即从字符串集中从头往后搜,看每个字符串是否为字符串集中某个字符串的前缀,复杂度为O(n^2)。 

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

3、 3、 使用Trie:因为当查询如字符串abc是否为某个字符串的前缀时,显然以bcd....等不是以a开头的字符串就不用查找了,这样迅速缩小查找的范围和提高查找的针对性。所以建立Trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度只是O(len)

 

(四)经验和体会

Trie树的基本性质:

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

 

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

缺点:

Trie树的内存消耗非常大.当然,或许用左儿子右兄弟的方法建树的话,可能会好点.

 

四、附录

(一)头文件

#ifndef TEST_H_INCLUDED

#define TEST_H_INCLUDED

#include 

#include 

#include 

#include 

#include

#define henggang cout << " " ;for(int i =0;i<77;i++)cout << "-" ;

#define henggang1 for(int i =0;i<77;i++)cout << " " ;

#define xingxing cout << " " ;for(int i =0;i<77;i++)cout << "*" ;

#define kongge1 for(int i =0;i<6;i++)cout << " " ;

#define kongge2 for(int i =0;i<20;i++)cout << " " ;

#define kongge3 for(int i =0;i<30;i++)cout << " " ;

#define shuxian cout<< "|";

#define dingyi0 "Trie 字典树的应用"

#define dingyi1 "Trie树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。"

#define dingyi2 "典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经 "

#define dingyi3 "常被搜索引擎系统用于文本词频统计。                               "

#define dingyi4 "优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。     "

#define checkTrie if(root==NULL){printf("请创建一棵字典树\n");break;}

#define caidan cout<< "\n\t\t\t\t新建一棵字典树\n\t\t\t\t插入新单词\n\t\t\t\t查找单词\n\t\t\t\t删除单词\n\t\t\t\t删除整个字典树\n\t\t\t\t查询前缀个数\n\t\t\t\t查看所有单词\n\t\t\t\t退出\n";

#endif

 

(二)主程序

#include 

#include 

#include 

#include 

#include "head.h"

 

using namespace std;

 

typedef struct TrieNode

{

    int count;                  //用来统计单词前缀出现的次数

    struct TrieNode* next[26];  //指向各个子树的指针

    bool exist;                 //标记该节点处是否构成单词

    char trans[11];             //当前节点对应的单词

 

}TrieNode ,*Trie;

 

/*

    创建Tire

        用于创建Trie节点,初始化count0existfalse,子节点为NULL

    参数      :无

    返回值    :Trie 已初始化的TrieNode节点指针

*/

Trie CreateTrieNode()

{

    TrieNode* tnode = (TrieNode *)malloc(sizeof(TrieNode));

    tnode->count    = 0;

    tnode->exist    = false;

    memset(tnode->next,0,sizeof(tnode->next));

    return tnode;

}

 

/*

    插入单词元素

        用于向指定节点插入指定字符串节点

    参数      :Trie 指定节点

              :word 指定单词字符串

    返回值    :无

*/

void InsertTrieNode(Trie root , char *word)

{

    if(root==NULL)return ;

    if(word==NULL)return ;

    Trie node   = root;

    char* p     = word;

    int id;

    while(*p)

    {

        id = *p-'a';

        if(node->next[id]==NULL)

        {

            node->next[id] = CreateTrieNode();

        }

        node = node->next[id];

        node->count     += 1;

        p++;

    }

    node->exist = true;

    if(strcpy(node->trans , word)!=NULL)

    cout <<"插入成功!\n";

}

 

/*

    查找字符串

        用于在指定根节点中查找字符串是否存在

    参数      :Trie 指定节点

              :word 指定单词字符串

    返回值    :bool 是否存在指定字符串

*/

bool SearchString(Trie root , char* word)

{

    if(root==NULL)return false;

    if(word==NULL)return false;

    Trie node   = root;

    char* p     = word;

    int id;

    while(*p)

    {

        id = *p-'a';

        if(node->next[id]==NULL)

            return false;

        node = node->next[id];

        p++;

    }

 

    return (node->exist);

}

 

/*

    查找单词前缀出现的次数

        用于在指定根节点中查找单词前缀次数,类似SearchString

    参数      :Trie 指定节点

              :word 指定单词字符串

    返回值    :int 单词前缀出现的次数

*/

int SearchTrieNode(Trie root , char* word)

{

    if(root==NULL)return 0;

    if(word==NULL)return 0;

    char* p     = word;

    Trie node   = root;

    int id;

    while(*p)

    {

        id = *p-'a';

        if(node->next[id]==NULL)

            return 0;

        node = node->next[id];

        p++;

    }

    return node->count;

}

 

/*

    删除指定单词

        用于在指定根节点中删除指定单词

    参数      :Trie 指定节点

              :word 指定单词字符串

    返回值    :bool 删除成功与否

*/

bool DelectString(Trie root , char* word)

{

    if(!SearchString(root,word))

        return false;

    char* str   = word;

    int id      = *str-'a';

    int len     = strlen(str),i=0;

    Trie p      = root;

    Trie q      = p->next[id];

    while(*str)

    {

        if(q==NULL||q->count==0)

            return false;

        else if(q->count==1)

        {

            p   = q;

            q   = q->next[*(str+1)-'a'];

            if(!i==len)

            {

                free(p);

                p   = NULL;

            }

            else

            {

                p->exist    = false;

                free(p->trans);

            }

            p->count=0;

        }

        else

        {

            p   = q;

            q   = q->next[*(str+1)-'a'];

            if(i==len-1)

            {

                p->exist    = false;

                free(p->trans);

            }

            p->count--;

        }

        str++;

        i++;

    }

    return true;

}

 

/*

    删除整棵树

        因为现实中删除某个元素的情况较少见,而删除整棵树较为常见,

        因此提供删除整棵树的方法,用于释放整个字典树占的堆空间

    参数      :Trie 指定字典树

    返回值    :无

*/

void DelectTrie(Trie root)

{

    int i;

    for(i=0;i<26;i++)

    {

     if(root->next[i]!=NULL)

            DelectTrie(root->next[i]);

    }

    root->exist = false;

    free(root);

    root = NULL;

}

 

/*

    遍历整个字典树

        输出字典树中的每一个单词

    参数      :Trie 指定节点

    返回值    :无

*/

void TraverTrie(Trie root)

{

    int i;

    for(i=0;i<26;i++)

    {

     if(root->next[i]!=NULL)

            TraverTrie(root->next[i]);

    }

    if(root->exist)

        printf("--------------%s\n",root->trans);

}

 

/*

    输出顶部介绍

        这是一个输出顶部介绍的函数,因为不想在这里打太多代码,

        所以使用了宏定义,具体代码包含在head.h的头文件中。

    参数      :无

    返回值    :无

*/

void introdution()

{

    henggang;

    cout<< endl;

    shuxian kongge3 cout<< dingyi0;kongge3 shuxian

    cout<< endl;

    shuxian henggang1 shuxian

    cout<< endl;

    shuxian  kongge1 cout<< dingyi1; kongge1 shuxian

    cout<< endl;

    shuxian  kongge1 cout<< dingyi2; kongge1 shuxian

    cout<< endl;

    shuxian  kongge1 cout<< dingyi3; kongge1 shuxian

    cout<< endl;

    shuxian  kongge1 cout<< dingyi4; kongge1 shuxian

    cout<< endl;

    henggang

    cout<< endl;

}

 

/*

    输出菜单的函数

        使用了宏定义,包含在head.h

    参数      :无

    返回值    :无

*/

void memu()

{

    xingxing

    caidan

    xingxing

    cout<< endl;

}

 

/*

    主要工作的函数

        主函数的运行主要在此,但在主函数里面调用此函数即可,

        用于完成增删查改

    参数      :无

    返回值    :无

*/

void working()

{

    Trie root = NULL;     // 字典树的根节点

    char str1[300] , str2[30] , str3[30] , *p;

    int i , k;

    int chose;

    while(true)

    {

        memu();

        cout <<"请选择要操作的序号,回车结束      ";

        cin>> chose;

        if(chose==0)

            break;

        switch(chose)

        {

            case 1:

                    cout <<"Before Init...\n";

                    Sleep(1000);

                    root = CreateTrieNode();

                    if(root==NULL)

                    {

                       cout <<"\t字典树创建失败\n";

                       exit(0);

                    }

                    cout <<"Init Success!\n";

                    break;

            case 2:

                    checkTrie

                    cout <<"请输入需要插入的单词,回车键结束        ";

                    getchar();

                    gets(str1);

                    if(str1!=NULL||str1!=" ")

                    {

                       InsertTrieNode(root,str1);

                    }

                    break;

            case 3:

                    checkTrie

                    cout <<"请输入需要查询的单词,回车键结束        ";

                    getchar();

                    gets(str2);

                    if(str2!=NULL||str2!=" ")

                    {

                        if(SearchString(root,str2))

                        {

                            printf("字符串【%s】存在字典树中\n",str2);

                        }

                        else

                        {

                            printf("字符串【%s】不存在字典树中\n",str2);

                        }

                    }

                    else

                    {

                        cout <<"请按格式输入!\n";

                    }

                    break;

 

            case 4:

                    checkTrie

                    cout <<"请输入需要删除的单词,回车键结束        ";

                    getchar();

                    gets(str3);

                    if(str3!=NULL||str3!=" ")

                    {

                        if(DelectString(root,str3))

                        {

                            printf("字符串【%s】删除成功\n",str3);

                        }

                        else

                        {

                            printf("删除失败,字符串【%s】不存在字典树中\n",str3);

                        }

                    }

                    else

                    {

                            cout <<"请按格式输入!\n";

                    }

                    break;

 

            case 5:

                    checkTrie

                    MessageBox(NULL, "此操作将无法撤销\n(cannot be undone!)", "警告", MB_OK);//弹窗警告

                    cout <<"输入Y确定,其他返回。     ";

                    char config;

                    getchar();

                    config = getchar();

                    getchar();

                    if(config=='Y')

                    {

                        DelectTrie(root);

                        if(root->next[1]==NULL)

                        {

                            cout <<"字典树删除成功\n";

                        }

                        else

                        {

                            cout <<"字典树删除失败\n";

                        }

                    }

                    break;

            case 6:

                    checkTrie

                    cout <<"请输入需要的前缀,回车键结束        ";

                    getchar();

                    gets(str2);

                    if(str2!=NULL&&str2!=" ")

                    {

                        int count = SearchTrieNode(root,str2);

                        if(count>0)

                        {

                            printf("前缀【%s】存在字典树中出现[%d]\n",str2,count);

                        }

                        else

                        {

                            printf("前缀【%s】不存在字典树中\n",str2);

                        }

                    }

                    else

                    {

                            cout <<"请按格式输入!\n";

                    }

                    break;

 

            case 7:

                    checkTrie

                    TraverTrie(root);

                    break;

            default:

                    printf("请按格式输入!\n");

                    break;

        }

    }

}

 

/*

    主函数

        程序的入口,分别调用其他函数以实现其功能

    参数      :无

    返回值    :int 

*/

int main()

{

    introdution();//输出顶部介绍

    working();      //主要运行函数

    cout<<"谢谢使用!\n";

    return 0;

}



=================================我是分割线=======================================

因为源码中有宏定义,并且包含于头文件中,这个懂C语言的基本知道如何使用,不行的话自行百度一下。

最后,如果对上面的某处有疑问,可以留言,一起讨论学习,我会很乐意的。感谢博主hackbuteer1的一篇文章,此课程设计主要参考他写的教程。

http://blog.csdn.net/hackbuteer1/article/details/7964147

还有
http://blog.csdn.net/hguisu/article/details/8131559

----------------------------------------------------------------------------------------------转载无需注明出处-by guin_guo


你可能感兴趣的:(C/C++)