定义:后缀数组(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开始的下标)。
使用最“笨”的方法可以在时间复杂度内构建后缀数组,其中是字符串长度。思路是使用的排序算法对后缀字符串排序,同时保持后缀起始字符下标。因为两个字符串比较大小需要复杂度,所以整体复杂度为。
本文介绍一种的后缀数组构建算法。简单期间,首先考虑复杂度的算法。出发点是充分利用所有后缀字符串都是来源于一个字符串的特点,同时借鉴radix排序思路。算法由轮排序完成,第1轮排序只对第1个字符排序,第2轮排序对前2个字符排序,第3轮排序对前4个字符排序,第k轮排序对前个字符排序。注意这里有一点很重要,如果所有后缀字符串已经按照前字符排序,则可以使用时间按照前排序,原因是两个suffix可以在常数(而不是)时间内比较大小。后缀数组
例:构建字符串banana的后缀数组
为每个suffix分配一个序号rank,例如,对第个字符串分配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的计算复杂性为。同时注意上面算法使用了标准(快速)排序,时间复杂度为,可以使用基数Radix排序(时间复杂度为)来降低整个算法复杂性为。
参考资料