KMP算法next數組求解實現
首先我們通過應用場景將KMP算法中用到的名詞做一個說明:
在一個字符串(string1)中查詢是否存在另一個字符串(string2)。
在字符串匹配算法中,我們通常將字符串string1成爲主串;字符串string2成爲子串。
下面我們將分別說明樸素模式匹配算法和KMP算法,並重點說明KMP中next的求解方式。
1. 樸素模式匹配算法
樸素模式匹配算法步驟:
- 從主串第一個元素和子串第一個元素開始匹配,
- 如果相等,同時後移匹配後續字符;
- 如果不相等,主串指針和子串指針同時回溯(子串回溯到第一個元素,主串回溯到開始匹配元素的下一個元素),之後重複這三個步驟。
效果圖:
樸素匹配算法的C代碼實現如下:
int findMatch(char *Str, char *match)
{
int i=0, j=0;
/*注意:這裏下標從0開始計算*/
while(i < strlen(Str) && j < strlen(match)){
if(Str[i] == match[j]){
i++;
j++;
}else{
i = i - j + 1;
j = 0;
}
}
if( j == strlen(match)){
return i - j;/*指出匹配成功的主串上的位置*/
}else{
return -1;/*匹配失敗*/
}
}
2. KMP算法
計算機界的先人們認爲上述的匹配算法由於主串和子串指針在元素不相等時需要同時回溯,導致匹配效率低下。經過他們的一系列研究,提出了KMP匹配算法。該算法在匹配時無需主串指針進行回溯,執行回溯子串指針即可。
KMP是通過提前求取子串的特徵來優化匹配流程的,該特徵我們稱之爲next數組。它的作用是:當某一位置元素不匹配時,通過next數組來確定子串指針回溯的位置,從而避免每次都從子串的第一個元素開始。
KMP算法步驟:
- 獲取子串的next數字信息
- 字符串匹配
- 如果兩個元素相等,同時向後移動指針,匹配後續字符
- (如果主串元素與子串第一個元素都不等,則向後移動主串指針)
- 如果兩個元素不相等,則回溯子串指針,回溯到的位置爲next中當前位置對應的值
- 重複上述三個步驟
同樣以上述例子爲例進行說明:
KMP算法C代碼實現如下:(代碼實現上從0開始)
int getNext(char *str, int next[])
{
int i = 0;
int j = -1;
if(!str || !next){
printf("Parameters can't be NULL or can't be zero\n");
return -1;
}
/* 下標從1開始
** index : 1 2 3 4 5 6 7 8 9 --i
** value : a b a b a a a b a
** next : 0 1 1 2 3 4 2 2 3 --j
**/
/* 下標從0開始
** index : 0 1 2 3 4 5 6 7 8 --i
** value : a b a b a a a b a
** next :-1 0 0 1 2 3 1 1 2 --j
**/
/* next回溯
** index : 0 1 2 3 4 5 6 7 8
** value : a b a b a a a b a
** a b a b a a a b a
**/
next[0] = -1;
printf("%2.2d ", next[0]);
while(i < strlen(str)-1){
if(j == -1 || str[i] == str[j]){
i++;
j++;
next[i]=j;
printf("%2.2d ", next[i]);
}else{
j = next[j];
}
}
printf("\n");
return 0;
}
// char *str="ababaaaaba";
// char *match="aaa";
int kmp(char *Str, char *match)
{
int i=0,j=0;
int next[100] = {0};
int ret =getNext(match, next);
if(ret != 0){
printf("Get next error\n");
return -1;
}
while(i<(int)strlen(Str) && j<(int)strlen(match)){
if(j == -1 || Str[i] == match[j]){
i++;
j++;
}else{
j = next[j];
}
}
if(j == strlen(match)){
return i - j;
}else{
return -1;
}
}
3. next數組的求解
很多書上在講解KMP算法時,元素都是從1開始的,而實際使用過程中都是從0開始的。這個並不是什麼問題,除此之外,對於代碼的實現也遇到了疑問,下面我將個人在學習KMP的疑問記錄下來:
- i,j下標的初值
- 從0開始與從1開始的區別
- next數組第一個元素的值設置依據
- 兩個元素不等時,j爲什麼要如此回溯?
下面對這幾個疑問進行說明:
int getNext(char *str, int next[])
{
int i = 0;
int j = -1;
if(!str || !next){
printf("Parameters can't be NULL or can't be zero\n");
return -1;
}
next[0] = -1;
while(i < strlen(str)-1){
if(j == -1 || str[i] == str[j]){
i++;
j++;
next[i]=j;
}else{
j = next[j];
}
}
return 0;
}
3.1 j=next[j]的理解
next數組用來表示當前字符之前的串相似程度。那個如果對相似度進行量化呢?這裏我們使用下標來量化相似度。例如:
下標 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
a | b | a | b | a | a | a | b | a | |
next數組 | 0 | 1 | 1 | 2 | 3 | 4 | 2 | 2 | 3 |
上表默認下標從1開始,這也是很多講解KMP算法時的經常的方式。
什麼叫使用下標來量化相似度呢?
比如第5個元素之前的字符串爲"abab",它的前後相似的串爲"ab", 因此當遇到以下情況時,
主串 | a | b | a | b | c | d | e | f | g | i | j |
---|---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | a | b | b | ||||||
匹配情況 | √ | √ | √ | √ | × | ||||||
next | 0 | 1 | 1 | 2 | 3 |
直接從子串的第三個元素開始比較即可(S[5] == T[3] ?),而無需從第一個元素進行(前面元素通過next數組能保證一定相等):
主串S | a | b | a | b | c | d | e | f | g | i | j |
---|---|---|---|---|---|---|---|---|---|---|---|
子串T | a | b | a | b | b | ||||||
匹配情況 | ☆ | ||||||||||
0 | 1 | 1 | 2 | 3 |
next的值代表如果主串和子串元素不等時,主串位置i無需回溯,只需要將子串位置回溯,回溯的位置下標就是對應next數據的值。也就是代碼中的j=next[j]。
那麼j=next[j]
怎麼理解呢?
這裏有個默認前提:對子串遞歸使用KMP。這樣就比較容易理解j=next[j]
。
比如在求取第5個位置的next的值時:
已知第五個元素時,需要知道前四個元素的相似度,在第四個元素時我們已經知道T[1]=T[3], 現在只需要比較T[2] == T[4] ?即可,如果相等,那麼next[5]=next[4]+1; 如果不相等,比如"ab"!=“ac”,那麼就應該回溯到第一個位置的值,即j=next[j]
下標 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
子串T | a | b | a | c | c | a | a | b | a |
子串T | a | b | a | b | c | a | a | ||
next | 0 | 1 | 1 | 2 | ? |
3.2 next數組第一個位置的值
next的第一個值無法進行計算,因爲它之前沒有元素,就沒有辦法計算相似度。這裏是設置默認值的。
那麼默認值的要求是什麼呢?
我們已經知道next的實際上就是下標,而next的第一個元素的值不得與現有的下標衝突:
- 如果下標從1開始,則next[1]可以是小於1的任意整數,因此默認使用0。此時ij的初始值分別爲i=1;j=0;
- 如果下標從0開始,則next[0]可以是小於0的任意整數,因此默認使用-1。此時ij的初始值分別爲i=0;j=-1;
4. 代碼
下面列出完整的實現(包括KMP和樸素匹配算法,下標從0開始)
/*************************************************************************
> File Name: kmp.c
> Author: Toney Sun
> Mail: [email protected]
> Created Time: 2020年06月27日 星期六 21時07分12秒
************************************************************************/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int getNext(char *str, int next[])
{
int i = 0;
int j = -1;
if(!str || !next){
printf("Parameters can't be NULL or can't be zero\n");
return -1;
}
/*
** index : 1 2 3 4 5 6 7 8 9 --i
** value : a b a b a a a b a
** next : 0 1 1 2 3 4 2 2 3 --j
**/
/*
** index : 0 1 2 3 4 5 6 7 8 9 --i
** value : a b a b a a a b a
** next :-1 0 0 1 2 3 1 1 2 --j
**/
/*
** index : 0 1 2 3 4 5 6 7 8
** value : a b a b a a a b a
** a b a b a a a b a
**/
next[0] = -1;
printf("%2.2d ", next[0]);
while(i < strlen(str)-1){
if(j == -1 || str[i] == str[j]){
i++;
j++;
next[i]=j;
printf("%2.2d ", next[i]);
}else{
j = next[j];
}
}
printf("\n");
return 0;
}
// char *str="ababaaaaba";
// char *match="aaa";
int kmp(char *Str, char *match)
{
int i=0,j=0;
int next[100] = {0};
int ret =getNext(match, next);
if(ret != 0){
printf("Get next error\n");
return -1;
}
while(i<(int)strlen(Str) && j<(int)strlen(match)){
if(j == -1 || Str[i] == match[j]){
i++;
j++;
}else{
j = next[j];
}
}
if(j == strlen(match)){
return i - j;
}else{
return -1;
}
}
int findMatch(char *Str, char *match)
{
int i=0, j=0;
while(i < strlen(Str) && j < strlen(match)){
if(Str[i] == match[j]){
i++;
j++;
}else{
i = i - j + 1;
j = 0;
}
}
if( j == strlen(match)){
return i - j;
}else{
return -1;
}
}
void main(int argc, char *argv[])
{
char *str="ababaaaaba";
char *match="c";
int index = kmp(str, match);
printf("-------index=%d------\n",index);
index = findMatch(str, match);
printf("-------index=%d------\n",index);
}