作者:@阿亮joy.
专栏:《数据结构与算法要啸着学》
座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
字符串匹配就是在主串str
中查找子串sub
(也称为模式串),看子串sub
是否在主串str
中。如果存在,就返回子串sub
在第一次在主串str
中出现的位置或者地址(指针);如果不存在,就返回 -1 或者NULL
。那么,我想有小伙伴就会问,如果子串sub
为空字符串,函数的返回值是什么?库函数strstr
的返回值是主串str
的首元素地址。那么以下将要介绍的BF
算法和KMP
算法也和库函数strstr保持一致,即当子串sub为空字符串时,BK
算法和KMP
算法的返回值为 0(博主的BF
算法和KMP
的算法返回值为int
,当然也可以设置为char*
)。
在学习BF
算法和KMP
算法之前呢,我们来看一下库函数strstr
的源码和模拟实现库函数strstr
。
strstr
的函数原型:
char * strstr ( const char *str1, const char * str2);
char* __cdecl strstr(const char* str1,const char* str2)
{
char* cp = (char*)str1;
char* s1, * s2;
if (!*str2)
return((char*)str1);
while (*cp)
{
s1 = cp;
s2 = (char*)str2;
while (*s2 && !(*s1 - *s2))
s1++, s2++;
if (!*s2)
return(cp);
cp++;
}
return(NULL);
}
上面就是库函数strstr
的源码了,那这代码是什么意思呢?现在就带着大家学习一下(见下图)。希望大家可以将下图仔细地看一下,写得非常的详细。
以上就是库函数strstr
的实现逻辑和图解了。其实库函数strstr
的可读性较差,同时还没有对传进来的str1
和str2
进行判断为不为NULL
,健壮性较差。所以,我们就模拟实现库函数strstr
。
//模拟实现库函数strstr
//my_strstr不会修改str和sub的内容,所以用const修饰str和sub,增强代码的健壮性
//str:主串 sub:子串
#include
#include
char* my_strstr(const char* str, const char* sub)
{
//对str和sub进行是否等于NULL判断
assert(str != NULL && sub != NULL);
//sub为空字符串,直接返回str
if (*sub == '\0')
{
return str;
}
const char* s1 = NULL;//遍历主串str
const char* s2 = NULL;//遍历子串sub
const char* mark = str;//标记位置
while (*mark)
{
//回溯
s1 = mark;
s2 = sub;
while (*s1 && *s2 && (*s1 == *s2))
{
s1++;
s2++;
}
//如果*s2 == '\0',说明sub是str的子串,返回mark
//反之,则mark++,继续查找
if (*s2 == '\0')
{
return (char*)mark;
}
mark++;
}
//sub不是str的子串,返回NULL
return NULL;
}
int main()
{
char str[] = "ababcabcde";
char sub[] = "abcd";
char* ret = my_strstr(str, sub);
printf("%s\n", ret);
return 0;
}
其实,不管是库函数strstr
还是自定义函数my_strstr
的算法思想就是BF
算法思想。那BF
算法是什么呢?接下来,我们来学习一下。
BF
算法,即暴力(Brute Force)
算法,是普通的模式匹配算法,BF
算法的思想就是将目标串str
的第一个字符与模式串sub
的第一个字符进行匹配,若相等,则继续比较str
的第二个字符和sub
的第二个字符;若不相等,则比较str
的第二个字符和sub
的第一个字符,依次比较下去,直到得出最后的匹配结果。BF
算法是一种暴力算法。
看完BF
算法的定义,我们就可以发现库函数strstr
和自定义函数my_strstr的算法都是BF
算法,是一种暴力的算法。
博主再借助上面的例子给大家讲解一下BF
算法。假定我们给出字符串 ”ababcabcdabcde”
作为主串, 然后给出子串 ”abcd”
,现在我们需要查找子串是否在主串中
出现,出现就返回主串中的第一个匹配的下标,没有出现则返回 -1。
只要在匹配的过程当中,匹配失败,那么,i 回退(或者回溯)到刚刚位置的下一个,j 回退到 0 下标重新开始。如此反复,直到最终找到或者找不到。
BF
算法的核心操作就是回溯。当str[i] != sub[j]
时,那么,i 回溯到刚刚位置的下一个,j 回溯到 0 下标重新开始。j 的回溯到 0 比较简单,那 i 的回溯可以借助一个临时变量mark
来记住。当然,i 也有有个回溯公式i = i - j + 1
。因为 i 和 j 都是每次自增都是自增一的,所以才会有这个回溯公式,大家可以通过上面的例子来验证一下。在此,就不再进行证明了。
#include
#include
#include
//str: 主串 sub: 子串
//返回值:返回子串在主串中第一次出现的下标,没有出现则返回-1
//sub为空字符串时,返回0(与库函数strstr保持一致)
int BF(const char* str, const char* sub)
{
//判断str和sub是不是NULL
assert(str && sub);
int strLen = strlen(str);
int subLen = strlen(sub);
//sub为空字符串的情况
if (subLen == 0) return 0;
//主串长度小于子串长度的情况
if (strLen < subLen) return -1;
//上面的主串长度小于子串长度情况会包含在while循环中
//对这种情况进行单独处理,不用进入while循环
int i = 0;
int j = 0;
while (i < strLen && j < subLen)
{
if (str[i] == sub[j])
{
i++;
j++;
}
else
{
//回溯
i = i - j + 1;
j = 0;
}
}
if (j == subLen)//j走完了子串sub,找到了
{
return i - j;
}
return -1;//没找到子串sub
}
int main()
{
printf("%d\n", BF("ababcabcdabcde", "abcd"));//5
printf("%d\n", BF("ababcabcde", "abcdf"));//-1
printf("%d\n", BF("ababcabcde", "ab"));//0
printf("%d\n", BF("ababcabcde", "\0"));//0
printf("%d\n", BF("\0", "a"));//-1
return 0;
}
时间复杂度分析:最坏情况为O(m*n)
;m 是主串长度,n 是子串长度。
KMP
算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP
算法)。KMP
算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next
数组实现,函数本身包含了模式串的局部匹配信息。KMP
算法的时间复杂度O(m+n)
。
KMP
算法和BF
算法最大的区别就是:当str[i] != sub[j]
时,i 并不会回退,并且 j 回退到特定位置(由next
数组决定)。这就是KMP
算法比BF
算法高效的原因所在了。
为什么 i 可以不回退?
j 的回退位置
其实 j 回退的位置就是 j 前面的字符串的最长相同前后缀的长度,而next
数组中存储的就是当前 j 位置前面的字符串的最长相同前后缀的长度。那最长相同前后缀是什么呢?我们现在来学习一下。
现在博主给出一个例子,大家可以看一下。相信聪明的大家,一定能够看懂。
字符串
"abcdab"
前缀的集合:{"a","ab","abc","abcd","abcda"}
后缀的集合:{"b","ab","dab","cdab","bcdab"}
注意:前后缀的集合要求为非空真子集。
那么,最长相同前后缀就是"ab"
,其长度为 2。这个概念非常地重要,希望大家能够掌握。因为next
数组存储的信息就是 j 位置前的字符串的相同前后缀的最大长度。当str[i] != sub[j]
时,j 回退的位置就是next[j]
。
其实通过肉眼观察,很容易就可以知道一个字符串的相同前后缀的最大长度。但是,如何用代码来求取子串sub
的每个位置的相同前后缀的最大长度呢?接下来,我们就来学习next
数组的引入。
KMP
算法的精髓就是next
数组:也就是用next[j] = K
来表示,不同的 j 来对应一个 K 值。当str[i] != sub
[j]时,此时 j 就要回退到下标为 K的位置。
K值求取的规则
1、规则:找到匹配成功部分的两个相等的真子串(不包含本身),一个以下标 0 字符开始,另一个以 j-1 下标 字符结尾。
2、不管什么数据,next[0] = -1,next[1] = 0
;在这里,我们以下标来开始,而说到的第几个第几个是从 1 开始。
练习 1: 对于字符串
”ababcabcdabcde”
, 求其的 next 数组?
练习2: 对于字符串”abcabcabcabcdabcde”
,求其的 next 数组?
这两道练习就是考察对最长相同前后缀概念的理解,如果还是不懂的话,再看多几次最长相同前后缀的概念。下图是两道练习题的答案:
到这里,相信大家对如何求next
数组应该问题不大了。接下来的问题就是,已知next[i] = k
,怎么求next[i+1] = ?
如果我们能够通过next[i]
的值,通过公式转换得到next[i+1]
的值,那么我们就能够通过代码实现这部分。那么其中公式怎么来的呢?见下图证明:
#include
#include
#include
#include
void GetNext(char* sub, int* next, int subLen)
{
next[0] = -1;
if (subLen == 1)
{
return;
}
next[1] = 0;
int i = 2;//当前的i下标
int k = 0;//前一项的k
while (i < subLen)
{
if (k == -1 || sub[i - 1] == sub[k])
{
next[i] = k + 1;
i++;
k++;
}
else
{
k = next[k];
}
}
}
//str:主串
//sub:子串
//pos:从主串的pos位置开始找
int KMP1(const char* str, const char* sub, int pos)
{
assert(str && sub);
int strLen = strlen(str);
int subLen = strlen(sub);
if (subLen == 0) return 0;
if (strLen < subLen) return -1;
if (pos < 0 || pos >= strLen) return -1;
int* next = (int*)malloc(sizeof(int) * subLen);
assert(next);
//引入next数组
GetNext(sub, next, subLen);
int i = pos;//遍历主串
int j = 0;//遍历子串
while (i < strLen && j < subLen)
{
if (j == -1 || str[i] == sub[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
if (j == subLen)
{
free(next);
next = NULL;
return i - j;
}
else
{
free(next);
next = NULL;
return -1;
}
}
int main()
{
printf("%d\n", KMP1("ababcabcde", "abcd", 0));//5
printf("%d\n", KMP1("ababcabcde", "abcdf", 0));//-1
printf("%d\n", KMP1("ababcabcde", "ab", 0));//0
printf("%d\n", KMP1("ababcabcde", "a", 0));//0
printf("%d\n", KMP1("ababcabcde", "a", -1));//-1
printf("%d\n", KMP1("ababcabcde", "a", 11));//-1
printf("%d\n", KMP1("\0", "a", 0));//-1
return 0;
}
next
数组的优化,即如何得到nextval
数组。
字符串:"aaaaaaaab"
next
数组:[-1 0 1 2 3 4 5 6 7]
nextval
数组:[-1 -1 -1 -1 -1 -1 -1 -1 7]
那么如何对next
数组来进行优化呢?
为了让大家熟悉next
数组的优化,请大家完成下面的练习。
练习:模式串 t= “abcaabbcabcaabdab” ,该模式串的 next 数组的值为( ) , nextval 数组的值为 ()。
A.0 1 1 1 2 2 1 1 1 2 3 4 5 6 7 1 2
B.0 1 1 1 2 1 2 1 1 2 3 4 5 6 1 1 2
C.0 1 1 1 0 0 1 3 1 0 1 1 0 0 7 0 1
D.0 1 1 1 2 2 3 1 1 2 3 4 5 6 7 1 2
E.0 1 1 0 0 1 1 1 0 1 1 0 0 1 7 0 1
F.0 1 1 0 2 1 3 1 0 1 1 0 2 1 7 0 1
答案:D
和F
,大家尝试做一下,做不出来再看下面的解析。
#include
#include
#include
#include
//next数组的优化
//sub:子串
//str:主串
void NextVal(char* sub, int* nextval , int subLen)
{
assert(sub);
nextval[0] = -1;
if (subLen == 1)
{
return;
}
nextval[1] = 0;
int i = 2;//当前的i下标
int k = 0;//前一项的k值
//先将nextval数组初始化为next数组
while (i < subLen)
{
if (k == -1 || sub[i - 1] == sub[k])
{
nextval[i] = k + 1;
i++;
k++;
}
else
{
k = nextval[k];
}
}
//修改nextval数组
i = 2;
for (; i < subLen; i++)
{
//回退到的位置上的字符和当前i位置上的字符相等
//nextval[i]为回退到的位置
//sub[nextval[i]]为回退到的位置上的字符
if (sub[i] == sub[nextval[i]])
{
nextval[i] = nextval[nextval[i]];
}
}
}
int KMP2(char* str, char* sub, int pos)
{
assert(str && sub);
int strLen = strlen(str);
int subLen = strlen(sub);
if (subLen == 0) return 0;//子串sub为空字符串
if (strLen < subLen) return -1;//主串长度小于子串长度
if (pos < 0 || pos >= strLen) return -1;//索引位置非法
//nextval数组
int* nextval = (int*)malloc(sizeof(int) * subLen);
assert(nextval);//判断申请空间是否成功
NextVal(sub, nextval, subLen);
int i = pos;//遍历主串
int j = 0;//遍历子串
while (i < strLen && j < subLen)
{
if (j == -1 || str[i] == sub[j])
{
i++;
j++;
}
else
{
j = nextval[j];
}
}
if (j == subLen)//在主串中找到子串
{
free(nextval);
nextval = NULL;
return i - j;
}
else//在主串中找不到子串
{
free(nextval);
nextval = NULL;
return -1;
}
}
int main()
{
printf("%d\n", KMP2("ababcabcde", "abcd", 0));//5
printf("%d\n", KMP2("ababcabcde", "abcdf", 0));//-1
printf("%d\n", KMP2("ababcabcde", "ab", 0));//0
printf("%d\n", KMP2("ababcabcde", "a", 0));//0
printf("%d\n", KMP2("ababcabcde", "a", -1));//-1
printf("%d\n", KMP2("ababcabcde", "a", 11));//-1
printf("%d\n", KMP2("\0", "a", 0));//-1
return 0;
}
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:
输入:
haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad"
在下标 0 和 6 处匹配。第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:
haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto"
没有在"leetcode"
中出现,所以返回 -1 。
void NextVal(char* needle, int* nextval, int len2)
{
assert(needle);
nextval[0] = -1;
if(len2 == 1)
{
return;
}
nextval[1] = 0;
int i = 2;//当前i位置
int k = 0;//前一项的k值
//先将nextval数组初始化为next数组
while(i < len2)
{
if(k == -1 || needle[i - 1] == needle[k])
{
nextval[i] = k + 1;
i++;
k++;
}
else
{
k = nextval[k];
}
}
//修改nextval数组
for (i = 2; i < len2; i++)
{
//回退到的位置上的字符和当前i位置上的字符相等
//nextval[i]为回退到的位置
//sub[nextval[i]]为回退到的位置上的字符
if (needle[i] == needle[nextval[i]])
{
nextval[i] = nextval[nextval[i]];
}
}
}
int strStr(char * haystack, char * needle)
{
assert(haystack && needle);
int len1 = strlen(haystack);//haystack字符串的长度
int len2 = strlen(needle);//needle字符串的长度
if(len1 < len2) return -1;//主串长度小于子串长度
//引入nextval数组
int* nextval = (int*)malloc(sizeof(int) * len2);
assert(nextval);//判断申请空间是否成功
NextVal(needle, nextval, len2);
int i = 0;//遍历主串
int j = 0;//遍历子串
while(i < len1 && j < len2)
{
if(j == -1 || haystack[i] == needle[j])
{
i++;
j++;
}
else
{
//回溯
j = nextval[j];
}
}
if(j == len2)//在主串中找到子串
{
free(nextval);
nextval = NULL;
return i - j;
}
else//在主串中找不到子串
{
free(nextval);
nextval = NULL;
return -1;
}
}
本篇博客主要讲解了库函数
strstr
的源码、模拟实现库函数strstr
、BF算法以及KMP算法。博主相信这是全网最详细的字符串匹配算法讲解了,如果大家能够认真看完,肯定会有不少的收获。那就麻烦大家给个三连支持一下!谢谢大家啦!❣️