簡單的z-box算法的實現

  1. 一 Z-BOX的概念
  2. 二 Z-BOX算法的計算過程
  3. 三 Z-BOX算法的代碼實現C語言版
  4. 四 Z-BOX算法在具體的模式匹配字符串查找中的應用
  5. 五 總結
    在字符串的模式匹配中,有單模匹配和多模匹配之分。本系列文章將對單模匹配和多模匹配逐一進行講解。其中單模匹配中將會講解以下幾個算法:
  1. Z-BOX算法
  2. KMP算法
  3. BM算法
  4. sunday算法
第一篇文章,我們從Z-BOX算法開始講起,因爲Z-BOX算法的思想可以做爲KMP算法和BM算法中求子串匹配過程的基礎。

一 Z-BOX的概念

對於一個模式串P,記爲a0,a1,a2...an。
Zi(P)的值表示,從下標i開始的子串,能夠和從P開頭的子串進行匹配的長度。
舉例說明,例如模式串P = aabaaabd
                                下標:01234567
那麼對應的Zi(P)值如下:
i Zi(p)值 說明
0 略過  
1 1 p[1] = p[0]
2 0 p[2] != p[0]
3 2 p[3, 4] = p[0, 1]
4 3 p[4,5,6] = p[0,1,2]
5 1 p[5] = p[0]
6 0 p[6] != p[0]
7 0 p[7] != p[0]

之所以稱爲Z-BOX,是因爲能夠匹配上的子串,像一個盒子一樣,例如上例中的Z4(P) = 3,也就是從P[4]開始有3個字符能夠和字符串P開頭的3個字符匹配,這裏的P[4-6]就組成了一個盒子,所以該算法被形象的稱爲Z-BOX算法。

二 Z-BOX算法的計算過程

在計算Zi(P)值的時候,我們可以採用遞推的方法。假設現在我們已經知道了Z0(P)至Zi(P)的值,我們如何求Zi+1(P)的值?

首先,我們需要進一步明確BOX的概念,還是用上面的模式串P = aabaaabd來舉例,i=5這個位置,也就是P[5],其實同時被2個BOX覆蓋,因爲在Z4(P)和Z5(P)中都包含了P[5]。所以我們再定義兩個值:left和right,表示對於i位置,包含了P[i]的所有BOX中右邊值最大的BOX的範圍。
所以當i=5時,left5 = 4,right5 = 6。

接下來我們分析如何得到Zi+1(P)的值。假設Zi(P)時,left,right的值如下圖所示(圖中k位置表示i+1)。

情況1:
k在right左邊,left,right所確定的範圍(紅色標記爲a的box)是一個box的話,那麼a有一個從模式串開頭匹配的範圍a’。
所以在a範圍內的k,必然在a’中有一個對應的k’,Zk’(P)的值=b,也就是圖中綠色框的範圍b,如果b的範圍在a的範圍之內,則Zk(P) = b = Zk’(P)。

情況2:

k在right左邊或k=right,但是k對應的k’處的Zk’(P)的值超過了(或等於)a’的範圍。此時,我們可以確定的是k到right之間的字符和0到right-k之間的字符是匹配的,right之後的字符是否匹配,則需要我們逐個字符去驗證。當我們找到最長的匹配串之後,我們就可以得到Zk(P)的值,同時需要更新left和right的數值爲:left = k,right = 匹配的最右端下標。

情況3:
如果k > right,此時a box將不能給我們提供有用的輔助了,我們只能從k位置逐個字符去檢驗是否和模式串的前綴匹配。如果有能夠匹配的前綴,那麼我們更新Zk(P)的值,同時也更新left和right的數值。

上面3種情況說的比較抽象,下面我們來看一個具體的例子。
對於模式串P = aabaaab,我們從0下標開始計數,並初始化left = right = 0,我們從下標爲1的字符開始計算Zi(P)的值。
因爲P[1] = P[0],且P[2] != P[1],所以Z1(P) = 1,left = 1,right = 1。



