KMP算法实现一行多次匹配详解

本文深入解析KMP算法,包括其避免回溯的核心思想,详细介绍了KMP算法的匹配过程、next数组的计算方法,并展示了优化后的nextval数组。同时,给出了文件中一行多次模式匹配的代码实现,强调了nextval数组在提高效率方面的作用。
摘要由CSDN通过智能技术生成


问题描述

  已知主串存储在文件中,请使用 KMP算法实现一行多次的匹配效果,通过输入待查找的模式串,判断文件中是否包含该模式串,如果包含,则显示模式串在文件中的位置(显示行数和该行第几个字符)


朴素模式匹配算法

  首先遍历主串,找出主串中与模式串长度相同的子串,再让子串与模式串进行对比。

  主串指针 i 和模式串指针 j 都从第 1 个字符开始匹配,如果 i = 4 时发生不匹配,即该位置的字符不是 b,那么有可能是 a。所以我们让 i 回到 2, j 回到 1,继续匹配。
在这里插入图片描述
  这里我们可以看到,当主串指针 i 回到 2 且模式串指针 j 回到 1时,这就导致模式串多进行了两次重复的对比,其实这样的对比是没有意义的。

朴素模式匹配的缺点:
  当某些子串与模式串能够部分匹配时,主串指针 i 就需要回溯,导致时间开销增加。最坏时间复杂度 O(nm)


KMP算法

  KMP算法是一个非常优秀的模式匹配算法,其对于任何模式和目标序列,都可以在线性时间范围内完成匹配查找。KMP模式匹配算法,就很好的解决了主串指针 i 经常回溯的问题。

基本思想: 当子串与模式串发生不匹配时,主串指针 i 不用回溯,而让模式串指针 j 发生回溯。
平均时间复杂度 O(n + m)

KMP算法的匹配过程(图解)

如果 i 当前指向位置的字符不是 e,那么应该怎么做?
在这里插入图片描述
若 j = 6 时发生不匹配,则应让 j 回到 1
这里主串指针 i 并没有发生回溯!

如果 i 当前指向位置的字符不是 l,那么应该怎么做?
在这里插入图片描述
若 j = 5 时发生不匹配,则应让 j 回到 2

如果 i 当前指向位置的字符不是 g,那么应该怎么做?
在这里插入图片描述
若 j = 4 时发生不匹配,则应让 j 回到 1

如果 i 当前指向位置的字符不是 o,那么应该怎么做?
在这里插入图片描述
若 j = 3 时发生不匹配,则应让 j 回到 1

如果 i 当前指向位置的字符不是 o,那么应该怎么做?
在这里插入图片描述
若 j = 2 时发生不匹配,则应让 j 回到 1

结合上述图解,我们总结一下:

  • 若当前两个字符匹配,则应让 i++,j++
  • 若 j = 1 时发生不匹配,则应让 i++ ,而让 j 依然是 1
    这里 j = 1 可以等价于先令 j = 0 再进行 j++
  • 若 j = 2 时发生不匹配,则应让 j 回到 1
  • 若 j = 3 时发生不匹配,则应让 j 回到 1
  • 若 j = 4 时发生不匹配,则应让 j 回到 1
  • 若 j = 5 时发生不匹配,则应让 j 回到 2
  • 若 j = 6 时发生不匹配,则应让 j 回到 1

模式串 google,对应的 next 数组为:

序号 j123456
模式串google
next [ j ]011121

如果 j = k 时才发生匹配失败,令 j = next [ k ]

KMP算法实现一行多次匹配代码实现

void KMP(String p, String s, int next[], int row) {  //KMP算法,p为主串,s为模式串,row为读取文件的行数 
	int i = 0;
	int j = 1;
	int count = 0;  	    //记录模式串 s和主串 p匹配成功的次数 
	int index[MAX_SIZE];	//记录主串匹配成功的位置
	while (i < p.length) {  //遍历主串所有字符 
		//如果模式串第 1个字符不匹配,则让模式串指针 j = 1不变,而让主串指针 i后移 <==> 令 j = 0,然后 j++且 i++
		if (j == 0 || p.str[i] == s.str[j - 1]) {	//主串 p和模式串 s都是从下标为0的位置开始存储
			i++;
			j++;
		} else {  
			j = next[j];   //如果模式串第 j个字符匹配失败,则让指针 j跳到 next[j]的位置 
		}
		if (j > s.length) {   //如果完全匹配  
			index[count++] = i - s.length + 1;   //这里 +1是因为文件中列数从 1开始 
			j = 1;            //重置模式串指针 j指向第 1个字符 
		}
	} 
	if (count != 0) {         //如果匹配成功,输出行和列的值 
		printf("\t\t\t%d", row);
		for (i = 0; i < count; i++) {
			printf("\t%d", index[i]);
		}
		printf("\n");
	}
}

