一篇幫助理解KMP算法的文章(附C++/Java源碼)

先上需求

給定一個 str 字符串和一個 value 字符串,在 str 字符串中找出 value 字符串出現的第一個位置 (從0開始)。其實就是C語言的 strstr() 以及 Java的 indexOf() 實現原理。

輸入案例:str:"hello" value=“ll”
輸出案例:2

暴力解法

用一個長指針i跟短指針j分別指向主串跟模式串,從左往右,一個一個匹配,如果匹配,如果出現不匹配,那就把長指針移回第 i - j + 1位(假設下標從0開始),j移動到模式串的第0位,然後又重新開始這個步驟:




直到匹配成功,返回出去就好了
於是上代碼

/**

 * 暴力破解法

 * @param str 主串

 * @param value 模式串

 * @return 如果找到,返回在主串中第一個字符出現的下標,否則爲-1

 */

int match(string str, string value) {
    int i = 0; 
    int j = 0; 
    while (i < str.length() && j < value.length()) {
       if (str[i] == value[j]) { 
		   // 當兩個字符相同,就比較下一個
           i++;
           j++;
       } else {
		   // i後退,j重頭開始
           i = i - j + 1; 
           j = 0; 
       }
    }
    if (j == str.length()) {
       return i - j;
    } else {
       return -1;
    }
}

這樣的方法也可以得到結果,但是時間複雜度來到了(n*m),n爲主串的長度,m爲模式串的長度,一般這種暴力匹配的方法就會被老闆暴力地辭退。
這時候Donald Knuth、Vaughan Pratt、James H. Morris三大高叟就想到了能不能不讓i指針不回退,只移動短指針j阿,於是就有了牛逼(頭痛)的KMP算法

KMP算法

由於動畫效果不好製作,貼上知乎的大佬講解,裏面有動畫的呈現
知乎KMP
於是有了我自己對KMP算法的理解
其實KMP算法的基本操作流程如下:

  1. 假設現在文本串 S 匹配到 i 位置,模式串 P 匹配到 j 位置 如果 j = -1,或者當前字符匹配成功(即 S[i] ==
    P[j] ),都令 i++,j++,繼續匹配下一個字符;
  2. 如果 j != -1,且當前字符匹配失敗(即 S[i] != P[j] ),則令 i 不變,j =
    next[j]。此舉意味着失配時,模式串 P相對於文本串 S 向右移動了 j - next [j] 位
  3. 換言之,將模式串 P 匹配位置的 next 數組的值對應的模式串 P 的索引位置移動到未重複出現位置

在這裏插入圖片描述

例如這樣的小串,如果用暴力解法 主串就得從A後的BC重新開始,此時i指針指向主串的B,j指向主串的A
在這裏插入圖片描述

這種情況還不算特別慢,如果是在主串“SSSSSSSSSSSSSA”中查找“SSSSB”,比較到最後一個才知道不匹配,然後i回溯,這個的效率是顯然是最低的。其實用人類的思維,其實只要讓i不動j回到AB第一次重複後的下一個位置就可以
在這裏插入圖片描述此時i還是原來的指向,只是短指針j指向了C
所以,整個KMP的重點就在於當某一個字符與主串不匹配時,我們應該知道j指針要移動到哪?
據說這是有公式的,翻了很久資料(搜索引擎),終於找到了關於KMP的公式:
Value[0 ~ k-1] == Value[j-k ~ j-1]
公式的推導:
當str[i] != Value[j]時
有str[i-j ~ i-1] == Value[0 ~ j-1]
由Value[0 ~ k-1] == Value[j-k ~ j-1]
必然:str[i-k ~ i-1] == Value[0 ~ k-1]

這時候我們就列個表,看看這幾個公式在作甚
模式串value子串對應的各個前綴後綴的公共元素的 最大長度表 下圖。
在這裏插入圖片描述公共元素的最長重複元素就得到是2了
這時候我們只需要的到當前匹配不等的元素在模型串的位置,只需要回溯到上次不等的位置,並且將索引一起移動
將該步驟翻譯成計算機步驟如下
1)找出前綴pre,設爲pre[0~m];
2)找出後綴post,設爲post[0~n];
3)從前綴pre裏,先以最大長度的s[0~m]爲子串,即設k初始值爲m,跟post[n-m+1~n]進行比較:
  如果相同,則pre[0~m]則爲最大重複子串,長度爲m,則k=m;
