<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
我們此前學了字首樹Trie的實現原理以及Java程式碼的實現。Trie樹很好,但是它只能基於字首匹配實現功能。但是如果我們的需求是:一個已知字串中查詢子串,並且子串並不一定符合字首匹配,那麼此時Trie樹就無能為力了。
實際上這種字串匹配的需求,在開發中非常常見,例如判斷一個字串是否包括某些子串,然後進行分別的處理。
這是最常見的演演算法字串匹配演演算法,暴力匹配也叫樸素匹配。
思路很簡單,從主串的第i個字元開始遍歷,依次與子串的每個字元進行匹配,如果某個字元匹配失敗,則主串回溯第i+1個字元,子串回溯到第1個字元,重新開始匹配,直到遍歷完主串匹配失敗或者遍歷完子串匹配成功。
很明顯這種演演算法需要在一個雙重for迴圈中實現,時間複雜度為O(m*n),m為主串長度,n為子串長度。隨著字串長度的增長,時間複雜度快速上升。
Java中字串的contains方法實際上就是採用的BF演演算法。
public static int bf(String word, String k) { char[] wordChars = word.toCharArray(); char[] keyChars = k.toCharArray(); for (int i = 0; i < wordChars.length; i++) { int j = 0, x = i; //依次匹配 while (x < wordChars.length && j < keyChars.length && wordChars[x] == keyChars[j]) { x++; j++; } if (j == keyChars.length) { return i; } } return -1; }
KMP 演演算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 於1977年共同提出的,稱之為 Knuth-Morria-Pratt 演演算法,簡稱 KMP 演演算法。
KMP演演算法是一種改進的字串匹配演演算法,核心是利用之前的匹配失敗時留下的資訊,選擇最長匹配長度直接滑動,從而減少匹配次數。KMP 演演算法時間複雜度為O(m+n),m為主串長度,n為子串長度。
BF匹配失敗之後,主串和子串都會最大回溯,但是很多時候都是沒有必要的。例如對於主串abababcd,子串ababc,第一次匹配之後,很明顯主串和子串會匹配失敗,但是我們能夠知道他們的能夠匹配的字首串,即abab:
如果在第二次匹配的時候,主串不回溯,子串滑動兩個字元長度,那麼我們就能在第二次的時候實現匹配成功。
到這裡,這種加速的方法已經呼之欲出了,但是我們先介紹兩個重要概念:
1.匹配字首:在某一次主串和子串的匹配失敗之後,前面匹配成功的那部分子串就被稱為匹配字首。這就是一次匹配失敗時留下的資訊。
例如主串abcde,子串abcc,那麼在第一次匹配的時候,匹配字首為abc。
2.最長匹配長度:對於每次匹配失敗後的匹配字首串,其字首子串(連續,且一定包括第一個字元,不包括最後一個字元)和字尾子串(連續,且一定包括最後一個字元,不包括第一個字元)中,相同的前字尾子串的最長子串長度,此時的字首、字尾字串也被稱為最長真字首、字尾子串。
有了這兩個概念,那麼我們才能進行跳躍式滑動,對於主串,在匹配失敗的位置不進行回溯,對於子串,則是回溯(滑動)到其匹配字首的最長匹配長度的位置上繼續匹配,這樣就跳過了之前的部字串的匹配,且只需要匹配剩下的部分字串即可。
我們再詳細解釋下,這裡子串跳過的到底什麼?實際上它跳過的就是匹配字首串的最長匹配長度串。
設主串abababcd,子串ababc,第一次匹配失敗之後,主串匹配索引i=4,子串匹配索引j=4,此時匹配的相同字首串為abab,它的最長匹配長度為2,即最長字首串ab和最長字尾串ab。
那麼第二次匹配之前,字串匹配索引j直接跳到第一匹配的相同字首串的最長匹配長度的索引位置上即j=2。我們可以這麼理解,主串的第一次匹配的相同字首串的最長匹配字尾,與子串第一次匹配的相同字首串的最長匹配字首相等(或者說重合)。這是我們在底層一次失敗匹配之後得到的有效資訊,在第二次匹配時自然可以利用起來,利用最長的前字尾匹配資訊,跳過這些多餘的匹配,實現加速。(後續學習的AC自動機也是採用了前字尾匹配的思想)
這就是KMP演演算法加速的核心原理,每次匹配失敗之後,利用匹配失敗的資訊,找到最長匹配長度,然後主串不回溯,子串儘可能少的回溯,相比於BF演演算法,減少了沒必要的匹配次數。
基於上面的原理,我們知道可能會不止一次查詢最長匹配長度,而且我們會發現,最長匹配長度的範圍只能在子串長度範圍之內,而且其計算結果只和子串有關。那麼我們就可以先初始化一個陣列,用來儲存不同長度的字首的最長匹配長度。
這就是所謂的next陣列,也被稱為部分匹配表(Partial Match Table),也是KMP演演算法的核心。next陣列的大小就是子串的長度,每個的索引位置i表示長度為i+1的子串的匹配字首子串,值v表示對應匹配字首子串的最長匹配長度。
假設子串為ababc,那麼next陣列值為:
假設子串為abcabdabcabc,那麼對應的next陣列如下:
其實很好理解:
子串匹配字首串 | 最長匹配長度 |
---|---|
a | 0 |
ab | 0 |
abc | 0 |
abca | 1 |
abcab | 2 |
abcabd | 0 |
abcabda | 1 |
abcabdab | 2 |
abcabdabc | 3 |
abcabdabca | 4 |
abcabdabcab | 5 |
abcabdabcabc | 3 |
現在,我們的首要問題變成了求next陣列。
首先,切next陣列的問題實際上就是求最大的前、字尾長度的問題,那麼我們可以使用最樸素的方式求解:
public static int[] getNext(String word) { int[] next = new int[word.length()]; //從兩個字元的子串開始遍歷 for (int i = 1; i < word.length(); i++) { int k = i; //從最大的最長匹配值開始縮短 while (k > 0) { //如果字首等於字尾,那麼表示獲取到了最長匹配,直接返回 if (word.substring(0, k).equals(word.substring(i - (k - 1), i + 1))) { next[i] = k; break; } k--; } } return next; }
不難發現,求解next陣列的時間複雜度為O(n^2),是否有更快速的方法呢?當然有,可以發現,在求next[i]的最長匹配長度的時候,next[0], next[1], … next[i-1]的結果已經求出來了。因此我們嘗試利用此前的結果直接推匯出後面的結果。下面是分情況討論。
設子串為str=ababc,i=3,那麼next[i-1]=1,即子串aba的的最長匹配長度為1,那麼str[next[i-1]]實際上就是最長匹配子串字首後一個字元,即str[1]=b。
如果str[i]=str[next[i-1]],就相當於在前一個子串的最長匹配長度的基礎上增加了一位,即next[i]=next[i-1]+1。如下圖:
如果str[i]!=str[next[i-1]],此時就會複雜一些。此時我們需要縮短最長匹配子串的長度,具體怎麼縮短呢?
設str = abcabdabcabc,設i = 11,即最後一個字元c,那麼next[i-1] = 5,但是由於str[i] != str[next[i-1]],即d != c,那麼此時我們需要求i-1的最長匹配長度子串abcab的最長匹配長度子串,即next[next[i-1]-1] = 2,然後判斷str[i]是否等於str[next[next[i-1]-1]],如果相等則同第一種情況,否則繼續縮減直到next[next[i-1]-1]為0為止,此時表示當前子串的最長匹配長度也為0。如下圖:
基於上面的規律,我們的改進演演算法如下:
public static int[] getNext2(String k) { int[] next = new int[k.length()]; char[] chars = k.toCharArray(); //i表示匹配的字元索引,pre表示前一個子串的最長匹配長度,即next[i-1] int i = 1, pre = next[i - 1]; while (i < k.length()) { //如果新增的字元與前一個子串的最長匹配子串字首的後一個字元相等 if (chars[i] == chars[pre]) { //next[i]=next[i-1]+1 pre++; next[i] = pre; //繼續後移 i++; } //如果不相等,且前一個子串的最長匹配長度不為0 //那麼求i-1的最長匹配長度子串的最長匹配長度子串,即pre=next[next[i-1]-1] //然後在下一輪迴圈中繼續比較chars[i] == chars[pre],此時i並沒有自增 else if (pre != 0) { //next[next[i-1]-1] pre = next[pre - 1]; } //如果不相等,且前一個子串的最長匹配長度為0,那麼說明當前子串的最長匹配長度也為0 else { //當前子串的最長匹配長度為0 next[i] = 0; //繼續後移 i++; } } return next;
這種演演算法的時間複雜度為O(n),大大縮短了求next陣列的時間。
有了next陣列,那麼KMP演演算法就很容易實現了。
使用i和j分別表示主串和子串的匹配進度,i永遠不會回退,依次匹配主串和子串的字元:
1.如果字元相等則推進i、j,並且判斷如果匹配到了一個完整的子串,那麼返回起始索引。
2.如果不相等:
如果i進度匹配完畢,那麼退出迴圈,表示沒有匹配到任何完整的子串,返回-1。
public static int kmp(String word, String k) { int[] next = getNext(k); //i,j分別表示主串和子串的匹配進度 int m = word.length(), n = k.length(), i = 0, j = 0; //如果i匹配完畢,那麼退出迴圈 while (i < m) { //如果字元相等,那麼向後推進i、j if (word.charAt(i) == k.charAt(j)) { i++; j++; //如果匹配到了一個完整的子串 if (j == n) { //返回起始索引 return i - n; } } //如果當前子串進度為0,那麼子串不需要回退,主串向後推進i else if (j == 0) { i++; } //如果當前子串進度不為0,那麼子串需要回退,主串不需要向後推進i else { //子串進度j回退 j = next[j - 1]; } } return -1; }
上面我們的實現是返回第一個匹配到的模式串的起始索引,那麼如果我們需要返回所有匹配到的模式串的起始索引呢?
其實也很簡單。在每次匹配某個字元成功之後判斷,如果匹配到了一個完整的子串,那麼我們求起始索引並且加入結果集,然後子串點位j需要回退,繼續迴圈。
public static List<Integer> kmpAll(String word, String k) { List<Integer> res = new ArrayList<>(); int[] next = getNext(k); //i,j分別表示主串和子串的匹配進度 int m = word.length(), n = k.length(), i = 0, j = 0; //如果i匹配完畢,或者j匹配完畢,那麼退出迴圈 while (i < m) { //如果字元相等,那麼向後推進i、j if (word.charAt(i) == k.charAt(j)) { i++; j++; //如果匹配到了一個完整的子串 if (j == n) { //將起始索引加入結果集 res.add(i - n); //子串進度j回退 j = next[j - 1]; } } //如果當前子串進度為0,那麼子串不需要回退,主串向後推進i else if (j == 0) { i++; } //如果當前子串進度不為0,那麼子串需要回退,主串不需要向後推進i else { //子串進度j回退 j = next[j - 1]; } } return res; }
KMP演演算法是一種優化的字串匹配演演算法,m為主串長度,n為子串長度。由於構建了 next 陣列,空間複雜度為 O(m)。匹配時主串不會回退,子串回退不會超過n,總體演演算法時間複雜度為O(m+n)。
next陣列是實現演演算法加速的關鍵,它的核心是查詢最長前字尾匹配長度,這也是理解KMP演演算法的核心。
到此這篇關於Java資料結構之KMP演演算法詳解以及程式碼實現的文章就介紹到這了,更多相關Java KMP演演算法內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45