字符串匹配

  • 主串模式串
    在字符串A中查找字符串B,那字符串A就是主串,字符串B就是模式串。
    我們把主串的長度記作n,模式串的長度記作m。因爲我們是在主串中查找模式串,所以n>m。

幾種單模式串匹配算法

  1. BF(暴力)算法
  2. RK算法
  3. BM算法
  4. KMP算法

1. BF(Brute Force)算法

在這裏插入圖片描述
時間複雜度O(n*m),其中n是主串長度,m是模式串長度。
缺陷:忽略了已檢測過的文本信息。

2. RK(Rabin-Karp)算法

如果模式串長度爲m,主串長度爲n,那在主串中,就會有n-m+1個長度爲m的子串。
BF算法需要對比n-m+1次,每次對比都需要依次對比m個字符。

RK算法的思路是:

  • 通過哈希算法對主串中的n-m+1個子串分別求哈希值
  • 然後逐個與模式串的哈希值比較大小
  • 如果某個子串的哈希值與模式串相 等,那就說明對應的子串和模式串匹配了(這裏先不考慮哈希衝突的問題,後面我們會講到)。因爲哈希值是一個數字,數字之間比較是否相等是非常快速的, 所以模式串和子串比較的效率就提高了。

在這裏插入圖片描述
簡單的哈希算法,需要遍歷子串的每個字符,儘管模式串與子串比較的效率提高了,但是,算法整體的效率並沒有提高。

巧妙的設計哈希算法。假設要匹配的字符串的字符集中只包含K個字符,我們可以用一個K進制數來表示一個子串,這個K進制數轉化成十 進制數,作爲子串的哈希值。

假設字符串中只包含a~z這26個小寫字符,我們用二十六進制來表示一個字符串,對 應的哈希值就是二十六進制數轉化成十進制的結果。
在這裏插入圖片描述
這種哈希算法有一個特點,在主串中,相鄰兩個子串的哈希值的計算公式有一定關係。
在這裏插入圖片描述
相鄰兩個子串s[i-1]和s[i] (i表示子串在主串中的起始位置,子串的長度都爲m)。我們可以使用s[i-1]的哈希值很快的計算出s[i]的哈希值。
在這裏插入圖片描述
h[i] = (h[i-1] - 26^(m-1)*(s[i-1]-‘a’)) * 26 + (s[i+m-1] - ‘a’)

可以提前計算26^(m-1)這部分的值,然後通過查表的方式提高效率。
在這裏插入圖片描述
RK算法包含兩部分,計算子串哈希值和模式串哈希值與子串哈希值之間的比較。

  • 第一部分,我們前面也分析了,可以通過設計特殊的哈希算法,只需要掃描一遍主串就能計算出所有子串的哈希值了,所以這部分的時間複雜度是O(n)。
  • 第二部分,模式串哈希值與每個子串哈希值之間的比較的時間複雜度是O(1),總共需要比較n-m+1個子串的哈希值,所以,這部分的時間複雜度也是O(n)。

所以,RK算法整體的時間複雜度就是O(n)。

如上這種哈希算法是不會有哈希衝突的,因爲我們是基於進制來表示一個字符串的,也就是說,一個字符串與一個二十六進制數一一對應,不同的字符串的哈希值肯定不一樣。

問題:模式串很長,相應的主串中的子串也會很長,通過上面的哈希算法計算得到的哈希值就可能很大,可能會超過了計算機中整型數據可以表示的範圍。

建議:設計數值較小的哈希函數,可能會有哈希衝突。在哈希值相等時,還需再對比一下子串和模式串本身。

3. BM(Boyer-Moore)算法

我們把模式串和主串的匹配過程,看作模式串在主串中不停地往後滑動。當遇到不匹配的字符時,BF算法和RK算法的做法是,模式串往後滑動一位,然後從模式 串的第一個字符開始重新匹配。
在這裏插入圖片描述
在這個例子裏,主串中的c,在模式串中是不存在的,所以,模式串向後滑動的時候,只要c與模式串有重合,肯定無法匹配。所以,我們可以一次性把模式串往 後多滑動幾位,把模式串移動到c的後面。
在這裏插入圖片描述
字符串匹配的關鍵,就是模式串的如何移動最高效。