如果不相同,則k=k-1;縮小前綴的子串一個字符,在跟後綴的子串按照尾巴對齊,進行比較,是否相同。
如此下去,直到找到重複子串,或者k沒找到。
這時候就引進了一個next數組來解決最大長度值的問題,並且要給初始0位置賦上-1(其實這一步是爲了next數組記錄下一個字串位置,並且確保匹配不成功,短指針能夠下移,這一步得看到最後再回來理解)
在這裏插入圖片描述通過next數組我們就能夠記錄模型串的最大長度值
比如模式串的D 與文本串的 C 失配了,找出失配處模式串的 next數組 裏面對應的值,這裏爲 0,然後將索引爲 0 的位置移動到失配處。
貼個代碼,繼續講

int* getNext(string value) {
	int *next = new int[value.length()];
	next[0] = -1;
	int j = 0;
	int k = -1;
	while (j < value.length() - 1) {
		if (k == -1 || value[j] == value[k]) {
			next[++j] = ++k;
		} else {
			k = next[k];
		}
	}
	return next;
}

k其實扮演了很重要的作用,通過k,我們回到重複串上一次結束重複的位置
根據別的博主博客,總結出這樣的規律:
當value[k] == value[j]時,
有next[j+1] == next[j] + 1
其實這個是可以證明的:
因爲在P[j]之前已經有value[0 ~ k-1] == value[j-k ~ j-1]。(next[j] == k)
這時候現有value[k] == value[j],我們是不是可以得到value[0 ~ k-1] + value[k] == value[j-k ~ j-1] + value[j]。

即:value[0 ~ k] == value[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。
其實說了這麼多,next數組的主要構建思路就是,

  1. 用k去臨時記錄重複串的個數,如果value[j]與value[k]相等,此時讓next[j]位置保存之前出現該值的位置,利於回溯
  2. 如果不等,將k值回到上一次不等的位置,繼續1操作,最後把next裏對應模型串的所有位置保存應該回溯的位置。

當理解了next數組其實剩下的就很容易了

int KMP(string str, string value) {
	int i = 0; 
	int j = 0; 
	int* next = getNext(value);
	while (i < str.length() && j < value.length()) {
		if (j == -1 || str[i] == value[j]) { 
			// 當j爲-1時,要移動的是i,當然j也要歸0
			i++;
			j++;
		} else {
			// j回到指定位置
			j = next[j]; 
		}
	}
	if (j == str.length()) {
		return i - j;

	} else {
		return -1;
	}

}

所以整個KMP的算法其實就是在next數組,確保模串中相同子串回溯的位置相等
該算法有小小瑕疵,這裏有更優的算法
在這裏插入圖片描述在這裏插入圖片描述如果兩個字串已經相等,其實回溯是沒有意義的

int* getNext(string value) {
	int *next = new int[value.length()];
	next[0] = -1;
	int j = 0;
	int k = -1;
	while (j < value.length() - 1) {
		if (k == -1 || value[j] == value[k]) {
			if (value[++j] == value[++k]) { 
				// 當兩個字符相等時要跳過
				next[j] = next[k];
			} else {
				next[j] = k;
			}
		} else {
			k = next[k];
		}
	}
	return next;
}

下面貼上java的代碼,這是leecode上strStr()的用KMP解法
在這裏插入圖片描述

    public int strStr(String haystack, String needle) {
    	if(needle.length() == 0) {
    		return 0;
    	}
        if(needle.length() > haystack.length()){
            return -1;
        }
    	int i = 0;
    	int j = 0;
    	int[] next = getNext(needle);
    	char[] t = haystack.toCharArray();
    	char[] p = needle.toCharArray();
    	
    	while(i < t.length && j <p.length) {
    		//1、j回到起點的時候或者值相等都應該下移
    		if(j == -1 || t[i] == p[j]) {
    				j++;
        			i++;
    			
    		}else {
    			j = next[j];
    		}
    	}
    	if(j == p.length) {
    		return i-j;
    	}
    	
    	
    	return -1;
    }
    public int[] getNext(String needle) {
    	char[] p = needle.toCharArray();
    	int[] next = new int[p.length];
    	int j = 0;
    	int k = -1;
    	next[0] = -1;
    	while(j < p.length - 1) {
    		//兩者相等或者k處於next的初始位置
    		if(k == -1 || p[j] == p[k]) {
    			j++;
    			k++;
    			if(p[j] == p[k]) {
    				next[j] = next[k];
    			}else {
    				next[j] = k;
    			}
    			
    		}else {
    			k = next[k];
    		}
    	}
    	return next;
    }

如果有不正確的地方,希望大家能夠指出來,第一次看KMP算法,以前都是直接那indexOf套

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章