数据结构与算法6 -- 字符串匹配

1,657 阅读22分钟

前言

字符串匹配问题:给你两个任意的字符串
字符串A = "afhasoidfhaiodfaodfnoahfadfnad";
字符串B = "dfaod";
让你求出在A字符串中,B字符串首次出现的位置,如果没有,就返回-1。

注:通常字符串A被称为主串,字符串B被称为模式串。


字符串匹配是一个很常见的问题,但是这里面涉及到的算法却有好多种,并且各有各自的特色。相关的算法有以下几种:

  1. BF算法(不是boy friend,是brute force),暴力法,最简单直接,效率最低。
  2. RK算法,算是BF的优化版,把匹配字符串变成了匹配字符串的hash值。
  3. KMP算法,教科书级别的算法,效率较高,有点绕。
  4. BM算法,据说command + f快捷键搜索使用的就是这个算法,效率很高,大约是KMP的3~5倍。
  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值。

首先,要知道怎么把一个字符串计算成一个数值?我们知道十进制数:123 = 1 * 10^2 + 2 * 10^1 + 3 * 10^0
那么,我是不是可以把26个字母,认为是26进制呢?a = 1,b = 2, ···
于是,问题来了,如何把a转化为1 ???

char a = 'a';
int aInt = a - 'a' + 1;     // 之所以要加1是因为我们要把a转化为1,而非0
// 同理,b, c, d ··· 都可以这样计算。

再回到上面那个问题,怎么优化hash的计算?
还是先回到十进制
123 = 1 * 10^2 + 2 * 10^1 + 3 * 10^0
那么
234 = (1 * 10^2 + 2 * 10^1 + 3 * 10^0 - 1 * 10^2) * 10 + 4 * 10^0

相信看了这两个公式,都应该懂优化方法是什么了吧?
也就是利用已经计算过的上一个子串的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