BM算法本質上就是尋找一種規律,使得模式串和主串匹配過程中,當遇到不字符不匹配的時候,能夠跳過一些肯定不會匹配的情況,將模式串儘量往後多滑動幾位。

BM算法包含兩部分

  • 壞字符規則(bad character rule)
  • 好後綴規則(good suffix shift)

BM算法模式串的匹配方式是從末尾往前倒着匹配。
在這裏插入圖片描述

壞字符規則

從模式串的末尾往前倒着匹配,當我們發現某個字符沒法匹配的時候。我們把這個沒有匹配的字符叫作壞字符(主串中的字符)。
在這裏插入圖片描述

  • 壞字符c在模式串不存在,這個時候,我們可以將模式串直接往後滑動三(模式串長度)位,將模式串滑動到c後面的位置,再從模式串的末尾字符開始比較。
    在這裏插入圖片描述
  • 壞字符a在模式串中存在,模式串中下標是0的位置也是字符a。這種情況下,我們可以將模式串往後滑動兩位,讓兩個a上下對齊,然後再從模式串的末尾字符開 始,重新匹配。
    在這裏插入圖片描述
  • 規律:當發生不匹配的時候,我們把壞字符對應的模式串中的字符下標記作si。如果壞字符在模式串中存在,我們把這個壞字符在模式串中的下標記作xi。如果不存在, 我們把xi記作-1。那模式串往後移動的位數就等於si-xi。(注意,我這裏說的下標,都是字符在模式串的下標)
    在這裏插入圖片描述
    若xi有多個,選擇下標最大的那個,即最靠後的那個。

利用壞字符規則,BM算法在最好情況下的時間複雜度非常低,是O(n/m)。

  • 比如,主串是aaabaaabaaabaaab,模式串是aaaa。每次比對,模式串都可以直接後移四位,所以,匹配具有類似特點的模式串和主串的時候,BM算法非常高效。

  • 不過,單純使用壞字符規則還是不夠的。因爲根據si-xi計算出來的移動位數,有可能是負數,比如主串是aaaaaaaaaaaaaaaa,模式串是baaa。不但不會向後滑動模式串,還有可能倒退。所以,BM算法還需要用到“好後綴規則”。

好後綴規則

在這裏插入圖片描述
我們把已經匹配的bc叫作好後綴,記作{u}。我們拿它在模式串中查找,如果找到了另一個跟{u}相匹配的子串{u*},那我們就將模式串滑動到子串{u*}與主串 中{u}對齊的位置。
在這裏插入圖片描述

  • 若在模式串中找不到另一個等於{u}的子串,我們就直接將模式串,滑動到主串中{u}的後面,因爲之前的任何一次往後滑動,都不可能匹配主串中{u}的情況。
    在這裏插入圖片描述
    當模式串中不存在等於{u}的子串時,直接將模式串滑動到主串{u}的後面。是否有點太過頭呢?
    在這裏插入圖片描述
    如果好後綴在模式串中不存在可匹配的子串,那在我們一步一步往後滑動模式串的過程中。
  • 只要主串中的{u}與模式串有重合,那肯定就無法完全匹配。
  • 但是當模式串滑動到前綴與主串中{u}的後綴有部分重合的時候,並且重合的部分相等的時候,就有可能會存在完全匹配的情況。
    在這裏插入圖片描述
    所以,在好後綴模式下,若模式串中找不到和好後綴完全匹配的子串,那麼:
  • 先看好後綴在模式串中,是否有另一個匹配的子串
  • 還要考察好後綴的後綴子串,是否存在跟模式串的前綴子串匹配的。
    在這裏插入圖片描述
    BM算法會分別計算好後綴和壞字符往後滑動的位數,然後取兩者中的大者,作爲模式串往後滑動的位數。

BM算法實現