next 数组

  当模式串第 j 个字符匹配失败时,令模式串指针 j 跳到 next [ j ],再继续匹配。
  想要求解 next 数组,我们先了解一下什么是串的前缀和后缀?
串的前缀:包含第一个字符,且不包含最后一个字符的子串。
串的后缀:包含最后一个字符,且不包含第一个字符的子串。

例:模式串 ababc
前缀:abab,aba,ab,a
后缀:babc,abc,bc,c

next 数组求解方法

在这里插入图片描述

方法总结:
  当第 j 个字符匹配失败,由前 1 ~ ( j - 1 ) 个字符组成的串,记为 S, 则 next [ j ] = S的前后缀字符串能匹配上的最大长度 + 1
特别地,next [1] = 0

例:模式串 aaaab,求解其 next 数组

  • next [1] = 0
  • 第2个字符前只有1个字符,组成的S串为 a,其前后缀能匹配上的最大长度为 0,则 next [2] = 0 + 1 = 1
  • 第3个字符前有2个字符,组成的S串为 aa,其前后缀能匹配上的最大长度为 1,则 next [2] = 1 + 1 = 2
  • 第4个字符前有3个字符,组成的S串为 aaa,其前后缀能匹配上的最大长度为 2,则 next [2] = 2 + 1 = 3
  • 第5个字符前有4个字符,组成的S串为 aaaa,其前后缀能匹配上的最大长度为 3,则 next [2] = 3 + 1 = 4

模式串 aaaab,对应的 next 数组为:

序号 j12345
模式串aaaab
next [ j ]01234

例:模式串 ababaa,求解其 next 数组
如果 i 当前指向位置的字符不是 a,那么应该怎么做?
在这里插入图片描述
若 j = 6 时发生不匹配,由前 1 ~ 5 个字符组成的串为 ababa
串的前缀:abab,aba,ab,a
串的后缀:baba,aba,ba,a
  当发生不匹配时,通过让模式串右移,在 j = 4 时模式串前面的字符串 aba 都匹配上了, 其长度为 3,所以我们只需要让 next [6] = 3 + 1 = 4
  同样的,虽然模式串继续往右移,在 j = 2 时模式串前面的字符也能够匹配上,但是我们应该优先考虑模式串能匹配上的长度最长这种情况

  后续的推导过程类似,感兴趣的小伙伴可以自己推导一下。

模式串 ababaa,对应的 next 数组为:

序号 j123456
模式串ababaa
next [ j ]011234

  经过之前的分析,我们可以发现 next [2] 一定是等于 1。因为当 j = 2 时,其前后缀字符串能匹配上的最大长度显然为 0,即 next [2] = 0 + 1 = 1

next 数组代码实现(推荐)

typedef struct {
	char str[MAX_SIZE];
	int length;        //串长
} String;

void getNext(String s, int next[]) {  //获取模式串 next数组,s为模式串 
	int i = 0, j = -1;
	next[0] = -1;
	next[1] = 0;
	while (i < s.length - 1) {
		if (j == -1 || s.str[i] == s.str[j]) {
			i++;
			j++;
			next[i + 1] = j + 1;
		} else {
			j = next[j];
		}
	}
} 

暴力求解 next 数组

void getNext(String s, int next[]) {  //获取模式串 next数组,s为模式串 
	int i = 3;
    //如果模式串第 1个字符不匹配,则让模式串指针 j = 1不变,而让主串指针 i后移 <==> 令 j = 0,然后 j++且 i++ 
	next[1] = 0;  
	next[2] = 1;   //模式串第 2个字符对应位置的 next[2]一定为 1,因为没有前后缀字符串 
	while (i < s.length + 1) {
        //模式串第 i个位置,前后缀字符串最多有 (i - 2)组,且字符串长度最大优先匹配
		for (int k = 0; k < i - 2; k++) {   
            //字符串完全匹配标志位,每组初始默认 key为真 <==> key = 1
			int key = 1;  
            //遍历第 k组前缀字符串,第 k组前(后)缀字符串共有(i - 2 - k)个字符
			for (int j = 0; j < i - 2 - k; j++) {   
                //如果有一对字符不匹配,置标志位 key为假 <==> key = 0
				if (s.str[j] != s.str[j + k + 1]) {  
					key = 0;
					break; 
				}
			}
			if (key == 1) {  //如果第 k组匹配成功,置 next[i] = (i - 2 - K) + 1 
				next[i] = i - k - 1;
				break;
			}
			if (k == i - 3) {  //如果最后一组也没有匹配成功
				next[i] = 1;
			}
		}
		i++;
	}
}

next 数组

这里回顾一下前面对模式串 google 的分析过程。

  若 j = 4 时发生不匹配,即 i 当前指向位置的字符不是 g,按照我们之前求 next 数组的方法,应该让 j 回到第1个字符的位置。但是第1个字符刚好也是 g,很明显和当前不匹配的这个字符是一样的,所有这就导致模式串多进行了一次无意义的对比。
