Trie树(单词查找树)

前言:Tire树,又称之为字典树或者单词查找树。是一种树形结构,是哈希树的变种。典型应用是用于统计、排序或保存大量的字符串(不仅限于字符串),所以经常被搜索引擎系统用于文本词频的统计。因为相同的字符串前缀会共享同一条分支,所以优点是可以利用不同字符串的相同前缀来减少无谓的字符串比较,查找效率比hash表/hash树高。

有三个基本的性质:

1.除了根节点以外,每个节点都包含一个字符;
2.从根节点到当前节点路径上的字符连接起来组成该节点对应的字符串;
3.每个节点的所有子节点所包含的字符是不相同的。

Tire树的查找和插入操作是同时进行的,如果当前节点的颜色为红色表示当前字符串是存在的,否则,建立一个节点,颜色为红色即可插入一个单词。当然颜色只是一个标记位,用于标记是否当前路径是否构成一个单词。如果我们有and,as,at,cn,com这些关键词,那么trie树(字典树)是这样的:


说了这么多,是时候开始给出数据结构和算法描述了:

#define MaxNum 26
typedef struct TrieNode
{
    bool IsWord;//标记是否构成单词
    struct TrieNode *Child[MaxNum];//当前节点的所有子节点
    TrieNode(bool WordFlag) : IsWord(WordFlag) 
    {
        for (int i=0;i

数据结构:用一个bool型变量IsWord标记当前节点是否构成单词,用一个指针数组表示当前节点的所有可能的子节点。有了数据结构以后,插入和查找操作是比较容易的。

bool FindTrieNode(TrieRoot pRoot,char *pWord)
{
    TrieNode *pCur=pRoot;
    while (*pWord)
    {
        int Id=*pWord-'a';//求id
        if (pCur->Child[Id]==NULL)
            return false;
        pCur=pCur->Child[Id];//向下查找,更新pCur
        pWord++;    
    }
    if (pCur->IsWord==true)
        return true;
    return false;
}

查找算法的时间复杂度为O(strlen(pWord)),即与单词的长度成正比;

void InsertTrieNode(TrieRoot pRoot,char *pWord)
{
    TrieNode *pCur=pRoot;
    while (*pWord)
    {
        int Id=*pWord-'a';//求id
        if (pCur->Child[Id]==NULL)
        {
            pCur->Child[Id]=new TrieNode(false);//新建一个节点
        }
        pCur=pCur->Child[Id];
        pWord++;    
    }
    pCur->IsWord=true;
}

插入操作与查找操作类似,只是在查找不成功的时候要创建新的节点,最后更新pCur的IsWord标识。时间复杂度同样是O(strlen(pWord));

注意一个问题,Trie树的最大高度是所有单词的最大长度。所以查找操作的最大时间复杂度为Trie树的高度。创建Trie树的过程和插入是同时的,这一点和随机创建二叉搜索树是一样的,就是进行不断地插入操作。

void CreateTrie(TrieRoot pRoot,vector DataSet)
{
    for (int i=0;i<(int)DataSet.size();i++)
    {
        const char *pWord=DataSet[i].c_str();
        InsertTrieNode(pRoot,pWord);
    }
}

Tire树的常见应用集锦:

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

  1. 最长公共前缀问题:给定N个字符串,求两个串的最长公共前缀;两个字符串的最长公共前缀的长度即由根节点到最近公共祖先节点路径上字符的个数。

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

    首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线 (Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。
    而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:
    (1)利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;
    (2)求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;

  2. 给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。思路:将N个单词的熟词表建立一棵Tire树,对于文章中的每个单词,查找其在Tire树中是否存在。

  3. 实现映射,比如同一个东西有两种表示方法A和B,现在给你了A,让你转换成B。最容易想到的是hash,用Trie树实现也是比较简单的,可以在Trie数据结构中新增一项string Data_B;这样如果IsWord=true,访问Data_B即可获取方法B的表示。

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

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

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


你可能感兴趣的:(数据结构和算法)