『C語言學(xué)習(xí)筆記』BF,KMP,SUNDAY,BM

字符串匹配

假設(shè)有兩個串s和p,字符串匹配就是在s中查找與p相同的子串的操作。將s稱為目標(biāo)串,p稱為模式串。

BF

BF(Brute Force)算法又稱為暴力匹配算法。將p與s的所有的子串進行匹配。最壞情況O(m*n)。

// return: first match index of s
int bf_match(char const * s, char const * p)
{
    int p_len = strlen(p);
    int max_s_len = strlen(s) - p_len + 1;
    int s_ind = 0;
    int p_ind = 0;

    while (s_ind <  max_s_len && p_ind < p_len)
    {
        if (s[s_ind] == p[p_ind])
        {
            s_ind++;
            p_ind++;
        }
        else
        {
            s_ind = s_ind - p_ind + 1;
            p_ind = 0;
        }
    }

    return p_ind == p_len ? s_ind : -1;
}

KMP

Knuth-Morris-Pratt字符串查找算法,簡稱為 “KMP算法”由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年聯(lián)合發(fā)表。

Part 1

定義:綠色為SP匹配部分;紅色為SP不匹配部分,此時的下標(biāo)S[j],P[i],并且P[0:i-1] == S[j-i:j-1]。

bf算法的最大問題是:不會跳過無意義的步驟。如下圖,在S[4]:e和P[4]:f匹配失敗后,P[0]:a依然和S[1:3]依次進行匹配,做無用功。

sp0.png

結(jié)論1:匹配部分長度大于0(i>0),如果P[0]在當(dāng)前P[0:i-1]只出現(xiàn)一次,那么去匹配P[0]和S[j];匹配部分長度等于0(i=0),P[0] != S[j],那么去匹配P[0]和S[j+1]。

Part 2

如下圖,與結(jié)論1相反,在匹配部分出現(xiàn)多次P[0]:a是P[2]和P[4],在S[6]:e和P[6]:f處匹配失敗。

sp2.png

如下圖,如果將P[0]:a與S[2]:a匹配,會在P[1]:b和S[3]:c處匹配失敗,那么現(xiàn)在的情況又回到了結(jié)論1,將會進行P[0]:a與S[3]:c匹配,一樣會失敗。所以,這樣的匹配會進行多次的無用功。在S[6]:e與P[6]:f匹配失敗時就可以觀察到P[0]:a與S[2]:a匹配最終一定會失敗的,這個信息是在P[6]:f匹配失敗時通過P串就能獲得的,并不需要S串。

sp3.png

如下圖,如果將P[0]:a與S[4]:a匹配,P[1]:b和S[5]:b也是匹配的,在S[6]:e與P[6]:f匹配失敗時就可以觀察到P[0]:a,b與S[4]:a,b匹配,這個信息是在P[6]:f匹配失敗時就能獲得的,并不需要S串。至于P[2]和S[6]是否匹配需要進行,不能單靠P[6]:f匹配失敗時的P串獲得這個信息。

sp4.png

如上圖,這個匹配方法相比于bf跳過了P[0]與S[1:5]的比較,P[0]:a,b與S[4]:a,b是不需要匹配的,直接進行P[2]:a與S[6]:e的匹配,S串不進行回溯,P串進行短回溯。重點是如何找到這樣的位置進行匹配:

∵ S[6]:e != P[6]:f
∴ S[0:5] == P[0:5]
export 0 <= n <= 4
if S[0:n] == P[0:n] && S[5-n:5] == P[0:n]
then P[0:n] == P[5-n:5]

結(jié)論2:P[i]匹配失敗,i > 1 && 0 <= n <= i-2,如果存在P[0:n] == P[i-1-n:i-1],就是P[0:i-1]存在一個前綴和后綴是相同的,那么進行P[n+1]與S[j]匹配。

為什么是P[n+1]?觀察上圖,n == 1,前綴P[0:1]的長度為2,需要和S[6]進行匹配的P的下標(biāo)也是2,都剛好是n+1。結(jié)論是,如果存在前綴,則進行P[前綴長度]和S[j]的匹配。

Part 3

sp5.png

如上圖,根據(jù)結(jié)論2會出現(xiàn)下圖兩種情況都滿足。

sp6.png

按照從S左到右的匹配順序,不應(yīng)該錯過P[0]與S[2]對齊時的P[4]與S[6]的匹配,直接進行P[0]與S[4]對齊時的P[2]與S[6]的匹配。在多種情況時,應(yīng)該選擇P[0]與S[最小能夠?qū)R的下標(biāo)]進行對齊。