1.壞字符規則實現

“壞字符規則”本身不難理解。當遇到壞字符時,要計算往後移動的位數si-xi,其中xi的計算是重點,我們如何求得xi呢?
如果我們拿壞字符,在模式串中順序遍歷查找,這樣就會比較低效,勢必影響這個算法的性能。

  • 利用哈希表,將模式串中的每個字符及其下標都存到哈希表中

假設字符集爲256,每個字符大小爲1個字節,可用大小爲256的數組,記錄每個字符在模式串中出現的位置,數組下標對應字符的ASCII碼值,數組值爲字符在模式串中的下標。
在這裏插入圖片描述
如上過程翻譯成代碼如下,其中變量b是模式串,m是模式串長度,bc是剛剛講的哈希表:

const SIZE int = 256

func generateBC(b []byte, m int, bc []int) {
        for i := 0; i < SIZE; i++ {
              bc[i] = -1//初始化bc
        }
        for i := 0; i < m; i++ {
             ascii := int(b[i])//計算b[i]的asccii值
             bc[ascii] = i
        }
}

在這裏插入圖片描述
單有壞字符規則的BM算法,代碼如下:

func bm(mainStr []byte, modeStr []byte) int {
	bc := make([]int, SIZE)
	n := len(mainStr)
	m := len(modeStr)
	generateBC(modeStr, m, bc) //構建壞字符哈希
	i := 0                     //i表示主串與模式串對齊的第一個字符位置
	for i <= n-m {
		j := 0
		for j = m - 1; j >= 0; j-- { //模式串從後往前匹配
			if mainStr[i+j] != modeStr[j] {
				break //壞字符對應模式串中的下標是j
			}
		}
		if j < 0 {
			return i //匹配成功,返回主串與模式串第一個匹配的字符的位置
		}
		moveNum := j - bc[int(mainStr[i+j])]
		if moveNum <= 0 { //壞字符可能產生負數的移位
			moveNum = 1
		}
		i = i + moveNum
	}
	return -1
}

好後綴規則實現

回顧一下,前面講過好後綴的處理規則中最核心的內容:

  • 在模式串中,查找跟好後綴匹配的另一個子串;
  • 在好後綴的後綴子串中,查找最長的、能跟模式串前綴子串匹配的後綴子串;

若這兩個操作直接使用“暴力”匹配,會使得BM算法效率不高。而好後綴也是模式串本身的後綴子串,所以在正式匹配之前,可以對模式串做預處理操作,預先計算好模式串中的每個後綴子串,對應的另一個可匹配子串的位置。
在這裏插入圖片描述

  • 通過長度可以唯一確定一個後綴子串。

現在引入suffx數組,數組下標k表示後綴子串的長度,下標對應的數組值表示,在模式串中跟好後綴{u}相匹配的子串{u*}的起始下標位置。
在這裏插入圖片描述

  • 若模式串中有多個(大於1個)子串跟後綴子串{u}匹配,那suffix數組中該存儲模式串中最靠後的那個子串的起始位置,也就是下標最大的那個子串的起始位置。

suffx數組可以解決好後綴在模式串中能找到另一個可匹配的情況,但是我們還要在好後綴的後綴子串中,查找最長的能跟模式串前綴子串匹配的後綴子串。

  • 用bool類型的prefix數組,來記錄模式串的後綴子串是否能匹配模式串的前綴子串。
    在這裏插入圖片描述
  • 我們拿下標從0到i的子串(i可以是0到m-2)與整個模式串,求公共後綴子串。
  • 如果公共後綴子串的長度是k,那我們就記錄suffix[k]=j(j表示公共後綴子串的起始下標)。
  • 如果j等於0,也就是說,公共後綴子串也是模式串的前綴子串,我們就記錄prefix[k]=true。
    在這裏插入圖片描述
    把suffix數組和prefix數組的計算過程,用代碼實現出來,如下所示:
