KMP算法

KMP算法

克努特-莫里斯-普拉特操作(KMP算法),由朴素模式匹配算法改进。

适用范围,复杂度分析等,教材写的就很清楚,我之后不再赘述。

不同资料虽然核心思想相同,但具体实现不同,比如一些变量定义等,就看本文就好……

朴素模式匹配算法

说明

主串长度为n,模式串长度为m

朴素模式匹配算法:

将主串中所有长度为m的子串依次与模式串对比,直到找到一个完全匹配的子串或所有的子串都不匹配为止。

其它:

主串中最多有n-m+1个子串

注意,字符串

char str[] = "The String";

'\0'

结尾。

没有变量直接记录其长度,当然也不需要。

记主串为major,模式串为target,即

const char* major = "abaabaabcabaabc";
const char* target = "ababaa"; 

声明三个指针,作用与初始如下

char* it = major;//“正在”匹配的子串
char* it1 = major;//主串遍历用
char* it2 = target;//模式串遍历用

某次对比过程
在这里插入图片描述

例子

主串S:abaabaabcabaabc
模式串T:abaabc
1it
abaabaabcabaabc
abaabcX
2it
abaabaabcabaabc
aX
3it
abaabaabcabaabc
abX
4it
abaabaabcabaabc
abaabc

顺便说一下,it的移动,代表模式串的“滑动”

代码

针对字符串的代码,注意字符串’\0’代表结束。

/**
 * 字符串朴素模式匹配.
 *
 * \param major 主串
 * \param target 模式串
 * \return 第一次出现位置的地址
 * \return NULL没找到
 */
char* mathingNative(const char* major, const char* target);

char* mathingNative(const char* major, const char* target)
{
	char* it = major;//“正在”匹配的子串
	char* it1 = major;//主串遍历用
	char* it2 = target;//模式串遍历用
	while ((*it1 != '\0') && (*it2 != '\0'))
	{
		if (*it1 == *it2)
		{
			it1++;
			it2++;
		}
		else//某次匹配失败,则看下一个子串
		{
			it++;//下一个(模式串滑动到下一个)
			it1 = it;//回溯至“下一个”的开头
			it2 = target;//回溯至模式串开头
		}
	}
	//*it2 == '\0'说明都匹配成功了
	if (*it2 == '\0')
		return it;
	else
		return NULL;	
}

其实就是实现了标准库<string.h>中的strstr函数的功能。

返回的是地址,如果想知道是第几个(从0开始数数,从1开始数就再+1),只需要

(int)(mathingNative(s, t) - s)

KMP算法说明

由朴素模式匹配算法改进。

仍然

记主串为major,模式串为target

声明三个指针,作用与初始如下

char* it = major;//“正在”匹配的子串
char* it1 = major;//主串遍历用
char* it2 = target;//模式串遍历用

const char* target = "ababaa";

在朴素模式匹配算法中,一旦发现当前这个子串(it)中某个字符(*it1和*it2)不匹配,就只能转而匹配下一个子串(从头开始)。

也就是,it++,it1回溯到it,it2回溯到子串开头target

注意到:当出现部分匹配时,不匹配的字符之前,一定是和模式串一致的。模式串可以“滑动”一下,跳过主串的部分子串,并从另一位置继续比较。

例如
主串majorabaab??????????
模式串targetabaabcX
abaabc

具体,模式串中,当某个元素匹配失败时滑动多少(it变成啥),之后从哪里继续比较(it2跳转到哪里),由模式串本身内容决定,和主串没有关系(请往后看)。

模式串中,某个元素匹配失败时,it2跳到哪里,用next[]数组记录

根据模式串T,求出 next 数组,然后利用next数组进行匹配(主串指针it1不回溯),

模式串的“滑动”与it2的“跳转”

next[]数组、it2的“跳转”、it的“滑动”说明

注意下文中next[]数组中保存的是编号

另next[ j ] = k,则 next [ j ]表明当模式串中 j 号字符与主串中相应字符“失配”时,在模式串中需重新和主串中该字符进行比较的字符的位置。

也就是,it2要“跳转”到的位置的编号。

而 it 滑动的长度为, j - k ,即 it += j - k.

请看

根本没有出现部分匹配

某次对比中发现0号(开头元素)不匹配,也就是在之前的匹配中根本没有出现部分匹配

