KMP算法代码及图文方式详解

KMP算法中的next数组

KMP算法是经典的字符串模式匹配算法,比如主串str = “goodinitialize”,模式(子)串s = “init”。需要找到模式串s 在主串str中的位置。

相比暴力解法的时间复杂度为O( n×m ),其中n为主串长度,m为模式串长度,KMP算法可将时间复杂度降为O( n+m )

KMP算法,需要求解模式串的next数组,在字符串模式匹配的过程中,当模式串不匹配时,不进行回溯,而是根据已经得到的部分匹配结果,将模式串尽可能向右滑动,继续比较

注:以 1 作为模式串下标起始以及next数组起始下标,next[0]不存储数据,规定next[ 1 ] = 0


 前后缀以及真前后缀的概念

前缀:符号串左部的任意子串(或者说是字符串的任意首部,可包含本身)
真前缀(又称前缀真子串):是指不包含本身的前缀
后缀:符号串右部的任意子串(或者说是字符串的任意尾部,可包含本身)
真后缀:是指不包含本身的后缀

举个例子:字符串"initialize"

所有前缀: i   in   ini   init   initi   initia   initial   initaili   initializ   initialize

所有后缀:e  ze  ize  lize  alize  ialize  tialize  itialize  nitialize  initialize

所有真前缀:i   in   ini   init   initi   initia   initial   initaili   initializ

所有真后缀:z  iz   liz   aliz   ializ   tializ   itializ   nitializ   initializ

注:单个字符存在前后缀,就是其本身,但不存在真前后缀


引入next数组求解方式

规定next[ 1 ] = 0

当第 i 个位置,其真前后缀部分,不存在相同的真前后缀,则next[ i ] = 1

当第 i 个位置,其真前后缀部分,存在相同的最长真前后缀,且长度位 m 时,next[ i ] = m + 1

 

通过真前后缀方式,快速求解模式串next数组过程,以模式串aabaaab为例

  ①规定:next[ 1 ]  = 0

 

 ② 字符aa,其真前后缀(字符串a),不存在真前后缀相等的情况,next[ 2 ] = 1

  ③ 字符aab,其真前后缀(字符串aa),存在相同最长真前缀a与最长真后缀a,字符串长度为1,next[ 3 ] = 1 + 1 = 2

 

 ④ 字符串aaba,其真前后缀(字符串aab),不存在真前后缀相等的情况,next[ 4 ] = 1

  ⑤ 字符串aabaa,其真前后缀(字符串aaba),存在相同的最长真前缀a与最长真后缀a,字符串长度为1,next[ 5 ] = 1 + 1 = 2

 

 ⑥ 字符串aabaaa,其真前后缀(字符串aabaa),存在相同的最长真前缀aa与最长真后缀aa,字符串长度位2,next[ 6 ] = 1 + 2 = 3

 

 ⑦ 字符串aabaaab,其真前后缀(字符串aabaaa),存在相同的最长真前缀aa与最长真后缀aa,字符串长度2,next[ 7 ] = 1 + 2 = 3


 

关于某个next[ i ] 的值求解,图解原理

以next[ 11 ] 为例,假设next[ 10 ] = 5

next[ 10 ] = 5,说明前面4个字符与后面4个字符相同,若此时模式串s[ 5 ]与s[ 10 ]相同,

则next[ 11 ] = next[ 10 ] + 1 = 6

 若next[ 5 ] 与next[ 10 ] 不相同,假设next[ 5 ] = 2,说明前面1个字符与后面一个字符相同

(即s[ 1 ] = s[ 4 ]),又因为两个红色矩形框部分字符相同,可推出,第二个红色矩形框内存在

s[ 6 ] = s[ 9 ],即四个黑色矩形框内的字符相同

若此时s[ 2 ]与s[ 10 ] 相等,则next[ 11 ] = 2 + 1 = 3,若s[ 2 ] 与s[ 10 ] 不相同,继续递归操作

比较s[ 1 ]与s[ 10 ] 是否相同,相同则next[ 11 ] = 1 + 1 = 2,当比较到第一位字符时仍不相同,递归结束,next[ 11 ] = 1


 

 求解next数组代码

//计算next数组
void Next(SString T, int* next) {
	next[1] = 0;
	int j = 0;
	int i = 1;
	while (i < (int)(T[0])) {
		if (j == 0 || T[i] == T[j]) {
			i++;
			j++;
			next[i] = j;
		}
		else {
			j = next[j];
		}
	}
}

next 数组考虑的是除当前字符外的最长相同前缀后缀,即最长真前缀,当主串与模式串的某一位字符不匹配时,模式串回退next[ j ]位置继续比较,next[ j ]的值:第 j 为 位字符真前缀重合字符加1

 

仍以模式串aabaaab为例,求模式串的next数组