接下來計算Z2(P),2 > right(值爲1),所以此時滿足情況3,從P[2]開始進行匹配檢查,P[2] != P[0]。所以left,right的值不變,Z2(P) = 0。


再來看Z3(P),依然是情況3(i > right),不同的是此時有能夠匹配上的字符P[3,4] = P[0,1],所以Z3(P) = 2,同時更新left = 3,right = 4。


再來看Z4(P),此時i <= right,所以不滿足 情況3了,這時把box(P[3,4])搬到模式串開頭的話(P[3]對應到P[0]位置),i對應的就是1,所以i’ = 1。
Zi’(P) = Z1(P) = 1。所以Z4(P)對應的box大小至少爲1,而此時right = 4,下標4 + (下標1處的box大小-1)= 4(>= right)(也就是從當前下標處加上Zi’(p)的值到達或者超過了right的值),所以滿足情況2,能夠匹配的部分我們就不用再匹配了(P[4] = P[0]),我們直接從匹配部分的後面開始進行比較,也就是從P[5]和P[1]開始繼續比較,P[5]=P[1],P[6]=P[2]。得到的結果如下:

情況2比較少遇到,且多繞一個彎,需要讀者好好揣摩。我在一開始沒有見到這個例子的時候,也是沒想通,怎麼會存在這種情況呢?當時我認爲只有情況1,不會有情況2,後來看到這個例子才明白過來,自己大腦裏面少轉了一個彎。

求Z5(P)時,i’ = 5 - left = 5 - 4 = 1,Z1(P) = 1,下標5 + (下標1處的box大小-1)= 5(< right),滿足情況1,所以Z5(P) = Z1(P) = 1。left和right值不變。


同樣Z6(P)也滿足情況1


三 Z-BOX算法的代碼實現(C語言版)

經過上面例子的具體分析我們可以寫出Z-BOX算法的代碼
    #include <string.h>  
    #include <stdio.h>  
      
    void ZBox(const char* pattern, unsigned int length, unsigned int zbox[])    
    {  
        zbox[0] = 0;  
        unsigned int left = 0;  
        unsigned int right = 0;  
        for (unsigned int i = 1; i < length; i++)  
        {  
            if (i > right)//情況3  
            {  
                int n = 0;  
                for ( ; pattern[n] == pattern[i+n]; n++);  
                if (0 != n)  
                {  
                    right = i+n-1;  
                    left = i;  
                }  
                zbox[i] = n;  
            }  
            else  
            {  
                if (zbox[i-left] < right-i+1)//情況1  
                {  
                    zbox[i] = zbox[i-left];  
                }  
                else//情況2  
                {  
                    int n = 1;  
                    for (;pattern[right-i+n] == pattern[right+n]; n++);  
                    zbox[i] = right-i+n;  
                    right += n-1;  
                    left = i;  
                }  
            }  
            printf(" zbox[%d] = %d, left is %d, right is %d\r\n", i,  zbox[i], left, right);  
        }  
    }  
      
    int main(int argc, char* argv[])  
    {  
        const char pattern[] =   "aabaaab";  
        unsigned int zbox[100];  
        ZBox(pattern, strlen(pattern), zbox);  
        return 0;  
    }  


上面的程序輸出結果爲:
 zbox[1] = 1, left is 1, right is 1
 zbox[2] = 0, left is 1, right is 1
 zbox[3] = 2, left is 3, right is 4
 zbox[4] = 3, left is 4, right is 6
 zbox[5] = 1, left is 4, right is 6
 zbox[6] = 0, left is 4, right is 6
和我們在前面遞歸推導的結果一致。

四 Z-BOX算法在具體的模式匹配、字符串查找中的應用

