Rabin-Karp 算法(字符串快速查找)算法
Go 語言的 strings 包(strings.go)中用到了 Rabin-Karp 算法。Rabin-Karp 算法是基於這樣的思路:即把字符串看做是字符集長度進制的數,由數值的比較結果得出字符串的比較結果。 樸素的字符串匹配算法爲何慢?由於它太健忘了,前一次匹配的信息其實有部分能夠應用到後一次匹配中去,而樸素的字符串匹配算法只是簡單的把這個信息扔掉,從頭再來,所以,浪費了時間。好好的利用這些信息,天然能夠提升運行速度。 因爲完成兩個字符串的比較須要對其中包含的字符進行逐個比較,所需的時間較長,而數值比較則一次就能夠完成,那麼咱們首先把「搜索詞」中各個字符的「碼點值」經過計算,得出一個數值(這個數值必須能夠表示出字符的先後順序,並且能夠隨時去掉某個字符的值,能夠隨時添加一個新字符的值),而後對「源串」中要比較的部分進行計算,也得出一個數值,對這兩個數值進行比較,就能判斷字符串是否匹配。對兩個數值進行比較,速度比簡單的字符串比較快不少。 好比咱們要在源串 "9876543210520" 中查找 "520",由於這些字符串中只有數字,因此咱們可使用字符集 {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} 來表示字符串中的全部元素,而且將各個字符映射到數字 0~9,而後用 M 表示字符集中字符的總個數,這裏是 10,那麼咱們就能夠將搜索詞 "520" 轉化爲下面的數值: ("5"的映射值 * M + "2"的映射值) * M + "0"的映射值 = (5 * 10 + 2) * 10 + 0 = 520 固然,若是「搜索詞」很長,那麼計算出來的這個數值就會很大,這時咱們能夠選一個較大的素數對其取模,用取模後的值做爲「搜索詞」的值。 分析一下這個數值:520,它能夠表明字符串 "520",其中: 表明字符 "5" 的部分是「 "5"的映射值 * (M 的 n - 1 次方) = 5 * (10 的 2 次方) = 500」 表明字符 "2" 的部分是「 "2"的映射值 * (M 的 n - 2 次方) = 2 * (10 的 1 次方) = 20」 表明字符 "0" 的部分是「 "0"的映射值 * (M 的 n - 3 次方) = 0 * (10 的 0 次方) = 0」 (n 表明字符串的長度) 咱們能夠隨時減去其中一個字符的值,也能夠隨時添加一個字符的值。 「搜索詞」計算好了,那麼接下來計算「源串」,取「源串」的前 n 個字符(n 爲「搜索詞」的長度)"987",按照一樣的方法計算其數值: ("9"的映射值 * M + "8"的映射值) * M + "7"的映射值 = (9 * 10 + 8) * 10 + 7 = 987 而後將該值與搜索詞的值進行比較便可。 比較發現 520 與 987 不相等,則說明 "520" 與 "987" 不匹配,則繼續向下尋找,這時候該如何作呢?下一步應該比較 "520" 跟 "876" 了,那麼咱們如何利用前一步的信息呢?首先咱們把 987 減去表明字符 "9" 的部分: 987 - ("9"的映射值 * (M 的 n - 1 次方)) = 987 - (9 * (10 的 2 次方)) = 987 - 900 = 87 而後再乘以 M(這裏是 10),再加上 "6" 的映射值,不就成了 876 了麼: 87 * M + "6"的映射值 = 87 * 10 + 6 = 876 固然了,因爲採用了取模操做,當兩個數值相等時,未必是真正的相等,咱們須要進行一次細緻的檢查(再進行一次樸素的字符串比較)。若不匹配,則能夠排除掉。繼續下一步。 若是咱們要在 ASCII 字符集範圍內查找「搜索詞」,因爲 ASCII 字符集中有 128 個字符,那麼 M 就等於 128,好比咱們要在字符串 "abcdefg" 中查找 "cde",那麼咱們就能夠將搜索詞 "cde" 轉化爲「("c"的碼點 * M + "d"的碼點) * M + "e"的碼點 = (99 * 128 + 100) * 128 + 101 = 1634917」這樣一個數值。 分析一下這個數值:1634917,它能夠表明字符串 "cde",其中: 表明字符 "c" 的部分是「 "c"的碼點 * (M 的 n - 1 次方) = 99 * (128 的 2 次方) = 1622016」 表明字符 "d" 的部分是「 "d"的碼點 * (M 的 n - 2 次方) = 100 * (128 的 1 次方) = 12800」 表明字符 "e" 的部分是「 "e"的碼點 * (M 的 n - 3 次方) = 101 * (128 的 0 次方) = 101」 (n 表明字符串的長度) 咱們能夠隨時減去其中一個字符的值,也能夠隨時添加一個字符的值。 「搜索詞」計算好了,那麼接下來計算「源串」,取「源串」的前 n 個字符(n 爲「搜索詞」的長度)"abc",按照一樣的方法計算其數值: ("a"的碼點 * M + "b"的碼點) * M + "c"的碼點 = (97 * 128 + 98) * 128 + 99 = 1601891 而後將該值與「搜索詞」的值進行比較便可。 比較發現 1634917 與 1601891 不相等,則說明 "cde" 與 "abc" 不匹配,則繼續向下尋找,下一步應該比較 "cde" 跟 "bcd" 了,那麼咱們如何利用前一步的信息呢?首先去掉 "abc" 的數值中表明 a 的部分: (1601891 - "a"的碼點 * (M 的 n - 1 次方)) = (1601891 - 97 * (128 的 2 次方)) = 12643 而後再將結果乘以 M(這裏是 128),再加上 "d" 的碼點值不就成了 "bcd" 的值了嗎: 12643 * 128 + "d"的碼點 = 1618304 + 100 = 1618404 這樣就能夠繼續比較 "cde" 和 "bcd" 是否匹配,以此類推。 若是咱們要在 Unicode 字符集範圍內查找「搜索詞」,因爲 Unicode 字符集中有 1114112 個字符,那麼 M 就等於 1114112,而 Go 語言中使用 16777619 做爲 M 的值,16777619 比 1114112 大(更大的 M 值能夠容納更多的字符,這是能夠的),並且 16777619 是一個素數。這樣就可使用上面的方法計算 Unicode 字符串的數值了。進而能夠對 Unicode 字符串進行比較了。 其實 M 能夠理解爲進位值,好比 10 進制就是 10,128 進制就是 128,16777619 進制就是 16777619。 下面是 Go 語言中字符串匹配函數的源碼,使用 Rabin-Karp 算法進行字符串比較: // primeRK 是用於 Rabin-Karp 算法中的素數,也就是上面說的 M const primeRK = 16777619 // 返回 Rabin-Karp 算法中「搜索詞」 sep 的「哈希值」及相應的「乘數因子(權值)」 func hashstr(sep string) (uint32, uint32) { // 計算 sep 的 hash 值 hash := uint32(0) for i := 0; i < len(sep); i++ { hash = hash*primeRK + uint32(sep[i]) } // 計算 sep 最高位 + 1 位的權值 pow(乘數因子) // 也就是上面說的 M 的 n 次方 // 這裏經過遍歷 len(sep) 的二進制位來計算,減小計算次數 var pow, sq uint32 = 1, primeRK for i := len(sep); i > 0; i >>= 1 { if i&1 != 0 { // 若是二進制最低位不是 0 pow *= sq } sq *= sq } return hash, pow } // Count 計算字符串 sep 在 s 中的非重疊個數 // 若是 sep 爲空字符串,則返回 s 中的字符(非字節)個數 + 1 // 使用 Rabin-Karp 算法實現 func Count(s, sep string) int { n := 0 // 特殊狀況判斷 switch { case len(sep) == 0: // 空字符,返回字符個數 + 1 return utf8.RuneCountInString(s) + 1 case len(sep) == 1: // 單個字符,能夠用快速方法 c := sep[0] for i := 0; i < len(s); i++ { if s[i] == c { n++ } } return n case len(sep) > len(s): return 0 case len(sep) == len(s): if sep == s { return 1 } return 0 } // 計算 sep 的 hash 值和乘數因子 hashsep, pow := hashstr(sep) // 計算 s 中要進行比較的字符串的 hash 值 h := uint32(0) for i := 0; i < len(sep); i++ { h = h*primeRK + uint32(s[i]) } lastmatch := 0 // 下一次查找的起始位置,用於確保找到的字符串不重疊 // 找到一個匹配項(進行一次樸素比較) if h == hashsep && s[:len(sep)] == sep { n++ lastmatch = len(sep) } // 滾動 s 的 hash 值並與 sep 的 hash 值進行比較 for i := len(sep); i < len(s); { // 加上下一個字符的 hash 值 h *= primeRK h += uint32(s[i]) // 去掉第一個字符的 hash 值 h -= pow * uint32(s[i-len(sep)]) i++ // 開始比較 // lastmatch <= i-len(sep) 確保不重疊 if h == hashsep && lastmatch <= i-len(sep) && s[i-len(sep):i] == sep { n++ lastmatch = i } } return n } 我是初學者,這些學習筆記參考了網絡上的一些資料,因爲參考的內容比較多雜,因此不一一列出了,感謝各位網絡朋友的無私奉獻!