1. 程式人生 > IOS開發 >資料結構與演演算法6 -- 字串匹配

資料結構與演演算法6 -- 字串匹配

前言

字串匹配問題:給你兩個任意的字串
字串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",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,(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,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,a);
    }
    return 0;
}
複製程式碼

總結

本篇文章提供了經典問題字串匹配的5種解決方案,並對這5種方案的原理進行了例述。

文章地址https://juejin.im/post/5eb524d2e51d454dac4b7f13