結(jié)論3:應(yīng)該選擇滿足 結(jié)論2 并且是最長的相同前后綴。

觀察上圖,在P[0]與S[2]對齊的匹配失敗后,會自然落到P[0]與S[4]對齊的情況。

Part 4

基于以上結(jié)論,在P[i]和S[j]在匹配失敗后,S串不回溯,P串回溯到i等于P[0:i-1]的最長相等前后綴長度。

現(xiàn)在需要構(gòu)造一個next數(shù)組,使得在P[i]匹配失敗后通過next[i]獲得P[0:i-1]的前綴長度。

static int * get_next(char const *p);

// return: first match index of s
int kmp_match(char const *s, char const *p)
{
    int p_len = strlen(p);
    int max_s_len = strlen(s) - p_len + 1;
    int s_ind = 0;
    int p_ind = 0;
    int *next = _get_next(p);

    while (s_ind <  max_s_len && p_ind < p_len)
    {
        if (s[s_ind] == p[p_ind])
        {
            s_ind++;
            p_ind++;
        }
        else
        {
            //=======================
            // s_ind = s_ind - p_ind + 1;
            p_ind = next[p_ind];
            //=======================
        }
    }
    free(next);

    return p_ind == p_len ? s_ind : -1;
}

上面的kmp_match是通過bf_match改造的:s_ind不變,p_ind回溯。

但是存在一個bug,已知P[0]匹配失敗時的前綴長度是0,按照 結(jié)論1 是P[0]去和S[j+1]匹配,匹配失敗的代碼是:

    else
    {
        //=======================
        // s_ind = s_ind - p_ind + 1;
        p_ind = next[p_ind];
        //=======================
    }

p_ind是0沒有問題,但是s_ind沒有更新,進入下一次循環(huán)還是P[0]和S[j]匹配,那么while是一直原地踏步。

應(yīng)該增加一條P[0]匹配失敗的條件分支,則:

    if (s[s_ind] == p[p_ind])
    {
        // 匹配成功
        s_ind++;
        p_ind++;
    }
    else if (p_ind)
    {
        // 匹配失敗,并且i=0時,更新j為j+1
        s_ind++;
    }
    else
    {
        // 匹配失敗,更新i,如果i=0,依舊更新為0
        //=======================
        // s_ind = s_ind - p_ind + 1;
        p_ind = next[p_ind];
        //=======================
    }

為了簡化代碼,規(guī)定P[0]匹配失敗時的前綴長度(next[0])為-1,則:

    if (p_ind == -1 || s[s_ind] == p[p_ind])
    {
        // i=-1時,二次進入將i更新為0,j更新為j+1
        s_ind++;
        p_ind++;
    }
    else
    {
        // 匹配失敗,更新i,如果i=0,更新為-1
        p_ind = next[p_ind];
    }

只看這段代碼:只要進入循環(huán)時i == -1, 那么i = 0, j++。

Part 5

構(gòu)造next數(shù)組有兩種方式:1. 暴力構(gòu)造;2.遞推構(gòu)造

原理:next[i]是P[i]匹配失敗時P[0:i-1]的最長相等前后綴的長度。

暴力構(gòu)造

static int _strcmp(
    const char *p, 
    unsigned int len, 
    unsigned int index_1, 
    unsigned int index_2
)
{
    int cnt = 0;
    while (p[index_1] - p[index_2] == 0 && cnt < len) {            
        index_1++;
        index_2++;
        cnt++;
    }

    return cnt == len ? 1 : 0;
}

static int * _get_next(char const *p) 
{
    int p_len = strlen(p);
    int *next = malloc(sizeof(int) * p_len);
    next[0] = -1;
    unsigned int next_ind = 1;
    unsigned int substr_len;
    unsigned int max_preffix_len;

    for (; next_ind < p_len; next_ind++)
    {
        substr_len = next_ind;
        max_preffix_len = sub_str_len - 1; // 從最長的前綴和后綴開始
        while (max_preffix_len > 0) 
        {                                
            if (_strcmp(p, max_preffix_len, 0
                , substr_len - max_preffix_len))
            {                    
                break;
            } else {
                max_preffix_len--;
            }
        }
        next[next_ind] = max_preffix_len;
    }

    return next;
}

遞推構(gòu)造

原理:根據(jù)前面構(gòu)造好的最長前后綴長度推出這個前綴長度。

