字符串匹配算法(一):BF(BruteForce)算法和RK(RabinKarp)算法
文章目錄
- BF
- 思路
- 實(shí)現(xiàn)
- RK
- 思路
- 實(shí)現(xiàn)
字符串匹配是計(jì)算機(jī)科學(xué)中最古老、研究最廣泛的問(wèn)題之一。一個(gè)字符串是一個(gè)定義在有限字母表∑上的字符序列。例如,ATCTAGAGA是字母表∑ = {A,C,G,T}上的一個(gè)字符串。字符串匹配問(wèn)題就是在一個(gè)大的字符串T中搜索某個(gè)字符串P的所有出現(xiàn)位置。
為了方便后面講解,在這里先提出幾個(gè)概念。
- 主串和模式串,主串即字符串的本體,模式串即用于比對(duì)的子串,即我們將要在主串中查找模式串。例如我們要在字符串A中查找字符串B,則A為主串,B為模式串。
- 單模式匹配算法:即在一個(gè)主串中匹配一個(gè)模式串,例如BF、RK、BM、KMP等算法就是單模式
- 多模式匹配算法:即在一個(gè)主串中匹配多個(gè)字符串,例如之前講過(guò)的Trie樹(shù),以及之后會(huì)寫的AC自動(dòng)機(jī)。
BF
思路
BF算法是Brute Force的縮寫,中文名叫做暴力匹配算法,故名思意,其算法簡(jiǎn)單粗暴,但對(duì)應(yīng)的性能也不高,算法邏輯如下。
如上圖,BF算法的思路就是從主串的每一個(gè)位置進(jìn)行對(duì)比,如果當(dāng)前位置模式串無(wú)法匹配,就移動(dòng)到下一個(gè)位置,繼續(xù)匹配
實(shí)現(xiàn)
該算法的實(shí)現(xiàn)十分簡(jiǎn)單,代碼如下
#include<string> #include<iostream>using namespace std;int bruteForce(const string& str, const string& pattern) {//不滿足條件則直接返回falseif(str.empty() || pattern.empty() || str.size() < pattern.size()){return -1;}int i = 0, j = 0;int len1 = str.size(), len2 = pattern.size();while(i < str.size()){while(j < pattern.size()){//如果當(dāng)前不匹配if(str[i + j] != pattern[j]){i++; //主串移動(dòng)到下一個(gè)位置j = 0; //模式串回到起始位置break;}j++; //如果模式串當(dāng)前位置匹配,則繼續(xù)匹配下一個(gè)位置}//如果模式串全部匹配,則返回匹配的位置if(j == pattern.size()){return i;}}return -1; }可以看出,這種算法存在著大量無(wú)意義的匹配,在最壞的情況下,我們需要依次匹配完主串中的所有位置,因此其時(shí)間復(fù)雜度高達(dá)O(N * M) N為主串長(zhǎng)度,M為模式串長(zhǎng)度。
RK
思路
那么有什么方法可以減少那些不必要的匹配,提高效率呢?這時(shí)候就可以巧妙地借助哈希算法來(lái)完成這個(gè)任務(wù)。
如果對(duì)于哈希不了解的可以查看我往期的博客
高級(jí)數(shù)據(jù)結(jié)構(gòu)與算法 | 哈希 :哈希沖突、負(fù)載因子、哈希函數(shù)、哈希表、哈希桶
RK算法的全稱是RabinKarp,是Rabin 和 Karp這兩位科學(xué)家在BF算法的基礎(chǔ)上,加上了哈希的思想后是實(shí)現(xiàn)的。
BF算法的主要缺陷在于模式串會(huì)去嘗試匹配主串的任何一個(gè)位置,并且其中每次的匹配都會(huì)去一個(gè)一個(gè)字符進(jìn)行對(duì)比,導(dǎo)致效率的下降。
而RK算法則想到了一個(gè)好方法,可以先進(jìn)行一個(gè)預(yù)匹配,即哈希值的匹配,如果哈希值不同則說(shuō)明字符串不同,沒(méi)有比較的意義,而當(dāng)哈希值相同時(shí),為了避免哈希沖突的情況,再進(jìn)行字符串的匹配即可。
由于哈希值只是一個(gè)整型,其比較起來(lái)的代價(jià)比起字符串大大的降低了
不過(guò),哈希值的計(jì)算也需要通過(guò)遍歷字符串來(lái)獲得,那么效率不是又變低了嗎?這時(shí),就可以通過(guò)合理的設(shè)計(jì)哈希函數(shù),來(lái)解決這個(gè)問(wèn)題。
在這里,我選擇使用最簡(jiǎn)單的按位相加來(lái)進(jìn)行舉例
首先,我們將模式串以及主串中第一個(gè)用來(lái)匹配的子串的哈希值計(jì)算出來(lái),進(jìn)行比較。
int hashFunc(const string& str) {int hashCode = 0;for(int i = 0; i < str.size(); i++){hashCode += (str[i] - 'a');}return hashCode; }當(dāng)其不匹配時(shí),我們需要匹配主串的下一個(gè)位置,由于字符串發(fā)生變化,哈希值也應(yīng)該發(fā)生變化。
考慮到匹配子串的變化起始就是從主串中向后移動(dòng)一位,也就是刪除了最高位,增加了最低位,所以我們可以借助這個(gè)性質(zhì),進(jìn)行增量計(jì)算,即減去首位的哈希值,加上末尾的哈希值,就可以避免每次都要重復(fù)計(jì)算哈希值的問(wèn)題。
//獲取移動(dòng)到下一個(gè)位置后的字符串哈希值,即減去開(kāi)頭的哈希值,加上末尾的哈希值 int nextHash(const string& str, int hash, int begin, int end) {hash -= (str[begin] - 'a');hash += (str[end] - 'a');return hash; }上面這種哈希算法的計(jì)算十分簡(jiǎn)單,但是也存在著大量的哈希沖突,所以選擇一個(gè)合理的哈希算法,對(duì)效率的提升有很大的幫助,但是每種哈希算法也存在著一定的缺陷,例如26進(jìn)制,雖然其幾乎避免了哈希沖突,但是在字符串過(guò)長(zhǎng)時(shí)會(huì)導(dǎo)致哈希值的溢出,所以要結(jié)合使用場(chǎng)景對(duì)哈希函數(shù)進(jìn)行選擇。
由于這里只是算法的一個(gè)介紹,就直接使用這種最簡(jiǎn)單的哈希函數(shù)。
此時(shí),整個(gè)RK算法就包含兩個(gè)部分,第一部分是進(jìn)行哈希值的計(jì)算,時(shí)間復(fù)雜度為O(N),第二部分就是進(jìn)行字符串的匹配,因?yàn)楣V档谋容^的時(shí)間復(fù)雜度為O(1),而哈希值的更新也只是簡(jiǎn)單的增量計(jì)算,也是O(1),所以整個(gè)算法的時(shí)間復(fù)雜度為O(N),但是在最壞情況下,如存在大量的哈希沖突時(shí),每次都需要將進(jìn)行字符串的對(duì)比,這時(shí)的時(shí)間復(fù)雜度就會(huì)退化回O(N * M)
實(shí)現(xiàn)
算法實(shí)現(xiàn)如下
#include<string> #include<iostream>using namespace std;//字符串哈希函數(shù),這里使用的是最簡(jiǎn)單的按位相加 int hashFunc(const string& str) {int hashCode = 0;for(int i = 0; i < str.size(); i++){hashCode += (str[i] - 'a');}return hashCode; }//獲取移動(dòng)到下一個(gè)位置后的字符串哈希值,即減去開(kāi)頭的哈希值,加上末尾的哈希值 int nextHash(const string& str, int hash, int begin, int end) {hash -= (str[begin] - 'a');hash += (str[end] - 'a');return hash; }int rabinKarp(const string& str, const string& pattern) {//不滿足條件則直接返回falseif(str.empty() || pattern.empty() || str.size() < pattern.size()){return -1;}int len1 = str.size(), len2 = pattern.size();int patternHash = hashFunc(pattern);int subHash = hashFunc(str.substr(0, len2));for(int i = 0; i < (len1 - len2 + 1); i++){//如果當(dāng)前的哈希值相同,則遍歷比較字符串是否也相同,如果不同則沒(méi)有必要進(jìn)行比較if(patternHash == subHash && pattern == str.substr(i, len2)){return i; //如果當(dāng)前匹配,則返回匹配主串的起始位置}//如果主串中剩余字符還能與模式串進(jìn)行對(duì)比,則更新哈希值if(len1 - i > len2){subHash = nextHash(str, subHash, i, i + len2);}}return -1; }總結(jié)
以上是生活随笔為你收集整理的字符串匹配算法(一):BF(BruteForce)算法和RK(RabinKarp)算法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Trie(字典树) : 如何实现搜索引擎
- 下一篇: 字符串匹配算法(二):BM(BoyerM