1.前提介绍
如果有一个串,我们提取其中的部分连续的字符形成的串叫作子串,原来被提取的串叫则叫作主串。
我们想要尝试在主串中寻找的子串,叫作模式串,当然这个串不一定找得到,即这个串不一定是主串的子串。
这个寻找过程叫作串的模式匹配。
例子:
主串:”hello world!!!“
子串:”ello“
模式串:”er“,”orld“
下面的算法介绍下标从0开始。
2.暴力算法(brute force)
暴力算法又叫串的朴素匹配,听这个名字就知道这个算法有多朴素(baoli)。
暴力算法是我们一开始就能够想起做到的,它就是将主串和模式串作比较,如果这次没有匹配,主串和子串一起回溯,直至匹配成功或失败。
如:
主串s: ”ababc“
模式串r:”abc“
第一次:a和a匹配 第二次:b和b匹配 第三次:a和c不匹配 第四次:b和a不匹配
第五次:a和a匹配 第六次:b和b匹配 第七次:c和c匹配
匹配成功。其中第三次和第四次都发生了回溯的现象,尽管第四次不太明显。
我们回头再来看,第三次主串s[2]和子串r[2]不匹配,因此主串退回1下标,子串退回0下标,继续匹配,不匹配则主串回溯这是这个算法带来时间耗费长的原因。
假设主串有n个字符,子串有m个字符。最差情况下,每次都因子串最后一位不匹配,则时间复杂度为O(n*m),例如主串为“aaaaaaaaaaab”,子串为“aaab”。
代码如下:
int brute_force(const string& a, const string& b)
{
//如果子串比主串长,返回错误
if (b.size() > a.size())
return -1;
for (int i = 0; i < a.size(); i++)
{
int j;
for (j = 0; j < b.size(); j++)
{
if (i + j >= a.size() || a[i + j] != b[j])
break;
}
if (j == b.size())
return i;
}
return -1;
}
其实,我们第四次的匹配过程做了无用功,因为第一次匹配和第二次匹配都成功了,而其中子串拿去和主串匹配的a和b是不同的字符,因此当第三次发现不匹配的时候,我们其实完全可以不用第四次的匹配。
利用数学公式来表达就是s[1]=r[1],r[0]≠r[1],则r[0]≠s[1]。
利用这点来跳过第四次,利用一个数组来记录r[0]≠r[1]的信息,这就是KMP算法。
3.KMP算法
KMP名字其实是三个大佬名字的首字母,并没有什么深刻的含义。
KMP算法就是依靠所谓的next数组来实现主串不回溯,而子串回溯的算法,但又因为多求一个子串的next数组,因此时间复杂度只有O(n+m)。
在介绍KMP算法的next数组之前,我们先来介绍前后缀。
(1)最长公共前后缀
一个串的前缀是包含第一个字符且不包含最后一个字符的子串。
后缀是包含最后一个字符而不包含第一个字符的子串。
我们求next数组需要的就是最长公共前后缀。
举个例子:
”helleh“
前缀有”h“,”he“,”hel“,”hell“,”helle“。
后缀有”h“,”eh“,”leh“,”lleh“,”elleh“。
则最长公共前后缀为”h“
”ababab“ 同理最长公共前后缀为”abab“,而不是”ababab“。
前后缀的作用就是为了求出下面的next数组。
(2)next数组的定义和手算方法
next数组长度大小和子串长度大小相等,不包含\0符号。而next数组保存的是子串和主串匹配到该位不匹配,子串应该回溯的下标。
但下标为0的元素是个例外,因为假如主串和子串第一个字符就不匹配,这时,应该将主串往后移一位,子串重新匹配,我们将下标为0的元素标为-1,表示这是个特殊情况(其实有更深层的作用,在下面有解释)。
对于next数组作用的公式解释:
假如有:
主串:”,
,…
…,
,
…,
“
模式串:”,
,
,…
…,
,
“
我们发现有=
,i=0,1,2,……,k,但
≠
。
但我们又发现=
,
=
,……,
=
,即k+1下标之前的最长公共前后缀为下标0到t-1的元素。
这样,又有
=
,
=
,……,
=
,我们就可以直接将子串回溯到t,而t就是最长公共前后缀的长度,将
和
继续相比较。
这就是最长前后缀的作用,就是当该字符与主串相应字符不等时,计算到该字符之前的部分的最长前后缀的长度t,然后把子串移到t下标继续和主串比较。
举个例子:
”abababh“
next第一个元素为-1
next第二个元素为0,”a“最长公共前后缀长度为0。
next第三个元素为0,”ab“最长公共前后缀长度为0。
next第四个元素为1,”aba“最长公共前后缀长度为1。
next第五个元素2,”abab“最长公共前后缀长度为2。
等等……,最后求得的next数组为-1 0 0 1 2 3 4。
这样,next数组一旦求出就可以用来匹配任意一个主串。
我们如何用代码取得这样的一个数组呢?我们利用动态规划的思想来求出。
已知next[0]=-1,假设我们用i表示遍历子串字符的变量,再用j来表示当前字符next数组的值。用subString来表示子串。因此,j一开始为-1,i一开始为0。
至于为什么next[0]=-1,是因为我们在循环里设定了一个if,当j=-1时,则i++,j++这样一开始就可以比较next[i]和next[j]。
注意j的意义,j既是子串该字符之前部分的最长前后缀的长度,也是当前字符和主串字符匹配失败,子串应当回退的下标,则有subString[0],subString[1],……,subString[j-1]和subString[j],subString[j+1],……,subString[i-1]对应相等,因此subString[i] == subString[j]时,next[i+1]=next[j+1]。
且当subString[i] ≠ subString[j]时,则我们需要求出subString[0],subString[1],……,subString[j-1],subString[j],……,subString[i]的最长前后缀,我们知道这时已经没有比j更长的最长前后缀,我们开始将j=next[j],往后回溯,即等价于求subString[0],subString[1],……,subString[j-1]的最长前后缀,这时i没有变化,以待下一次的比较。
因此分为下面三种情况:
1.j=-1时,说明这时子串之前的字符也和主串当前字符没有能够匹配的,因此主串要后移一位,子串重新回退到0,重新匹配。
2.subString[i] == subString[j]时,则next[i+1]=next[j+1]。
3.subString[i] ≠subString[j]且j≠-1时,j=next[j],待下一次循环重复这三种情况。
void get_next(const string& subString, int* next)
{
int i = 0, j = -1;
next[0] = -1;
while (i < subString.size() - 1)
{
if ((j == -1) || (subString[i] == subString[j]))
{
i++;
j++;
next[i] = j;
}
else j = next[j];
}
for (int i = 0; i < 4; i++)
cout << next[i] << " ";
cout << endl;
}
(3)KMP算法解释
假如说我们要匹配"cabaabababh",我们字串匹配到这步:
发现子串b和主串a不相等,我们将子串回溯到next[不匹配b的下标]即1位置,变成下面:
很神奇的是,我们发现主串并没有回溯,而字串已经有一个a与主串匹配。
这样一来,KMP的匹配方法有三种情况:
第一种情况:a[i]==a[j],则i++,j++。
第二种情况:a[i]!=a[j],且j=0,则j=next[0]=-1,等待下一次进入循环时i++,j++这样主串就往后移一位,而子串重新匹配。
第三种情况:a[i]!=a[j],则j=next[j]
(4)KMP存在问题
主串:”gooloegoogle“
模式串:”google“
next数组为-1 0 0 0 1 0
到这一步:
我们发现子串g和l不匹配,于是子串回溯到0,发现子串还是g和主串l不匹配,这也是无用功。
因此,我们在求next数组时加上if(subString[i]!=subString[j]) next[i]=j;else next[i]=next[j];这样,就不会再出现这样的情况。
求next数组的代码:
void get_next(const string& subString, int* next)
{
int i = 0, j = -1;
next[0] = -1;
while (i < subString.size() - 1)
{
if ((j == -1) || (subString[i] == subString[j]))
{
i++;
j++;
if(subString[i]!=subString[j])
next[i]=j;
else next[i]=next[j];
}
else j = next[j];
}
}
int KMP(const string& mainString, const string& subString)
{
if (subString.size() > mainString.size())
return -1;
int i = 0, j = 0;
int* next = new int[b.size()];
get_next(b, next);
while (i < mainString.size() && j < subString.size())
{
if (j == -1 || a[i] == b[j])
i++, j++;
else j = next[j];
}
if (j >= b.size())
return i - b.size();
else return -1;
}