sp7.png

上圖中,無論?處是多少,next[11]都應(yīng)該是5,那??處該是多少。

P[11]最長相等前后綴的長度為5,如果P[11] == P[5],如下圖,那會出現(xiàn)P[0:5] == P[6:11] ,相當(dāng)于將P[0:4] == P[6:10]等式兩邊都延長一個單位,等同于next[12] = next[11] + 1。

sp8.png

遞推結(jié)論1:如果i > 1 && P[next[i-1]] == P[i-1], 那么next[i] = next[i-1] + 1。

如果P[11] != P[5],那上面的情況是沒戲了。那就找比最長短一點的,需要在最長前綴P[0:4]中找一個前綴,在最長后綴P[6:10]中找一個后綴。如下圖。

sp9.png

因為P[0:4] == P[6:10],所以就相當(dāng)于在P[0:4]中找一對最長前后綴,如下圖,前綴長度顯然存放在next[5]中。

sp10.png

此時,如果P[11] == P[2],那么next[12] = next[5] + 1。如下圖。

sp11.png

如果P[11] != P[2],像P[11] != P[2]一樣。那就找更短一點的,需要在前綴P[0:1]中找一個前綴,在最長后綴P[9:10]中找一個后綴。
因為P[0:1] == P[9:10],所以就相當(dāng)于在P[0:1]中找一對最長前后綴,如下圖,前綴長度顯然存放在next[2]中。

sp12.png

此時,如果P[11] == P[0],那么next[12] = next[2] + 1。如下圖。

sp13.png

如果P[11] != P[0],已經(jīng)沒有更短的前后綴了,next[12] = next[0] + 1 = -1 + 1 = 0。

遞推結(jié)論2:如下圖,為得到next[i],需要P[i-1]的最長前后綴,如果P[i-1] == P[next[i-1]],那么next[i] = next[i-1] + 1;如果不相等,找第二長的前后綴,如果P[i-1] == P[next[next[i-1]]], next[i] = next[next[i-1]] + 1; ...直到最后沒有前后綴,P[i-1] == P[0], 則next[i] = 0 + 1, 這個0是在圖中next[2]這樣得來的,P[i-1] != P[0], 則next[i] = -1 + 1 = 0。

sp14.png

在過程中好像一直在找更短的前后綴,使得滿足P[i-1] == P[前后綴長度],致使P[i]得到最長的前后綴長度。實際只測試了條件,不滿足就去找更短的前后綴長度來測試條件,前后綴長度之間的關(guān)系:next[當(dāng)前前后綴長度] == 更短的前后綴長度。

代碼實現(xiàn):

static int * _get_next(char const *p) 
{
    int p_len = strlen(p);
    int *next = malloc(sizeof(int) * p_len);
    next[0] = -1; // P[0]的next[0]
    int index = 1; // next數(shù)組當(dāng)前索引,需要求next[index]

    // k為next[index-1],求next[index]時需要next[index-1]遞推
    int k = next[0];

    while (index < p_len) 
    {
        if (k == -1) // P[i-1]不等于P[0],或i=1時,當(dāng)前的前綴長度更新為0;
        {
            next[index] = k + 1;
            index++;
            k++;
            // index++,原k=next[index-1],更新為k=next[index]
        }
        // k=next[index-1],匹配p[index - 1]和p[index-1的前綴長度]
        else if (p[index - 1] == p[k]) 
        {
            next[index] = k + 1;
            index++;
            k++;
        }
        else 
        {
        // 更新k為更短的前綴長度,在p[index-1] != p[k]且k=0時,會將k更新為-1
            k = next[k] ;
        }
    }

    return next;
}

// 合并前兩個分支
static int * _get_next(char const *p) 
{
    int p_len = strlen(p);
    int *next = malloc(sizeof(int) * p_len);
    next[0] = -1; 
    int index = 1; 
    int k = -1;

    while (index < p_len) 
    {
        if (k == -1 || p[index - 1] == p[k])
        {
            next[index] = k + 1;
            index++;
            k++;
        }
        else 
        {
            k = next[k] ;
        }
    }

    return next;
}

// 優(yōu)化index,從求next[index]變成求next[index+1]
static int * _get_next(char const *p) 
{
    int p_len = strlen(p);
    int *next = malloc(sizeof(int) * p_len);
    next[0] = -1; 
    int index = 0; // 從0開始
    int k = -1;

    while (index < p_len -  1) 
    {
        if (k == -1 || p[index] == p[k])
        {
            index++;
            k++;
            next[index] = k;
        }
        else 
        {
            k = next[k] ;
        }
    }

    return next;
}