在这里插入图片描述

  其实我们本来就可以利用模式串 google 已存在的这些信息,提前就能知道第1个字符和第4个字符是相同的。所以如果第4个字符发生不匹配,那么肯定和第1个字符也不匹配,其实我们就可以直接让 next [4] = next [1]

对比模式串 google 之前的 next 数组和优化后的 nextval 数组:

序号 j123456
模式串google
next [ j ]011121
nextval [ j ] (优化)011021

nextval 数组求解方法

  • 先求出模式串对应的 next 数组
  • 令 nextval [1] = 0,从左到右依次遍历。
  • 如果当前第 i 个字符与其 next [ i ] 位置的字符相同,则让 nextval [ i ] = nextval [ next [ i ] ]
  • 否则,nextval [ i ] = next [ i ]

例:已知模式串 aaaab,求解其优化后的 nextval 数组

序号 j12345
模式串aaaab
next [ j ]01234

next 数组优化过程如下:

  • nextval [1] = 0
  • 因为 next [2] = 1,第2个字符和第1个字符相同,则 nextval [2] = nextval [1] = 0
  • 因为 next [3] = 2,第3个字符和第2个字符相同,则 nextval [3] = nextval [2] = 0
  • 因为 next [4] = 3,第4个字符和第3个字符相同,则 nextval [4] = nextval [3] = 0
  • 因为 next [5] = 4,第5个字符和第4个字符不相同,则 nextval [5] = next [5] = 4

模式串 aaaab,对应的 nextval 数组为:

序号 j12345
模式串aaaab
nextval [ j ]00004

例:已知模式串 ababaa, 求解其优化后的 nextval 数组

序号 j123456
模式串ababaa
next [ j ]011234

nextval 数组求解过程如下:

  • nextval [1] = 0
  • 因为 next [2] = 1,第2个字符和第1个字符不相同,则 nextval [2] = next [2] = 1
  • 因为 next [3] = 1,第3个字符和第1个字符相同,则 nextval [3] = nextval [1] = 0
  • 因为 next [4] = 2,第4个字符和第2个字符相同,则 nextval [4] = nextval [2] = 1
  • 因为 next [5] = 3,第5个字符和第3个字符相同,则 nextval [5] = nextval [3] = 0
  • 因为 next [6] = 4,第6个字符和第4个字符不相同,则 nextval [6] = next [6] = 4

模式串 ababaa,对应的 nextval 数组为:

序号 j123456
模式串ababaa
nextval [ j ]010104

  到此为止,我们已经学习了 nextval 数组的求解。在 KMP算法中,我们只需要用 nextval 数组替代原来的 next 数组,就可以得到更高的效率。

KMP算法的优化

当 j = k 时发生不匹配,令 j = nextval [ k ]

nextval 数组代码实现

	// next数组的优化
	nextval[1] = 0;
	for (i = 2; i <= s.length; i++) {
		//如果模式串第 i个字符等于要跳转的第 next[i]个字符,则让 nextval[i] = nextval[next[i]]
		if (s.str[i - 1] == s.str[next[i] - 1]) {  
			nextval[i] = nextval[next[i]];
		} else {    //否则,next值保持不变 
			nextval[i] = next[i];
		}
	} 

文件的分行读取

  文件的读取借助 fopen 函数实现,这里我们采取分行读取方式。想要实现一行多次匹配,这里需要定义 index 数组来存储模式串在每一行出现的位置。

一行多次匹配实现方法:
  在进行模式匹配时,如果主串与模式串完全匹配,则把模式串在主串中出现的位置,记录到 index 数组中,再重置模式串指针 j 的位置(令 j = 1 指向第1个字符),而主串指针 i 继续移动,直到遍历完主串所有字符为止。

  采用上述方法,我们能够实现单行多次的模式匹配,那么只需逐行重复上述方法,就可以实现对文件中每一行的多次匹配。

分行读取代码实现

void fileByKMP(String s) {  //逐行重复使用单行多次的模式匹配,s为模式串
	int row = 0;	        //记录行数 
	FILE *fp; 
	char bufStr[MAX_SIZE];
	String p;
	int next[s.length + 1];   // next数组从下标为1的位置开始存储   
	getNext(s, next);         //获取模式串 s对应的 next数组     
	fp = fopen("D:/test.txt", "r");   //打开文件,文件的绝对路径 D:/test.txt 
	if (fp == NULL) {
		perror("\n\n打开文件失败...");
		return;
	}
	printf("\n\n模式串所在位置:\n");
	printf("\t\t\t行\t列\n");
	while (!feof(fp)) {                //如果没有读到文件末尾         
		fgets(bufStr, MAX_SIZE, fp);   //获取文件每行字符串  
		row++;                     
		strcpy(p.str, bufStr);   
		p.length = strlen(p.str);
		KMP(p, s, next, row);     //调用 KMP算法,文件中每一行字符串相当于一个主串
	}
	fclose(fp);                   //关闭文件
	fp = NULL; 
}
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值