Java數(shù)據(jù)結(jié)構(gòu)之KMP算法的實(shí)現(xiàn)
本次我們介紹數(shù)據(jù)結(jié)構(gòu)中的KMP算法,我們會(huì)從下面幾個(gè)角度來(lái)介紹:
問(wèn)題介紹
首先我們先介紹適用于KMP算法的問(wèn)題:
- 給定一個(gè)字符串S,以及一個(gè)模式串P,所有字符串中只包含大小寫(xiě)英文字母以及阿拉伯?dāng)?shù)字。
- 模式串P在字符串S中多次作為子串出現(xiàn)。
- 求出模式串P在字符串S中所有出現(xiàn)的位置的起始下標(biāo)。
我們給出一個(gè)問(wèn)題的簡(jiǎn)單示例:
// 輸入 p長(zhǎng)度 p s長(zhǎng)度 s 3 aba 5 ababa // 輸出結(jié)果 0 2
暴力求解
所有問(wèn)題我們都是在暴力求解的基礎(chǔ)上進(jìn)行更新迭代的,所以我們首先給出暴力求解:
// 下面為偽代碼,只是起到思路作用 // 首先我們需要?jiǎng)?chuàng)造s[],p[],并賦值 S[N],P[N] // 然后我們開(kāi)始匹配,我們會(huì)從S的第一個(gè)字符開(kāi)始匹配,設(shè)置一個(gè)flag判斷該字符開(kāi)始的字符串是否與P字符匹配 // 該算法從每個(gè)i開(kāi)始,全部進(jìn)行匹配 for(int i = 1;i <= n;i++ ){ boolean flag = true; for(int j = 1;j <= m;j++){ if(s[i+j-1] != p[j]){ flag = false; break; } } } // 我們給出一套完整的暴力求解方法 /** * 暴力破解法 * @param ts 主串 * @param ps 模式串 * @return 如果找到,返回在主串中第一個(gè)字符出現(xiàn)的下標(biāo),否則為-1 */ public static int bf(String ts, String ps) { char[] t = ts.toCharArray(); char[] p = ps.toCharArray(); int i = 0; // 主串的位置 int j = 0; // 模式串的位置 while (i < t.length && j < p.length) { if (t[i] == p[j]) { // 當(dāng)兩個(gè)字符相同,就比較下一個(gè) i++; j++; } else { i = i - j + 1; // 一旦不匹配,i后退(從之前i的下一位開(kāi)始,也是遍歷所有i) j = 0; // j歸0 } } // 當(dāng)上面循環(huán)結(jié)束,必定是i到頭或者j到頭,如果是j到頭,則說(shuō)明存在子串符合父串,我們就將頭位置i返回 if (j == p.length) { return i - j; } else { return -1; } } // 但是我們會(huì)發(fā)現(xiàn):我們可以不讓i回退而是讓j回退,使j回退到能夠與當(dāng)前i相匹配的點(diǎn)位,然后繼續(xù)進(jìn)行主串和模式串的匹配
首先我們會(huì)發(fā)現(xiàn)這個(gè)算法的時(shí)間復(fù)雜度為O(n^2)
我們其中可以?xún)?yōu)化的點(diǎn)就是i的位置更新,我們可以根據(jù)p字符串的特性來(lái)判斷i在失敗后最近可以移動(dòng)到哪個(gè)點(diǎn)位!
知識(shí)補(bǔ)充
我們?yōu)榱藢W(xué)習(xí)KMP算法,我們需要補(bǔ)充一些下面會(huì)用到的知識(shí):
- s[ ]是模式串,即比較長(zhǎng)的字符串。
- p[ ]是模板串,即比較短的字符串。(這樣可能不嚴(yán)謹(jǐn)。。。)
- “非平凡前綴”:指除了最后一個(gè)字符以外,一個(gè)字符串的全部頭部組合。
- “非平凡后綴”:指除了第一個(gè)字符以外,一個(gè)字符串的全部尾部組合。(后面會(huì)有例子,均簡(jiǎn)稱(chēng)為前/后綴)
- “部分匹配值”:前綴和后綴的最長(zhǎng)共有元素的長(zhǎng)度。
- next[ ]是“部分匹配值表”,即next數(shù)組,它存儲(chǔ)的是每一個(gè)下標(biāo)對(duì)應(yīng)的“部分匹配值”,是KMP算法的核心。(后面作詳細(xì)講解)。
我們所用到的思想是:
- 在每次失配時(shí),不是把p串往后移一位,而是把p串往后移動(dòng)至下一次可以和前面部分匹配的位置,這樣就可以跳過(guò)大多數(shù)的失配步驟
- 而每次p串移動(dòng)的步數(shù)就是通過(guò)查找next[ ]數(shù)組確定的
Next示例
我們給出一個(gè)簡(jiǎn)單的Next示例:
// 首先我們給出一個(gè)next手寫(xiě)實(shí)例 /* 模板串為:ABABAA next[0]代表t[0]-t[0],即"A" , "A"的前綴和后綴都為空集,共有元素的長(zhǎng)度為0. next[1]代表t[0]-t[1],即"AB",前綴為“A”,后綴為“B”,共有元素的長(zhǎng)度為0.. next[2]代表t[0]~t[2],即"ABA",前綴為“AB",后綴為"BA",最大前后綴即"A",長(zhǎng)度為1. next[3]代表t[0]~t[3],即"ABAB",前綴為"ABA"后綴為"BAB”,最大前后綴即"AB ",長(zhǎng)度為2. next[4]代表t[0]~t[4],即"ABABA",前綴為"ABAB",后綴為"BABA",最大前后綴即" ABA",長(zhǎng)度為3. next[5]代表t[0]-t[5],即" ABABAA",前綴為“ABABA",T后綴為“BABAA";最大前后綴即"A",長(zhǎng)度為1. */ // 我們next的作用是使next[j]=k使 P[0 ~ k-1] == P[j-k ~ j-1]、 // 當(dāng)?shù)趎個(gè)數(shù)不匹配時(shí),我們讓j回退到k,這時(shí)我們的主串和模式串的前綴還屬于匹配狀態(tài),我們繼續(xù)進(jìn)行匹配 例如 ababc 我們?nèi)绻ヅ涞絚不符合時(shí),我們可以使j回退到k(這里的k是2,即a)再繼續(xù)進(jìn)行匹配 因?yàn)槲覀兊腸前面的ab和開(kāi)頭的ab是匹配的,我們主串中的i前面肯定也是ab,我們的l前面也是ab,所以?xún)烧咂ヅ?,我們可以繼續(xù)后面的匹配 相當(dāng)于我們的x不變,我們將j放在2的位置,前面的ab已完成匹配,我們只需要匹配abc即可 // 公式書(shū)寫(xiě)就是下述: 當(dāng)T[i] != P[j]時(shí) 有T[i-j ~ i-1] == P[0 ~ j-1] 由P[0 ~ k-1] == P[j-k ~ j-1] 必然:T[i-k ~ i-1] == P[0 ~ k-1]
Next代碼
我們給出求解Next的代碼展示:
public static int[] getNext(String ps) { char[] p = ps.toCharArray(); int[] next = new int[p.length]; // 這里的next[0]需要等于-1 // 因?yàn)閖在最左邊時(shí),不可能再移動(dòng)j了,這時(shí)候要應(yīng)該是i指針后移。所以在代碼中才會(huì)有next[0] = -1;這個(gè)初始化。 next[0] = -1; // 這里設(shè)置j的初始值從第一個(gè)開(kāi)始(我們需要得到全部next數(shù)組) int j = 0; // 這里設(shè)置k,k就是應(yīng)該返回的位置,也就是我們常說(shuō)的前綴和后綴匹配區(qū)域的前綴的后一個(gè)位置 int k = -1; // 進(jìn)行循環(huán),得到next數(shù)組 while (j < p.length - 1) { // 首先是k==-1時(shí),說(shuō)明前面已無(wú)匹配狀態(tài),我們重新開(kāi)始 // 然后是p[j] == p[k],說(shuō)明循環(huán)時(shí)新添加的值,與我們應(yīng)該返回比對(duì)的位置相同 // 同時(shí)由于我們之前的部分都是已經(jīng)匹配成功的,所以加上這個(gè)數(shù)使我們的匹配長(zhǎng)度又增加一位 if (k == -1 || p[j] == p[k]) { // 當(dāng)兩個(gè)字符相等時(shí)要跳過(guò)(因?yàn)閜[k]與S[i]不符合的話(huà),由于我們的p[j]=p[k],所以肯定也不符合,我們直接跳下一步) if (p[++j] == p[++k]) { next[j] = next[k]; } else { // 因?yàn)樵赑[j]之前已經(jīng)有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k) // 這時(shí)候現(xiàn)有P[k] == P[j],我們是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。 // 即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1 // 前面我們已經(jīng)進(jìn)行了j++和k++,所以這里直接賦值即可 next[j] = k; } } else { // 如果當(dāng)前狀態(tài)無(wú)法匹配,我們就跳回上一個(gè)前綴后綴相同部分再來(lái)判斷是否前后綴相同 k = next[k]; } } return next; }
匹配示例
我們給出簡(jiǎn)單的匹配示例:
匹配相對(duì)而言就比較簡(jiǎn)單了
主串:abababc
模式串:abc
我們首先進(jìn)行i++,j++范圍的匹配,當(dāng)?shù)谌?,即a和c匹配不成功時(shí),我們不移動(dòng)i,而是移動(dòng)j
我們將j=2,移動(dòng)到j(luò)=0,即next[2]的位置,在之后一直匹配并再對(duì)j進(jìn)行一次移動(dòng),到最后匹配成功為止
匹配代碼
我們給出對(duì)應(yīng)的匹配代碼:
/*該代碼實(shí)際上是由暴力求解代碼改造過(guò)來(lái)的*/ public static int KMP(String ts, String ps) { char[] t = ts.toCharArray(); char[] p = ps.toCharArray(); int i = 0; // 主串的位置 int j = 0; // 模式串的位置 int[] next = getNext(ps); // 開(kāi)始判斷(設(shè)置邊界值) while (i < t.length && j < p.length) { // 當(dāng)j為-1時(shí),要移動(dòng)的是i,當(dāng)然j也要?dú)w0 // 如果匹配成功,兩者都進(jìn)行移動(dòng),開(kāi)始下一位比對(duì) if (j == -1 || t[i] == p[j]) { i++; j++; } else { // 如果比對(duì)失敗,我們將 j 返回next數(shù)組指定位置繼續(xù)匹配 // i不需要回溯了 // i = i - j + 1; j = next[j]; // j回到指定位置 } } // 最后同樣進(jìn)行判斷,是否符合條件 if (j == p.length) { return i - j; } else { return -1; } }
完整代碼
最后為大家展示一下完整代碼:
import java.util.Scanner; class ppp { /** * 主代碼 * @param args */ public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String ts = scanner.nextLine(); String ps = scanner.nextLine(); int kmp = KMP(ts, ps); System.out.println(kmp); } /** * kmp算法 * @param ts * @param ps * @return */ public static int KMP(String ts, String ps) { char[] t = ts.toCharArray(); char[] p = ps.toCharArray(); int i = 0; // 主串的位置 int j = 0; // 模式串的位置 int[] next = getNext(ps); // 開(kāi)始判斷(設(shè)置邊界值) while (i < t.length && j < p.length) { // 當(dāng)j為-1時(shí),要移動(dòng)的是i,當(dāng)然j也要?dú)w0 // 如果匹配成功,兩者都進(jìn)行移動(dòng),開(kāi)始下一位比對(duì) if (j == -1 || t[i] == p[j]) { i++; j++; } else { // 如果比對(duì)失敗,我們將 j 返回next數(shù)組指定位置繼續(xù)匹配 // i不需要回溯了 // i = i - j + 1; j = next[j]; // j回到指定位置 } } // 最后同樣進(jìn)行判斷,是否符合條件 if (j == p.length) { return i - j; } else { return -1; } } /** * next數(shù)組求解 * @param ps * @return */ public static int[] getNext(String ps) { char[] p = ps.toCharArray(); int[] next = new int[p.length]; // 這里的next[0]需要等于-1 // 因?yàn)閖在最左邊時(shí),不可能再移動(dòng)j了,這時(shí)候要應(yīng)該是i指針后移。所以在代碼中才會(huì)有next[0] = -1;這個(gè)初始化。 next[0] = -1; // 這里設(shè)置j的初始值從第一個(gè)開(kāi)始(我們需要得到全部next數(shù)組) int j = 0; // 這里設(shè)置k,k就是應(yīng)該返回的位置,也就是我們常說(shuō)的前綴和后綴匹配區(qū)域的前綴的后一個(gè)位置 int k = -1; // 進(jìn)行循環(huán),得到next數(shù)組 while (j < p.length - 1) { // 首先是k==-1時(shí),說(shuō)明前面已無(wú)匹配狀態(tài),我們重新開(kāi)始 // 然后是p[j] == p[k],說(shuō)明循環(huán)時(shí)新添加的值,與我們應(yīng)該返回比對(duì)的位置相同 // 同時(shí)由于我們之前的部分都是已經(jīng)匹配成功的,所以加上這個(gè)數(shù)使我們的匹配長(zhǎng)度又增加一位 if (k == -1 || p[j] == p[k]) { // 當(dāng)兩個(gè)字符相等時(shí)要跳過(guò) //(因?yàn)閜[k]與S[i]不符合的話(huà),由于我們的p[j]=p[k],所以肯定也不符合,我們直接跳下一步) if (p[++j] == p[++k]) { next[j] = next[k]; } else { // 因?yàn)樵赑[j]之前已經(jīng)有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k) // 這時(shí)候現(xiàn)有P[k] == P[j],我們是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。 // 即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1 // 前面我們已經(jīng)進(jìn)行了j++和k++,所以這里直接賦值即可 next[j] = k; } } else { // 如果當(dāng)前狀態(tài)無(wú)法匹配,我們就跳回上一個(gè)前綴后綴相同部分再來(lái)判斷是否前后綴相同 k = next[k]; } } return next; } }
以上就是Java數(shù)據(jù)結(jié)構(gòu)之KMP算法的實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于Java KMP算法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java計(jì)算工作時(shí)間除去節(jié)假日以及雙休日
這篇文章主要為大家詳細(xì)介紹了java計(jì)算工作時(shí)間除去節(jié)假日以及雙休日的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06SSM項(xiàng)目使用攔截器實(shí)現(xiàn)登錄驗(yàn)證功能
這篇文章主要介紹了在SSM項(xiàng)目中如何使用攔截器,實(shí)現(xiàn)登錄驗(yàn)證功能。文中的實(shí)現(xiàn)步驟講解詳細(xì),感興趣的小伙伴可以了解一下2022-01-01Java中構(gòu)造器內(nèi)部的多態(tài)方法的行為實(shí)例分析
這篇文章主要介紹了Java中構(gòu)造器內(nèi)部的多態(tài)方法的行為,結(jié)合實(shí)例形式分析了java構(gòu)造器內(nèi)部多態(tài)方法相關(guān)原理、功能及操作技巧,需要的朋友可以參考下2019-10-10基于Java解決華為機(jī)試實(shí)現(xiàn)密碼截取?
這篇文章主要介紹了基于Java解決華為機(jī)試實(shí)現(xiàn)密碼截取,文章圍繞主題相關(guān)資料展開(kāi)詳細(xì)內(nèi)容,具有一的參考價(jià)值,需要的小伙伴可以參考一下,希望對(duì)你有所幫助2022-02-02Java?深入淺出解析面向?qū)ο笾橄箢?lèi)和接口
本章具體介紹了抽象類(lèi)和接口,整篇文章用目前流行的手機(jī)來(lái)舉例,圖解穿插代碼案例。?JAVA成仙路從基礎(chǔ)開(kāi)始講,后續(xù)會(huì)講到JAVA高級(jí),中間會(huì)穿插面試題和項(xiàng)目實(shí)戰(zhàn),希望能給大家?guī)?lái)幫助2022-03-03動(dòng)態(tài)配置Spring Boot日志級(jí)別的全步驟
這篇文章主要給大家介紹了關(guān)于動(dòng)態(tài)配置Spring Boot日志級(jí)別的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04