后缀树(suffix tree & array)

定义:后缀数组(suffix array)是将字符串的所有后缀进行排序放入数组中。后缀树(suffix tree)则是所有后缀形成的字典树(trie)的一种压缩表示。后缀数组相对后缀树来说,使用的存储空间更小(只用保存原始字符串和一个长度相同的整数数组)。
后缀树在字符串的很多算法(例如查找,匹配,最长公共子串等)中有广泛应用,是一种非常实用的数据结构。



:对于单词banana,有如下后缀数组表示:

index suffix 排序后index 排序后suffix
0 banana 5 a
1 anana 3 ana
2 nana 1 anana
3 ana 0 banana
4 na 4 na
5 a 2 nana

后缀数组的表示为: [5,3,1,0,4,2], 其中数字表示后缀首字母再整个字符串中的位置(即从0开始的下标)。

使用最“笨”的方法可以在O(n^2\log n)时间复杂度内构建后缀数组,其中n是字符串长度。思路是使用O(n\log n)的排序算法对后缀字符串排序,同时保持后缀起始字符下标。因为两个字符串比较大小需要O(n)复杂度,所以整体复杂度为O(n^2\log n)

本文介绍一种O(n\log_2^n)的后缀数组构建算法。简单期间,首先考虑O(n \log n \cdot \log n)复杂度的算法。出发点是充分利用所有后缀字符串都是来源于一个字符串的特点,同时借鉴radix排序思路。算法由k=\lceil log_2^n \rceil轮排序完成,第1轮排序只对第1个字符排序,第2轮排序对前2个字符排序,第3轮排序对前4个字符排序,第k轮排序对前2^k个字符排序。注意这里有一点很重要,如果所有后缀字符串已经按照前2^i字符排序,则可以使用O(n \log n)时间按照前2^{i+1}排序,原因是两个suffix可以在常数O(1)(而不是O(n))时间内比较大小。后缀数组

:构建字符串banana的后缀数组
为每个suffix分配一个序号rank,例如,对第i个字符串分配rank为str[i]-'a'。这样,得到下面rank表:

index suffix rank
0 banana 1
1 anana 0
2 nana 13
3 ana 0
4 na 13
5 a 0

根据前2个字符排序
对于每个字符,同时保存和它相邻的下一个字符的序号(记为next rank)。特殊情况,对于最后一个字符,它的next rank标记为-1。得到下面表:

Index Suffix Rank Next Rank
0 banana 1 0
1 anana 0 13
2 nana 13 0
3 ana 0 13
4 na 13 0
5 a 0 -1

先根据rank,再根据next rank对所有后缀字符串排序,结果如下表:

Index Suffix Rank Next Rank
5 a 0 -1
1 anana 0 13
3 ana 0 13
0 banana 1 0
2 nana 13 0
4 na 13 0

根据前4个字符排序
对前面排序结果,给所有后缀字符串逐个分配新rank。第1个suffix的rank为0;从第2个suffix开始,根据其rank和next rank 组合是否和上一个组合相同分配rank,如果不同,rank加1,如果相同,rank不变。如下面表所示:

Index Suffix Rank
5 a 0 初始值
1 anana 1 (0, 13) 和前面不同
3 ana 1 (0, 13) 和前面相同
0 banana 2 (1, 0) 和前面不同
2 nana 3 (13, 0)和前面不同
4 na 3 (13, 0) 和前面相同

对每个suffix (str[i]),同时存储str[i+2]的rank为next rank。如果i+2>=n,则next rank为-1,得到下表:

Index Suffix Rank Next Rank
5 a 0 -1
1 anana 1 1
3 ana 1 0
0 banana 2 3
2 nana 3 3
4 na 3 -1

按照上面表中Rank和Next Rank对所有suffix排序,结果如下表:

Index Suffix Rank Next Rank
5 a 0 -1
3 ana 1 0
1 anana 1 1
0 banana 2 3
4 na 3 -1
2 nana 3 3

至此,排序结束,后缀数组生成。

#include <iostream>
#include <cstring>
#include <algorithm>  // sort 函数
using namespace std;

struct suffix
{
    int index;        // 后缀串原始index(起始下标)
    int rank[2];      // rank 和next rank,用于排序
};

