演算法學習 模式匹配

2021-07-10 01:33:14 字數 4358 閱讀 2718

子串的定位操作通常稱為串的模式匹配。模式匹配的應用很常見,比如在文字處理軟體中經常用到的查詢功能。我們用如下函式來表示對字串位置的定位:

int index(const string &tag,const string &ptn,int pos)

其中,tag為主串,ptn為子串(模式串),如果在主串tag的第pos個位置後存在與子串ptn相同的子串,返回它在主串tag中第pos個字元後第一次出現的位置,否則返回-1。

我們先來看bf演算法(brute-force

,最基本的字串匹配演算法),bf演算法的實現思想很簡單:我們可以定義兩個索引值i和j,分別指示主串tag和子串ptn當前正待比較的字元位置,從主串tag的第pos個字元起和子串ptn的第乙個字元比較,若相等,則繼續逐個比較後續字元,否則從主串tag的下乙個字元起再重新和子串ptn的字元進行比較,重複執行,直到子串ptn中的每個字元依次和主串tag中的乙個連續字串相等,則匹配成功,函式返回該連續字串的第乙個字元在主串tag中的位置,否則匹配不成功,函式返回-1。

用c++**實現如下:

/*

返回子串ptn在主串tag的第pos個字元後(含第pos個位置)第一次出現的位置,若不存在,則返回-1

採用bf演算法,這裡的位置全部以從0開始計算為準,其中t非空,0<=pos<=tlen

*/int index(const string &tag,const string &ptn,int pos)

以上演算法完全可以實現要求的功能 ,而且在字元重複概率不大的情況下,時間複雜度也不是很大,一般為o(plen+tlen)。但是一旦出現如下情況,時間複雜度便會很高,如:子串為「111111110」,而主串為 「111111111111111111111111110」 ,由於子串的前8個字元全部為『1』,而主串的的前面一大堆字元也都為1,這樣每輪比較都在子串的最後乙個字元處出現不等,因此每輪比較都是在子串的最後乙個字元進行匹配前回溯,這種情況便是此演算法比較時出現的最壞情況。因此該演算法在最壞情況下的時間複雜度為o(plen*tlen),另外該演算法的空間複雜度為o(1)。

上述演算法的時間複雜度之所以大,是由於索引指標的回溯引起的,針對以上不足,便有了kmp演算法。kmp演算法可以在o(plen+tlen)的時間數量級上完成串的模式匹配操作。其改進之處在於:每一趟比較重出現字元不等時,不需要回溯索引指標

i,而是利用已經得到的部分匹配的結果將子串向右滑動盡可能遠的距離,繼續進行比較。它的時間複雜度為o(plen+tlen),空間複雜度為o(plen),這從後面的**中可以看出。

以下面兩個字串的匹配情況來分析

主串:ababcabcacbab

子串:abcac

如果採用簡單的bf演算法,則每趟比較i都要回退,而採用kmp演算法,每趟比較時,i保持不變,只需將j向右滑動即可,也即是減少了中間一些趟次的比較。kmp演算法匹配以上兩個字元的過程如下(黃色部分表示匹配成功的位置,黃色部分的第乙個字元表示該趟比較開始匹配的第乙個字元,紅色部分表示匹配失敗的位置,綠色表示尚未進行比較的位置):

第一趟比較:在i=2和j=2處出現不匹配,如下圖中紅色部分所示

第二趟比較:i不變,依然在主串的第2個字元處,子串向右滑動,相當於j回退,此趟比較從i=2和j=0處開始,在i=6和j=4處出現不匹配,如下圖中紅色部分所示

第三趟比較:i依然在主串的第6個字元處,子串向右滑動,此趟比較從i=6和j=1處開始,最終匹配成功

我們可以看到,只用3趟就可以完成匹配,而採用bf演算法則要6趟才能完成。為什麼可以這樣移動呢?我們從第一趟比較的結果得知,主串的第2個字元為b,而子串的第乙個字元為a,因此,因此可以直接將bf演算法的第二趟比較省去,而直接進入第三趟比較,也就是kmp演算法的第二趟比較,再往後面,通過該趟比較,我們又知道主串的第4、5、6個字元必然是bca,它們無須再與子串的第乙個字元比較,這樣便可以直接從i=6和j=1處進行比較。

這裡的關鍵問題:每趟匹配過程中產生失配時,子串該向右滑動多遠,換句話說,當主串的第i個字元和子串的第j個字元失配時,下一趟比較開始時,主串的第i個字元應該與子串的哪個字元再去比較。這個問題我們在後面討論,我們先將kmp演算法的**給出,我們假設失配時,主串的第i個字元與子串中的第next[j]個字元進行比較,並令j=0時,即在第乙個字元處適時,next[j]=-1,則那麼我們可以寫出kmp演算法的**如下:

