zoukankan      html  css  js  c++  java
  • 字典树(Trie tree)

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

    性质

    它有3个基本性质:

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

    [编辑]图示

    这是一个Trie结构的例子: Trie example.svg

    在这个Trie结构中,保存了A、to、tea、ted、ten、i、in、inn这8个字符串,仅占用8个字节(不包括指针占用的空间)。

    实例

    这是一个用于词频统计的程序范例,因使用了getline(3),所以需要glibc才能链接成功,没有glibc的话可以自行改写。代码由User:JohnBull捐献,遵从GPL版权声明。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
     
    #define TREE_WIDTH 256
     
    #define WORDLENMAX 128
     
    struct trie_node_st {
            int count;
            struct trie_node_st *next[TREE_WIDTH];
    };
     
    static struct trie_node_st root={0, {NULL}};
     
    static char *spaces=" \t\n/.\"\'()";
     
    static int
    insert(const char *word)
    {
            int i;
            struct trie_node_st *curr, *newnode;
     
            if (word[0]=='\0') {
                    return 0;
            }
            curr = &root;
            for (i=0; ; ++i) {
                    if (curr->next[ word[i] ] == NULL) {
                            newnode=(struct trie_node_st*)malloc(sizeof(struct trie_node_st));
                            memset(newnode, 0, sizeof(struct trie_node_st));
                            curr->next[ word[i] ] = newnode;
                    }
                    if (word[i] == '\0') {
                            break;
                    }
                    curr = curr->next[ word[i] ];
            }
            curr->count ++;
     
            return 0;
    }
     
    static void
    printword(const char *str, int n)
    {
            printf("%s\t%d\n", str, n);
    }
     
    static int
    do_travel(struct trie_node_st *rootp)
    {
            static char worddump[WORDLENMAX+1];
            static int pos=0;
            int i;
     
            if (rootp == NULL) {
                    return 0;
            }
            if (rootp->count) {
                    worddump[pos]='\0';
                    printword(worddump, rootp->count);
            }
            for (i=0;i<TREE_WIDTH;++i) {
                    worddump[pos++]=i;
                    do_travel(rootp->next[i]);
                    pos--;
            }
            return 0;
    }
     
    int
    main(void)
    {
            char *linebuf=NULL, *line, *word;
            size_t bufsize=0;
            int ret;
     
            while (1) {
                    ret=getline(&linebuf, &bufsize, stdin);
                    if (ret==-1) {
                            break;
                    }
                    line=linebuf;
                    while (1) {
                            word = strsep(&line, spaces);
                            if (word==NULL) {
                                    break;
                            }
                            if (word[0]=='\0') {
                                    continue;
                            }
                            insert(word);
                    }
            }
     
    /* free(linebuf); */
     
            do_travel(&root);
     
            exit(0);
    }

    在给一个例子:

    #define MAX_NUM 26
    enum NODE_TYPE{ //"COMPLETED" means a string is generated so far.
      COMPLETED,
      UNCOMPLETED
    };
    struct Node {
      enum NODE_TYPE type;
      char ch;
      struct Node* child[MAX_NUM]; //26-tree->a, b ,c, .....z
    };
     
    struct Node* ROOT; //tree root
     
    struct Node* createNewNode(char ch){
      // create a new node
      struct Node *new_node = (struct Node*)malloc(sizeof(struct Node));
      new_node->ch = ch;
      new_node->type == UNCOMPLETED;
      int i;
      for(i = 0; i < MAX_NUM; i++)
        new_node->child[i] = NULL;
      return new_node;
    }
     
    void initialization() {
    //intiazation: creat an empty tree, with only a ROOT
    ROOT = createNewNode(' ');
    }
     
    int charToindex(char ch) { //a "char" maps to an index<br>
    return ch - 'a';
    }
     
    int find(const char chars[], int len) {
      struct Node* ptr = ROOT;
      int i = 0;
      while(i < len) {
       if(ptr->child[charToindex(chars[i])] == NULL) {
       break;
      }
      ptr = ptr->child[charToindex(chars[i])];
      i++;
      }
      return (i == len) && (ptr->type == COMPLETED);
    }
     
    void insert(const char chars[], int len) {
      struct Node* ptr = ROOT;
      int i;
      for(i = 0; i < len; i++) {
       if(ptr->child[charToindex(chars[i])] == NULL) {
        ptr->child[charToindex(chars[i])] = createNewNode(chars[i]);
      }
      ptr = ptr->child[charToindex(chars[i])];
    }
      ptr->type = COMPLETED;
    }

    Trie树的基本实现

    字母树的插入(Insert)、删除( Delete)和查找(Find)都非常简单,用一个一重循环即可,即第i 次循环找到前i 个字母所对应的子树,然后进行相应的操作。实现这棵字母树,我们用最常见的数组保存(静态开辟内存)即可,当然也可以开动态的指针类型(动态开辟内存)。至于结点对儿子的指向,一般有三种方法:

    1、对每个结点开一个字母集大小的数组,对应的下标是儿子所表示的字母,内容则是这个儿子对应在大数组上的位置,即标号;

    2、对每个结点挂一个链表,按一定顺序记录每个儿子是谁;

    3、使用左儿子右兄弟表示法记录这棵树。

    三种方法,各有特点。第一种易实现,但实际的空间要求较大;第二种,较易实现,空间要求相对较小,但比较费时;第三种,空间要求最小,但相对费时且不易写。

    3、 Trie树的高级实现

    可以采用双数组(Double-Array)实现。利用双数组可以大大减小内存使用量,具体实现细节见参考资料(5)(6)。

    4、 Trie树的应用

    Trie是一种非常简单高效的数据结构,但有大量的应用实例。

    (1) 字符串检索

    事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。

    举例:

    @  给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。

    @  给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。

    (2)字符串最长公共前缀

    Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。

    举例:

    @ 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少?

    解决方案:首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。

    而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:

    1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;

    2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;

    (关于并查集,Tarjan算法,RMQ问题,网上有很多资料。)

    (3)排序

    Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。

    举例:

    @ 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。

    (4) 作为其他数据结构和算法的辅助结构

    如后缀树,AC自动机等

    5、 Trie树复杂度分析

    (1) 插入、查找的时间复杂度均为O(N),其中N为字符串长度。

    (2) 空间复杂度是26^n级别的,非常庞大(可采用双数组实现改善)。

    6、 总结

    Trie树是一种非常重要的数据结构,它在信息检索,字符串匹配等领域有广泛的应用,同时,它也是很多算法和复杂数据结构的基础,如后缀树,AC自动机等,因此,掌握Trie树这种数据结构,对于一名IT人员,显得非常基础且必要!

    7、 参考资料

    (1)wiki:http://en.wikipedia.org/wiki/Trie

    (2) 博文《字典树的简介及实现》:

    http://hi.baidu.com/luyade1987/blog/item/2667811631106657f2de320a.html

    (3) 论文《浅析字母树在信息学竞赛中的应用》

    (4)  论文《Trie图的构建、活用与改进》

    (5)  博文《An Implementation of Double-Array Trie》:

    http://linux.thai.net/~thep/datrie/datrie.html

    (6) 论文《An Efficient Implementation of Trie Structures》:

    http://www.google.com.hk/url?sa=t&source=web&cd=4&ved=0CDEQFjAD&url=http%3A%2F%2Fciteseerx.ist.psu.edu%2Fviewdoc%2Fdownload%3Fdoi%3D10.1.1.14.8665%26rep%3Drep1%26type%3Dpdf&ei=qaehTZiyJ4u3cYuR_O4B&usg=AFQjCNF5icQbRO8_WKRd5lMh-eWFIty_fQ&sig2=xfqSGYHBKqOLXjdONIQNVw

    7)刚刚分享了:"算法合集之《浅析字母树在信息学竞赛中的应用》.pdf" 下载链接:http://t.cn/zOlasAH http://t.cn/zOlasAQ

  • 相关阅读:
    codevs 1115 开心的金明
    POJ 1125 Stockbroker Grapevine
    POJ 2421 constructing roads
    codevs 1390 回文平方数 USACO
    codevs 1131 统计单词数 2011年NOIP全国联赛普及组
    codevs 1313 质因数分解
    洛谷 绕钉子的长绳子
    洛谷 P1276 校门外的树(增强版)
    codevs 2627 村村通
    codevs 1191 数轴染色
  • 原文地址:https://www.cnblogs.com/rollenholt/p/2468932.html
Copyright © 2011-2022 走看看