文章目錄
一、KMP演算法介紹
KMP(Knuth、Morris、Pratt三人設計的演算法);
KMP演算法主要用於模式比對,簡單地說就是字串的匹配,比如A="abc",B="b",問:B是否是A的子串,此時就需要用到KMP演算法;因為普通的演算法效率太低;
而KMP可以做到O(m+n)的線性時間;
二、普通模式比對演算法
模式比對演算法簡單地說就是給定兩個字串A、B,看是否B是A的子串,Java 中String類的indexOf()就是實現這個功能;
普通模式比對演算法思想:
比如有兩個字串A:"ababc",B:"abc";
第一步:初始化 i=1,j=1;(A[0]與B[0]可以空著或者存放字串的長度);
第二步:迴圈遍曆A和B,如果A[i]==B[j],則i++,j++;
第三步:當跳出迴圈時,判斷是否B是A的子串;
步驟:
1. i = 1,j=1;
2.因為A[i]==B[j],因此i++,j++;
3.因為A[i]==B[j],因此i++,j++;
4.因為A[i]!=B[j],所以 j還原為1,i還原為 i-j+2;
5.因為A[i]!=B[j],所以 j還原為1,i還原為 i-j+2;
6.因為A[i]==B[j],因此i++,j++;
7.因為A[i]==B[j],因此i++,j++;
8.因為A[i]==B[j],因此i++,j++;
9.因為i和j同時越界,說明在最後匹配,所以B是A的子串;
從上面的步驟中可以看出:
普通的模式比對演算法是效率低下的,有許多步驟都是冗餘的,比如第5步:A[2]和B[1]的比較,因為在之前,我們已經比較過:A[2]=B[2],B[1]!=B[2],所以A[2]!=B[1],因此這步就是多餘的。
演算法如下:
private static int indexOf(String a, String b) {//1.定義兩個指標,分別指向a和bint i = 0;int j = 0;//2.遍曆while(i<a.length()&&j<b.length()){if(a.charAt(i)==b.charAt(j)){i++;j++;}else{i = i-(j-1);j = 0;}}/* * 匹配 * 1.匹配a的中間字串i<a.length() * 2.匹配a的最後字串(i==a.length()&&j==b.length()) * */if((i==a.length()&&j==b.length())||i<a.length()){return i-b.length();}else{return -1;}}
三、KMP演算法
KMP演算法主要思想:i不用回朔,只需要不斷遞增即可,只需要j回朔即可;
回朔j的規則:
(1)將前面的已經匹配的字串取出來,找出一個子串,使得此子串既為匹配的字串的最長首碼也是最長尾碼;比如:
匹配的字串為"ababa",可以看出,"a"、“aba”既是首碼也是尾碼,但是“aba”長度較長,所以 j'=“aba".length();
用數學語言表達就是:
當A[i-j+1...i]=B[1...j]且A[i+1]!=B[j+1],則需要調整j,使得重新A[i-j+1....i]=B[1...j];
而我們又將回朔j的這個過程用一個數組進行預先計算,記作p[],p[j]表示當有j個字串已經匹配,但是第j+1個字元不匹配時回朔後的j'的新值;
比如前面的例子p[5]=3;因為"ababa"的長度為5,回朔後的j=3;
比如如下例子:
A[1...5]=B[1...5],A[6]!=B[6],i=6,j=5,因此我們需要回朔j(根據預先算好的p[]數組,回朔就是 j=p[j]);
回朔原理:公用的部分為“ababa”,我們可以從此字串中看出:"aba"是最長的首碼和尾碼,因此我們可以進行如的變換,使得j=3;
A[3...5]=B[1...3],但是A[6]!=B[4],i=6,j=3,因此繼續回朔,使得j=1;如所示
A[5]=B[1],但是A[6]!=B[2],i=6,j=1,所以繼續回朔,如所示,使得j=0:
因為j=0,所以不能繼續回朔,又因為i索引到了A字串的尾端,但是j仍然不是B的尾端,因此說明B不是A的子串;
KMP演算法如下:
/** * O(m+n) 平攤分析 * * @param a 表示文本字串 * @param b 表示模式字串 * @return */public static int KMP_indexOf(String a,String b){int n = a.length();int m = b.length();//變成字元數組的原因是因為我們需要從索引1開始記錄資料,比如a = "aba",則cha = {' ','a','b','a'};char cha[] = (" "+a).toCharArray();char chb[] = (" "+b).toCharArray();int j = 0;//b的指標int[] p = computePArray(chb);//根據b字串預先計算出p數組for(int i=1;i<=n;i++){while(j>0 && chb[j+1]!=cha[i]){//j值最多隻能減少m次,通過把m次攤還給n次for迴圈j = p[j];//回朔}if(chb[j+1]==cha[i]){j++;}if(j==m){//j已經匹配到了尾端,所以全部匹配return i-m;}}return -1;}private static int[] computePArray(char[] chb) {int[]p = new int[chb.length+1];p[1] = 0;int j = 0;for(int i=2;i<chb.length;i++){while(j>0&&chb[j+1]!=chb[i]){j = p[j];}if(chb[j+1]==chb[i]){j++;}p[i] = j;}return p;}
預先計算p數組的規則:
舉個例子,比如B=“ababac”,
1.初始化p[],並且將p[1]=0 ,i=2,j=0;
2.因為B[2]!=B[1],所以p[2]=0,i=3,j=0;p[2]=0;
3.因為B[3]=B[1],所以 j++,即j=1,i=4;p[3]=1;
4.因為B[4]=B[2],所以j++,即j=2,i=5;p[4]=2;
5.因為B[5]=B[3],所以j++,即j=3,i=6,p[5]=3;
6.因為j>0,並且B[6]!=B[4],所以j=p[j]=2;
7.因為j>0,且B[6]!=B[2],所以j=p[j]=0;
8.因為j=0,並且B[6]!=B[2],所以p[6]=0;
參考文獻:
http://www.matrix67.com/blog/archives/115/ 這篇文章寫的很不錯;
此文的實現方法也很好:
public static void computePArray(String T,int p[]){T = " "+T;int j=0;p[1] = 0;for(int i=2;i<p.length;i++){while(j>0&&T.charAt(j+1)!=T.charAt(i)){j = p[j];}if(T.charAt(j+1)==T.charAt(i)){j++;}p[i] = j;}}