優(yōu)化,連續(xù)失敗情況:

sp15.png

上圖中,在S[j]:?和P[11]處匹配失敗,d != ?,下一步應(yīng)該將P[5]和S[j]:? 匹配。觀察得出P[5] == P[11],那么P[5]和S[j]:?是一定匹配失敗的。

也就是在P[i]處的匹配失敗時,如果P[next[i]] == P[i],那么P[next[i]]也一定是匹配失敗的。這樣說明構(gòu)建的next數(shù)組是不合理的,所以在P[i] == P[next[i]]時,應(yīng)該構(gòu)建短一點的前后綴。如下圖。

sp16.png

上圖,將next[11]設(shè)置為2更合理。疑問:如果P[2]不是c,也是d怎么辦?

sp17.png

上圖中,無論是將next[11]設(shè)置為8、5、2都會匹配失敗,應(yīng)該設(shè)為0。但是這是反向遞推,需要next回退幾步;實際正向遞推是這樣:

sp18.png

得到next[2]=0,就得到next[5],next[8],next[11]。

優(yōu)化后的方法:

static int * _get_next(char const *p) 
{
    int p_len = strlen(p);
    int *next = malloc(sizeof(int) * p_len);
    next[0] = -1; 
    int index = 0; // 從0開始
    int k = -1;

    while (index < p_len -  1) 
    {
        if (k == -1 || p[index] == p[k])
        {
            index++;
            k++;
            // next[index] = k; 會出現(xiàn)p[next[index]] = p[index]連續(xù)失敗的情況
            if (p[index] != p[k])
            {
                next[index] = k;
            }
            else
            {
                // p[index] == p[k]時,回退一步k
                next[index] = next[k];
            }
        }
        else 
        {
            k = next[k] ;
        }
    }

    return next;
}

說明next出現(xiàn)非索引0處為-1的情況:

在上上面的kmp算法 結(jié)論1 中,next[i]=-1時,使i=0,j++;就是出現(xiàn)了P[0]處匹配失敗。如下圖:

sp19.png

圖中,P[3]和P[6]都是前綴長度為0,匹配S[j]失敗時,原本都是和P[0]再進行匹配,又因為P[3]和P[6]都等于P[0],所以next[3]和next[6]都應(yīng)該等于-1,使得進行P[0]和S[j+1]匹配。

在next數(shù)組構(gòu)造中體現(xiàn)于:

    if (p[index] != p[k]) 
    {
        next[index] = k;
    }
    else
    {
        // 在p[index] == p[0]時,next[index] = next[0] = -1
        next[index] = next[k];
    }

Sunday

Sunday算法是DanielM.Sunday于1990年提出的字符串模式匹配。

在S串和P串存在匹配的字串s,必然有s == P,如果S串中包含P串沒有的字符k,那么s中一定不包含k。Sunday算法就是基于這個事實。

sp20.png

上圖中,P串中不包含字符S[5],那么P與S匹配的情況可能是P1、P2、P3,絕不可能是P4。

Part 1

sp21.png

上圖中,P串長度為7,與S串在P[4]處匹配失敗,接下來P串要相對S串向后移一位?,F(xiàn)在的問題是P串中是否包含S[7]?

sp22.png

上圖中,P不包含字符S[7],那么P串與S串處于上圖中P1~7狀態(tài)的對齊匹配都是會與S[7]匹配失敗,只有在P8狀態(tài)跳過與S[7]的匹配是有可能成功的。

結(jié)論1:P串長為length,P[0]與S[k]對齊進行匹配,在P串的任何匹配失敗時,如果P串不包含字符S[k+length],則P[0]與S[k+length+1]對齊匹配。

Part 2

sp23.png

如果P串中包含S[7]呢?如上圖,P[1]、P[3]、P[5]都等于S[7]。

sp24.png

在上圖中,S[7]與P[1]、P[3]、P[5]對齊時的P2、P4、P6狀態(tài)都是有可能匹配成功的,而P1、P3、P5、P7對齊狀態(tài)是絕不可能匹配成功的。

按照匹配順序,P2狀態(tài)是步幅最小的、合理的。

結(jié)論2:P串長為length,P[0]與S[k]對齊進行匹配,在P串的任何匹配失敗時,如果P串包含字符S[k+length],則S[k+length]與其在P串中最右出現(xiàn)的位置對齊,從P[0]匹配。

