浅入深出KMP算法,求解next表的原理及优化

文章介绍了KMP算法的核心思想,即通过构建最大前后缀相等串的长度表(len表)来优化字符串匹配过程,减少了不必要的比较。通过next表和nextval表进一步提高了查询效率,避免重复回溯。并提供了暴力方法和KMP算法的代码示例,展示如何查找字符串的匹配位置。
摘要由CSDN通过智能技术生成

1.什么是最大前后缀相等串

字符串abcab,他的所有前缀串是a,ab,abc,abca他的所有后缀串是bcab,cab,ab,b。他们的最长相等的串是ab。

下图是查找字符串abcaba的过程,当最后一个字符c不等于a时需要把模式串右移,暴力做法是每次不相等时移动一位,然后比较每个字符是否相等直到完全匹配为止。看下图可知第1步到第4步其实就是查询abcab的的最大相等的前后缀串的长度。如果我们事先已经求出了这个值,那么第2,3步直接可以跳过,并且直接从c字符往后继续比较就行。这就是kmp算法的核心。

                                                        ab是表格第一行字符串abcab的后缀串

步骤

a

b

c

a

b

c

a

b

a

b

a

1

a

b

c

a

b

a

2

a

b

c

a

b

a

3

a

b

c

a

b

a

4

a

b

c

a

b

a

                                                        ab是表格第四行字符串abcab的前缀串

2.如何求最大前后缀相等串的长度

求ABBACABBAB的所有子串的最大前后缀相等的串的长度。

索引i

最大前后缀相等的串长度len[i]的值

next[i]nextval

子串字符串pstr

0

0

-1-1

A

1

0

00

AB

2

0

00

ABB

3

1

0-1

ABBA

4

0

11

ABBAC

5

1

0-1

ABBACA

6

2

10

ABBACAB

7

3

20

ABBACABB

j-1

4

3-1

ABBACABBA

j

?       len[j]

?  next[j]?  nextval[j]

ABBACABBAB

求子串pstr[j]的最大前后缀串长度len[j]的值。

通过表观察很显然len[j]的值依赖于前一个子串pstr[j-1]的解,如果字符p[j] = p[len[j-1]]相等,通俗的说就是前面最大前后缀串的基础上再加长1个字符也相等。那么len[j]=len[j-1] + 1。如果不相等就需要向前回溯。下面详细解释回溯。

例如本题,当j=9时,字符C不等于BABBACABBAB的最大前后缀串的长度。

第一步,在红色ABBA字符串中找到一个最长前缀串str1,在蓝色字符串ABBA中找到一个最长后缀串str2,使得str1= str2;

第二步,找到最长str1=str2后,本例子是红色串"A",  ABBA,再比较p[j]和p[strlen(str1)]这2字符,如果相等那么ABBACABBAB 的最大前后缀串的长度就是strlen(str1) + 1。如果不相等再次同样的逻辑回溯,直到回溯到第一个字符。本例子中ABBACABBAB,p[strlen(str1)] = B, p[j] = B,所以n[j] = 2。

重点:因为红色串是ABBA是pstr[j-1]的最大前缀串,蓝色串ABBA是pstr[j-1]的最大后缀串,所以ABBA=ABBA,所以第一步的问题就是找pstr[ n[j-1] - 1](长度1开始,索引0开始,长度对应到索引需要-1) 即pstr[3]的最大前后缀串。即是之前已经求出的n[3]值。这样取上一个结果的步骤就是回溯。

步骤

a

b

c

a

b

c

a

b

a

b

a

1

a

b

c

a

b

a

2

a

b

c

a

b

a

3

a

b

c

a

b

a

4

a

b

c

a

b

a

从上面表格看,当字符c和a不等时,其实是找abcab的最大前后缀相等的串,是取用的不等字符往前偏移一位的len值。如果从哪个字符不等就取哪个位置的len值,那只需在len表的最前面插入一位就行,通常插入-1。这就是常说中的next表。 kmp的核心是len表,只需len表就能进行查找,为了编码方便引入了next表。为了进一步提高查询效率引入了nextval表。

index

0

1

2

3

a

a

a

c

len

0

1

2

0

next

-1

0

1

2

nextval

-1

-1

-1

2

步骤

a

a

a

b

a

a

a

c

1

a

a

a

c

2

a

a

a

c

3可优化

a

a

a

c

4可优化

a

a

a

c

5

a

a

a

c

上表是按照next表进行比较的流程,很明显第3,4步骤可以省略掉,因为第2步已经比较了b和a,后面步骤3,4的二次比较必定不相等。所以可以在next的表基础上再进行优化得道nextval表。

当p[j] != p[next[j]] 时nextval[j] = next[j],当p[j] == p[next[j]]时往前回溯。

下面代码展示了使用暴力循环,len表,next表,nextval表查询匹配字符串的方法。