0
----------------------------------------------------------------------------------------------------------------------------------------------------------------
当前:it/it1
master??????
ababaa
it2
之后:it/it1
master??????
ababaa
it2

要直接看下一个“子串”。记next[0] = 0;吧

之后需要

it++;
it1 = it;
出现部分匹配

在之前的匹配过程中,出现部分匹配,前面是匹配的

某次对比中发现1号(开头后一个元素)不匹配

1
----------------------------------------------------------------------------------------------------------------------------------------------------------------
当前:itit1
mastera?????
ababaa
it2
之后:it/it1
mastera?????
ababaa
it2

it2指向了0号,故

next[1] = 0;

显然任何串的,next[1]都一样为0。

模式串向后滑动 1 - 0 = 1

某次对比中发现2号不匹配

2
----------------------------------------------------------------------------------------------------------------------------------------------------------------
当前:itit1
masterab????
ababaa
it2
之后:it/it1
masterab????
ababaa
it2

it2指向了0号,故

next[2] = 0;

模式串向后滑动 2 - 0 = 2

某次对比中发现3号不匹配

3
----------------------------------------------------------------------------------------------------------------------------------------------------------------
当前:itit1
masteraba???
ababaa
it2
滑动后:itit1
masteraba???
ababaa
it2

it2指向了1号,故

next[3] = 1;

模式串向后滑动 3 - 1 = 2

某次对比中发现4号不匹配

4
----------------------------------------------------------------------------------------------------------------------------------------------------------------
当前:itit1
masterabab??
ababaa
it2
滑动后:itit1
masterabab??
ababaa
it2

it2指向了2号,故

next[4] = 2;

模式串向后滑动 4 - 2 = 2

某次对比中发现5号不匹配

5
----------------------------------------------------------------------------------------------------------------------------------------------------------------
当前:itit1
masterababa?
ababaa
it2
滑动后:itit1
masterababa?
ababaa
it2

it2指向了3号,故

next[5] = 3;

模式串向后滑动 5 - 3 = 2

显然,所有一般不匹配情况都只需

//得到不匹配的元素编号,用it1 - it 也行
uint16_t j = it2 - target;

it += j - next[j];//“滑动”,记录位置,用 it = it1 - next[j];也行
it2 = target + next[j];//it2跳转

利用next数组进行匹配

首先是,利用next数组进行匹配的代码

/**
 * 字符串模式匹配(KMP).
 *
 * \param major 主串
 * \param target 模式串
 * \return 第一次出现位置的地址
 * \return NULL没找到
 */
char* matchingKMP(const char* major, const char* target);

char* matchingKMP(const char* major, const char* target)
{
	int* next = getNext(target);//得到next数组
	
	char* it = major;//“正在”匹配的子串
	char* it1 = major;//主串遍历用
	char* it2 = target;//字串遍历用

	while (*it1 != '\0' && *it2 != '\0')
	{
		if (*it1 == *it2)
		{
			it1++;
			it2++;
		}
		else
		{
			if (it2 == target)//之前根本没有匹配的
			{//看下一子串
				it++;
				it1 = it;
			}
			else//之前有匹配的
			{
				it2 = target + next[(it2 - target)];//“跳转”,“滑动”
				it = it1;//更新位置
			}			
		}
	}

	free(next);

	if (*it2 == '\0')
	{
		return it;
	}
	else
		return NULL;
}

求next[]数组

说明

观察前面的例子

--------------------------------------------------------------------------------------------------------------------------------
3aba???
ababaa
4abab??
ababaa
5ababa?
ababaa

it2要跳到的位置,编号next[j]为(j>=2)

对前j个字符(0到j-1,不含j号字符)形成的新字符数组,寻找最大正整数k,且k<j,使得“前k个和后k都一样

其中前k个称“前缀”,后k个称“后缀

例如:

3中,0号’a’和2号’a’一样,则next[3]=1

4中,0-1和2-3,“ab”,"ab"一样,则next[4]=2

5中,0-2和2-4,“aba”,"aba"一样,则next[5]=3

--------------------------------------------------------------------------------------------------------------------------------
3aba
4abab
5ababa

如果找不到,则next[j]为0

例如:

2中,找不发到,则next[2]为0

--------------------------------------------------------------------------------------------------------------------------------
2ab

或者说:

next[j]=

前j个字符(0到j-1,不含j号字符)形成的新字符数组的最大前后缀长度

对于next[1],it2跳到0号位置,所以next[1]=0。

其实也符合上面的要求,就是找不到的情况

1--------------------------------------------------------------------------------------------------------------------------------
a?????
ababaa
ababaa

特殊的next[0],it2跳到0号位置,所以next[0]=0,(按我的代码其实不用管它,是几都行)

0--------------------------------------------------------------------------------------------------------------------------------
??????
ababaa
ababaa
暴力求解

由上面的说明,可以直接得到容易理解的代码,其效率当然不怎么样。

/**
 * 求next数组便于理解的方法.
 *
 * \param str 要求的数组
 * \return 求得的数组首地址
 * \return NULL 空间申请失败或参数错误
 */
uint16_t* getNextEasyUnderstand(const char* str);

uint16_t* getNextEasyUnderstand(const char* str)
{
	//得到字符串长度
	uint16_t length = strlen(str);	

	//不应当传入空字符串
	if (length < 1) return NULL;

	uint16_t* next = (uint16_t*)malloc(length * sizeof(uint16_t));
	if (next == NULL) return NULL;
	for (uint16_t i = 0; i < length; i++) next[i] = 0;//开始都置为0

	for (uint16_t j = 1; j < length; j++)
	{
		//对前j个字符(0到j-1,不含j号字符)形成的新字符数组
		//看看前k个和后k个是不是都一样
		//k从大到小,得到的就是最大值
		for (uint16_t k = j - 1; k > 0; k--)
		{			
			if (strncmp(str, str + j - k, k) == 0)//它==0代表相等
			{//找到了
				next[j] = k;
				break;
			}
		}
	}

	return next;
}
next[]正常求解方法

需要使用“递推”的思想,并且看成字符串匹配问题,模式串既是“主串”也是“模式串”

const char* target = "ABACABABAC";//长度10

可以暴力求解得到其next[]数组为

uint16_t next[10] = {0, 0, 0, 1, 0, 1, 2, 3, 2, 3};

以此为例进行说明

设,已知next[j] = k, j>=1

一、k>=1

​ 说明,前k个和后k个完全一致,即
′ t 0 . . . t k − 1 ′ = ′ t j − k . . . t j − 1 ′ 't_0...t_{k-1}' = 't_{j-k}...t_{j-1}' t0...tk1=tjk...tj1
​ 看第k+1个(t[k])和第j+1个(t[j])

1.若 t[j] == t[k]

′ t 0 . . . t k − 1 t k ′ = ′ t j − k . . . t j − 1 t j ′ 't_0...t_{k-1}t_{k}' = 't_{j-k}...t_{j-1}t_{j}' t0...tk1tk=tjk...tj1tj

​ 说明,前k+1个和后k+1个完全一致。 则

next[j+1] = k + 1;

​ 例如,在target中,已知next[6]=2,求next[7]时

----------------------------------------------------------------------------------------------------------------
itit1(t[j])j=6
ABACABABAC
ABACABABAC
it2(t[k])k=2

​ 发现t[6]==t[2],故next[7] = 2 + 1 = 3。

循环中这部分对应代码为

uint16_t j = it1 - target;
uint16_t k = it2 - target;//注意这里!
//在循环中,k必须这样得到,而不能 k = next[j];,原因看后面
    
next[j + 1] = k + 1;
it1++;
it2++;
2.若 t[j] != t[k]

​ 就是,部分匹配,而k号元素不匹配了,这就是前面将KMP中的情况。

​ 需要滑动,跳转,然后在循环中继续进行判断(也就是可能需要滑动不止一次)。最终会转为其它情况。

​ “滑动、跳转”后得到新的k, k’= next[k],k’‘=next[k’]…… k 不再是开始的next[j],所以在循环中必须

uint16_t k = it2 - target;//注意这里!
//在循环中,k必须这样得到,而不能 k = next[j];

​ 例如,在target中,已知next[7]=3,求next[8]时

--------------------------------------------------------------------------------------------------------------------------------
itit1(t[j])j=7
ABACABABAC
ABACABABAC
it2(t[k])k=3
itit1
ABACABABAC
ABACABABAC
it2

​ 然后,在循环中继续进行判断。

​ 这部分的代码

uint16_t k = it2 - target;//注意这里!
//在循环中,k必须这样得到,而不能 k = next[j];

it2 = target + next[k];//it2跳转,“滑动”
it = it1 - next[k];//记录位置
二、k==0

之前不存在已经匹配的

1.若 t[j] == t[k]

之前没有匹配的,现在有了一个匹配的,故

next[j+1] = 1;

也可以在循环中使用,一、中1.的代码,(k=0)

next[j+1] = k + 1;

例如,在target中,已知next[4]=0,求next[5]时

--------------------------------------------------------------------------------------------------------------------------------
it/it1(t[j])j=4
ABACABABAC
ABACABABAC
it2(t[k])k=0

得next[5]=1

2.若 t[j] != t[k]

之前没有匹配的,现在任然没有,故

next[j+1] = 0;

然后看后面的

it++;
it1 = it;

例如(换个例子啊)

abcdabcd中next[3]=0

--------------------------------------------------------------------------------------------------------------------------------
it/it1(t[j])j=3
abcdabcd
abcdabcd
it2(t[k])k=0

而t[3]!=t[0],故next[4]=0。

循环结束条件

注意循环的结束条件,next[j]由0号至j-1号字符决定,故当it1指向最后一个字符时结束循环。

例如next[8]=2

--------------------------------------------------------------------------------------------------------------------------------
itit1j=8
ABACABABAC
ABACABABAC
it2

求得next[9]=3,这时求完了整个next数组,it1++,到了最后一个字符,此时应当结束循环

求next[]数组代码

由上面的说明,可以得到代码

/*
描述:求next数组
注意:next数组长度等于要求的字符串的数组长度
不应当传入空字符串
*/
uint16_t* getNext(const char* target)
{
	//得到字符串长度
	uint16_t length = strlen(target);

	//不应当传入空字符串
	if (length < 1) return NULL;

	uint16_t* next = (uint16_t*)malloc(length * sizeof(uint16_t));
	if (next == NULL) return NULL;

	next[0] = 0;
	if (length < 2) return next;

	next[1] = 0;

	const char* theEnd = target + length - 1;//最后一个非'\0'字符

	char* it = target+1;//正在比较的子串
	char* it1 = it;//主串遍历用
	char* it2 = target;//字串遍历用
	//递推,j=1的已知知道j=2,故初始it为target + 1,it2为target
		

	while (it1 != theEnd )//到了最后一个字符,此时应当结束循环
	{
		uint16_t j = it1 - target;
		uint16_t k = it2 - target;
		if (*it1==*it2)
		{			
			next[j + 1] = k + 1;
			it1++;
			it2++;
		}
		else
		{
			if (k==0)//根本没有出现部分匹配,特殊不匹配情况
			{
				next[j + 1] = 0;
				it++;//看下一子串
				it1++;//看下一子串

			}
			else//有部分匹配,(一般不匹配情况)
			{				
				it2 = target + next[k];//it2跳转,“滑动”
				it = it1 - next[k];//记录位置
			}
		}
	}

	return next;
}
优化

前面定义的next[]数组在某些情况下仍有不足。

例如

const char* target = "aaaab";//模式串
uint16_t next[] = {0, 0, 1, 2, 3};

进行KMP算法匹配,当2号字符不匹配时

--------------------------------------------------------------------------------
itit1
masteraa?????
aaaab
it2
滑动
itit1
masteraa?????
aaaab
it2
滑动
it/it1
masteraa?????
aaaab
it2

需要滑动两次

注意到,当2号字符’a’不匹配时,it1所指位置必不为’a’。

而next[2]=1,1号位置字符仍然为’a’,此次比较必然失败。完全可以跳过,it2转而直接到next[1]=0的位置。

4号字符不匹配时也类似,完全可以跳过几次滑动过程。

也就是说我们可以把next[]数组优化成nextval[]数组,

优化方法是,如果 j 号字符,与 k = next[j] 号字符相同,则next[j]优化成next[k]

示例中

const char* target = "aaaab";//模式串
uint16_t next[] = {0, 0, 1, 2, 3};

可将next[]优化成

uint16_t nextval[] = {0, 0, 0, 0, 3}

代码

/**
 * 对next[]数组进行优化.
 * 
 * \param target 模式串
 * \param next next[]数组
 */
void nextOptimize(const char* target, uint16_t* next);

void nextOptimize(const char* target, uint16_t* next)
{
	uint16_t number = strlen(target);
	if (number < 3)//至少有三个元素才有“优化”一说
		return;

	for (uint16_t j = 0; j < number; j++)
	{
		uint16_t k = next[j];
		if (target[j] == target[k])
		{
			next[j] = next[k];
		}
	}
}

显然,我们可以直接求优化后的数组,只需在给next[j+1]赋值后加个判断就好。

//省略一些代码
//...
next[j + 1] = k + 1;
if (target[j + 1] == target[k + 1])//优化
	next[j + 1] = next[k + 1];
//...
//省略一些代码

完整代码

/**
 * 求nextval数组.
 *
 * \param target 模式串
 * \return 求得的next数组地址
 * \return NULL 传入了空字符串或内存申请失败
 */
uint16_t* getNextval(const char* target);

uint16_t* getNextval(const char* target)
{
	//得到字符串长度
	uint16_t length = strlen(target);

	//不应当传入空字符串
	if (length < 1) return NULL;

	uint16_t* next = (uint16_t*)malloc(length * sizeof(uint16_t));
	if (next == NULL) return NULL;

	next[0] = 0;
	if (length < 2) return next;

	next[1] = 0;

	const char* theEnd = target + length - 1;//最后一个非'\0'字符

	char* it = target + 1;//正在比较的子串
	char* it1 = it;//主串遍历用
	char* it2 = target;//字串遍历用
	//递推,j=1的已知知道j=2,故初始it为target + 1,it2为target


	while (it1 != theEnd)
	{
		uint16_t j = it1 - target;
		uint16_t k = it2 - target;
		if (*it1 == *it2)
		{
			next[j + 1] = k + 1;
			if (target[j + 1] == target[k + 1])//优化
				next[j + 1] = next[k + 1];
			it1++;
			it2++;
		}
		else
		{
			if (k == 0)//根本没有出现部分匹配,特殊不匹配情况
			{
				next[j + 1] = 0;
				it++;//看下一子串
				it1++;//看下一子串

			}
			else//有部分匹配,(一般不匹配情况)
			{
				it2 = target + next[k];//it2跳转,“滑动”
				it = it1 - next[k];//记录位置
			}
		}
	}

	return next;
}
### KMP算法的实现 KMP算法是一种高效的字符串匹配算法,它通过构建部分匹配表(也称为`next`数组)来减少不必要的回溯操作[^2]。以下是基于Python语言KMP算法实现: ```python def compute_next_array(pattern): next_arr = [-1] * len(pattern) i, j = 0, -1 while i < len(pattern) - 1: if j == -1 or pattern[i] == pattern[j]: i += 1 j += 1 next_arr[i] = j else: j = next_arr[j] return next_arr def kmp_search(text, pattern): m, n = len(text), len(pattern) next_arr = compute_next_array(pattern) i, j = 0, 0 while i < m and j < n: if j == -1 or text[i] == pattern[j]: i += 1 j += 1 else: j = next_arr[j] if j == n: return i - j # 返回匹配起始位置 return -1 # 表示未找到匹配项 ``` 上述代码分为两部分: - `compute_next_array()` 函数用于计算模式串的部分匹配表(即`next`数组)。这部分的核心在于利用已知的最大公共前后缀长度来优化后续匹配过程[^5]。 - `kmp_search()` 函数则负责执行具体的字符串匹配逻辑。 --- ### KMP算法的应用场景 #### 文本编辑器中的查找功能 在文本编辑器中,当用户输入一段文字并希望快速定位某个关键词时,可以采用KMP算法完成这一任务。相比传统的暴力匹配方法,KMP能够在更短的时间内返回结果,尤其适用于大规模文档环境下的搜索需求[^1]。 #### 数据清洗与预处理 在大数据领域,经常需要对海量日志文件或其他形式的数据集进行过滤或提取特定字段的操作。此时如果目标子串固定不变,则可预先生成对应的`next`数组,在多次查询过程中显著提升效率[^3]。 #### 生物信息学研究 DNA序列由四种碱基组成(A,T,C,G),因此对于某些基因片段的研究工作而言,频繁涉及相似结构单元之间的对比分析。借助于KMP技术,研究人员能够更加便捷地识别出感兴趣的区域及其分布规律[^4]。 --- ### 性能优势总结 总体来看,由于引入了额外的信息存储机制——即所谓的“失败指针”,使得整个流程无需反复跳转至初始状态重新尝试;从而大幅降低了最坏情况下的时间开销,并保持相对稳定的内存占用水平[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜哥万岁万岁万万岁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值