// return a hash table, key : byte, val : the byte in rightest
static int * _get_rightest(char const *p);

// return: first match index of s
int sunday_match(char const *s, char const *p)
{
    int p_len = strlen(p);
    int max_s_len = strlen(s) - p_len + 1;
    int s_ind = 0;
    int p_ind = 0;
    int *rightest = _get_rightest(p);

    while (s_ind < max_s_len && p_ind < p_len)
    {
        if (s[s_ind] == p[p_ind])
        {
            s_ind++;
            p_ind++;
        }
        else
        {
            // 獲取匹配失敗時,類似上面S[7]位置的byte
            int ac_index_s = s_ind + p_len - p_ind;
            unsigned char ac = s[ac_index_s];

            // ac字符在p串最右面的出現(xiàn)的位置
            int ac_index_rp = rightest[ac];
            if (index == -1) 
            {
                // ac在p串中不存在,S串在ac字符的下一個字符開始比較
                s_ind = ac_index_s + 1;
            }
            else
            {
                // 將ac與p[ac_index_rp]對齊
                s_ind = ac_index_s - ac_index_rp;
            }
            p_ind = 0;
        }
    }
    free(rightest);

    return p_ind == p_len ? s_ind : -1;
}

Part 3

結(jié)論2需要一個統(tǒng)計P串中所有出現(xiàn)的字符和其在P串中最右邊出現(xiàn)的位置。

c中的字符編碼問題:linux64位c默認(rèn)是使用utf-8編碼,中英文占用字節(jié)不等長,沒有高級api的加持很難統(tǒng)計,在匹配字符的情況下;如果,只是看作是字節(jié)匹配,那么無論什么編碼都沒關(guān)系,只要S串和P串的編碼相同,并且在字節(jié)匹配最多有255種字節(jié)。linux64位下wchar_t是4個字節(jié),按字符統(tǒng)計的效率更高,在utf-16編碼的情況下字符大概有65535種,但是S串和P串可能需要向wchar_t轉(zhuǎn)換。

// 無符號字符為index, 在p中出現(xiàn)的最右位置為值
static int * _get_rightest(char const *p)
{
    int p_len = strlen(p);
    int *char_on_rightest = mollac(sizeof(int) * 256);

    int i;
    for (i = 0; i < 256; i++)
    {
        char_on_rightest[i] = -1; // -1代表p中不存在等于i的字節(jié)
    }

    for (i = 0; i < p_len; i++)
    {
        char_on_rightest[p[i]] = i;
    }

    return char_on_rightest;
}

BM

1977年,德克薩斯大學(xué)的Robert S. Boyer教授和J Strother Moore教授發(fā)明了這種算法。
Sunday算法是在BM算法的基礎(chǔ)之后出現(xiàn)的,并且有相通的思想。

Part 1

BM算法的匹配順序是從P[m-1]向P[0]方向匹配,P串從S串頭向S串尾對齊。

sp25.png

Part 2

壞字符規(guī)則,這個思想和Sunday的思想是一致的,但是還沒有Sunday算法完善。

sp26.png

上圖中在P[3]:c處匹配失敗,S[3]:a就叫壞字符。接下來P向后移,如果P[0]、P[1]、P[2]都不等于a,P[0]與P[4]對齊;多個等于a的,S[3]就和P位置小于3且靠右的a對齊。

sp27.png

上圖中,P1[2]與S[3]對齊,這是理想的方式:實現(xiàn)時,如果S[j]和P[i]不匹配,那就要遍歷P[0:i-1]尋找P[?]等于S[j],或者記錄P中所有字符在P串中的出現(xiàn)的所有的位置。按照這種理想實現(xiàn)的壞字符規(guī)則是在Horspool算法中,也只用這種規(guī)則。

實際上,只記錄了P的中所有字符及其在P中最右出現(xiàn)的位置,和Sunday算法構(gòu)造的字符位置hash數(shù)組是一樣的。所以:

sp28.png

在BM算法中,壞字符規(guī)則會出現(xiàn)P相對S反向移動對齊。顯然,這個規(guī)則是不足以支撐算法的,所以需要一個規(guī)則——好后綴規(guī)則。

BM算法的壞字符規(guī)則的最壞情況:

sp29.png