void CreateTables(const char* pattern, int nlen[], int next[], int nextval[])
{
	//当p[j] != src[i] 时,实际找的是pstr[j-1]的最大前后缀长度
	//在nlen前插入-1,就是next数组
	//   	ABABABD
	//nlen	0012340
	//next -1001234
	
	int j = 0;
	next[0] = -1;
	nlen[j] = 0;//一个字符的串,最大前后缀串长度必定是0.
	//循环求pattern所有子串的最大前后缀串长度
	for(int j = 1; j < strlen(pattern); j++)
	{
		//前个解的基础上再加一个字符也相等,显然新解就是前面的解+1.
		if(pattern[j] == pattern[ nlen[j-1] ])
		{
			nlen[j] = nlen[j-1] + 1;
		}
		else
		{
			int pre = nlen[j-1] - 1; // 回溯上一个解。 即回溯到字符串长度为n[j-1],索引位置为n[j-1]-1的解 
			while (pre >=0) 
			{
				if(pattern[j] == pattern[ nlen[pre] ])
				{
					nlen[j] = nlen[pre] + 1;
					break;
				}
				else
				{
					pre = nlen[pre] - 1; // 不相等继续回溯。
				}
			}
			if(pre == -1)
				nlen[j] = 0;
			
		}

		next[j] = nlen[j - 1];
	}
	
//	根据nlen算偏移一位的nextval'
//	for(int j = 1; j < strlen(pattern); ++j)
//	{
//		int k = nlen[j-1];
//		while(k > 0)
//		{
//			if(pattern[j] != pattern[ k ])
//			{
//				nval[j-1] = k;
//				break;
//			}
//			else
//			{
//				k = nlen[k -1];
//			}
//		}
//		if(k == 0)
//			nval[j-1] = 0;
//	}
	
	
	//根据next算nextval
	for(int i = 0 ; i < strlen(pattern); ++i)
	{
		int k =  next[i];
		if(k>=0)
		{
			if(pattern[i] != pattern[k])
				nextval[i] = k;
			else
				nextval[i] = nextval[k];
		}
		else
			nextval[i] = k;
	}
	

	
	
	
}

int FindStr(const char* src, const char* pattern)
{
	if(strlen(src) < strlen(pattern))
		return -1;
	
	int *pnlen = new int[strlen(pattern)];
	int *pnext = new int[strlen(pattern)];
	int *pnextval = new int[strlen(pattern)];
	CreateTables(pattern, pnlen, pnext, pnextval);
	
	for(int i = 0 ;i < strlen(pattern); ++i)
	{
		printf("nlen:%d, next:%d,  \tnextval:%d,  \t%.*s\n", pnlen[i], pnext[i], pnextval[i],  i+1 , pattern);
	}
	
	int index0 = -1;
	//暴力循环 查找出匹配的第一个位置索引。
	for(int start=0; start<strlen(src); ++start)
	{
		int i = start;
		if(strlen(src) - i >= strlen(pattern))
		{
			bool bfind = true;
			for(int j=0; j<strlen(pattern); ++j, ++i)
			{
				if(src[i] != pattern[j])
				{
					bfind = false;
					break;
				}	
			}
			
			if(bfind)
			{
				index0 = start;
				break;
			}
		}
	}
	
	int index1 = -1;
	//根据nlen 查找出匹配的第一个位置索引。
	{
		for(int i = 0, j = 0; j < strlen(pattern) && i <strlen(src); )
		{
			if(src[i] != pattern[j])
			{
				if(j>=1)
					j =  pnlen[j-1];
				else
					i++;
			}
			else
			{
				if(j == strlen(pattern) - 1)
				{
					index1 = i - (int)strlen(pattern) + 1;
					break;
				}
				else
				{
					j++;
					i++;
				}
			}
		}
	}
	int index2 = -1;
	//根据next 查找出匹配的第一个位置索引。
	{
		for(int i = 0, j = 0 ; i < strlen(src);)
		{
			if(src[i] != pattern[j])
			{
				j = pnext[j];
				if(j<0)
				{
					i++;
					j = 0;
				}
			}
			else
			{
				if(j == strlen(pattern) - 1)
				{
					index2 = i - (int)strlen(pattern) + 1;
					break;
				}
				else
				{
					i++;
					j++;
				}
			}
		}
		
	}
	int index3 = -1;
	//根据nextval 查找出匹配的第一个位置索引。
	{
		for(int i = 0, j = 0 ; i < strlen(src);)
		{
			if(src[i] != pattern[j])
			{
				j = pnextval[j];
				if(j<0)
				{
					i++;
					j = 0;
				}
			}
			else
			{
				if(j == strlen(pattern) - 1)
				{
					index3 = i - (int)strlen(pattern) + 1;
					break;
				}
				else
				{
					i++;
					j++;
				}
			}
		}
		
	}
	assert(index0 == index1);
	assert(index1 == index2);
	assert(index2 == index3);
	return index1;
}

int main()
{
	int i = FindStr("BABABABDAA", "ABABDA");
	cout << "index:" << i << endl;
	return 0;
}

运行结果

nlen:0, next:-1,  	nextval:-1,  	A
nlen:0, next:0,  	nextval:0,  	AB
nlen:1, next:0,  	nextval:-1,  	ABA
nlen:2, next:1,  	nextval:0,  	ABAB
nlen:0, next:2,  	nextval:2,  	ABABD
nlen:1, next:0,  	nextval:-1,  	ABABDA
index:3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值