Knuth-Morris-Pratt算法(簡稱KMP)是經常使用的字符串匹配算法之一。html
假設如今有一個模式串a="ABACABAD"和一個主串b="BBC ABACABACABAD ABCDABDE",要判斷主串b是否包含模式串a,若是包含,則返回出模式串在主串的位置下標。算法
易知使用暴力匹配算法的時間複雜度爲O(m*n),其中m和n爲模式串和主串的長度。而使用KMP算法,則能在線性時間O(m+n)中完成匹配工做。數組
使用暴力匹配算法時,每次不匹配,都須要從主串下一個位置從頭匹配一次模式串,這種回溯工做,致使效率低下。KMP算法核心思想是充分利用上次不匹配時的計算結果,避免"一切從新開始"的計算工做。ide
如下經過一個簡單的例子進行說明:3d
一、首先,使用主串的第一位與模式串的第一位進行比較,若是不一樣,則將主串的第二位與模式串的第一位進行比較,以此類推。日誌
比較主串第一位與模式串第一位的字符比較主串第二位與模式串第一位的字符htm
二、直到主串有一個字符與模式串的第一位相同,則比較主串下一個位置的字符,是否與模式串的第二位相同,以此類推。blog
主串中的字符匹配到模式串的第一位字符比較主串的下一個字符與模式串的第二位字符字符串
三、當匹配到某個位置,主串與模式串的字符不一樣時,此時不直接從主串下一個位置,再從頭逐個比較。由於在比較過程當中,咱們能夠知道兩個細節:get
(1)模式串的前面部分的字符串內容是與主串的部分字符是相同的。
(2)在該模式串"ABACABAD"中,下標0~2的字符是與下標4~6的字符是相同的。
所以,咱們直接使用下標位置爲3的字符與主串進行比較,這樣就能大大提升效率了。
主串字符C與模式串D不匹配模式串下標0~2的字符是與下標4~6的字符相同,所以也與主串的前三個位置的字符是匹配的不重頭開始比較,而是比較模式串下標3的字符與主串中的字符是否相同
四、以此類推,直到匹配到模式串的最後一位,或者掃描完主串。
匹配到模式串的最後一位
在匹配步驟3中,其實利用了模式串自己字符的組合順序信息,在KMP算法中,咱們須要將該字符組合順序信息記錄起來,稱之爲"部分匹配表"。
"部分匹配表"是如何產生的呢?首先,要了解兩個概念:"前綴"和"後綴"。 "前綴"指除了最後一個字符,一個字符串的所有頭部組合,"後綴"指除了第一個字符,一個字符串的所有尾部組合。
例如字符串a="ABCAB",前綴字符串集合爲[A, AB, ABC,ABCA],後綴字符串集合爲[B, AB, CAB,BCAB],能夠看到前綴和後綴有相同的子串[AB]。
部分匹配值,其實就是計算出下標在0~i的子字符串中(i<=a.length),前綴與後綴最長相同子串的長度。
"部分匹配表"計算規則可參考阮一峯老師的日誌「 字符串匹配的KMP算法」。
咱們根據這個規則,可計算模式串a="ABACABAD"的部分匹配表,以下:
1.計算部分匹配值。
public static int[] kmpnext(String dest){ int[] next = new int[dest.length()]; next[0] = 0; for(int i = 1,j = 0; i < dest.length(); i++){ while(j > 0 && dest.charAt(j) != dest.charAt(i)){ j = next[j - 1]; } if(dest.charAt(i) == dest.charAt(j)){ j++; } next[i] = j; } return next; }
代碼說明:
1)聲明部分匹配表數組,用於存儲匹配值。
2)當字符串爲空字符串str="",沒有先後綴字符串,所以最長匹配值爲0,next[0] = 0。
3)循環字符串,計算出下標在0~i的字符串的部分匹配表,i初始化爲1。j用於記錄前綴與後綴最長相同子串的長度。
(a) 若是在0~i的子字符串,j=0,而且dest.charAt(j) != dest.charAt(i)時,表示在0~i這一段中,先後綴字符串集合中沒有相同字符串,所以next[i]=j(即next[i]=0)。
(b) 若是在0~i的子字符串,j=0,dest.charAt(j) == dest.charAt(i)時,表示在0~i這一段中,先後綴字符串集合中有一個字符串相同,所以j++;next[i]=j;(即next[i]=1)。
(c) 若是在0~i的子字符串,dest.charAt(j) == dest.charAt(i)時,若是j>0,則表示上一輪比較,在0~i-1的子字符串中,前綴與後綴有相同子串。所以在0~i這一段中,前綴與後綴也有相同子串,而且最長的共有字符串長度爲j++。所以j++;next[i]=j。
(d) 若是在0~i的子字符串,j>0,dest.charAt(j) != dest.charAt(i)時,則表示上一輪比較時,字符串[0~j-1]是字符串[0~i-1]中,先後綴的最長相同字符串,若是咱們找到在字符串[0~j-1]中的最長先後綴相同字符串(記做maxComStr),繼續比較maxComStr下一位與dest.charAt(i),則能減小比較次數。經過部分匹配表中可見,next[j-1]爲[0~j-1]中先後綴最長相同字符串的長度,咱們也能夠理解爲是最長相同字符串下一個字符的下標,所以j=next[j-1],舉例說明:
dest.charAt(j) != dest.charAt(i)字符串[0~j-1]中,前綴字符串集合爲[A,AB],後綴字符串集合爲[A,BA],最長共有元素爲A,j=next[j-1],則j移動到了該最長前綴字符串下一位繼續比較該最長前綴字符串下一位與dest.charAt(i)
2.比較模式串和主串。
public static int kmp(String str, String dest){ //1.首先計算出部分匹配表 int[] next = kmpnext(dest); //2.查找匹配位置 for(int i = 0, j = 0; i < str.length(); i++){ while(j > 0 && str.charAt(i) != dest.charAt(j)){ j = next[j-1]; } if(str.charAt(i) == dest.charAt(j)){ j++; } if(j == dest.length()){ return i-j+1; } } return -1; }
代碼說明:
1)計算部分匹配表。
2)j爲模式串a下標,i爲主串b下標。循環主串,查找匹配位置。
(1) 若是j=0,而且str.charAt(i) != dest.charAt(j)時,則移動主串下標位置,比較主串下一位字符是否與模式串第一位字符相同。
(2) 若是str.charAt(i) == dest.charAt(j)時,則同時移動主串下標位置和模式串下標位置,依次比較下一位。
(3) 若是比較到模式串某個位置(j>0),str.charAt(i) != dest.charAt(j)時,則根據部分匹配表,移動到[0~j-1]字符串的先後綴最長相同字符串的後一位,繼續進行比較。如在該模式串dest ="ABACABAD"中,當j=7時,dest.charAt(7)與主串的字符不一樣。而dest[0~6]這部分字符串是與主串str[i-6~i-1]匹配的,dest[0~2]字符是與dest[4~6]的字符是相同的,由此能夠推斷出dest[0~2]的字符也與主串str[i-3~i-1]的字符是相同的。經過部分匹配表中可見,next[j-1]爲先後綴最長相同字符串的長度,咱們也能夠理解爲是最長相同字符串下一個字符的下標,所以j=next[j-1]。
(4) 當j == dest.length()時,代表完成模式串的比較,返回匹配起始位置(i-j+1)。
完整代碼以下:
public class Kmp { public static void main(String[] args){ String a = "ABACABAD"; String b = "BBC ABACABACABAD ABCDABDE"; int result = kmp(b, a); //打印結果:和字符串得到匹配的位置 System.out.println("resultPosion:"+result); } /** * KMP 匹配 */ public static int kmp(String str, String dest){ //1.首先計算出 部分匹配表 int[] next = kmpnext(dest); System.out.println("next ="+Arrays.toString(next)); //2.查找匹配位置 for(int i = 0, j = 0; i < str.length(); i++){ while(j > 0 && str.charAt(i) != dest.charAt(j)){ j = next[j-1]; } if(str.charAt(i) == dest.charAt(j)){ j++; } if(j == dest.length()){ return i-j+1; } } return -1; } /** * 計算部分匹配表 */ public static int[] kmpnext(String dest){ int[] next = new int[dest.length()]; next[0] = 0; for(int i = 1,j = 0; i < dest.length(); i++){ while(j > 0 && dest.charAt(j) != dest.charAt(i)){ j = next[j - 1]; } if(dest.charAt(i) == dest.charAt(j)){ j++; } next[i] = j; } return next; }}