Algorithm StringMatching Of Suffix&HeightArr


字符串匹配

  • 前缀树:字典树 Tire
  • 后缀数组
  • 后缀自动机

后缀数组

什么是后缀数组?

就是字符串的所有后缀子串按字典序排序后,在数组中记录后缀的起始下标,后缀数组就是:排名和原下标的映射。

例如:suffixArray[0] = 5,即起始下标为 5 的后缀在所有后缀中字典序最小

与之对应的有一个 rank 数组:给定后缀的下标,返回其字典序,rank[5] = 0;

映射关系:rank[suffixArray[i]] = i

例如母串:”A B A B A B A B B”,模式串:”B A B B”

我们就先把母串的所有后缀子串列出来:

后缀子串 下标
A B A B A B A B B 0
B A B A B A B B 1
A B A B A B B 2
B A B A B B 3
A B A B B 4
B A B B 5
A B B 6
B B 7
B 8

将所有子串按照字典序排序后就得到后缀数组

1、常规方法构建后缀数组

最简单的构造方法就是遍历母串,获取每一个子串,然后使用快排进行排序,由于需要对每个字符串进行比较,所以构建后缀数组的时间复杂度是 O(n²logn)。

构建完成后,将模式串与后缀数组中的元素进行比较,这里用到二分查找法,而且也需要进行字符串比较,所以时间复杂度是 O(nlogn) ,总体的时间复杂度:

T(n) = O(n²logn) + O(nlogn)

public class SunffixArrayDemo {

    public static void main(String[] args) {
        
        String txt = "ABABBBABABBABB";
        
        String pattern = "BABB";
        
        Suffix[] suffixArray = buildSuffixArray(txt);

        search(suffixArray, pattern);
    }

    /**
     * 利用后缀数组匹配模式串
     * @param suffixArray 后缀数组
     * @param pattern 模式串
     */
    private static void search(Suffix[] suffixArray, String pattern) {
        
        int left = 0;
        int right = suffixArray.length - 1;
        
        while (left <= right) {
            
            int mid = (left + right) >>> 1;
            
            Suffix suffix = suffixArray[mid];
            
            int compareRes = -1;
            
            if (suffix.getSuffix().length() > pattern.length()) {
                String subSuffix = suffix.getSuffix().substring(0, pattern.length());
                compareRes = pattern.compareTo(subSuffix);
            } else
                compareRes = pattern.compareTo(suffix.getSuffix());
            
            
            if (compareRes == 0) {
                System.out.println("match index from: " + suffix.getIndex());
                System.out.println("match String: " + suffix.getSuffix());
                break;
            } else if (compareRes > 0) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
    }

    /**
     * 为字符串构建后缀数组
     * 由于使用了快排,而且利用了字符串的比较,所以 T(n) = O(n²log(n))
     * @param txt 目标字符串
     * @return 后缀数组
     */
    private static Suffix[] buildSuffixArray(String txt) {
        
        int len = txt.length();

        Suffix[] suffixArray = new Suffix[len];
        
        for (int i = 0; i < len; i++) {
            String subStr = txt.substring(i);
            suffixArray[i] = new Suffix(subStr, i);
        }

        Arrays.sort(suffixArray);
        
        return suffixArray;
    }
}

class Suffix implements Comparable<Suffix> {
    
    private String suffix;

    private int index;

    public Suffix(String suffix, int index) {
        this.suffix = suffix;
        this.index = index;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    @Override
    public int compareTo(@NotNull Suffix o) {
        
        return getSuffix().compareTo(o.getSuffix());
    }
}

2、倍增法构建后缀数组

不难看出如果我们想要优化该算法,就得在构建后缀数组上做一些处理,一种相对简单的方法是倍增法。

具体原理就是通过之前提到的 rank 数组,它代表索引与字典序的一种映射关系。

用空间换时间来优化。

T(n) = O(nlog²(n))

public class SunffixArrayDemo {

    public static void main(String[] args) {
        
        String txt = "ABBABBBBABBABABB";
        
        String pattern = "BABB";

        Suffix[] suffixArray = redoubleSuffixArray(txt);

        search(suffixArray, pattern);
    }

