第五章 字串專題 ---------------- 字串匹配(三)----字尾陣列演算法

Curtis_發表於2019-03-19

一、什麼是字尾陣列:

  字串字尾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;
    }

 

相關文章