/*

返回子串ptn在主串tag的第pos個字元後(含第pos個位置)第一次出現的位置,若不存在,則返回-1

採用kmp演算法,這裡的位置全部以從0開始計算為準,其中t非空,0<=pos<=tlen

*/int kmp_index(const string &tag,const string &ptn,int pos)

以上**很簡單,也很容易理解,對照bf演算法,並沒有該幾行**,關鍵在於如何求next陣列(也叫字首陣列),下面我們著重來看next陣列的求法。

我們假設主串為「t0t1…tn-1」,子串為「p0p1…pm-1」在失配後,主串中的第i個字元應與子串中的第k(0

p0 p1 … pk-1 = ti-k ti-k+1 … ti-1

而我們由上一趟的比較,已經得到了如下匹配結果:

p0 p1 … pj-1 = ti-j ti-j+1 … ti-1

我們取其中的部分匹配結果,如下:

pj-k pj-k+1 … pj-1 = ti-k ti-k+1 … ti-1

比較第乙個公式和第三個公式,我們便可以得出如下結果:

p0 p1 … pk-1 = pj-k pj-k+1 … pj-1

這樣,所有的問題就轉移到子串ptn上了,因此next陣列元素的值只與子串的形式有關,而與主串沒有任何關係。如果在子串中存在滿足上式的的兩個子字串,則在失配後,下一趟比較僅需從子串ptn的第k個字元與主串tag的第i個字元開始。於是可以令next[j]的表示式如以下三種情況所示:

(1)當j>0時,next[j] = max

(2)當j=0時,next[j] = -1

(3)當j>0且又不存在滿足 p0 p1 … pk-1 = pj-k pj-k+1 … pj-1 的k時,next[j] = 0

先來看如何手算next陣列的值,再看如何用程式求解next陣列的值。

首先看如何手算next陣列各元素值。

我們來看如下字串:

abaabcac

只要子串不是很長,可以一眼求出next陣列中各元素的值。

下面看如何用程式來求next陣列的值。

如果已有next[j] = k(假設k為已知),這說明在子串的前j個字元中,存在前面推論出來的關係式:

p0 p1 … pk-1 = pj-k pj-k+1 … pj-1

下面我們繼續求next[j+1],這就要分兩種情況開看:

1、若pk = pj,則表明在子串中,有如下關係式:

p0 p1 … pk != pj-k pj-k+1 … pj

那麼就有next[j+1] = k+1,即next[j+1] = next[j] + 1.

2、若pk != pj,則表明在子串中,

p0 p1 … pk != pj-k pj-k+1 … pj

此時,我們同樣可以將其看做是乙個模式匹配的過程(子串與主串均為ptn),由於pk != pj,首次出現不匹配,那麼應該取子串的第next[k]個字元與主串的第j個字元再做比較。

我們假設next[k] = t,重複上面的比較,如果pt = pj,則next[j+1] = t + 1 = next[k] + 1,而如果pt != pj,則繼續講子串向右滑動,取其第next[t]個字元與子串的第j個字元再做比較,直到pj和子串中的某個字元匹配成功,此時next[j+1]即為求得的值,或不存滿足上述等式的k,此時next[j+1] = 0.

同樣以如下字串為例進行計算:

abaabcac

依照kmp演算法,我們可以得到求next陣列的演算法,**如下:

/*

求next陣列中各元素的值,儲存在長為len的next陣列中

*/void get_next(const string &ptn,int *next,int len)

else}}

演算法學習 KMP匹配演算法

最近資料結構上看到這個演算法,之前沒有進行過學習,現在來記錄一下 kmp演算法,主要用於字串的匹配 它比bf暴力演算法來說,減少時間複雜度,不用重複匹配太多 在我學習的過程中,我發現kmp演算法的精髓就是求解next陣列 我們先看看過程 1。對這個字串進行匹配,首先第乙個匹配,不對,移到下乙個 2。...

模式匹配演算法

brute force演算法 kmp演算法 kmp演算法的改進 模式匹配 子串的定位操作被叫做串的模式匹配。串相等 串長度相等且各個對應位置的字元都相等。當兩個串不相等時,判斷兩個串大小的方法 給定兩個串 s1 a1a2a3a4 an 和s2 b1b2b3b4 bm 當滿足以下條件之一時,s1n存在...

演算法 模式匹配

你有兩個字串,即pattern和value。pattern字串由字母 a 和 b 組成,用於描述字串中的模式。例如,字串 catcatgocatgo 匹配模式 aabab 其中 cat 是 a go 是 b 該字串也匹配像 a ab 和 b 這樣的模式。但需注意 a 和 b 不能同時表示相同的字串。...