演算法之KMP

it_was發表於2020-09-30

kmp演算法是為了解決傳統字串匹配問題而誕生的

有以下兩個字串A和B,長度分別為 N 和 M,判斷B是否是A的子串,以及開始匹配的位置,不匹配就返回-1
A:”ababababca”
B:”abababca”

傳統的方法就是主串和目標串對位比較,不對則主串重新從下一位開始,時間複雜度接近 O( N × M )

而kmp演算法精妙之處在於透過找尋目標串 B 每一位的資訊,利用這個資訊,使得在字元不匹配的情況下,不需要整體重新來過,只需要移動一定的位置,然後繼續匹配即可。

而這個資訊就是——字首與字尾相等的最大長度:boom::boom::boom:

下面介紹一下kmp到底如何加速字串匹配的!

  • 首先生成字串B的 next 陣列,而這個陣列的含義是: next[i]表示 next[0....i-1]字首與字尾相等的最大長度,舉個例子:”abababca”這個字串中 字元 ‘c’ 的字首和字尾相同的有 字首”abab” 和 字尾 “abab”是最大的,故 字元”c” 的next陣列位置就是 4
    下圖展示了”abababca”的next陣列:

演算法之KMP

  • 生成了這個next陣列之後,我們就可以加速主串和目標串的匹配過程了
    對於主串”ababababca”,當目標串”abababca”匹配到下標為 j 時,發現 'a' != 'c',此時主串並沒有重新從i = 1開始匹配,而是讓目標串的 j 開始跳,跳到next[j]的下標,即 j = 4,即從 j = 4開始與i繼續匹配!如下圖所示

演算法之KMP

那這樣做的原理是什麼呢 :question:

如下圖

首先當主串和目標串匹配到A 和 B 字元的時候發生不匹配,我們知道 1 區域等於3 區域,而 3 區域等於2區域,故1區域一定等於2區域,因為next的陣列的定義就是這樣,字首和字尾相等的最大長度,所以這次不需要主串重新回到 i + 1 的 位置,只需要目標串退回到2區域最後位置 + 1 ,直接從目標串的 C字元開始和 A 字元匹配即可:exclamation: :exclamation: :exclamation:

理解了之後,直接上程式碼!

public class Main(){
    public static void main(String[] args){

    }
    public int kmp(String s1, String s2){
        if(s1 == null || s2 == null || s2.length() > s1.length() || s2.length() == 0){
            return -1;
        }
        char[] arr1 = s1.toCharArray();
        char[] arr2 = s2.toCharArray();
        int len1 = arr1.length;
        int len2 = arr2.length;
        int[] next = getNext(arr2);
        int i = 0;
        int j = 0;
        while(i < len1 && j < len2){
            if(arr1[i] == arr2[j]){
                i++;
                j++;
            }else if(next[j] == -1){
                i++;
            }else{ //加速的過程體現在這,如果兩個字元不相等,j 是跳到 最大字首長度的下一個位置
                j = next[j];
            }
        }
        return j == len2 ? i - j : -1; 
    }

    public int[] getNext(char[] arr){
        if(arr.length == 1){
            return new int[]{-1};
        }
        int len = arr.length;
        int[] next = new int[len];
        next[0] = -1;
        next[1] =  0;
        int pos = 0;
        int cur = 2;
        while(cur < len){
            if(arr[pos] == arr[cur - 1]){
                next[cur] = pos + 1;
                cur++;
                pos++;
            }else if(next[pos] > 0){
                pos = next[pos];
            }else{
                next[cur] = 0;
                cur++;
            }
        }
        return next;
    }
}

kmp的應用可以用在判斷一棵樹是否是另一顆樹的子樹!!!時間複雜度控制在O(M + N) M和N為兩個樹的結點數。感覺樹的拓撲結構複雜的情況下用kmp不錯,用遞迴的話接近O(M × N)

public boolean isSubTree(TreeNode A, TreeNode B) {
        if(A==null||B==null){
            return false;
        }
        return kmp(preString(A),preString(B)) != -1; //kmp一判斷,完事!
    }
    public String preString(TreeNode root1) {
        //先找到兩棵樹的先序序列
        if (root1 == null) {
            return "!_";
        }
        String s = root1.val + "_";
        s += preString(root1.left);
        s += preString(root1.right);
        return s;
    }

演算法之KMP

此題的遞迴解法::eyes:
public boolean isSubtree(TreeNode s, TreeNode t) {
        if(s == null && t != null){
            return false;
        }
        if(s == null && t == null){
            return true;
        }
        //無非就是,兩個樹相同,t是s的左子樹,t是s的右子樹!這三種情況
        return isSameTree(s,t) || isSubtree(s.left,t) || isSubtree(s.right,t);

    }

    public boolean isSameTree(TreeNode s, TreeNode t){
        if(s == null && t == null){
            //兩樹都一起到達底部
            return true;
        }
        if(s == null || t == null){
            //一個樹有值,另一個樹為空,則一定不為子樹
            return false;
        }
        if(s.val != t.val){
            //不相等則更不用說
            return false;
        }
        return isSameTree(s.left,t.left) && isSameTree(s.right,t.right);
    }

演算法之KMP

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章