    /**
     * 利用后缀数组匹配模式串
     * @param suffixArray 后缀数组
     * @param pattern 模式串
     */
    private static void search(Suffix[] suffixArray, String pattern) {
        
        int left = 0;
        int right = suffixArray.length - 1;
        
        while (left <= right) {
            
            int mid = (left + right) >>> 1;
            
            Suffix suffix = suffixArray[mid];
            
            int compareRes = -1;
            
            if (suffix.getSuffix().length() > pattern.length()) {
                String subSuffix = suffix.getSuffix().substring(0, pattern.length());
                compareRes = pattern.compareTo(subSuffix);
            } else
                compareRes = pattern.compareTo(suffix.getSuffix());
            
            
            if (compareRes == 0) {
                System.out.println("match index from: " + suffix.getIndex());
                System.out.println("match String: " + suffix.getSuffix());
                break;
            } else if (compareRes > 0) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
    }   
    
    // nlog²(n)
    private static Suffix[] redoubleSuffixArray(String txt) {

        int len = txt.length();
        
        // 需要使用到 rank 数组:下标到排名的映射
        int[] rank = new int[len];
        // 后缀数组
        Suffix[] suffixArray = new Suffix[len];

        // 倍增序列
        for (int k = 1; k <= len; k *= 2) {
            // 参于排序的字符串的长度倍增
            for (int i = 0; i < len; i++) {
                String suffix = txt.substring(i, i + k > len ? len : i + k);
                suffixArray[i] = new Suffix(suffix, i);
            }
            
            if (k == 1) {   // k = 1 表示字符串只有一个字符
                Arrays.sort(suffixArray);
            } else {
                // 填充完毕,开始排序 nlog(n)
                final int tmp = k;
                
                Arrays.sort(suffixArray, (o1, o2) -> {
                    // 利用 rank 数组进行排序: rank 是下标和排名的映射
                    int i = o1.getIndex();
                    int j = o2.getIndex();
                    
                    if (rank[i] == rank[j]) {
                        try {
                            return rank[i + tmp / 2] - rank[j + tmp / 2];
                        } catch (ArrayIndexOutOfBoundsException e) {
                            return o1.getSuffix().length() - o2.getSuffix().length();
                        }
                    } else {
                        return rank[i] - rank[j];
                    }
                });
            }
            
            // 排序完成后生成 rank 数组
            int r = 0;
            rank[suffixArray[0].getIndex()] = r;

            for (int i = 1; i < len; i++) {
                if (suffixArray[i].compareTo(suffixArray[i - 1]) == 0) { // 两串相同
                    rank[suffixArray[i].getIndex()] = r;    // 给定索引,返回该串字典序排名
                } else {
                    rank[suffixArray[i].getIndex()] = ++r;
                }
            }
        }
        
        return suffixArray;
    }
}

class Suffix implements Comparable<Suffix> {
    
    private String suffix;

    private int index;

    public Suffix(String suffix, int index) {
        this.suffix = suffix;
        this.index = index;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    @Override
    public int compareTo(@NotNull Suffix o) {
        
        return getSuffix().compareTo(o.getSuffix());
    }
}

3、倍增法改进

上面的倍增法构建后缀数组会消耗过多的内存,因为每一次构建后缀数组都要做大量的子串切割,我们可以进行优化,不必总是切分子串。

现在后缀数组只保存子串的首字符在母串中的下标。

public class SuffixArrayDemo2 {

    public static void main(String[] args) {

        String txt = "ABBABBBBABBABABB";

        String pattern = "BABB";

        SuffixR[] suffixArray = redoubleSuffixArray(txt);
        
        search(suffixArray, txt, pattern);
    }

    private static SuffixR[] redoubleSuffixArray(String txt) {

        int len = txt.length();
        
        SuffixR[] suffixArray = new SuffixR[len];

        for (int i = 0; i < len; i++) {
            suffixArray[i] = new SuffixR(txt.charAt(i), i);
        }
        
        Arrays.sort(suffixArray);

        // rank 数组
        int[] rank = new int[len];
        
        rank[suffixArray[0].getIndex()] = 1;

        for (int i = 1; i < len; i++) {
            rank[suffixArray[i].getIndex()] = rank[suffixArray[i - 1].getIndex()];
            if (suffixArray[i].getC() != suffixArray[i - 1].getC())
                rank[suffixArray[i].getIndex()]++;
        }
        
        // 倍增序列
        for (int k = 2; rank[suffixArray[len - 1].getIndex()] < len; k *= 2) {
            
            final int tmp = k;
            
            Arrays.sort(suffixArray, (o1, o2) -> {
                // 基于 rank 进行比较
                int i = o1.getIndex();
                int j = o2.getIndex();
                if (rank[i] == rank[j]) {
                    // 如果前半部分相同
                    if (i + tmp / 2 >= len || j + tmp / 2 >= len) {
                        // o1 子串比 o2 子串长, 根据子串的性质可知 i 比 j 小
                        return -(i - j);
                    }
                    return rank[i + tmp / 2] - rank[j + tmp / 2];
                } else {
                    return rank[i] - rank[j];
                }
            });
            
            // 更新 rank 数组, 之后要利用这个新的 rank 对后缀数组排序
            rank[suffixArray[0].getIndex()] = 1;
            
            for (int i = 1; i < len; i++) {
                int i1 = suffixArray[i].getIndex();
                int i2 = suffixArray[i - 1].getIndex();
                
                rank[i1] = rank[i2];
                try {
                    if (!txt.substring(i1, i1 + k).equals(txt.substring(i2, i2 + k)))
                        rank[i1]++;
                } catch (Exception e) {
                    rank[i1]++;
                }
            }
        }
        
        return suffixArray;
    }

