字典树(trie树) 后缀树 广义后缀树

转自:http://www.cnblogs.com/dong008259/archive/2011/11/11/2244900.html
(1)字典树(Trie树)

  Trie是个简单但实用的数据结构,通常用于实现字典查询。我们做即时响应用户输入的AJAX搜索框时,就是Trie开始。本质上,Trie是一颗存储多个字符串的树。相邻节点间的边代表一个字符,这样树的每条分支代表一则子串,而树的叶节点则代表完整的字符串。和普通树不同的地方是,相同的字符串前缀共享同一条分支。还是例子最清楚。给出一组单词,inn, int, at, age, adv, ant, 我们可以得到下面的Trie:
这里写图片描述
可以看出:
•每条边对应一个字母。
•每个节点对应一项前缀。叶节点对应最长前缀,即单词本身。
•单词inn与单词int有共同的前缀“in”, 因此他们共享左边的一条分支,root->i->in。同理,ate, age, adv, 和ant共享前缀”a”,所以他们共享从根节点到节点”a”的边。
•查询非常简单。比如要查找int,顺着路径i -> in -> int就找到了。
•搭建Trie的基本算法也很简单,无非是逐一把每则单词的每个字母插入Trie。插入前先看前缀是否存在。如果存在,就共享,否则创建对应的节点和边。比如要插入单词add,就有下面几步:•考察前缀”a”,发现边a已经存在。于是顺着边a走到节点a。
•考察剩下的字符串”dd”的前缀”d”,发现从节点a出发,已经有边d存在。于是顺着边d走到节点ad
•考察最后一个字符”d”,这下从节点ad出发没有边d了,于是创建节点ad的子节点add,并把边ad->add标记为d。

//copyright: www.acmerblog.com

#include <stdio.h>
#include <iostream>
using namespace std;
#define  MAX    26

typedef struct TrieNode
{
    int nCount;  // 该节点前缀 出现的次数
    struct TrieNode *next[MAX]; //该节点的后续节点
} TrieNode;

TrieNode Memory[1000000]; //先分配好内存。 malloc 较为费时
int allocp = 0;

//初始化一个节点。nCount计数为1, next都为null
TrieNode * createTrieNode()
{
    TrieNode * tmp = &Memory[allocp++];
    tmp->nCount = 1;
    for (int i = 0; i < MAX; i++)
        tmp->next[i] = NULL;
    return tmp;
}

void insertTrie(TrieNode * * pRoot, char * str)
{
    TrieNode * tmp = *pRoot;
    int i = 0, k;
    //一个一个的插入字符
    while (str[i])
    {
        k = str[i] - 'a'; //当前字符 应该插入的位置
        if (tmp->next[k])
        {
            tmp->next[k]->nCount++;
        }
        else
        {
            tmp->next[k] = createTrieNode();
        }

        tmp = tmp->next[k];
        i++; //移到下一个字符
    }

}

int searchTrie(TrieNode * root, char * str)
{
    if (root == NULL)
        return 0;
    TrieNode * tmp = root;
    int i = 0, k;
    while (str[i])
    {
        k = str[i] - 'a';
        if (tmp->next[k])
        {
            tmp = tmp->next[k];
        }
        else
            return 0;
        i++;
    }
    return tmp->nCount; //返回最后的那个字符  所在节点的 nCount
}

int main(void)
{
    char s[11];
    TrieNode *Root = createTrieNode();
    while (gets(s) && s[0] != '0') //读入0 结束
    {
        insertTrie(&Root, s);
    }

    while (gets(s)) //查询输入的字符串
    {
        printf("%d
", searchTrie(Root, s));
    }

    return 0;
}

(2)后缀树

  所谓后缀树,就是包含一则字符串所有后缀的压缩了的字典树。先说说后缀的定义。给定一长度为n的字符串S=S1S2..Si..Sn,和整数i,1 <= i <= n,子串SiSi+1…Sn都是字符串S的后缀。以字符串S=XMADAMYX为例,它的长度为8,所以S[1..8], S[2..8], … , S[8..8]都算S的后缀,我们一般还把空字串也算成后缀。这样,我们一共有如下后缀。对于后缀S[i..n],我们说这项后缀起始于i。
1.S[1..8], XMADAMYX, 也就是字符串本身,起始位置为1
2.S[2..8], MADAMYX,起始位置为2
3.S[3..8], ADAMYX,起始位置为3
4.S[4..8], DAMYX,起始位置为4
5.S[5..8], AMYX,起始位置为5
6.S[6..8], MYX,起始位置为6
7.S[7..8], YX,起始位置为7
8.S[8..8], X,起始位置为8
9.空字串。记为$。

所有这些后缀字符串组成一棵字典树:
这里写图片描述
仔细观察上图,我们可以看到不少值得压缩的地方。比如蓝框标注的分支都是独苗,没有必要用单独的节点同边表示。如果我们允许任意一条边里包含多个字母,就可以把这种没有分叉的路径压缩到一条边。另外每条边已经包含了足够的后缀信息,我们就不用再给节点标注字符串信息了。我们只需要在叶节点上标注上每项后缀的起始位置。于是我们得到下图:
这里写图片描述
这样的结构丢失了某些后缀。比如后缀X在上图中消失了,因为它正好是字符串XMADAMYX的前缀。为了避免这种情况,我们也规定每项后缀不能是其它后缀的前缀。要解决这个问题其实挺简单,在待处理的子串后加一个空字串就行了。例如我们处理XMADAMYX前,先把XMADAMYX变为 XMADAMYX$,于是就得到suffix tree。

这里写图片描述
这就形成一棵后缀树了。关于如何建立一棵后缀树,已有很成熟的算法,能在o(n)时间内解决。

(3)广义后缀树

  传统的后缀树只能处理一个单词的所有后缀。广义后缀树存储任意多个单词的所有后缀。例如字符串“abab”和“baba”,首先将它们使用特殊结束符链接起来,如表示成“ababbaba#”,然后求连接后的新字符的后缀树,遍历所得后缀树,如遇到特殊字符,如“”,”#”等则去掉以该节点为跟的子树,最后所得后缀树即为原字符串组的广义后缀树。其实质是将两个字符串的所有后缀,即:abab,bab,ab,b,baba#,aba#,ba#,a#,组成字典树,再进行压缩处理。广义后缀树的一个常应用就是判断两个字符串的相识度。

原文地址:https://www.cnblogs.com/luckycode/p/5255646.html