关于模式串匹配的KMP算法(附C++代码实现)

关于模式串匹配的KMP算法

1.为何写这篇博客

近段时间在学习算法的时候,偶遇匹配模式串的KMP算法,起初看不懂,在看了多篇博客之后,总算有点领悟,突然觉得算法很奥妙,想以这次学习为起始,开始写自己的学习笔记到博客之上。

2.问题背景

给定长度为n的正文串text以及长度为m的模式串pat(其中m≤n),找出text中pat出现的所有位置。

3.暴力解法

常规的解法是将text分为n - m + 1个长度为m的子串,检查是否与pat匹配。具体代码如下:

#include<iostream>
#include<vector>
using namespace std;
class KMP_Solution
{
private:
	int n;
	int m;
	char* text;
	char* pat;
	int* next;
	vector<int> ans;
	void get_next()
	{
		int i = 0, k = -1;//从-1开始是为了方便计算,代表前面无子串
		this->next[0] = -1;//第一个位置前面无子串
		while (i < this->m)
		{
			//当匹配成功的时候,很好理解,该位置的串长又增加了1,并且指针往后移动一个单位
			if (k == -1 || this->pat[i] == this->pat[k])
			{
				i++;
				k++;
				this->next[i] = k;
			}
			//当匹配失败时,考虑next数组的定义,假设j == next[k],回溯到k值j点再试图匹配
			else k = next[k];
		}
	}
	void get_ans()
	{
		int i = 0, k = 0;
		while (i < this->n && k < this->m)
		{
			//匹配成功,移动指针,并检查是否完成整个串的匹配,若完成将位置i - m加到ans数组,并将k回溯。
			if (this->text[i] == this->pat[k])
			{
				i++; k++;
				//检查整个pat是否匹配成功
				if (k == this->m)
				{
					this->ans.push_back(i - this->m);
					k = this->next[k];
				}
			}
			//匹配失败,进行回溯,若k == -1,说明当前text[i]字符不存在在pat中,移动指针
			else
			{
				k = this->next[k];
				if (k == -1)
				{
					i++;
					k++;
				}
			}
		}
	}
public:
	KMP_Solution(int n = 0, int m = 0, char* text = nullptr, char* pat = nullptr)
	{
		this->n = n;
		this->m = m;
		this->text = text;
		this->pat = pat;
		//当模式串为空时,next初始化为空指针
		if (this->m == 0) this->next = nullptr;
		//当模式串不为空时,将next指针定义为动态数组,并通过调用函数给数组赋值
		else
		{
			this->next = new int[m];
			get_next();
			get_ans();
		}
	}
	vector<int> return_ans()
	{
		if (this->ans.size() == 0) ans.push_back(-1);
		return this->ans;
	}
};

这个算法的复杂度为O(m(n - m + 1)) = O(mn),当m和n比较大时,算法的效率不高。接下来,我们看一下KMP算法,KMP的算法复杂度仅为O(n + m)。

4.KMP算法

KMP算法是典型的用空间换取时间的方案。在常规的方法中,我们每次在匹配失败后,正文串text的位置 i 回溯到 i + 1的位置,而模式串的位置 j 回溯到0的位置,这种方案导致算法效率不高。在KMP算法中,通过一个next数组,记录匹配失败时,模式串pat应该回溯到的点。

下面给出next数组中元素的定义。假设next [j] == k,则表示子串pat[0]~pat[k -1]与子串pat[j - k]~pat[j - 1]相等,此时回溯到k点,则前k - 1个长度的字符不需要重新匹配,提高了效率。

下面先贴上代码,在代码分注释中分析KMP算法的要点,最后再计算算法复杂度。

#include<iostream>
#include<vector>
using namespace std;
class KMP_Solution
{
private:
	int n;
	int m;
	char* text;
	char* pat;
	int* next;
	vector<int> ans;
	void get_next()
	{
		int i = 0, k = -1;//从-1开始是为了方便计算,代表前面无子串
		this->next[0] = -1;//第一个位置前面无子串
		while (i < this->m)
		{
			//当匹配成功的时候,很好理解,该位置的串长又增加了1,并且指针往后移动一个单位
			if (k == -1 || this->pat[i] == this->pat[k])
			{
				i++;
				k++;
				this->next[i] = k;
			}
			//当匹配失败时,考虑next数组的定义,假设j == next[k],回溯到k值j点再试图匹配
			else k = next[k];
		}
	}
	void get_ans()
	{
		int i = 0, k = 0;
		while (i < this->n && k < this->m)
		{
			//匹配成功,移动指针,并检查是否完成整个串的匹配,若完成将位置i - m加到ans数组,并将k回溯。
			if (this->text[i] == this->pat[k])
			{
				i++; k++;
				//检查整个pat是否匹配成功
				if (k == this->m)
				{
					this->ans.push_back(i - this->m);
					k = this->next[k];
				}
			}
			//匹配失败,进行回溯,若k == -1,说明当前text[i]字符不存在在pat中,移动指针
			else
			{
				k = this->next[k];
				if (k == -1)
				{
					i++;
					k++;
				}
			}
		}
	}
public:
	KMP_Solution(int n = 0, int m = 0, char* text = nullptr, char* pat = nullptr)
	{
		this->n = n;
		this->m = m;
		this->text = text;
		this->pat = pat;
		//当模式串为空时,next初始化为空指针
		if (this->m == 0) this->next = nullptr;
		//当模式串不为空时,将next指针定义为动态数组,并通过调用函数给数组赋值
		else
		{
			this->next = new int[m];
			get_next();
			get_ans();
		}
	}
	vector<int> return_ans()
	{
		if (this->ans.size() == 0) ans.push_back(-1);
		return this->ans;
	}
};

KMP有两个比较重要的点,一是next数组的获取,二是匹配的过程。在获取next数组的过程中,遍历了pat字符串,算法复杂度为O(m)。而在匹配的过程中,通过回溯pat上的指针,而不对text上的指针进行移动,则说明总的算法复杂度是将text字符串遍历了一遍,算法复杂度为O(n),于是,整个算法的复杂度为O(m + n)。

5.总结

这是我第一次以博客的形式阐述一个算法,仍然有一些不足。
比如由于最近比较忙,时间比较赶,我没有用图解,可能在理解获取next指针和匹配过程时,会出现比较难以理解的情况。但我尽可能用自己的理解把这部分内容通过注释写了出来,如果有不理解的地方欢迎评论来问我,我也视情况看看需不需要再写上图解。
再者就是代码实现部分没有通过数据检验,可能会有一点地方出现错误,欢迎各位指正。
最后,接下来我会按我最近所学,整理出一些自己觉得比较难的算法或者是知识点,希望能够给大家提供帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值