    private static void search(SuffixR[] suffixArray, String txt, String pattern) {
        
        int pLen = pattern.length();
        int tLen = txt.length();
        
        int left = 0;
        int right = suffixArray.length - 1;
        
        while (left <= right) {
            
            int mid = (left + right) >>> 1;
            int index = suffixArray[mid].getIndex();

            int compareRes;
            
            if (tLen - index >= pLen) {
                String subStr = txt.substring(index, index + pLen);
                compareRes = pattern.compareTo(subStr);
            } else {
                compareRes = pattern.compareTo(txt.substring(index));
            }
            
            if (compareRes == 0) {
                System.out.println("match index from: " + index);
                System.out.println("match subString: " + txt.substring(index));
                break;
            } else if (compareRes > 0) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
    }
}

class SuffixR implements Comparable<SuffixR> {
    
    private char c;
    
    private int index;

    public SuffixR(char c, int index) {
        this.c = c;
        this.index = index;
    }

    public char getC() {
        return c;
    }

    public void setC(char c) {
        this.c = c;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    @Override
    public int compareTo(@NotNull SuffixR o) {
        return this.c - o.getC();
    }

    @Override
    public String toString() {
        return "SuffixR{" +
                "c=" + c +
                ", index=" + index +
                '}';
    }
}

高度数组

高度数组是后缀数组的一个衍生产物,例如字符串 “ABA”,长度为 3,则后缀数组和高度数组的长度也是 3。

高度数组定义:height[i] 等于后缀数组中 suffixArray[i]suffixArray[i - 1]最长公共子串 的长度,注意 height[0] = 0

注:下面的 rank 数组是从 0 开始排名

子串首地址 子串 数组下标 rank 数组 后缀数组(升序) 高度数组 height[i]
0 ABA 0 1 A 0
1 BA 1 2 ABA 1
2 A 2 0 BA 0

记住一个结论:height[rank[i + 1]] >= height[rank[i]] - 1

以上面的 “ABA” 字符串得到的 rank 、后缀、高度数组为例:

i i + 1 rank[i] rank[i + 1] height[rank[i + 1] ] height[rank[i]] - 1
0 1 rank[0] = 1 rank[1] = 2 height[2] = 0 height[1] -1 = 0
1 2 rank[1] = 2 rank[2] = 0 height[0] = 0 height[2] - 1 = -1

常规解法

最简单的解法就是根据定义去处理高度数组。

遍历后缀数组(有序递增),计算相邻两串的最长公共子串,其长度就是高度数组的值。

但是字符串的比较时间复杂度:T(n) = O(n²)

优化

可以看出如果我们想要优化算法,必须在比较这一块做一些处理,普通方法字符串比较是平方级别的,我们可以借助 rank 数组、后缀数组及高度数组的关系:

height[rank[i + 1]] >= height[rank[i]] - 1

从而利用 height[s] 去推 height[s + 1] 的值。

  • 如果已经知道后缀数组中下标为 i 与 i + 1(由于后缀数组是升序的,所以下标 i 就是排名,从 0 开始) 的 lcp 为 h,那么 i 代表的字符串与 i + 1 代表的字符串去掉首字母后的 lcp 为 h - 1.
  • 注:lcp 即最长公共子串

根据这个我们可以发现,排名为 i 的字符串与排名为 i + 1 的字符串的 lcp 为 k,那么设它去掉首字母后得到的字符串排名为 j,则它与排名为 j + 1 的字符串的 lcp 大于等于 k - 1;

例如对于字符串 abcefabc,我们知道 abcefabcabc 的 lcp 为 3,那么 bcefabcbc 的 lcp 大于等于 3 - 1。

利用这一点就可以以 O(n) 的时间复杂度去求出高度数组。

private static int[] getHeightArray(String txt, SuffixR[] suffixArray) {

    int tLen = txt.length();

    int rank[] = new int[tLen];

    // 将 rank 表示为不重复的排名:0 - n-1
    for (int i = 0; i < tLen; i++) {
        rank[suffixArray[i].getIndex()] = i;
    }

    int[] height = new int[tLen];

    // 高度数组表示的是后缀数组中 suffixes[i] 和 suffixes[i - 1] 的最长公共子串的长度, 注意 height[0] = 0
    int k = 0;

    for (int i = 0; i < tLen; i++) {

        int rk_i = rank[i];    // 首字母下标为 i 的后缀子串的排名

        if (rk_i == 0) { // 如果首地址下标为 i 的后缀子串的排名为 0,则其高度数组为 0,因为没有比该串更小的后缀子串了
            height[0] = 0;
            continue;
        }

        // 首先得到 rank 数组中为 rk_i_1 的值,例如: rank[j] = rk_i_1
        int rk_i_1 = rk_i - 1;  
        // 然后根据排名去后缀数组找到该字符串的首地址下标
        int j = suffixArray[rk_i_1].getIndex(); // j 是字典序为 rk_i_1 的后缀子串首地址下标

        // 当 k 不是 0 时,说明现在高度数组 height[s] = k, 且 s > 0
        if (k > 0)
            k--;

        // i 和 j 分别代表首地址为 i 和 j 的后缀子串
        // 并且 i 和 j 在 rank 数组中是相邻的 rank[j] = rank[i] - 1
        // 这里为什么 j + k 在前面可以想一想
        while (j + k < tLen && i + k < tLen) {
            if (txt.charAt(j + k) != txt.charAt(i + k))
                break;
            k++;
        }

        height[rk_i] = k;
    }

    return height;
}

Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC