【数据结构与算法】Knuth-Morris-Pratt 算法(KMP算法):一种在字符串中查找子串的算法

本文介绍了KMP算法,一种高效的字符串搜索算法,通过预处理部分匹配表,避免回溯和重复扫描,实现线性时间复杂度。核心是利用部分匹配表处理字符不匹配时的跳跃,显著提高匹配效率。
摘要由CSDN通过智能技术生成

引言

KMP(Knuth-Morris-Pratt)算法是一个在字符串中查找子串的算法,由 Donald Knuth、Vaughan Pratt 和 James H. Morris 共同发明。这个算法的特点是在查找过程中,不会回溯主串,也不会重复扫描已经比较过的子串,因此它的时间复杂度是线性的。它的主要优点是在比对过程中,当一个字符不匹配时,可以跳过一些无需再次比对的字符,从而提高匹配效率。


相关概念

模式串(Pattern String)

在字符串搜索算法中,"模式串"是你正在查找的特定字符串。例如,如果你在一本书中查找单词 “apple”,那么 “apple” 就是你的模式串。

主串(Main String)

相对的,"主串"是你在其中进行查找的大段文本。在上面的例子中,整本书的文本就是主串。

前缀(Prefix)

对于字符串str,其长度为k的前缀是指从第一个字符开始的长度为k的子串。

后缀(Suffix)

对于字符串str,其长度为k的后缀是指从最后一个字符开始的长度为k的子串。

部分匹配表(Partial Match Table)

部分匹配表(Partial Match Table,也称为next数组或者失败函数)是KMP算法的核心组成部分,它是对模式串进行预处理得到的一个数组。这个表用于记录模式串中每个位置之前的子串的最长公共前后缀的长度。


基本思想

KMP算法的主要思想是利用已经部分匹配的有效信息,使得后续的匹配可以跳过那些已知肯定不会匹配的字符。

KMP 算法的核心是一个叫做部分匹配表(Partial Match Table, PMT)或者失败函数的预处理过程。这个表格记录了子串的前缀集合与后缀集合的最长公共元素的长度。在查找过程中,如果发生字符不匹配,我们可以通过这个表格快速移动子串的位置,跳过一些不可能匹配的位置。

例如,对于字符串"ABCDABD",其部分匹配表如下:

字符ABCDABD
PMT0000120

这个表的意思是,在模式串中,位置i之前的子串的最长公共前后缀的长度是PMT[i]。例如,位置5之前的子串是"ABCDAB",它的最长公共前后缀是"AB",长度是2,所以PMT[5]=2。

当在主串中进行匹配时,如果遇到不匹配的字符,可以利用部分匹配表中的信息,将模式串向右滑动一定的距离,从而跳过一些肯定不会匹配的位置,提高匹配效率。

在计算部分匹配表时,通常会使用动态规划的思想,对每个位置,都检查它之前的所有子串,找出最长的那个公共前后缀。

这个部分匹配表在KMP算法中有一个非常重要的作用,那就是当模式串中的某个字符与主串不匹配时,可以根据这个表直接跳过一部分字符,而不需要一个一个地去比对。

通过这种方法,KMP 算法可以在 O(n+m) 的时间复杂度内完成字符串的查找任务,其中 n 是主串的长度,m 是子串的长度。


详细步骤

  1. 预处理阶段
    生成部分匹配表。对于每个位置 i,我们计算子串 s2[1...i] 的最长相等的真前缀和后缀的长度 pmt[i]。这个长度就是当 s2[i+1] 和主串的某个字符不匹配时,我们需要将子串移动到的位置。
	const int N = 1e7 + 7;
	string s1, s2;
	int pmt[N];
	cin >> s1 >> s2;
	s1 = " " + s1;
	s2 = " " + s2;
	int l1 = s1.length() - 1;
	int l2 = s2.length() - 1;
	int j;	// 当前已经匹配的字符数量

	// 生成部分匹配表
	j = 0;
	for (int i = 2; i <= l2; i++) {
		// 下一个字符不匹配
		while (j && s2[i] != s2[j + 1]) {
			// 向前回溯
			j = pmt[j];
		}
		// 下一个字符匹配
		if (s2[i] == s2[j + 1]) {
			// j 后移一位
			j++;
		}
		// 更新部分匹配表
		pmt[i] = j;
	}

  1. 查找阶段
    从左到右扫描主串,同时移动子串的位置。如果主串的某个字符和子串的当前字符不匹配,我们就通过部分匹配表移动子串的位置。具体的移动方法是,将子串的位置向右移动 i - pmt[i] 个位置,其中 i 是子串中最后一个与主串匹配的字符的位置。
	// 查找字符串
	j = 0;
	for (int i = 1; i <= l1; i++) {
		// 下一个字符不匹配
		while (j && s1[i] != s2[j + 1]) {
			// 向前回溯
			j = pmt[j];
		}
		// 下一个字符匹配
		if (s1[i] == s2[j + 1]) {
			// j 后移一位
			j++;
		}
		// 匹配到字符串
		if (j == l2) {
			cout << i - l2 + 1 << endl;
			// 向前回溯,继续查找
			j = pmt[j];
		}
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值