上圖中,在S[1]和S[2]處循環(huán)連續(xù)失敗。Sunday算法的壞字符在上圖在P1情況下會選擇S[6]為壞字符,在P2情況下選擇S[5]為壞字符,所以P相對S的位移永遠是正向的。

Part 3

好后綴規(guī)則,和壞字符規(guī)則思想是一致的。

sp30.png

上圖中,在P[4]處匹配失敗,P[5:6]和S[5:6]匹配,那么P[5:6]就是好后綴。接下來P相對S向后移動,如果在P[0:5]中存在"ab","ab"應(yīng)該與S[5:6]對齊,存在多個"ab"取P[0:5]中最后一個;不存在P[0]與P[7]對齊。

sp31.png

P1在P[2:3]為"ab",P2在P[0:5]中不包含"ab"。

另一種情況,如下圖:

sp32.png

在P1[5]處匹配失敗,P1[0:7]中不包含"abc",但是存在P1前綴"bc"等于好后綴"abc"的子后綴"bc",可以使P2[0:1]與S[7:8]對齊。

sp33.png

上圖中,在P1[0:7]存在"abc",P相對S最短位移,P[3:5]應(yīng)與S[6:8]對齊。

結(jié)論:在P串存在與好后綴相同(但不是一個)的子串,子串與S串對齊;不存在與好后綴相同子串,如果存在與好后綴的子后綴相同的P串前綴,則前綴與S串對齊。

所以好后綴需要構(gòu)造一個子串位置表和相同前后綴表。

Part 4

Bm算法有壞字符和好后綴兩種規(guī)則,在P相對S移動時使用兩種規(guī)則中移動距離更遠的一個。

為什么是誰遠使用誰?

接下來的不滿足壞字符指P串中沒有出現(xiàn)該壞字符,不滿足好后綴指P串沒有出現(xiàn)子串等于該后綴也沒有前綴等于該后綴的子后綴。

不滿足壞字符 && 不滿足好后綴

sp34.png

匹配失敗時,按壞字符規(guī)則對齊為P1,但P[6:8]是不滿足好后綴規(guī)則的,P1的情況是注定匹配失敗的。P2是按照好后綴規(guī)則對齊的,是可能匹配成功的。

這種情況誰遠使用誰。

不滿足壞字符 && 滿足好后綴

情況1:子串等于好后綴,不是P前綴
sp35.png

P1是壞字符規(guī)則對齊,但是好后綴沒有對齊,P1是注定匹配失敗的;P2是好后綴規(guī)則對齊,但是P串中沒有壞字符,P[0]永遠不等于S[5],P2是注定匹配失敗的。兩種情況都是失敗的:

這種情況誰遠使用誰。

情況2:子串等于好后綴,是P前綴
sp36.png

P1是壞字符規(guī)則對齊,并且P前綴等于好后綴;P2是好后綴規(guī)則對齊。兩種情況都是成功的:

這種情況誰遠使用誰。

情況3:前綴等于好后綴的子后綴
sp37.png

P1是壞字符規(guī)則對齊,但是好后綴沒有對齊,失??;P2是好后綴規(guī)則對齊,可能會匹配成功。

這種情況誰遠使用誰。

滿足壞字符 && 不滿足好后綴

情況1:壞字符只在P好后綴出現(xiàn)
sp38.png

P1串是壞字符規(guī)則對齊,相對S反向移動,失?。籔2是不滿足好后綴規(guī)則對齊,可能匹配成功。

這種情況誰遠使用誰。

情況2:壞字符只在P匹配失敗位置前出現(xiàn)
sp39.png

P1是滿足壞字符的最大位移情況,但不滿足好后綴規(guī)則注定失??;P2是不滿足好后綴規(guī)則對齊,可能匹配成功。

這種情況誰遠使用誰。

情況1附加:壞字符出現(xiàn)在好后綴和P匹配失敗位置前
sp40.png

壞字符表只記錄a在P[6]出現(xiàn),沒有記錄P[2]處出現(xiàn)的a;P1和P2是情況1正常的移位;P3是假設(shè)P[2]:a和S[5]對齊的情況,如圖,不滿足好后綴規(guī)則,P3不能匹配成功。

滿足壞字符 && 滿足好后綴

情況1:位移相同
sp41.png

壞字符和好后綴在S串是連續(xù)的,在P串也是連續(xù)的,具有相同的位移。

這種情況誰遠使用誰。

情況2:壞字符——間隔——好后綴
sp42.png

這種情況下,P1壞字符對齊則好后綴對不齊,P2好后綴對齊則壞字符對不齊。兩種情況都會失敗。

