一、什么是后缀数组:
字符串后缀Suffix 指的是从字符串的某个位置开始到其末尾的字符串子串。后缀数组 Suffix Array(sa) 指的是将某个字符串的所有后缀按字典序排序之后得到的数组,不过数组中不直接保存所有的后缀子串,只要记录后缀的起始下标就好了。
比如下面在下面这张图中,sa[8] = 7,表示在字典序中排第9的是起始下标为7的后缀子串,这里还有一个比较重要的数组rank,rank[i] : sa[i]在所有后缀中的排名 ,比如rk[5]=0,表示后缀下标为5的子串在后缀数组中排第0个; rank数组与sa数组互为逆运算,rk[sa[i]]=i。
现在假如我们已经求出来了后缀数组,然后直接对已经排好序的后缀数组进行二分查找,这样就能匹配成功了,下面贴出代码:
import java.util.Arrays;
public class SuffixArrayTest {
public static void main(String[] args) {
match(); // 得到结果是5
}
static void match(){
String s = "ABABABABB";
String p = "BABB";
Suff[] sa = getSa(s); // 后缀数组
int l = 0;
int r = s.length()-1;
// 二分查找 ,nlog(m)
while(r>=l){
int mid = l + ((r-l)>>1);
// 居中的后缀
Suff midSuff = sa[mid];
String suffStr = midSuff.str;
int compareRes;
// 将后缀和模式串比较,O(n);
if (suffStr.length()>=p.length()) {
compareRes = suffStr.substring(0, p.length()).compareTo(p);
}else {
compareRes = suffStr.compareTo(p);
}
// 相等了 输出后缀的起始位置
if(compareRes == 0){
System.out.println(midSuff.index);
break;
}else if (compareRes<0) {
l = mid + 1;
}else {
r = mid - 1;
}
}
}
/**
* 直接对所有后缀排序,因为字符串的比较消耗O(N),所以整体为N²log(N)
* @param src
* @return
*/
public static Suff[] getSa(String src){
int strLength = src.length();
// sa 即SuffixArray,后缀数组
// sa 是排名到下标的映射,即sa[i]=k说明排名为i的后缀是从k开始的
Suff[] suffixArray = new Suff[strLength];
for (int i = 0; i < strLength; i++) {
String suffI = src.substring(i); //截取后缀
suffixArray[i] = new Suff(suffI, i);
}
Arrays.sort(suffixArray); //依据Suff的比较规则进行排序
return suffixArray;
}
static class Suff implements Comparable<Suff>{
String str; //后缀内容
int index; //后缀的起始下标
public Suff(String str, int index) {
super();
this.str = str;
this.index = index;
}
@Override
public int compareTo(Suff o2) {
return this.str.compareTo(o2.str);
}
@Override
public String toString() {
return "Suff{"+"str='"+str+"\'"+",index="+index+"}";
}
}
}
二、倍增法:
上面求后缀数组的方式时间复杂度为n²log(n),一般来说,时间复杂度只要达到了n平方级别都要想办法降低,于是就有一种叫做倍增法的方法来求后缀数组,基本思想就是:
1、先将每个字符排序 得到sa,rank数组,
2、然后给每个字符增添一个字符,这样就变成了两个字符,最后一个字符无法增添字符,就需要处理好边界问题。然后就是排序,排序规则的话就需要自定义规则
3、然后再在两个字符的基础上添加两个字符,就变成四个字符,然后再在上一次排序的规则上进一步排序。然后八个字符......
最主要的降低时间复杂度的方式就是根据每一步更新后的rank数组来进行下一步的排序,这样前面已经排好序的就不用比较了。嗯。。。具体的倍增法的思想的话只有自己在具体应用代码的时候慢慢琢磨,通过不断地调试慢慢理解。这里的代码直接都是封装好了方法直接调用即可。下面贴出代码:
import java.util.Arrays;
public class SuffixArray {
public static void main(String[] args) {
match(); // 得到结果是5
}
static void match(){
String s = "ABABABABB";
String p = "BABB";
// SuffixArray.Suff[] sa = SuffixArray.getSa(s); // 后缀数组
Suff[] sa = getSa2(s); // 后缀数组
int l = 0;
int r = s.length()-1;
// 二分查找 ,nlog(m)
while(r>=l){
int mid = l + ((r-l)>>1);
// 居中的后缀
Suff midSuff = sa[mid];
// String suffStr = midSuff.str;
String suffStr = s.substring(midSuff.index);
int compareRes;
// 将后缀和模式串比较,O(n);
if (suffStr.length()>=p.length()) {
compareRes = suffStr.substring(0, p.length()).compareTo(p);
}else {
compareRes = suffStr.compareTo(p);
}
// 相等了 输出后缀的起始位置
if(compareRes == 0){
System.out.println(midSuff.index);
break;
}else if (compareRes<0) {
l = mid + 1;
}else {
r = mid - 1;
}
}
}
/**
* nlg²n 构建后缀数组
*
* @param src
* @return
*/
public static Suff[] getSa2(String src) {
int n = src.length();
Suff[] sa = new Suff[n];
for (int i = 0; i < n; i++) {
sa[i] = new Suff(src.charAt(i), i, src);// 存单个字符,接下来排序
}
Arrays.sort(sa);
/** rk是下标到排名的映射 */
int[] rk = new int[n];// suffix array
rk[sa[0].index] = 1;
for (int i = 1; i < n; i++) {
rk[sa[i].index] = rk[sa[i - 1].index];
if (sa[i].c != sa[i - 1].c)
rk[sa[i].index]++;
}
// 倍增法
for (int k = 2; rk[sa[n - 1].index] < n; k *= 2) {
final int kk = k;
Arrays.sort(sa, (o1, o2) -> {
// 不是基于字符串比较,而是利用之前的rank
int i = o1.index;
int j = o2.index;
if (rk[i] == rk[j]) {// 如果第一关键字相同
if (i + kk / 2 >= n || j + kk / 2 >= n)
return -(i - j);// 如果某个后缀不具有第二关键字,那肯定较小,索引靠后的更小
return rk[i + kk / 2] - rk[j + kk / 2];
} else {
return rk[i] - rk[j];
}
});
/*---排序 end---*/
// 更新rank
rk[sa[0].index] = 1;
for (int i = 1; i < n; i++) {
int i1 = sa[i].index;
int i2 = sa[i - 1].index;
rk[i1] = rk[i2];
try {
if (!src.substring(i1, i1 + kk).equals(src.substring(i2, i2 + kk)))
rk[i1]++;
} catch (Exception e) {
rk[i1]++;
}
}
}
return sa;
}
public static class Suff implements Comparable<Suff> {
public char c;// 后缀内容
private String src;
public int index;// 后缀的起始下标
public Suff(char c, int index, String src) {
this.c = c;
this.index = index;
this.src = src;
}
@Override
public int compareTo(Suff o2) {
return this.c - o2.c;
}
@Override
public String toString() {
return "Suff{" + "char='" + src.substring(index) + '\'' + ", index=" + index + '}';
}
}
}
三、高度数组
高度数组是后缀数组伴生的一个东西。假设有字符串"ABABABB",那它的所有后缀为,以及后缀数组为:
高度数组为所有后缀排好序之后的相邻两个后缀之间的最大公共前缀(LCP),比如height[1],看下标为1的后缀ABABB与上一个下标0的后缀ABABABB,最大公共前缀为ABAB,四个,那么height[1] = 4其余的也是一样,那么可以得到高度数组为height[] = {0,4,2,0,1,3,1}
高度数组有一个重要规律就是:上一个下标i假如有k个公共前缀,并且k>0,那么下一个下标至少有一个k-1个公共前缀,那么前k个字符是不用比较的。
static int[] getHeight(String src,Suff[] sa){
// Suff[] sa = getSa2(src);
int strLength = src.length();
int []rk = new int[strLength];
// 因为原来的sa数组是按照字符串相同排名相同,现在调整排名为不重复的排名,重新排名后得到数组rk。
// 将rank表示为不重复的排名即0~n-1
for (int i = 0; i < strLength; i++) {
rk[sa[i].index] = i;
}
int []height = new int[strLength];
// (存在的规律是上一个下标i假如有k个公共前缀,并且k>0,
// 那么下一个下标至少有一个k-1个公共前缀,那么前k个字符是不用比较的)
// 利用这一点就可以O(n)求出高度数组
int k = 0;
for(int i=0;i<strLength;i++){
int rk_i = rk[i]; // i后缀的排名
if (rk_i==0) {
height[0] = 0;
continue;
}
int rk_i_1 = rk_i - 1;
int j = sa[rk_i_1].index;// j是i串字典序靠前的串的下标
if (k > 0)
k--;
for (; j + k < strLength && i + k < strLength; k++) {
if (src.charAt(j + k) != src.charAt(i + k))
break;
}
height[rk_i] = k;
}
return height;
}