1. 程式人生 > >【演算法】KMP經典演算法,你真的懂了嗎?

【演算法】KMP經典演算法,你真的懂了嗎?

有關KMP演算法的書籍、帖子、部落格鋪天蓋地,但是你真的能看懂?你知道為什麼要有next陣列,next陣列到底什麼意思,又該怎麼求next陣列,有了next陣列之後又該怎樣判斷模式串和主串是否匹配成功?本文絕對不是講解KMP演算法最細緻的一篇文章,但卻是為了解決大家的疑惑而寫的一篇文章。

KMP的概念

首先說說什麼是KMP演算法,說白了,就是不希望用簡單的兩層迴圈遍歷兩個串那樣去看能否匹配成功。簡單樸素的字串匹配是,一旦匹配不成功,主串要回到匹配開始的起始位置,然後加1再和模式串從頭匹配。
這裡寫圖片描述
如此效率太低,有沒有效率更高的呢,顯然是有的,這就是KMP,KMP演算法效率之所以高,是因為主串不用回溯,只要模式串回溯就可以,也就是上面的j=4,s=4發現不匹配之後,j=4不變,s改變,那s到底怎麼改變呢,這就是next陣列的作用了!

next陣列的概念

看過KMP的人都知道字串字首和字尾這兩個概念,而我並不會說這兩個概念。next陣列通俗易懂的理解就是,對於模式串中每一個字元,看它前面字串中存在的字首和字尾匹配的最長長度。什麼意思呢?
這裡寫圖片描述
仔細看,應該可以很快明白過來,所謂的next陣列就是求前面串中前後綴相等的最大長度而已。

next陣列的作用

儘管知道了next陣列,但是它做什麼作用呢?知道求這個陣列,但是仍然不能知道它有什麼意義,那也白搭不是?接下來,便是KMP中的精髓,即next陣列有什麼作用。請看下圖:
這裡寫圖片描述
從上面過程可以知道,next陣列就是儲存著當主串和模式串不匹配時,接下來與主串j比較的模式串中s的位置,即s=next[s]。

next陣列的求法

前面有對next求解的過程,然而那只是為了理解next的含義,真正演算法程式設計卻無法那樣直觀求出,那該如何求解next陣列呢?這裡用到了類似並查集的演算法。
主串:abababbbab

  • 首先next[0]=-1,next[1]=0;
  • 之後每一位j的next求解:
    比較j-1字元與next[j-1]是否相等,
    如果相等則next[j]=next[j-1]+1,
    如果不相等,則比較j-1字元與next[next[j-1]]是否相等,
    1) 如果相等則next[next[j-1]]+1,
    2)如果不相等則繼續以此下去,直到next[…]=-1,則next[j]=0.

通俗易懂的話來說就是你要求解當前位的next值,則看前一位與前一位next所在位字元是否相等,相等則當前位的next等於前一位next+1,如果不等就找前一位next的next與前一位比較,如果相等,當前位的next等於那個與前一位字元相等的字元所在位置+1,如果還是不相等,則以此類推,直到出現比較位為next[]=-1,那當前位的next就等於-1。
然而在演算法求解的時候,我們應該這樣去理解,求解下一位的next等於當前位與當前位的next比較。演算法如下:

//next的求解
private static void getNext(int[] next, String str) {
        next[0]=-1;//初始化
        int k=-1;//記錄當前位的next
        int j=0;//當前位下標
        while(j<str.length()-1){//求解完所有字元的next
            if(k==-1||str.charAt(j)==str.charAt(k)){//比較當前位與當前位next字元是否相等
                j++;
                k++;//當前位的next值+1作為下一位的next值
                next[j]=k;//求解出下一位的next值
            }else{
                k=next[k];//如果不相等則找當前位的next的next與當前位比較
            }
        }
    }

完整的KMP演算法

在搞懂上面所有東西之後,那字串匹配演算法KMP就呼之欲出了。簡言之,將主串與模式串匹配,相等的話彼此都+1,如果不等,主串j不回溯,將模式串的s=next[s]與主串j比較,相等彼此+1,以此類推,直到模式串完全被匹配,或者主串匹配到最末,結束。演算法如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class KMP{
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String str1 = sc.nextLine();//主串
        while(sc.hasNext()){
            String str2 = sc.nextLine();//模式串
            int next[] = new int[str2.length()];
                getNext(next,str2);//求解next陣列
                System.out.println("next陣列"+java.util.Arrays.toString(next));
            List<Integer> pos = new ArrayList<>();//可能存在多個位置起始的字串與模式串匹配,記錄這些在主串中的位置
            ifMatch(str1,str2,next,pos);//字串匹配過程
            System.out.println("匹配位置:"+pos);//輸出所有匹配的位置
        }

    }

    private static void ifMatch(String str1, String str2, int[] next,List<Integer> pos) {
        int j=0;//主串初始位置
        int s=0;//匹配串初始位置
        while(j<str1.length()){
            if(s==-1||str1.charAt(j)==str2.charAt(s)){//比較字元是否相等
                j++;
                s++;
                if(s>=str2.length()){//模式串被完全匹配
                    pos.add(j-str2.length());
                    s=0;
                    j--;
                }
            }else{
                s=next[s];//不等,主串j不變,模式串s變
            }
        }

    }
      //next陣列的求解
    private static void getNext(int[] next, String str) {
        next[0]=-1;
        int k=-1;
        int j=0;
        while(j<str.length()-1){
            if(k==-1||str.charAt(j)==str.charAt(k)){
                j++;
                k++;
                next[j]=k;
            }else{
                k=next[k];
            }
        }
    }
}

結語

或許其中有許多細節不是很明白,也沒有公式的求證,但是很多時候我們應該刪繁去雜,這樣或許才能體會到其中的奧祕。當然每個人有每個人的理解,或許我的理解有誤,歡迎大家拍磚!!!!