如果把P[1]:e替換成"abc",那么P1就可能成功。如果把P[1]:a替換成"k",那么就是情況1。

這種情況誰遠使用誰。

情況3:好后綴——間隔——壞字符
sp43.png

P1壞字符對齊時,好后綴沒有對齊,注定失敗;P2是好后綴對齊,?處可能是等于壞字符的,P2是有可能匹配成功的。

這種情況誰遠使用誰。

綜上所述:在BM算法中,壞字符規(guī)則和好后綴規(guī)則量誰移動的距離遠,就使用那個規(guī)則。在兩種規(guī)則中好后綴規(guī)則是占主體的。

Part 5

構(gòu)造BM算法需要一個壞字符表,一個好后綴表,一個相等前后綴表。

好后綴表以后綴長度為索引,以與其在P串最右的相等串的首索引為值。

相等前后綴表以后綴長度為索引,以0和1表示是否有前綴等于該后綴。

// return a hash table, key : byte, val : the index in rightest, -1 prefom no byte in p
static int * _get_bad_chars(char const *p);

// return a hash table, key : length of suffix, val : the index in rightest of substr equal suffix, -1 preform no substr equal suffix
static int * _get_good_suffixs(char const *p);

// return a hash table, key : length of suffix, val : 0 perform no prefix equal suffix, 1 perform a prefix equal suffix
static int * _get_prefix_equ_suffix(char const *p);

// return: first match index of s
int bm_match(char const *s, char const *p)
{
    int p_len = strlen(p);
    int s_len = strlen(s);
    int p_ind = p_len - 1; 
    int s_ind = p_ind;
    int *bc_ht = _get_bad_chars(p);
    int *gs_ht = _get_good_suffixs(p);
    int *pes_ht = _get_good_prefix_equ_suffix(p);

    while (s_ind < s_len && p_ind > -1)
    {
        if (s[s_ind] == p[p_ind])
        {
            s_ind--;
            p_ind--;
        }
        else
        {
            // 獲取匹配失敗時的壞字符在P最右的位置
            int bc_ind = bc_ht[s[s_ind]];
            // 壞字符對齊時,P相對S移動距離
            int bc_re_ins;
            if (bc_ind != -1)
            {
                bc_re_ins = p_ind - bc_ind;
            }
            else
            {
                bc_re_ins = p_ind + 1; // P[0]與P[p_ind+1]對齊
            }

            // 獲取好后綴在P最右的位置
            int gs_len = p_len - 1 - p_ind;
            int gs_ind = gs_ht[gs_len];
            // 好后綴對齊時,P相對S移動距離
            int gs_re_ins;
            if (gs_ind != -1)
            {
                gs_re_ins = p_ind + 1 - gs_ind;
            }
            else
            {
                // 沒有好后綴,查找相等前后綴
                int pes_len = gs_len - 1;
                while (pes_len > 0)
                {
                    if (pes_ht[pes_len]) {
                        break;
                    }
                    pes_len--;
                }

                if (pes_len > 0) 
                {
                    // 有相等的前后綴
                    gs_re_ins = p_len - pes_len; // P[0]與P[p_len - pes_len]對齊
                }
                else
                {
                    // 沒有相等的前后綴
                    gs_re_ins = p_len; // P[0]與P[p_len]對齊
                }
            }

            // 壞字符與好后綴中最大的相對位移。
            int max_re_ins = max(bc_re_ins, gs_re_ins);

            // 找到現(xiàn)在與P[0]對齊的S[j]
            int j = s_ind - p_ind;
            s_ind = j + max_re_ins + p_len;
            p_ind = p_len - 1;
        }
    }
    free(bc_ht);
    free(gs_ht);
    free(pes_ht);

    return p_ind ? -1 : s_ind;
}

優(yōu)化點是:好后綴表為-1時,P相對S的位移一定是最大的,這時不需要壞字符。

Part 6

壞字符表

完全復(fù)制Sunday表

// return a hash table, key : byte, val : the index in rightest, -1 prefom no byte in p
static int * _get_bad_chars(char const *p)
{
    int p_len = strlen(p);
    int *bc = malloc(sizeof(int) * 256);

    int i;
    for (i = 0; i < 256; i++)
    {
        bc[i] = -1; // -1代表p中不存在等于i的字節(jié)
    }

    for (i = 0; i < p_len; i++)
    {
        bc[p[i]] == i;
    }

    return char_on_rightest;
}

