返校后的第一天集训——字符串。
由于字符串内容没什么可写的,想到明天集训——kmp算法,就一起写字符串匹配的内容。
字符串匹配问题是一个非常经典的问题,给定字符串T和字符串B,判断字符串B是否是字符串T的子串,简单的字符串匹配已经有了非常成熟的算法。
先来说说朴素字符串匹配算法吧。
朴素算法的英文命名为BruteForce,暴力的意思,所谓的朴素算法就是算法分析上常讲的暴力求解方法。这是一种方法,也是一种算法思想,就是不考虑空间时间复杂度,以最简单的看待问题的视角去思考,去解决。
一个最朴素的思路就是从字符串T的第一个字符顺次比较模式串B,不匹配则重新从下一个字符开始匹配,直到文本末尾。
int BruteForce(char *T,char *B)//朴素字符串匹配算法
{
int i,j;
for(i=0;i<strlen(T)-strlen(B)+1;i++)
{
for(j=0;j<strlen(B);j++)
{
if(T[i+j]!=B[j])
break;
}
if(j==strlen(B))
return i;//匹配成功返回匹配位置
}
return 0;//匹配失败返回0
}
朴素算法时间复杂度为O((n-m+1)*m),因为每次匹配失败后,都会回到原来的匹配起点的下一个字符开始匹配,这些步骤很多情况下,并不是必要的。因此Knuth-Morris-Pratt三人改进朴素算法,简称KMP算法,这就是今天集训队内容。
算法的思想:
相比蛮力算法,KMP 算法预先计算出了一个哈希表,用来指导在匹配过程中匹配失败后尝试下次匹配的起始位置(而不是回到原来起点的下一个字符),以此避免重复的读入和匹配过程。这个哈希表被叫做“部分匹配值表”,它的设计是算法精妙之处。
部分匹配值表
要理解部分匹配值表,就得先了解字符串的前缀(prefix)和后缀(postfix)。
前缀:除字符串最后一个字符以外的所有头部串的组合。
后缀:除字符串第一个字符以外的所有尾部串的组合。
部分匹配值:一个字符串的前缀和后缀中最长共有元素的长度。
以"ABCDABD"为例,
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
所以"ABCDABD"的部分匹配表为"0000120";
如何写出部分匹配表的next函数呢。
具体的代码如下:
void getNext()
{
int l=strlen(B);
int i=0,j=-1;
next[0]=-1;
while(i<l)
{
if(j==-1||B[i]==B[j])
{
i++;j++;
next[i]=j;
}
else j=next[j];
}
}
求next数组的改进算法
void getNextval()
{
int i=1,j=0;
next[1]=0;
int l=strlen(B);
while(i<=l)
{
if(j==0 || B[i]==B[j])
{
++i;++j;
if(B[i]!=B[j]) next[i]=j;
else next[i]=next[j];
}
else j=next[j];
}
}
int kmp()//kmp算法
{
getNext();
int n=strlen(T);
int m=strlen(B);
int i=0,j=0;
while(i<n &&j<m)
{
if(j==-1 || T[i]==B[j])
{
i++; j++;
}else
j=next[j];
}
if(j==m)
return i-m+1;//匹配成功返回匹配位置
else
return -1;//匹配失败返回-1
}
KMP算法的时间复杂度为O(n+m),效率比BF算法快多了,但在实际上基本不用KMP算法,因为小数据匹配的话,BF完全够用了,还可以直接用<string.h>头文件中的strstr来匹配;大数据的话,KMP的效率明显不够,还有一种比KMP算法快3-5倍的的BM算法。但是KMP算法是前人解决问题的想法结晶,其核心在于next数组的运用。