【题目】
给定俩个字符串str和match,长度分别为N和M。实现一个算法,如果字符串str中含有字串match,则返回match在str中的开始位置,不含有则返回-1;
【举例】
str="acbc",match="bc",返回2.
str="acbc",match="bcc",返回-1.
【解答】
- 普通解法
从左到右遍历str的每一个字符。然后看如果以当前字符作为第一个字符出发是否匹配出match。
例子:str="aaaaaaaaaaaaaaaab"(16个a,一个b),match="aaaab"(4个a,一个b)
从str[0]出发,开始匹配,匹配到str[4]=='a'时发现和match[4]=='b'不匹配。所以匹配失败。
结论:说明从str[0]开始是不行的。
然后再从str[1]开始匹配,匹配到str[5]=='a'时发现和match[4]='b'不匹配。所以匹配失败。
结论:说明从str[1]开始是不行的。
...
从str[2..12]出发都会失败。
从str[13]开始,匹配到str[17]=='b'时发现和match[4]=='b'一样,且match已经全部匹配完,说明匹配成功,返回开始匹配的位置13.
总结:普通解法的时间复杂度较高,从str每个字符出发时,匹配的代价都可能是O(M) (M为match的长度),str一共有N个字符,所以整体的时间复杂度为O(M*N).普通解法的时间复杂度之所以这么高,是因为每次遍历到一个字符时,检查工作相当于从来都没有开始过(意思为不能很好的利用已经检查过的部分。即之前的检查结果虽然失败但是能有效的利用导致的。 - KMP算法
该算法时由Donald Knuth,Vaughan Pratt和James H.Morris于1977年联合发表的。
该算法主要是为了高效的解决字符串的匹配问题而诞生的一个算法。
该算法的必要准备工作:
1,求匹配字串match的nextArr数组.
该数组的定义为:nextArr[i]的含义是在match[i]之前的字符串match[0..i-1]中,
必须以match[i-1]结尾的后缀子串(不能包含match[0])于
必须以match[0]开头的前缀子串(不能包含match[i-1])最大匹配长度是多少。
这个长度就是nextArr[i]的值。
举例:match="aaaab" 那么nextArr[4]的值是多少呢?
根据定义match[4]=='b'的之前的子串"aaaa"的后缀子串和前缀子串最大匹配为"aaa".也就是match[0..2]和match[1..3]
所以nextArr[4]==3.
match="abc1abc1" 那么nextArr[7]的值是多少呢?
根据定义match[7]=='1'前面的子串的 前缀子串和后缀子串的最大匹配为"abc"。也即是match[0..2]和match[4..6]
所以nextArr[7]==3
那么nextArr数组有什么作用呢?我们后面会讲到!接下来先看KMP算法的原理:
2,KMP算法的匹配规则 - 如上图,假设从str[i]开始匹配,且str[i]==match[0],str[i..j-1]==match[0..j-i-1]。只有匹配到str[j]时match[j-i]!=str[j],匹配停止。因为我们已经有了match的nextArr数组那么我们接下来的匹配就无需再从str[i+1]开始匹配match[0]这样的操作了。
我们知道在match中a区域与b区域是匹配的,所以下次匹配只需要将match[k]和str[j]进行比较即可。之后一直进行这样的滑动匹配过程,直到在str的某一个位置把match完全匹配完,就说明str中有match。如果滑动到最后也没有匹配出来,就说明str中没有match。
验证匹配算法的正确性:
1,上述中,为什么要直接让match[k]和str[j]直接比较????
如上图,假设匹配到A字符和B字符才发生了不匹配,所以c区域必然等于b区域,又因为我们直到nextArr的定义,所以我们也直到b区域也等于a区域,那么我们就可以直接将match右滑动到如图所示的位置,直接让C字符和A字符比较。
2,为什么a区域和b区域直接肯定匹配不了match呢???
我们用假设法:假设BB区域开始位置是不用检查的其中一个位置,如果从这个位置可以匹配出match部分子串,那么,从match[0]开始也必然存在和BB区域相等的区域AA可以匹配出match部分子串。同时我们注意到AA比a区域大,BB比b区域大,我们找到了比b和a更大的前缀和后缀子串。这个结论和求nextArr的值是相互矛盾的。
匹配过程分析完毕:match一直向右滑动,最坏的时间复杂度为O(N).
代码:
int getIndexof(const char *strContent, const char *match)
{
if (strContent == NULL || match == NULL || strlen(strContent) < 1 || strlen(match) < 1)
{
return -1;
}
int si = 0;
int mi = 0;
int *next = getNextArray(mi);
while (si < strlen(strContent) && mi < strlen(match))
{
if (strContent[si] == match[mi])
{
si++;
mi++;
}
else if (next[mi] == -1)
{
si++;
}
else
{
mi = next[mi];
}
}
free(next);
if(next != NULL)
next = NULL;
return mi == strlen(match) ? si - mi : -1;
}
最后一步求得nextArr数组:
1,对于match[0]而言,它前面没有字符,所以nextArr[0]规定为-1.对于match[1]来说,在它之前只有match[0],但是nextArr的定义是要求任何子串的后缀不包括第一个match[0],故nextArr[1]==0.
2,之后的nextArr数组的值求解过程如下:
- 求nextArr数组的值因为是从左向右求,所以在求解nextArr[i]的时候nextArr[0..i-1]已经求得.
如上图已经知道了nextArr[i-1]的值,那么求nextArr[i]只需要判断C是否等于B,如果等于那么nextArr[i]=nextArr[i-1]+1. - 如果不等于那么:假设C的下标为cn,cn的值就是nextArr[i-1]的值。nextArr[cn]的值就是n区域和m区域的大小
现在只需要判断D是否等于B,如果等于那么nextArr[i]=nextArr[cn]+1;如果不等于接着按照上面步骤判断,直到cn
等于0为止。
代码:
void getNextArray(const char * match, int *next)
{
if (match == NULL || next == NULL)
return;
int length = strlen(match);
next = (int *)malloc(sizeof(int) * length);
if(next==NULL)
{
return ;
}
if (length == 1)
{
next[0] = -1;
return;
}
next[0] = -1;
next[1] = 0;
int pos = 2;//从下标2开始
int cn = 0;//
while (pos < length)
{
if (match[pos - 1] == match[cn])
{
next[pos++] = ++cn;
}
else if (cn > 0)
{
cn = next[cn];
}
else
{
next[pos++] = 0;
}
}
}
整个KMP算法的复杂度为O(M)(求解nextArr数组的过程)+O(N)(匹配的过程),因为有N>=M所以时间复杂度为O(N).