// 比较两个后缀字符串
int cmp(struct suffix a, struct suffix b)
{
    return (a.rank[0] == b.rank[0])?
    (a.rank[1] < b.rank[1] ?1: 0):
    (a.rank[0] < b.rank[0] ?1: 0);
}

// 构建长度为n的输入字符串txt的后缀数组
int *buildSuffixArray(char *txt, int n)
{
    // 使用struct方便按照rank排序,同时保留index
    struct suffix suffixes[n];
    // 初始化index和rank
    for (int i = 0; i < n; i++)
    {
        suffixes[i].index = i;
        suffixes[i].rank[0] = txt[i] - 'a';
        suffixes[i].rank[1] = ((i+1) < n)? (txt[i + 1] - 'a') : -1;
    }
    // 根据前2个字符排序
    sort(suffixes, suffixes+n, cmp);
    
    int ind[n]; // 保存suffix的在数组suffixes中的index 从原始index得到它在suffixes中的位置
                // 用于获取next rank.
    
    // 根据前k (4,8, 16, 32, ...) 个字符排序
    for (int k = 4; k < 2*n; k = k*2)
    {
        // 第一个suffix rank为0
        int rank = 0;
        int prev_rank = suffixes[0].rank[0];
        suffixes[0].rank[0] = rank;
        
        ind[suffixes[0].index] = 0;
        
        // 赋值rank
        for (int i = 1; i < n; i++)
        {
            
            // 和前面相同
            if (suffixes[i].rank[0] == prev_rank &&
                suffixes[i].rank[1] == suffixes[i-1].rank[1])
            {
                prev_rank = suffixes[i].rank[0];
                suffixes[i].rank[0] = rank;
            }
            else
            {
                prev_rank = suffixes[i].rank[0];
                suffixes[i].rank[0] = ++rank;
            }
            
            // 第suffixes[i].index个suffix是数组suffixes中的i个元素
            ind[suffixes[i].index] = i;
        }
        
        // 赋值next rank
        for (int i = 0; i < n; i++)
        {
            int nextindex = suffixes[i].index + k/2;
            suffixes[i].rank[1] = (nextindex < n)?
                suffixes[ind[nextindex]].rank[0] : -1;
        }
        
        sort(suffixes, suffixes+n, cmp);
    }
    
    // (有序)后缀数组,每个元素为后缀字符串的起始下标
    int *suffixArr = new int[n];
    for (int i = 0; i < n; i++)
        suffixArr[i] = suffixes[i].index;
    
    return suffixArr;
}

void printArr(int arr[], int n)
{
    for (int i = 0; i < n; i++)
        cout << arr[i] << " ";
    
    cout << endl;
}

int main()
{
    char txt[] = "banana";
    int n = (int)strlen(txt);
    
    int *suffixArr = buildSuffixArray(txt, n);
    cout << "Following is suffix array for "<< txt << endl;
    
    printArr(suffixArr, n);
    
    return 0;
}


程序输出:

Following is suffix array for banana
5 3 1 4 2 0


总结
可以发现,后缀数组的构建充分利用了后缀数组的特性(相邻suffix的rank)来快速比较两个字符串,来降低构建复杂性,rank的计算复杂性为O(n)。同时注意上面算法使用了标准(快速)排序,时间复杂度为O(n \log n \log n,可以使用基数Radix排序(时间复杂度为O(n))来降低整个算法复杂性为O(n \log n)

参考资料

  1. suffix -array
  2. http://www.cbcb.umd.edu/confcour/Fall2012/lec14b.pdf
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容

  • 字符串匹配KMP算法详解 1. 引言 以前看过很多次KMP算法,一直觉得很有用,但都没有搞明白,一方面是网上很少有...
    张晨辉Allen阅读 2,398评论 0 3
  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,380评论 0 5
  • 行为改进型养育 01 行为改进型养育告诉我们,孩子们的行为会由于父母如何构建孩子的成长环境而受到正面或负面的影响。...
    歌磐阅读 1,264评论 0 0
  • 因经典教育结缘教育者,物以类聚,自己是什么样的人?感召什么样的人?今年有好几件事发生了,想要什么就来什么。 1,我...
    童心元贞阅读 163评论 0 0
  • 早加餐:麦片午加餐:麦片 参考目标: 1份豆2份肉3份“新鲜”水果4份谷物/薯5份蔬菜,深绿色叶菜最好6杯水 今日...
    静趣_儿童心理师阅读 79评论 0 0