Trie树的详解及应用

Trie树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

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

Trie树特性:

1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。

2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。

3)每个节点的所有子节点包含的字符都不相同。

4)如果字符的种数为n,则每个结点的出度为n,这也是空间换时间的体现,浪费了很多的空间。

5)插入查找的复杂度为O(n),n为字符串长度。

基本思想(以字母树为例):

1、插入过程

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

2、查询过程

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

字典树的数据结构:

利用串构建一个字典树,这个字典树保存了串的公共前缀信息,因此可以降低查询操作的复杂度。

下面以英文单词构建的字典树为例,这棵Trie树中每个结点包括26个孩子结点,因为总共有26个英文字母(假设单词都是小写字母组成)。

则可声明包含Trie树的结点信息的结构体:

[cpp] view
plain
copy

  1. typedef struct Trie_node
  2. {
  3. int count;                    // 统计单词前缀出现的次数
  4. struct Trie_node* next[26];   // 指向各个子树的指针
  5. bool exist;                   // 标记该结点处是否构成单词
  6. }TrieNode , *Trie;

其中next是一个指针数组,存放着指向各个孩子结点的指针。

如给出字符串"abc","ab","bd","dda",根据该字符串序列构建一棵Trie树。则构建的树如下:

Trie树的根结点不包含任何信息,第一个字符串为"abc",第一个字母为‘a‘,因此根结点中数组next下标为‘a‘-97的值不为NULL,其他同理,构建的Trie树如图所示,红色结点表示在该处可以构成一个单词。很显然,如果要查找单词"abc"是否存在,查找长度则为O(len),len为要查找的字符串的长度。而若采用一般的逐个匹配查找,则查找长度为O(len*n),n为字符串的个数。显然基于Trie树的查找效率要高很多。

如上图中:Trie树中存在的就是abc、ab、bd、dda四个单词。在实际的问题中可以将标记颜色的标志位改为数量count等其他符合题目要求的变量。

已知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)。

Trie树的操作

在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)中的操作,直到遇到结束符‘\0‘,此时将当前结点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)中的操作直到遇到结束符‘\0‘,若当前结点p不为空并且 exist 为true,则返回true,否则返回false。

3、删除

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

    #include<iostream>
    #include<cstring>
    using namespace std;  

    typedef struct Trie_node
    {
        int count;                    // 统计单词前缀出现的次数
        struct Trie_node* next[26];   // 指向各个子树的指针
        bool exist;                   // 标记该结点处是否构成单词
    }TrieNode , *Trie;  

    TrieNode* createTrieNode()
    {
        TrieNode* node = (TrieNode *)malloc(sizeof(TrieNode));
        node->count = 0;
        node->exist = false;
        memset(node->next , 0 , sizeof(node->next));    // 初始化为空指针
        return node;
    }  

    void Trie_insert(Trie root, char* word)
    {
        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];  // 每插入一步,相当于有一个新串经过,指针向下移动
            ++p;
            node->count += 1;      // 这行代码用于统计每个单词前缀出现的次数(也包括统计每个单词出现的次数)
        }
        node->exist = true;        // 单词结束的地方标记此处可以构成一个单词
    }  

    int Trie_search(Trie root, char* word)
    {
        Trie node = root;
        char *p = word;
        int id;
        while( *p )
        {
            id = *p - 'a';
            node = node->next[id];
            ++p;
            if(node == NULL)
                return 0;
        }
        return node->count;
    }  

    int main(void)
    {
        Trie root = createTrieNode();     // 初始化字典树的根节点
        char str[12] ;
        bool flag = false;
        while(gets(str))
        {
            if(flag)
                printf("%d\n",Trie_search(root , str));
            else
            {
                if(strlen(str) != 0)
                {
                    Trie_insert(root , str);
                }
                else
                    flag = true;
            }
        }  

        return 0;
    }  

Trie树的应用:

1. 字符串检索,词频统计,搜索引擎的热门查询

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

举例:

1)有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

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

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

4)1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串

       5)寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。

2. 字符串最长公共前缀

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

1) 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少.  解决方案:

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

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

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

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

3.  排序

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

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

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

如后缀树,AC自动机等。

3、 Trie树的高级实现

可以采用双数组(Double-Array)实现。利用双数组可以大大减小内存使用量