func generateGS(modeStr []byte, suffix []int, prefix []bool) {
	m := len(modeStr)
	for i := 0; i < m; i++ {
		suffix[i] = -1    //默認是找不到和好後綴匹配的子串
		prefix[i] = false //初始化
	}
	for i := 0; i < m-1; i++ { //modeStr[:i]
		j := i
		k := 0                                       //公共後綴子串長度
		for j >= 0 && modeStr[j] == modeStr[m-k-1] { //與modeStr[:m-1]求公共後綴
			j--
			k++
			suffix[k] = j + 1 //j+1表示公共後綴子串在modeStr[:i]中的起始下標
		}
		if j == -1 {
			prefix[k] = true //表示有和後綴子串匹配的前綴子串
		}
	}

}

有了這兩個數組後,在模式串和主串匹配過程中,遇到不能匹配字符時,根據好後綴規則,移動過程如下:

  • 假設好後綴的長度是k。我們先拿好後綴,在suffix數組中查找其匹配的子串。
  • 如果suffix[k]不等於-1(-1表示不存在匹配的子串),那我們就將模式串往後移動j- suffix[k]+1位(j表示壞字符對應的模式串中的字符下標)。
    在這裏插入圖片描述
  • 如果suffix[k]等於-1,表示模式串中不存在另一個跟好後綴匹配的子串片段,可如下處理。
    在這裏插入圖片描述
  • 如果兩條規則都沒有找到可以匹配好後綴及其後綴子串的子串,我們就將整個模式串後移m位。
    在這裏插入圖片描述
    BM算法的完整版代碼如下:
func bm(mainStr []byte, modeStr []byte) int {
	bc := make([]int, SIZE)
	n := len(mainStr)
	m := len(modeStr)
	generateBC(modeStr, m, bc) //構建壞字符哈希
	suffix := make([]int, m)
	prefix := make([]bool, m)
	generateGS(modeStr, suffix, prefix)
	i := 0 //i表示主串與模式串對齊的第一個字符位置
	for i <= n-m {
		j := 0
		for j = m - 1; j >= 0; j-- { //模式串從後往前匹配
			if mainStr[i+j] != modeStr[j] {
				break //壞字符對應模式串中的下標是j
			}
		}
		if j < 0 {
			return i //匹配成功,返回主串與模式串第一個匹配的字符的位置
		}
		x := j - bc[int(mainStr[i+j])]
		y := 0
		if j < m-1 { //如果有好後綴,j = m-1時,表示沒有好後綴
			y = moveByGS(j, m, suffix, prefix) //返回好後綴規則下,模式串移動的位數
		}
		i = i + mathutil.Max(x, y) //壞字符規則和好後綴規則,取移動位數更多的
		//這裏i一定大於0,因爲若無好後綴,那壞字符規則得到的移動位數一定大於0
	}
	return -1
}

//j表示壞字符對應的模式串中的字符下標,m表示模式串長度
func moveByGS(j, m int, suffix []int, prefix []bool) int {
	k := m - 1 - j       //好後綴長度
	if suffix[k] != -1 { //模式串中存在和好後綴匹配的子串
		return j - suffix[k] + 1
	}
	
	//這個for循環就是遍歷好後綴的後綴子串,看是否存在prefix[m-r]爲true的情況
	for r := j + 2; r <= m-1; r++ { //有好後綴且模式串中沒有和好後綴匹配的子串
		//r+2若爲m,此時表示好後綴長度爲1,此時移動m位即可
		if prefix[m-r] == true { //m-r就是好後綴子串的長度
			return r
		}
	}
	//若好後綴的兩個規則都不命中,則移動m位
	return m
}

BM算法總結

  • BM算法核心思想是,利用模式串本身的特點,在模式串中某個字符與主串不能匹配的時候,將模式串往後多滑動幾位,以此來減少不必要的字符比較,提高匹配 的效率。
  • BM算法構建的規則有兩類,壞字符規則和好後綴規則。好後綴規則可以獨立於壞字符規則使用。因爲壞字符規則的實現比較耗內存,爲了節省內存,我 們可以只用好後綴規則來實現BM算法。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章