概念
串(string)的最重要的操作之一就是串的模式匹配。
说的这么高级,其实就是在一个主串里面找某一个子串的位置。
而KMP是一种很快速的字符串查找方法。
既然提到KMP的高效性,就不得不拿出朴素模式匹配算法来进行对比一下子。
朴素的模式匹配算法
若是在长度为m的主串里找一个长度为n的子串
时间复杂度:O(m*n)
举个小栗子吧~
图片:
虽然这个栗子可能看起来并不是特别复杂,但是实际上,计算机处理的并不仅仅只是单单的字符,而是二进制0和1的串,一个ASCII码是一个8位的二进制01串,所以,比一个字符,计算机其实要计算八次,因此,使用朴素模式匹配算法,确实有些低效。
KMP模式匹配算法
这个算法是由三个科学家发明的,所以由他们的名字缩写命名为KMP
它的原理
通过对子串的预处理(作出next数组),有效减少字符串匹配时的回溯问题。
这个时候又上栗子啦~
对于一个主串S,一个子串P
主串S匹配到了i,子串P匹配到了j
这个时候j+1不再匹配,若是朴素算法,i会回到i+1,而此时如果j>i,这就称作回溯(因此进行了一些不必要的查找)。
而若是KMP算法,j+1匹配失败时,令i不变,j=next[j](next数组的建立马上解释鸭),这个操作是指,匹配失败时,子串P相对于文本串向右移动了j-next[j]位。
也就是,匹配失败时,子串向右移动的位数等于:匹配失败字符所在位置-匹配失败幅度的next值,即移动的实际位数为:j-next[j](这个值大于等于1)
next数组
首先放上next数组的公式,注意,next数组是针对子串而言的,在我看来,是对子串的预处理,而不是针对主串而言的
所以说,next数组各值的含义就是:
代表当前字符之前的字符串中,最大长度的相同前缀和后缀。
如:abccccab的的最大相同前缀后缀就是ab,长度为2
(后缀无字符相等,当前next数组值为1,后缀有一个字符相等,当前next数组值为2,,依次推)
举个小栗子啦:
完整代码
输入主串和子串,输出子串的next数组,并输出子串下标
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
//注意此处引用的使用
//子串next数组创建函数
void kmp_next(string &s, vector<int> &next, int n)
{
int k = -1;
int j = 0;
while (j < n)
{
if (k == -1 || s[j] == s[k])
{
next[++j] = ++k;
}
else
{
k = next[k];
}
}
}
//kmp算法实现函数
int KMP(string t, string p)
{
int i = 0; //主串位置
int j = 0; //模式串位置(一般都是默认从零开始)
vector<int> next(p.size() + 1, -1); //为next数组赋初值,所有值都为-1
kmp_next(p, next, p.size()); //创建next数组
cout << "next数组如下:\n";
for (int k = 0; k < next.size(); k++)
cout << next[k] << " ";
cout << endl;
int tsize = t.size(); //这个地方size()需要转化,不然后面while循环会突然退出
int psize = p.size();
while (i < tsize && j < psize)
{
if (j == -1 || t[i] == p[j]) //若相等,则i和j右移,若是第一个元素,则默认是0
{
i++;
j++;
}
else // i不需要回溯了 i = i - j + 1;
{
j = next[j];
}
}
if (j == p.size())
return i - j;
else
return -1;
}
int main(void)
{
string T, S;
cout << "请先后输入主串和子串\n";
cin >> T >> S;
cout << KMP(T, S);
return 0;
}
运行结果
请先后输入主串和子串
abcdefgggg
fg
next数组如下:
-1 0 0
5
写在最后:
还是那句话哈哈哈,kmp其实也是个锦上添花,在一段大程序里降低一点时间复杂度的小算法。但是这个算法里面有很深的思想(小菜鸟表示很崇拜鸭),对于很对问题的解决还是有很大的借鉴之处的。