(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

时间: 2024-11-15 14:11:38

Trie树的详解及应用的相关文章

JQuery Easy Ui (Tree树)详解(转)

JQuery Easy Ui (Tree树)详解(转) 第一讲:JQuery Easy Ui到底是什么呢? 首先咱们知道JQuery是对Java Script的封装,是一个js库,主要提供的功能是选择器,属性修改和事件绑定等等.. JQuery ui是在jQuery的基础上,利用jQuery的扩展性,设计的插件. 那么JQuery Easy Ui到底是什么呢? 我的理解就是比JQuery ui更强大,同样都是实现绚丽的多功能效果! jQuery Easy UI的目的就是帮助Web开发者更轻松的打

线段树数据结构详解

线段树数据结构详解 Coded by Jelly_Goat. All rights reserved. 这一部分是线段树. 线段树,顾名思义,是一种树形数据结构,适用于各种求区间统一算法的动静两平衡的数据结构. 这里什么是统一算法?(自己口胡的统一算法) 比如求最大值or最小值.区间求和,一样的区间都是一样的算法,这也是和动态dp不同的地方. 前置知识1:二叉搜索树 二叉搜索树就是根节点比左儿子大,比右儿子小的一种二叉树. 前置知识2:向量存储 向量存储是用来存完全二叉树儿子和父亲关系的. 如果

Linux设备树语法详解

Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离.在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写.引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码.比如在ARM Linux内,一个.dts(device tree source)文件对应一个ARM的machine,一般放置在内核的

JQuery Easy Ui (Tree树)详解

在这里沉重悼念我辛辛苦苦写了4个小时的文章,因为我的失误,没保存,而夭折啦..... 我的痛苦谁能懂? 哎~~~~  吃一见长一智吧! 么的办法啊! 但是,但是!我狠了狠心,咬了咬牙!我决定,再写一遍!!! 谁知道哭的表情怎么写! 我已经无法用言语表达我此时此刻的沉重心情了! (以下内容,身为菜鸟的我是用颤抖的双手敲出来的,忘同志们珍惜!!!!) 记得4个小时之前,我写过一个随笔,它的内容是这样的: 每次写博客,都喜欢在前面和大家分享一些东西,今天要分享的是一个小故事: 一个世界著名的大酒店正在

线段树 入门详解

概念(copy度娘): 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN).而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩. 通俗地讲: 线段树就是把一个线段转变为一个二叉树,如下所示: 一个线段长度为4,则把它变成线段树,就是这个样子 这样如果你想改变一个区间的值,只用O(logn).比一般算法快了许多. 但是空间复杂度就会高一些,比如本来

Linux设备树语法详解【转】

本文转载自:http://www.cnblogs.com/xiaojiang1025/p/6131381.html 概念 Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离.在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写.引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码.比如在AR

B树操作详解

B树是一颗多路的平衡搜索树,其规定树根至少有两个孩子,每个内部节点有两个或以上的孩子.用来衡量B树规模的一个指标是"最小度数"t,其表示B树所有内部节点的孩子数为t~2t个. B树的一个结点有两个存储域,分别是关键字和孩子结点,关键字用于划分孩子节点,一个关键字的左右两侧各有一个孩子.类似于二叉搜索树,一个关键字x的左孩子的关键字值都比x小,右孩子的关键字值都比x大.因此,一个结点的关键字的个数是t-1~2t-1个. 下图是一个简单的B树,该树的最小度数t是2. B树有以下性质: 性质

easyUI同步树的详解

简介树控件 树控件,很常用,比如它可以做有层级的菜单.比如公司的部分划分.省市区的选择... 最大的好处就是有层级关系,看起来和选择起来都比较清晰,就像一串杂乱无章的json你用bejson去格式化一样的感觉. 所以,掌握树控件,是很有必要的,为什么?因为它很有用,用的恰当,也可以适当提升用户体验, 树的两种加载方式 tree的加载方式分为常规加载和异步加载两种,其中常规加载也就是一次性把整棵树都加载出来,而异步加载呢就是当展开父节点的时候才会去加载子节点. 一般肉眼上可以观察到异步加载的树在点

主席树入门详解+题目推荐

主席树学名可持久化线段树,就是这个可持久化,衍生了多少数据结构 为什么会有主席树这个数据结构呢?它被发明是用来解决什么问题的呢? 给定n个数,m个操作,操作类型有在某个历史版本下单点修改,输出某个历史版本下某个位置的值的值,n和m小于等于1e6 乍一看是不是一点头绪也没有.我们先来想想暴力怎么做,暴力存储第i个状态下每个数的值,显然这样做不是TLE就是MLE,我们不妨管这种状态叫做TM双LE. 如果没有这个历史状态显然处理很简单,一个线段树就解决了.那么加上历史状态呢?如果我们优化一下暴力,我们