初始状态,i = 1,规定next[ 1 ] = 1,j = 0

执行第一次循环,满足 if 条件( j == 0),此时 i 各增 1( i = 2,j = 1)next[ 2 ] = j = 1

执行第二次循环,满足 if 条件( T[ i ] == T[ j ] ),此时 i各增 1( i = 3,j = 2)next[ 3 ] = j = 2

执行第三次循环,不满足 if 条件,进入else语句,j = next[ j ] = 1,执行第四次循环,同第三次,进入else语句,j = next[ j ] = 0

执行第五次循环,满足 if 条件( j == 0),此时 i各增 1( i = 4,j = 1)next[ 4 ] = j = 1

执行第六次循环,满足 if 条件( T[ i ] == T[ j ] ) i各增 1( i = 5,j = 2)next[ 5 ] = j = 2

执行第七次循环,满足 if 条件( T[ i ] == T[ j ] ) i各增 1( i = 6,j = 3)next[ 6 ] = j = 3

执行第八次循环,不满足 if 条件,进入else语句,j = next[ j ] = 2

执行第九次循环,满足满足 if 条件( T[ i ] == T[ j ] ) i各增 1( i = 7,j = 3)next[ 7 ] = j = 3

算法很巧妙的在 if 条件语句中设置了 j == 0 这一条件,避免了访问模式串的数组越界,当满足该条件时,i 和 j 的值都增 1 


 

 KMP算法

当求得next数组后,在进行主串与模式串匹配的过程中,若出现的主串 i 位置不匹配时(此时模式串位于第 j 个位置),不再回溯主串,而是移动模式串,取模式串中的第next[ j ] 位置继续同主串第 i个位置进行比较

int KMPIndex(SString S, SString T, int pos) {
	int i = pos;
	int j = 1;
	int next_arr[255];
	//获取串T中的next数组
	Next(T, next_arr);
	//Nextval(T, next_arr);
	while (i <= (int)(S[0]) && j <= (int)(T[0])) {
		if (0 == j || S[i] == T[j]) {
			i++;
			j++;
		}
		else {
			j = next_arr[j];
		}
	}
	//模式匹配成功
	if (j > (int)(T[0])) {
		return i - (int)(T[0]);
	}
	else {
		return 0;
	}
}

 

仍以模式串aabaaab为例,主串为abcaabaaabc,匹配的起始位置pos假设为1,获取的next数组

进入第一次循环,比较 i j 所指字符是否相等,此时(i = 1,j = 1),字符相等,满足 if 条件(主串S[ i ]  == T[ j ]),i j 各增 1(i = 2,j = 2)

进入第二次循环,比较 i j 所指字符是否相等,不相等,不满足 if 条件,进入else语句

此时 j = next[ j ] = 1

进入第三次循环,比较 i j 所指字符是否相等,不相等,不满足 if 条件,进入else语句

此时 j = next[ j ] = 0

进入第四次循环, 满足 if 条件( j == 0 ),i j 各增 1(i = 3,j = 1)

进入第五次循环,比较 i j 所指字符是否相等,不相等,不满足 if 条件,进入else语句

此时 j = next[ j ] = 0

进入第六次循环, 满足 if 条件( j == 0 ),i j 各增 1(i = 4,j = 1)

此后,直至循环结束,满足 if 语句,模式串匹配成功,返回相应索引坐标


 

next数组算法改进

前面定义的next数组仍存在一定的缺陷,例如模式串为aaaab同主串aabaaaab匹配时,当 i = 4

j = 4 时,S[ 4 ] ≠ T[ 4 ] ,但next数组的指示还需要进行 i = 4,j = 3; i = 4,j = 2; i = 4,j = 1;

这三次重复比较,对next数组进行改进

void Nextval(SString T, int* next) {
	next[1] = 0;
	int j = 0;
	int i = 1;
	//T[0]存储模式串的长度
	while (i < (int)(T[0])) {
		//若j == 0时,索引下移一位
		//判断前缀与后缀是否相等
		if (0 == j || T[i] == T[j]) {
			i++;
			j++;
			if (T[i] != T[j]) {
				next[i] = j;
			}
			else {
				next[i] = next[j];
			}
		}
		else {
			j = next[j];
		}
	}
}

仍以模式串aabaaab为例,此时求出的next数组的值分别为:0、0、2、0、0、3、2,这里不做过多赘述


 

Java方式实现KMP算法

class Solution {
    public int strStr(String haystack, String needle) {
        int n = haystack.length(), m = needle.length();
        for (int i = 0; i + m <= n; i++) {
            boolean flag = true;
            for (int j = 0; j < m; j++) {
                if (haystack.charAt(i + j) != needle.charAt(j)) {
                    flag = false;
                    break;
                }
            }
            if (flag) {
                return i;
            }
        }
        return -1;
    }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值