关于模式串匹配的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指针和匹配过程时,会出现比较难以理解的情况。但我尽可能用自己的理解把这部分内容通过注释写了出来,如果有不理解的地方欢迎评论来问我,我也视情况看看需不需要再写上图解。
再者就是代码实现部分没有通过数据检验,可能会有一点地方出现错误,欢迎各位指正。
最后,接下来我会按我最近所学,整理出一些自己觉得比较难的算法或者是知识点,希望能够给大家提供帮助。