前言
字符串匹配问题:给你两个任意的字符串
字符串A = "afhasoidfhaiodfaodfnoahfadfnad";
字符串B = "dfaod";
让你求出在A字符串中,B字符串首次出现的位置,如果没有,就返回-1。
注:通常字符串A被称为主串,字符串B被称为模式串。
字符串匹配是一个很常见的问题,但是这里面涉及到的算法却有好多种,并且各有各自的特色。相关的算法有以下几种:
- BF算法(不是boy friend,是brute force),暴力法,最简单直接,效率最低。
- RK算法,算是BF的优化版,把匹配字符串变成了匹配字符串的hash值。
- KMP算法,教科书级别的算法,效率较高,有点绕。
- BM算法,据说command + f快捷键搜索使用的就是这个算法,效率很高,大约是KMP的3~5倍。
- Sunday算法,算是BM的优化版,效率极高,也被称为最快的字符串匹配算法。
下面主要就是针对这几种算法来说说他们各自的原理及实现方式。
BF算法
BF算法又叫暴力法,听名字就知道,简单粗暴。
这种算法的主要思想就是从头开始对比,一个字符一个字符的对比,直到全部匹配或者主串结束为止。这也是我们遇见这个问题第一时间就能想到的算法。
代码如下:
#import <Foundation/Foundation.h>
#define NO_FOUND -1
// 暴力法
int findSubstringInStringBF(char *str, char *subStr)
{
if (!*str || !*subStr) {
return NO_FOUND;
}
int i = 0, j = 0;
char *p;
while (str[i])
{
// 比较第一个字母是否相同
if (str[i] == subStr[0]) {
// 开始匹配
p = str + i;
j = 1; // 第一个字符已经比较过了
// 字符都存在,并且相同
while (p[j] && subStr[j] && p[j] == subStr[j]) {
j++;
}
if (!subStr[j]) { // 子串完全匹配
return i;
}
if (!p[j]) { // 主串已经到末尾了,之后字符串长度都不够,就不需要再比较了
return NO_FOUND;
}
}
i++;
}
return NO_FOUND;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = findSubstringInStringBF("aaabcabcde", "abcd");
if (a >= 0) {
printf("找到的子字符串存在的索引是:%d\n", a);
}
else {
printf("没有找到这个字符串\n");
}
}
return 0;
}
RK算法
对于算法的介绍(哪一年提出的?谁提出的?。。。)就不说了,这些东西除了装逼以外毫无意义。下 法 同 懂???
下面进入正题
算法的思想
从最开始的简介可以知道,RK算法是将对比字符变成了对比hash值。
举个例子:
现在有主串A = "abcdefg", 模式串B = "abc";
暴力法:
要分别对比3次,即对比a、对比b、对比c,全部匹配了,则匹配成功。
那么能不能只对比一次就可以了呢?
这时候就想到了使用hash。
RK算法:
由于模式串的长度是3,那么主串就可以分成很多个长度是3的子串。
abc,bcd,cde,def,efg 分别计算他们的hash值
得到hash(abc),hash(bcd),hash(cde),hash(def),hash(efg)
将这些hash值与模式串B的hash值进行比较
如果hash值都不对应,那这两个字符串肯定不匹配;
如果hash值匹配了,别高兴太早,因为可能会存在hash冲突,再逐个字符对比验证一遍即可。
算法的优缺点
优点:hash值不一样的可以直接pass掉,防止出现这种aaaaaaaaaaab和aaaaaaaaaaaa匹配的情况(我都对比到最后一个字符了,你跟我说不匹配???)。
缺点:计算hash值同样耗时,并且hash值很可能会超过int类型的最大限制,需要设计一个好的hash算法,尽量避免hash冲突。
优化
通过上一个子串的hash值计算下一个子串的hash值。
首先,要知道怎么把一个字符串计算成一个数值?我们知道十进制数:
那么,我是不是可以把26个字母,认为是26进制呢?a = 1,b = 2, ···
于是,问题来了,如何把a转化为1 ???
char a = 'a';
int aInt = a - 'a' + 1; // 之所以要加1是因为我们要把a转化为1,而非0
// 同理,b, c, d ··· 都可以这样计算。
再回到上面那个问题,怎么优化hash的计算?
还是先回到十进制
那么
相信看了这两个公式,都应该懂优化方法是什么了吧?
也就是利用已经计算过的上一个子串的hash值计算下一个子串的hash值达到优化的效果。
代码实现
#import <Foundation/Foundation.h>
#define NO_FOUND -1
long hashStr(char *str, int length)
{
char *p = str;
long num = 0;
for (int i = 0; i < length; i++) {
int a = p[i] - 'a' + 1;
// 这里没有使用优化,因为我发现会计算错误
// 猜测可能是26进制中没有0导致的,以后找到问题了再来修改吧。
num += a * pow(26, length - 1 - i);
}
return num;
}
// 判断这两个字符串在指定长度内是否相等
BOOL isEqual(char *str1, char *str2, int length)
{
int strl1 = (int)strlen(str1);
int strl2 = (int)strlen(str2);
if (strl1 < length || strl2 < length) {
return NO;
}
int i = 0;
for (; i < length; i++) {
if (str1[i] != str2[i]) {
break ;
}
}
if (i == length) {
return YES;
}
return NO;
}
// RK 算法匹配字符串
int substringIndexFromString(char *str, char *subStr)
{
if (!*str || !*subStr) {
return NO_FOUND;
}
int strLen = (int)strlen(str); // 主串长度
int subLen = (int)strlen(subStr); // 子串长度
long subHash = hashStr(subStr, subLen); // 子串hash
int i = 0;
while (i <= strLen - subLen) {
if (subHash == hashStr(str, subLen) && isEqual(str, subStr, subLen)) {
return i;
}
i++;
str++;
}
return NO_FOUND;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = substringIndexFromString("aaabcabcde", "abcd");
if (a >= 0) {
printf("找到的子字符串存在的索引是:%d\n", a);
}
else {
printf("没有找到这个字符串\n");
}
}
return 0;
}
KMP算法
这个算法理解起来可能不是那么的容易,但也不算太难。
首先,这个算法的关键点就在于模式串的回溯数组。
模式串的回溯数组
之所以叫模式串的回溯数组,就是因为这个数组是根据模式串计算出来,不同的模式串计算出的结果不同。
回溯:回头追溯,即在某种条件下,回到原来某个已经出现过的字符的位置。
直接说概念可能不是很懂,下面举个例子应该就明白了。
举例:
主串:abcabcabcabf
子串:abcabf // 也叫模式串
a b c a b f 子串字符串
0 1 2 3 4 5 下标
0 0 0 1 2 0 回溯数组(保存回溯到的下标,先别管怎么来的,后面会说)
// 开始匹配字符串
1、当abcab全部匹配时,此时判断主串第二个c 和 f 不匹配了
2、就会找 f 前一个字母的回溯下标,找到下标是2
3、从下标是2的地方开始对比主串第二个c
4、匹配,往后移,即子串中的前面的那个ab被跳过了
5、继续匹配,到了第三个c 和 f 对比,发现又不匹配了。
6、重复步骤3
7、重复步骤4
8、发现 f 和 f 匹配了,并且子串结束,匹配成功
从例中可以知道,因为模式串中存在重复的ab
,因此当第二个ab
后面的那个字符f
匹配失败时,但从这里我们也能获得一个信息,就是f
前面的那个ab
是匹配成功的,否则也不会匹配到f
的位置。
那么既然ab匹配成功了,下次匹配的时候还有必要再对比ab吗?
没必要了,因此可以找到f
前面的那个字符b
位置对应的回溯下标2
,然后就可以看到,下标2
对应的模式串字符是c
,直接从c
开始接着对比,就跳过了c
前面的那个ab
。
计算回溯数组
仍然以上面那个模式串为例:
仍然以上面的子串为例
a b c a b f
1、第一个a 是第一个元素,所以回溯下标是0
a b c a b f
0
2、将 i 设置到第一个b 的位置,j 设置到第一个a 的位置,对比 i 和 j 对应元素是否匹配
j i
a b c a b f
0
3、发现不匹配,并且 j 前面已经没有元素了,所以第一个b 的回溯下标是0,i 向后移一个
j i
a b c a b f
0 0
4、同理,c 也是0
j i
a b c a b f
0 0 0
5、发现i 和 j 匹配了(第一个a 和 第二个a),此时,第二个a 对应的回溯下标就是 j + 1 , i 和 j 同时向后移
j i
a b c a b f
0 0 0 1
6、和第5步同理
j i
a b c a b f
0 0 0 1 2
7、此时 i 和 j 不匹配了,但是j 前面还有元素,则将j 回溯到 j - 1 对应的回溯下标处,此时j - 1对应的是b,b 的回溯下标是0
j i
a b c a b f
0 0 0 1 2
8、此时 i 和 j 仍然不匹配,由于此时 j 前面已经没有了,所以将 f 的回溯下标设置为0
j i
a b c a b f
0 0 0 1 2 0
9、i 后面已经没有元素了,说明该子串的回溯数组计算完成了。
代码实现
#import <Foundation/Foundation.h>
#define NO_FOUND -1
int* getBacktrackArr(char *str, int strLen)
{
// 回溯数组
int *btArr = malloc(sizeof(int) * strLen);
btArr[0] = 0; // 第一个为0
int j = 0;
for (int i = 1; i < strLen; i++) {
if (str[i] == str[j]) {
btArr[i] = j + 1;
j++;
}
else {
if (j == 0) {
btArr[i] = 0;
}
else {
j = btArr[j - 1];
// 此时i 还不能向后移,使用 i-- 来抵消本轮的 i++
i--;
}
}
}
return btArr;
}
int findSubstringInStringKMP(char *str, char *subStr)
{
if (!*str || !*subStr) {
return NO_FOUND;
}
// 获取子串的回溯数组
int subLen = (int)strlen(subStr);
int *btArr = getBacktrackArr(subStr, subLen);
// 遍历字符
char *p = str;
int i = 0, j = 0;
while (p[i] && j < subLen) {
if (p[i] == subStr[j]) {
i++;
j++;
}
else {
if (j == 0) {
i++;
}
else {
j = btArr[j - 1];
}
}
}
free(btArr);
if (j == subLen) {
return i - subLen;
}
return NO_FOUND;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = findSubstringInStringKMP("abccbddfaaabcabcabcabcabcabxasabc", "abcabcabx");
if (a >= 0) {
printf("找到的子字符串存在的索引是:%d\n", a);
}
else {
printf("没有找到这个字符串\n");
}
}
return 0;
}
BM算法
看完了前面的KMP算法之后可以发现,这个算法的设计者还真是挺牛逼的,太巧妙了。然而这个算法在实际开发中用的并不多,顶多也就是考研和面试可能会遇到。
那这么巧妙的算法为什么实际开发中用的不多呢?效率太慢了,接下来要说的BM算法据说效率可是能达到KMP的3~5倍
(当然,这些都是笔者听别人说的,应该是有大牛测试过这两种算法的效率,反正我是没亲测过😁,错了别找我啊)。
BM算法原理
其他好多文章都说这个算法比KMP
更容易理解,我觉得不是。KMP
算法我研究了两遍字符串匹配的步骤就搞懂了原理,但是这个玩意,整整看了两天,看书,看别人的博客,最后才明白这个算法到底是如何进行字符串匹配的。
首先介绍几个这个算法独特的点
匹配顺序
从前面几个算法可以看到,匹配顺序都是从左到右匹配,这个算法不一样,是反的,从右到左开始匹配。
// 例如
// 主串:abcdefabcdefg
// 子串:abcdefg
// 如上,正向匹配,需要匹配成功abcdef 6次,第7次匹配失败
// 但是,逆向匹配,只需要1次就能判断匹配失败。
// 当然,这个并没什么用,因为我们无法确定要匹配的字符串到底是什么样的
// 例如
// 主串:afedcbagfedcba
// 子串:gfedcba
坏字符
所谓坏字符指的就是每一轮字符串匹配中,主串中第一个与模式串不匹配的那个字符。
例如
主串:abcdefg
子串:abd // 此时,主串的c就是坏字符
子串:abbce // 此时,主串的d就是坏字符(不是e,因为e匹配成功)
好后缀
所谓好后缀,就是在逆向匹配的过程中,匹配成功的那些字符组成的字符串。
例如
主串:abcdefg
子串:bacde // 此时的cde就是好后缀,主串的b是坏字符
问题来了,这些东西有什么用呢?
下面先说说坏字符
在字符串匹配中的作用。(懒得画图,就直接使用代码块咯)
例如
主串:abcdefg
子串:def
偏移: def
// 倒着匹配,发现c和f不匹配,此时c是坏字符,通过c查询子串中是否含有c
// 结果没有,那么就直接把子串向后偏移到c的下一个字符,因为c和c前面的字符不可能匹配了
再例如
主串:aaaabcd
子串:abcd
偏移: abcd
// 倒着匹配,发现a和d不匹配,此时a是坏字符,通过a查询子串中是否含有a
// 结果有,并且a在第0个位置,因此时子串匹配失败的字符是d,在第3个位置
// 因此偏移 3 - 0 个字符
再例如
主串: baabaaab
子串: aaab
偏移:aaab
// 倒着匹配,aab全都匹配,最后的b和a不匹配,主串b是坏字符,找子串的b
// 找到了,b在第3个位置,此时偏移 0 - 3 个字符
// 啊嘞?什么鬼?搞错了吧?怎么还会往前偏移嘞?
其实第3个例子中并没有错,按理说就应该是这样。而这一点也就是坏字符
匹配的缺点所在,所以除了坏字符匹配法,还要说说好后缀匹配法。
首先声明,坏字符和好后缀这两种匹配方法都属于BM算法,但是他们二者之间是没有关系的,也就说,哪怕你的算法中只使用了其中的一种,也是可以的,只是坏字符方法会存在上面例子中的那个往前偏移的问题(好后缀可以单独使用)。
下面就来说说好后缀
吧,这里才是困扰笔者两天时间的关键所在。
// 仍然以举例的方式讲
例如
主串:babacabdeabxxxx
子串:cabdeab
对齐: cabdeab
// 此时倒数第3个字符c和e不匹配,得到好后缀是ab,然后需要在子串中查找
// 除了好后缀本身以外,是否还有其他和好后缀完全相同的子串,找到了,在
// 子串的第1个字符处(c是第0个字符),因此就需要把主串中对应好后缀的ab和
// 子串中我们找到的那个ab对齐,继续往后匹配。(如果子串中有多个ab,
// 取除了好后缀本身以外的最后一个ab,因为这样偏移的数值最小,不会漏)
再例如
主串:aabbdabcddabcxxxx
子串:abcddabc
对齐: abcddabc
// 此时得到的好后缀是dabc,在子串中查找除好后缀本身外的其他的dabc
// 发现找不到,怎么办呢?<<重点:找好后缀的子字符串是否和前缀相同?>>
// 好后缀的子串有:abc,bc,c。至于da,ab这些就不算了,因为他们不是后缀,
// 没意义。接着,可以发现,好后缀dabc的子字符串abc刚好和子串的前缀相同
// 此时,将他们对齐,继续下一轮匹配。
再例如
主串:aabbefgabcdefgxxxx
子串:abcdefg
对齐: abcdefg
// 此时得到的好后缀是efg,发现子串中并没有除了好后缀外的其他efg
// 并且efg的子字符串也没有和子串前缀相同的,what?这怎么办?
// 这种情况最简单了,直接往后偏移整个子串即可。前面那些不可能匹配了。
接着,问题又来了,既然好后缀匹配法存在这3种不同的情况,那他们之间有没有优先级呢?如果这些情况同时出现了怎么办?(ps:第3种不可能与前两种同时出现)
例如:
主串:aabbabdabcdabxxxx
子串:abdabcdab
对齐: abdabcdab // 按情况1对齐
对齐: abdabcdab // 按情况2对齐
// 此例中,好后缀是dab。子串中有重复的dab(第1种情况)
// dab的子字符串ab和子串前缀相同(第2中情况)
// 这两种情况的对齐结果分别如上所示,相信不用多说谁优先级高了吧?
到这里,相信你已经知道坏字符匹配法和好后缀匹配法的匹配原理了吧?
预处理
知道匹配原理就能开始写代码了吗?想多了,接下来才是这个算法最精(e)妙(xin)的地方。
想想上面好后缀的两种情况
情况一
出现了好后缀,我需要在子串中查找该好后缀是否有重复出现的地方,如果有还要知道重复出现的位置在哪?
说的好听,不就是查找好后缀重复出现的位置吗?请问怎么找?好后缀的字符串长度能确定吗?
情况二
出现了好后缀,好后缀在子串中没有重复,那就找找好后缀的子字符串中有没有和前缀相同的?怎么找?好后缀有几个字符都不确定,更别说是好后缀的子字符串了?
因为直接找符合上面两种情况的,很难找,所以就需要我们先对子串进行预处理,使我们很轻易的就能找到需要的位置。
好后缀预处理的代码如下:
/// 预处理好后缀
/// @param pStr 模式串
/// @param pLen 模式串长度
/// @param suffix 返回模式串后缀字符在模式串中重复出现的位置(不包括原本的位置,即如果除了后缀没有出现过,那就是没重复,为-1)
/// 例如:模式串pStr = "abcdefg"; 由于后缀g只在最后出现,因此该模式串的后缀数组的全部元素都是-1,代表没有和后缀重复的
/// 例如:模式串pStr = "abcdefa"; 由于后缀a在第0个位置也有,并且后缀a只有1个字符,因此suffix[1] = 0
/// 例如:模式串pStr = "abcdeab"; 由于后缀b在第1个位置存在,并且b前面的a在第0个位置也存在,因此suffix[1(后缀第1个字符)] = 1(下标1的字符,是b), suffix[2] = 0;
/// 例如:模式串pStr = "abcabab"; 由于后缀ab在0~1位置存在,并且也在3~4的位置存在,因此suffix[1] = 4, suffix[2] = 3;(而非1和0)
///
/// @param prefix 好后缀的子串是否和前缀相同(注意:这是一个bool类型数组)
/// 例如:模式串pStr = "abcdefg"; 由于后缀g只在最后位置出现,即不存在前缀相同的情况,因此都是flase;
/// 例如:模式串pStr = "abcdefa"; 由于后缀a和前缀相同,因此就可以记prefix[1] = true;
/// 例如:模式串pStr = "abcdabc"; 由于后缀abc和前缀相同,因此可以记prefix[3] = true; // 3代表的是后缀长度3
///
void goodSuffix(char *pStr, int pLen, int *suffix, bool *prefix)
{
// 初始化suffix 和 prefix
for (int i = 0; i < pLen; i++) {
suffix[i] = -1; // 默认没有重复的位置
prefix[i] = false; // 默认不存在前后缀相同
}
// j 表示从前面数第几个字符(从0开始),k 表示从后面数第几个字符(从1开始)
int j = 0, k = 0;
for (int i = 0; i < pLen - 1; i++) {
j = i;
k = 1;
// j 代表模式串中从前面数的字符,pLen - k 代表后面的字符
while (j >= 0 && pStr[j] == pStr[pLen - k]) {
// 保存第k个后缀重复出现的位置
suffix[k] = j;
// 全部都往前挪一个字符
j--;
k++;
}
// 判断k长度的后缀是否和前缀相同
if (j == -1) {
prefix[k] = true;
}
}
}
注释很详细了,应该都能看懂,有看不懂的地方欢迎评论留言。
同样的,既然好后缀能够预处理,那我坏字符是不是也可以?
当然可以,而且没这么复杂。
坏字符预处理代码如下:
// 通过模式串计算坏字符
int* badChar(char *pStr, int pLen) {
// 坏字符数组
int *bc = malloc(sizeof(int) * SIZE);
// 初始全部设置为-1,代表模式串中没有这个字符
for (int i = 0; i < SIZE; i++) {
bc[i] = -1;
}
// 遍历模式串
// 为了实现通过 字符c 找到 字符c 在模式串中的位置的功能
// 如:模式串abcdef,通过 字符d 就能直接获取d在模式串中的位置为3,若字符重复,只记录最后一次出现的位置
// 这就是坏字符规则
for (int i = 0; i < pLen; i++) {
// 模式串中第i个字符对应的ascii码
int ascii = (int)pStr[i];
bc[ascii] = i; // 保存这个字符在模式串中的位置
}
return bc;
}
接下来就是字符串匹配的代码:
#import <Foundation/Foundation.h>
#define SIZE 256 // 字符集字符数
#define NO_FOUND -1
/*
-----------这里放上面那俩预处理方法即可--------------
*/
// 字符串匹配的方法
int findStringIndexWithSubstring(char *str, char *subStr, int stl, int ssl)
{
// 坏字符数组
int *bc = badChar(subStr, ssl);
// 好后缀
int *suffix = malloc(sizeof(int) * ssl);
// 不同长度的后缀是否和前缀相同,数组的下标表示的是后缀的长度
bool *prefix = malloc(sizeof(bool) * ssl);
// 向suffix 和 prefix中填充数据
goodSuffix(subStr, ssl, suffix, prefix);
// 正式开始匹配字符串
int i = 0, j = 0;
// 每次偏移的字符个数
int os1 = 0, os2 = 0;
// 当i > stl - ssl时,说明剩余的字符个数已经没有子字符串那么长了,不可能再匹配成功
while (i <= stl - ssl) {
// BM算法倒着匹配,从模式串的最后一个字符开始匹配
for (j = ssl - 1; j >= 0; j--) {
// str[i + j]表示主串中的要参与匹配的字符,subStr[j]表示模式串(子串)中要参与匹配的字符
if (str[i + j] != subStr[j]) {
// 不想等,匹配失败,退出循环
break;
}
}
// 如果j == -1,说明一直在匹配成功,不是通过break退出循环,而是通过循环条件结束的循环,说明字符串匹配成功
if (j == -1) {
// 释放空间
free(bc);
free(suffix);
free(prefix);
return i;
}
// 否则,即j >= 0,开始计算偏移字符的个数
// 1. 按照坏字符规则计算 str[i + j]为主串中匹配失败的那个字符,即坏字符
os1 = j - bc[str[i + j]];
// 2. 按照好后缀规则计算,如果有好后缀的话
os2 = 0;
if (j < ssl - 1) {
// 好后缀的字符串长度
int k = ssl - 1 - j;
// 如果 k 长度的后缀 有重复的字符串
if (suffix[k] != -1) {
os2 = j - suffix[k] + 1;
}
else {
int x = 0;
// x表示好后缀的子字符串的起始下标,ssl - x 表示好后缀的子字符串的长度
for (x = j + 1; x < ssl; x++) {
if (prefix[ssl - x]) {
os2 = x;
break ;
}
}
// 说明没有通过break退出
if (x == ssl) {
// 即没有与好后缀完全相同的字符串,也没有前缀和好后缀的子字符串相同的,那就偏移整个模式串的长度
os2 = ssl; // 直接跳过全部
}
}
}
// 计算实际偏移的大小(取坏字符和好后缀中最大的那个)
int offset = os1 > os2 ? os1 : os2;
// 偏移offset大小
i = i + offset;
}
// 没有匹配到这个字符串,释放空间,并返回-1
free(bc);
free(suffix);
free(prefix);
return NO_FOUND;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
char *mainStr = "aabcaababcaabcbabcdeaabc";
char *subStr = "aababcaa";
int a = findStringIndexWithSubstring(mainStr, subStr, (int)strlen(mainStr), (int)strlen(subStr));
printf("%d\n", a);
}
return 0;
}
Sunday算法(被称为最快的字符串匹配算法)
此算法可以说是BM算法的改良版,和BM算法的思想有些类似,不同的是他是从前往后,从左到右进行匹配。
Sunday算法在匹配失败时关注的也不再是坏字符和好后缀了,而是主串中 参加匹配的 最末位字符的 下一位字符
。
算法原理
这个算法的原理要比BM算法容易理解的多。但是笔者的语言总结能力不强,还是举例说明吧。
举例
主串:substring searching
子串:search
偏移: search // 偏移1
偏移: search // 偏移2
// 第一次匹配中
// 主串中 参与匹配 的字符串是substr,所以最末位的字符是r
// r的下一位字符是i。
// 查找子串中是否有i这个字符,结果没有,往后偏移子串长度+1
// 即从i后面的那个n开始第二次匹配
//
// 第二次匹配中
// 主串中 参与匹配 的字符串是ng sea,所以最末位的字符是a
// a的下一位字符是r。
// 查找子串中是否有r这个字符,结果有,就将这个r和子串中的r对齐。
// 即得到偏移2的结果,匹配成功
代码实现
Sunday算法的算法实现也比较简单。
代码如下:
#import <Foundation/Foundation.h>
#define SIZE 256
#define NO_FOUND -1
// 子串的偏移表
// 类似于KMP的next数组
int* offsetTable(char *str, int len)
{
int *os = malloc(sizeof(int) * SIZE);
for (int i = 0; i < SIZE; i++) {
// 默认偏移len + 1的长度
os[i] = len + 1;
}
for (int i = 0; i < len; i++) {
os[str[i]] = len - i;
}
return os;
}
// 查找子串所在的索引
int findSubstringIndex(char *str, char *subStr, int stl, int ssl)
{
// 获取偏移数组
int *os = offsetTable(subStr, ssl);
int i = 0, j = 0;
while (i <= stl - ssl) {
j = 0;
// 匹配字符串
while (str[i + j] == subStr[j]) {
j++;
// 匹配成功
if (j == ssl) {
return i;
}
}
// 匹配失败
i = i + os[str[i + ssl]];
}
return NO_FOUND;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
char *mainStr = "aabcaababcaabcbabcdeaabc";
char *subStr = "abcd";
int a = findSubstringIndex(mainStr, subStr, (int)strlen(mainStr), (int)strlen(subStr));
printf("%d\n", a);
}
return 0;
}
总结
本篇文章提供了经典问题字符串匹配的5种解决方案,并对这5种方案的原理进行了例述。
文章地址https://juejin.cn/post/6844904158865129486