字符串匹配(三)----後綴數組算法
一、什麽是後綴數組:
字符串後綴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數組來進行下一步的排序,這樣前面已經排好序的就不用比較了。嗯。。。具體的倍增法的思想的話只有自己在具體應用代碼的時候慢慢琢磨,通過不斷地調試慢慢理解。這裏的代碼直接都是封裝好了方法直接調用即可。下面貼出代碼:
1 import java.util.Arrays; 2 3 public class SuffixArray { 4 public static void main(String[] args) { 5 match(); // 得到結果是5 6 } 7 8 static void match(){ 9 String s = "ABABABABB"; 10 String p = "BABB"; 11 // SuffixArray.Suff[] sa = SuffixArray.getSa(s); // 後綴數組 12 Suff[] sa = getSa2(s); // 後綴數組 13 int l = 0; 14 int r = s.length()-1; 15 // 二分查找 ,nlog(m) 16 while(r>=l){ 17 int mid = l + ((r-l)>>1); 18 // 居中的後綴 19 Suff midSuff = sa[mid]; 20 // String suffStr = midSuff.str; 21 String suffStr = s.substring(midSuff.index); 22 int compareRes; 23 // 將後綴和模式串比較,O(n); 24 if (suffStr.length()>=p.length()) { 25 compareRes = suffStr.substring(0, p.length()).compareTo(p); 26 }else { 27 compareRes = suffStr.compareTo(p); 28 } 29 // 相等了 輸出後綴的起始位置 30 if(compareRes == 0){ 31 System.out.println(midSuff.index); 32 break; 33 }else if (compareRes<0) { 34 l = mid + 1; 35 }else { 36 r = mid - 1; 37 } 38 } 39 } 40 41 42 /** 43 * nlg²n 構建後綴數組 44 * 45 * @param src 46 * @return 47 */ 48 public static Suff[] getSa2(String src) { 49 int n = src.length(); 50 Suff[] sa = new Suff[n]; 51 for (int i = 0; i < n; i++) { 52 sa[i] = new Suff(src.charAt(i), i, src);// 存單個字符,接下來排序 53 } 54 Arrays.sort(sa); 55 56 /** rk是下標到排名的映射 */ 57 int[] rk = new int[n];// suffix array 58 rk[sa[0].index] = 1; 59 for (int i = 1; i < n; i++) { 60 rk[sa[i].index] = rk[sa[i - 1].index]; 61 if (sa[i].c != sa[i - 1].c) 62 rk[sa[i].index]++; 63 } 64 // 倍增法 65 for (int k = 2; rk[sa[n - 1].index] < n; k *= 2) { 66 67 final int kk = k; 68 Arrays.sort(sa, (o1, o2) -> { 69 // 不是基於字符串比較,而是利用之前的rank 70 int i = o1.index; 71 int j = o2.index; 72 if (rk[i] == rk[j]) {// 如果第一關鍵字相同 73 if (i + kk / 2 >= n || j + kk / 2 >= n) 74 return -(i - j);// 如果某個後綴不具有第二關鍵字,那肯定較小,索引靠後的更小 75 return rk[i + kk / 2] - rk[j + kk / 2]; 76 77 } else { 78 return rk[i] - rk[j]; 79 } 80 }); 81 /*---排序 end---*/ 82 // 更新rank 83 rk[sa[0].index] = 1; 84 for (int i = 1; i < n; i++) { 85 int i1 = sa[i].index; 86 int i2 = sa[i - 1].index; 87 rk[i1] = rk[i2]; 88 try { 89 if (!src.substring(i1, i1 + kk).equals(src.substring(i2, i2 + kk))) 90 rk[i1]++; 91 } catch (Exception e) { 92 rk[i1]++; 93 } 94 } 95 } 96 97 return sa; 98 } 99 100 public static class Suff implements Comparable<Suff> { 101 public char c;// 後綴內容 102 private String src; 103 public int index;// 後綴的起始下標 104 105 public Suff(char c, int index, String src) { 106 this.c = c; 107 this.index = index; 108 this.src = src; 109 } 110 111 @Override 112 public int compareTo(Suff o2) { 113 return this.c - o2.c; 114 } 115 116 @Override 117 public String toString() { 118 return "Suff{" + "char=‘" + src.substring(index) + ‘\‘‘ + ", index=" + index + ‘}‘; 119 } 120 } 121 }
三、高度數組
高度數組是後綴數組伴生的一個東西。假設有字符串"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; }
字符串匹配(三)----後綴數組算法