C# 資料庫查詢返回連續行
KMP演算法
1.應用場景
1.1 介紹:
KMP 演算法(Knuth-Morris-Pratt 演算法)是一個著名的字串匹配演算法,效率很高,但是確實有點複雜。
1.2 應用:
在實際生活中,字串查詢是非常常見的,在一段資訊中匹配到你需要尋找的資訊(就是在一段字串找到需要匹配的字串),那麼怎樣去尋找呢?
1.2.1 與尋常的暴力匹配尋找,它有怎樣的優化
我們運用暴力匹配的時候,每次都匹配了很多此無意義的字元,什麼意思呢? 請看:
我們從這裡開始匹配
因為主串【i】和匹配串【j】一直不相等,所以主串右移
現在主串【i】和匹配串【j】相等開始匹配,i和j一起右移
現在它們又不相等了,所以
按照暴力匹配的演算法思想: 此時應該將i回溯,將i回到原來開始的位置的下一個位置,j歸零。程式碼中的表現就是 i-(j-1),j=0,但這樣效率實在太差,我們已經知道關於字串一部分資訊了,我們怎樣運用這部分加速匹配資訊呢?這裡就要用到
KMP演算法了
2 演算法思想介紹
演算法簡介
主要是消除了主串指標的回溯,之後效率就提高
加速匹配資訊的介紹和提取
KMP演算法主要是通過消除主串指標的回溯來提高匹配的效率的,那麼,它是則呢樣來消除回溯的呢?就是因為它提取並運用了加速匹配的資訊!
這種資訊就是對於每模式串t的每個元素tj,都存在一個實數k,使得模式串t開頭的k個字元(t0t1…tk-1)依次與tj前面的k(tj-ktj-k+1…tj-1,這裡第一個字元tj-k最多從t1開始,所以k<j)個字元相同
加速資訊就是next陣列
next陣列
怎樣優化
遇到像上圖的情況怎樣優化?
如果我們自己用人腦去思考,用眼睛去看:
到這裡我們就可以得到了基本思路了,又該怎樣去實現它呢?
next陣列實現(語言層面)
實在不好描述,懂得都懂,因為我們現實生活比就是這樣比,如果硬要描述,請看下面:
到了這裡我們可以大概看出一點東西,當匹配失敗時,j要移動的下一個位置k。存在著這樣的性質:最前面的k個字元和j之前的最後k個字元是一樣的。
如果用數學公式來表示是這樣的
P[0 ~ k-1] == P[j-k ~ j-1]
數學描述
當T[i]!=P[j]時
有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]
字首表描述
0 A
0 AB
0 ABC
1 ABCA(這裡就是最大字首A和最大字尾B)
2 ABCAB(類似:AB)
0 ABCABA(AB)
最大字首和最大字尾的解釋:比如ABCABA的字首有{A,AB,ABC,ABCA,ABCAB}
最大字尾:{A,BA,ABA,CABA,BCABA}
這裡解釋下,因為自己最開始也理解不對
next陣列的演算法
在匹配串的每個位置都有可能發生不匹配,所以要計算沒一個k(其實最後一個可以不算,但也可以,這裡我們算,其實感覺不算更好)我們首先要知道next【j】=k,表示當它們不匹配時,j指標的下一個位置 (非常重要),還有就是,因為字串是從下標0開始的,所以j位置之前的最大重複子串(最大相同前後綴),注意是j位置之前
public int[] getNext(char[]parm){
int next[]=new int[parm.length];
next[0]=0;
int i=0;
int k=1;
while(k<parm.length){
if(parm[i]==parm[k]){
next[k++]=++i;
}else{ if(i>0){
i=next[i-1];
}else {
next[k++]=i;
}
}
return next;}
}
記住最重要的東西:next陣列中儲存的是當它們不匹配時,j指標要移動的位置
2種情況(匹配和不匹配)
匹配
當它們匹配的時候怎麼求next陣列呢?這裡我們可以發現用字首表看出來
比如:ABAB
next【3】=1
next【4】=2
所以當它們匹配的時候就有:next【j+1】=next【j】+1
證明:
因為在P[j]之前已經有P[0~k-1]==p[j-k~j-1]。(next[j]==k)
這時候現有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。
p為匹配串
不匹配
這裡也是next陣列最重要的部分
k=next【k-1】(我這裡用的是k=next【k-1】,很多部落格時用到的是k=next【k】,
因為我們求得是k位置之前的最大重複子串(最大相同前後綴,所以儲存的位置可能不同,這和後面的一個解釋也相互驗證)
其實就是當k和j不相同的時候,就把k之前的字串看做要匹配的串,後面的字串看做主串,所以當它們不匹配的時候,就要和kmp演算法的處理階段一樣,把它們移動到可能的地方.
其實可以用數學歸納法證明出來,
但解釋語言比較長,現在天色已晚,後面有時間再補
KMP演算法的實現
程式碼實現
class Solution {
public int strStr(String haystack, String needle) {
if("".equals(needle)){
return 0;
}
char[] noodleArr=haystack.toCharArray();
char[] parmsArr=needle.toCharArray();
int i=0;
int j=0;
int[] next=getNext(parmsArr);
while(i<noodleArr.length && j<parmsArr.length){
if(noodleArr[i]==parmsArr[j]){
i++;
j++;
}else{
if(j>0){
j=next[j-1];
}else{
i++;
}
}
}
if(j==needle.length()){
return i-j;
}else{
return -1;
}
}
public int[] getNext(char []parm){
int[] next=new int[parm.length];
next[0]=0;
int k=1;
int j=0;
while(k<parm.length){
if(parm[k]==parm[j]){
next[k++]=++j;
}else{
if(j>0){
j=next[j-1];
}else{
next[k++]=j;
}
}
}
return next;
}
}
一些對程式碼的補充說明
因為一些小細節的實現可能和其他部落格不一樣,所以特此進行說明
- 首先我並沒有把next【0】=-1
造成的結果是:
我的next陣列每個位置儲存的是當前位置的最大重複子串。但每次不匹配的時候,
要移動到的位置就是不匹配位置k之前的最大子串長度 (注意是k之前)
所以我的是k=next[k-1]
2.下面這段程式碼的一些小細節:
最開始我是這樣寫的(注意劃紅線的地方)
if(j>0){
j=next[j-1];
}else if(j==0){
i++;
}
最開始真的沒仔細思考,沒想到當j=next[j-1]=0,之後造成了又進去了一次,其實這次完全是沒必要的,這裡就是當j=0時,i++,就是當它們一開始(i=0,j=0)就不匹配的時候,
下面是leetcode不符合的情況
一些知道情況的錯誤
這就是上面程式碼說明中的第2種情況
這是沒考慮當要匹配的字串長度為0的情況
一些不知道情況的錯誤
自己也一臉矇蔽,當時也沒處理(debug)因為其他事耽擱了
KMP時間複雜度
建設字元主串的長度為m,模式串為n
有了程式碼過程就很容易得到時間複雜度 我們得到結果的條件是,字元主串或者模式串讀完(注意是讀完,不是讀到)結尾,所以很容易就知道是O(N+M)
leetcode題目
思考和問題
為什麼我用kmp演算法表現還差些?是它用測試用例很極端嗎?
暴力演算法:
KMP演算法:
這個題目還有個捷徑:直接用Java的內建函式(indexof())
問題:
1. 發現自己對Java基礎知識(比如集合)有點遺忘啊(過度依賴idea的提示功能了)
2. 自己應該以計算機的方式去思考