好后綴表

// return a hash table, key : length of suffix, val : the index in rightest of substr equal suffix, -1 preform no substr equal suffix
static int * _get_good_suffixs(char const *p)
{
    int p_len = strlen(p);
    int *gs = malloc(sizeof(int) * p_len);

    int i = 0;
    for (; i < p_len; i++) 
    {
        gs[i] = -1;
    }

    i = 0;
    const int last = p_len -1;
    while (i < last) 
    {
        int l_cur = i, r_cur = last, substr_len = 0;
        // 回溯
        while (l_cur >= 0 && r_cur >= 0 && p[l_cur] == p[r_cur]) 
        {
            substr_len++
            l_cur--;
            r_cur--;
        }
        if (substr_len)
            gs[substr_len] = i - substr_len + 1;
        i++;
    }

    return gs;
}
// return a hash table, key : length of suffix, val : 0 perform no prefix equal suffix, 1 perform a prefix equal suffix
static int * _get_prefix_equ_suffix(char const *p)
{
    int p_len = strlen(p);
    int *pes = malloc(sizeof(int) * p_len);

    int i = 0;
    for (; i < p_len; i++) 
    {
        pes[i] = 0;
    }

    i = 0;
    const int last = p_len -1;
    while (i < last) 
    {
        int l_cur = i, r_cur = last;
        // 回溯
        while (l_cur >= 0 && r_cur >= 0 && p[l_cur] == p[r_cur]) 
        {
            l_cur--;
            r_cur--;
        }
        if (l_cur == -1)
            pes[i+1] = 1;
        i++;
    }
}

合并3個表的方法

struct Bgp {
    int *bc;
    int *gs;
    int *pes;
}

static struct Bgp * _get_all(const char *p) 
{

    int p_len = strlen(p);
    struct Bgp *bgp = malloc(sizeof(struct Bgp));
    int *bc= malloc(sizeof(int) * 256);
    int *gs = malloc(sizeof(int) * p_len);
    int *pes = malloc(sizeof(int) * p_len);
    bgp->bc = bc;
    bgp->gs = gs;
    bgp->pes = pes;

    int i;
    for (i = 0; i < 256; i++)
        bc[i] = -1;

    for (i = 0; i < p_len; i++)
    {
        gs[i] = -1;
        pes[i] = 0;
    }

    i = 0;
    const int last = p_len -1;
    while (i < last) 
    {
        int l_cur = i, r_cur = last, substr_len = 0;
        // 回溯
        while (l_cur >= 0 && r_cur >= 0 && p[l_cur] == p[r_cur]) 
        {
            substr_len++
            l_cur--;
            r_cur--;
        }

        if (substr_len)
            gs[substr_len] = i - substr_len + 1;
        if (l_cur == -1)
            pes[i+1] = 1;
        bc[p[i]] = i;
        i++;
    }
    bc[p[last]] = last;

    return bgp;
}

總結(jié)

KMP、SUNDAY、BM字符串匹配算法。KMP只汲取P串自身的信息;SUNDAY算法汲取P和S的信息;BM汲取更多P和S的信息。

模式串長度為m,目標(biāo)串長度為n。

則KMP的時間復(fù)雜度為O(m+n),穩(wěn)定。

SUNDAY和BM算法的搜索最優(yōu)時間復(fù)雜度為O(n/m),就是一匹配就失敗并且壞字符不在模式串中,最差時間復(fù)雜度為O(m*n),總是在模式串匹配到最后一位(BM是第一位)的失敗,并且壞字符(BM還有好后綴)總是在比較后的位置。

SUNDAY的構(gòu)造時間復(fù)雜度為O(m + 字符集大小),BM算法的構(gòu)造時間復(fù)雜度為O(m + 字符集大小 + m ^ 2)。字符集越大,BM和SUNDAY越優(yōu), 因為壞字符不在模式串的可能性越大。比如在只有0和1的兩個字符的字符集,BM和SUNDAY和BF算法可能區(qū)別不大。模式串越短,壞字符不在模式串的可能性越大,構(gòu)造時間越短。所以BM和SUNDAY算法在大字符集的情況下進行搜詞的效率會高。

一般情況下,BM會比SUNDAY更優(yōu),因為BM是做壞字符加好后綴這一段字符的對齊,對齊機率比較低,模式串相對目標(biāo)串移動快;而SUNDAY是做一個字符的對齊,對齊機率高,模式串相對目標(biāo)串移動慢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容