要想使用Z-BOX算法進行字符串查找,我們將要查找的模式串放在被查找目標串的開頭部分組成一個新的串P,然後從前向後依次計算每一個位置的Zi(P)值(i從0開始計數)。當i >= length(模式串),且Zi(P) >= length(模式串),則i位置是一個查找到的匹配位置。
以下是實現代碼:
    #include <string.h>  
    #include <stdio.h>  
      
    int SearchWithZBox(const char* dest, const char* pattern)  
    {  
        int nDlen = strlen(dest);  
        int nPlen = strlen(pattern);  
      
        char *str = new char[nDlen+nPlen];  
        if (!str)  
        {  
            goto Exit0;  
        }  
        int *Z = new int[nDlen+nPlen];  
        if (!Z)  
        {  
            goto Exit0;  
        }  
        memcpy(str, pattern, nPlen);  
        memcpy(str+nPlen, dest, nDlen);  
      
        //create z box  
        Z[0] = 0;  
        int l = 0;  
        int r = 0;  
        for (int i = 1; i < nDlen+nPlen; i++)  
        {  
            if (i > r)  
            {  
                int n = 0;  
                for (; str[i+n] == str[n]; n++);  
                if (n > 0)  
                {  
                    l = i;  
                    r = i+n-1;  
                }  
                Z[i] = n;  
            }  
            else  
            {  
                if (Z[i-l] < r-i+1)  
                {  
                    Z[i] = Z[i-l];  
                }   
                else  
                {  
                    int n = 1;  
                    for (int s = r-i; str[s+n] == str[r+n]; n++);  
                    Z[i] = r-i+n;  
                    l = i;  
                    r = r+n-1;   
                }  
            }  
            if (i >= nPlen && Z[i] >= nPlen)  
            {  
                printf("Search With ZBox Find at %d\r\n", i - nPlen);  
            }  
        }  
    Exit0:  
        if (str)  
        {  
            delete str;  
            str = NULL;  
        }  
        if (Z)  
        {  
            delete Z;  
            Z = NULL;  
        }  
        return -1;  
    }  
      
    int main(int argc, char* argv[])  
    {  
        //                        0         1         2         3         4         5  
        //                        012345678901234567890123456789012345678901234567890123456789  
        const char dest[] =      "demoxdemoaaabaaaxdembbaaaddemobaaababdemoooabcxbaabaaadddemo";  
        const char pattern[] =   "aabaaab";  
        unsigned int zbox[100];  
        //ZBox(pattern, strlen(pattern), zbox);  
        SearchWithZBox(dest, "demo");  
        return 0;  
    }  


程序運行結果如下:
Search With ZBox Find at 0
Search With ZBox Find at 5
Search With ZBox Find at 26
Search With ZBox Find at 37
Search With ZBox Find at 56
表示在目標串dest中的下標爲0,5,26,37,56位置處找到了模式串“demo”。經驗證正確且無遺漏。

五 總結

Z-BOX算法思路並不複雜,代碼也不難寫出,唯一有點繞的地方是對情況2的理解和處理。其算法時間複雜度是線性的,但實際應用中比較少見該算法,大概是因爲要使用Z-BOX算法需要將模式串和目標串先組合起來,這需要額外的空間,另外用於輔助的Z-BOX數組也要求該算法的空間複雜度達到了O(len(P)+len(D))(模式串長度和目標串長度之和)。基於這些原因導致Z-BOX算法在實際應用中成爲雞肋。不過了解該算法的思路對於開闊我們的思維不無裨益,而且該算法可以應用到KMP、BM算法的求模式串最長匹配子串中,可以加速模式串的處理過程,我們在以後的文章中會提到。

六 參考資料

轉載至:http://blog.csdn.net/sunnianzhong/article/details/8784853

這篇英文文章提供的例子很好的覆蓋了3種情況
這篇博客中的圖片很形象的說明了情況1和情況2

這是一本全面介紹字符串處理算法的書籍,我沒有找到中文版,這是英文版


發佈了28 篇原創文章 · 獲贊 1 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章