算法之美-KMP快速串匹配

23-41-58.jpg

串匹配算法也稱作模式匹配算法,就是在目標(biāo)字符串中查找子字符串讹堤,常用于文本搜索吆鹤、入侵檢測(cè)等領(lǐng)域,將目標(biāo)字符串定義為T(t0,t1,... tn-1)洲守,將模式字符串定義為P(p0,p1...pm-1)疑务。下面將來逐步講解算法之美中的KMP算法。

1.BF算法

最簡(jiǎn)單的做法就是遍歷目標(biāo)字符串中的每一個(gè)字符與模式字符串中的字符進(jìn)行逐一比較梗醇,不相同的時(shí)候保持目標(biāo)字符串的索引不變模式字符串的索引加1知允,在進(jìn)行逐個(gè)字符的比較,這種方式也稱作BF(Brute Force)算法叙谨,如下圖所示:

23-51-09.jpg

由此可見温鸽,BF算法的時(shí)間復(fù)雜度為O((n-m)*m),該方法簡(jiǎn)單手负、直觀,但是由于每遇到一次失配都要將目標(biāo)字符串的索引回溯,顯然效率很低。

2.MP算法

MP算法是由詹姆斯·莫里斯和沃恩·普萊特在1970年提出的一種快速匹配算法。該算法的主要特點(diǎn)是當(dāng)失配情況發(fā)生時(shí)撵渡,目標(biāo)字符串的索引不需要回溯,利用模式字符串的內(nèi)部特征可以完全避免目標(biāo)字符串的回溯充岛,這樣可以極大的提高檢索效率盹憎。先舉一個(gè)簡(jiǎn)單的例子對(duì)于目標(biāo)字符串a(chǎn)babcababd與模式字符串a(chǎn)babd來說:

23-22-43.jpg

第一次失配發(fā)生在t[i = 4] = cp[j = 4] = d 處如果按照BF檢索的話,下一次比較時(shí)字符串T和字符串P的起始位置分別為1和0瘾境,但是這樣做完全沒有必要歧杏,通過觀察模式字符串可以發(fā)現(xiàn),ababd中前四個(gè)字符具有相同的特征就是a[0]a[1] = a[2]a[3]迷守,那么下次匹配時(shí)犬绒,就不需要將目標(biāo)字符串的索引回溯,我們只需要將模式字符串的索引位置變?yōu)閖 = 2就可以進(jìn)行下輪的比較:

23-31-25.jpg

此時(shí)發(fā)現(xiàn)t[4] != p[2]兑凿,需要比較t[4]與p[0]得到如下圖:

23-33-14.jpg

發(fā)現(xiàn)t[4] != p[0]凯力,然后將i加1,再與p進(jìn)行比較:

23-35-38.jpg

逐個(gè)比較之后發(fā)現(xiàn)完全相等礼华。
MP算法就是在對(duì)字符串進(jìn)行匹配之前咐鹤,先求出模式字符串中各個(gè)字符間的關(guān)系(由于模式字符串在進(jìn)行匹配之前就可以確定),然后依據(jù)此關(guān)系與目標(biāo)字符串進(jìn)行匹配圣絮。記錄模式字符串P中各個(gè)字符之間的關(guān)系的函數(shù)也叫作字符串的失效函數(shù)祈惶。讓我們先來看看這個(gè)失效函數(shù),先舉一個(gè)簡(jiǎn)單的例子:目標(biāo)字符串為aaaaab與模式字符串aaab的比較扮匠,當(dāng)?shù)谝惠啽容^到i=3時(shí)捧请,失配發(fā)生了,此時(shí)t[i] = ap[i] = b不相等棒搜,由于模式字符串在i=3之前的字符都與目標(biāo)字符串的字符相同即:t[0]t[1]t[2] = p[0]p[1]p[2]疹蛉,而且模式字符串中的前三個(gè)字符也完全相同即:p[0]=p[1]=p[2]那么下次比較的位置我們能準(zhǔn)確的定位:目標(biāo)字符串t[3]與模式字符串p[2]進(jìn)行比較,因?yàn)?code>t[1] = p[0] ;t[2] = p[1]力麸,所以如果從t[1]開始逐個(gè)與模式字符串中的字符比較就會(huì)造成浪費(fèi)可款。失效函數(shù)是記錄當(dāng)失配發(fā)生時(shí),模式字符串索引應(yīng)該跳轉(zhuǎn)的位置末盔。
<b>我們用f(i)記錄當(dāng)失配發(fā)生時(shí)筑舅,模式字符串應(yīng)該回退的索引位置,則定義如下:對(duì)于長(zhǎng)度為n的字符串陨舱,位于第m位的字符如果存在如下關(guān)系:p[0]...p[k] = p[m-k]...p[m]翠拣,且滿足該條件的k最大,那么f[m]=k游盲,若不存在f(i) = 1误墓,其中n > m , m > k蛮粮。</b>,當(dāng)遇到失配發(fā)生時(shí)谜慌,我們只需要將模式字符串的指針移動(dòng)到f(m-1) + 1處然想,而不需要移動(dòng)目標(biāo)字符串的指針。
可以簡(jiǎn)單的論證一下當(dāng)存在匹配模式時(shí)(讀者可以自行論證當(dāng)不存在模式匹配時(shí)的情況):對(duì)于目標(biāo)字符串T和模式字符串P(我們假設(shè)都從下標(biāo)1開始)欣范,當(dāng)T[i + 1 ... i + k - 1]的k-1個(gè)元素與P[1 ... k-1]相同变泄,但是T[i+k] 與 P[k]比較時(shí)失配,如果f(k - 1) = j ,那么根據(jù)定義P[1...j] == P[k-j... k-1]恼琼,也就是說<u>T[i+k-j...i+k-1] = P[1...j]</u>妨蛹,那么我們下次比較的就是T[i+k]與P[j+1]了,所以晴竞,當(dāng)P[k]失配時(shí)蛙卤,我們將下次比較的位置定位在P[f(k-1)+1],我們假設(shè)next[k] = f(k-1)+1噩死,當(dāng)失配在第k個(gè)字符串發(fā)生時(shí)颤难,next[k]表示下次比較時(shí),模式字符串的索引值已维。
下面的C代碼就是求取模式字符串的next函數(shù)如下:

void MPPattern(const char * var , int *mpArr) {
  size_t length = strlen(var);
  int i = 0;
  int j = mpArr[0] = -1;
  while (i < length) {
    while (j > -1 && var[i] != var[j]) { // 4
      j = mpArr[j]; // 5
    }
    i++; // 1
    j++; // 2
    mpArr[i] = j; // 3
   }
}

var 即為待求解的模式字符串行嗤,將最后求得的next[]存放在mpArr中,將j衣摩、mpArr[0]賦值為-1昂验。首先看看簡(jiǎn)單的1、2艾扮、3這幾個(gè)步驟既琴,如果我們要求解的是aaaaa這樣字符全部相同的模式字符串,自然而然泡嘴,隨著字符索引的增加甫恩,next[i]根據(jù)定義也應(yīng)該是遞增+1的,這三步很好理解酌予。再來看看4磺箕、5步,如果向前遍歷的過程中x[i]!=x[j]抛虫,由于i總比j大松靡,需要將j回溯,回溯的位置就是mpArr[j](其實(shí)就是f(k-1)+1)建椰,在進(jìn)行比較雕欺,如果j = -1(mpArr[0])時(shí),執(zhí)行1、2屠列、3步啦逆。
對(duì)于字符串aaabc執(zhí)行的結(jié)果為

image.png

目標(biāo)字符串與模式字符串比較代碼如下:

int isTargetContain(const char* target , const char* pattern) {
  size_t pLength = strlen(pattern);
  int * a = calloc(pLength, sizeof(int));
  MPPattern(pattern, a);
  size_t tLength = strlen(target);
  int i = 0 , j = 0;
  while (j < tLength) {
    while (i > -1 && target[j] != pattern[i]) {
      i = a[i];
    }
    i++;
    j++;
    if (i >= pLength) {
      int index = j - i;
      i = a[i];
      free(a);
      return index;
    }
  }
  free(a);
  return -1;
}

其中i = a[i]依然表示的是回溯。

KMP算法

KMP算法與MP算法相似笛洛,唯一的不同點(diǎn)就是夏志,f(m)不僅要滿足p[0]...p[k-1] = p[m-k]...p[m-1],同時(shí)要滿足條件P[k]!=P[m]苛让。KMP是在MP算法上的優(yōu)化沟蔑,在與目標(biāo)字符串比較時(shí),T[i ... i + k - 1]的k-1個(gè)元素與P[0 ... k-1]相同狱杰,但是T[i+k] 與 P[k]比較時(shí)失配溉贿,按照MP算法的話,模式字符串應(yīng)該回溯到f(k-1)+1這個(gè)位置浦旱,如果 P[f(k-1)+1]與P[k]相同的話,再次與目標(biāo)字符串比較九杂,肯定會(huì)失敗颁湖。我們用kmpNext[]數(shù)組來記錄KMP算法下失配時(shí)模式串的偏移:

  1. 如果kmpNext[j] = -1 表示P[j] = P[0],且P[j]前面k個(gè)字符與P開頭的k個(gè)字符不等例隆,或者相等但是P[j]!=P[k]
  2. 如果kmpNext[j] = k 表示模式字符串P中甥捺,字符P[j]前面k個(gè)字符與模式字符串開頭k個(gè)字符相同,且 P[j]!=P[k]
  3. 其它情況kmpNext[j] = 0
    kmp計(jì)算kmpNext[]的算法實(shí)現(xiàn)如下:
void KMPPattern(const char * var , int *kmpArr) {
  int i ,j;
  size_t length = strlen(var);
  i = 0;
  j = kmpArr[0] = -1;
  while (i < length) {
    while (j > -1 && var[i] != var[j]) {
      j = kmpArr[j];
    }
    i++;
    j++;
    if (var[i] == var[j]) { // 1
      kmpArr[i] = kmpArr[j];
    } else {
      kmpArr[i] = j;
    }
  }
}

其中回溯那部分代碼與MP算法相同镀层,唯一不同的是1處的代碼镰禾,如果我們的模式字符串為aaaa,那么根據(jù)定義可知道唱逢,kmpNext[] = [-1,-1,-1,-1]吴侦,這就是var[i]==var[j]這個(gè)判斷做的事情,但是如果是字符串aaaab坞古,那么kmpNext[] = [-1,-1,-1,-1,3]备韧,最后的3就是else里做的事情,復(fù)雜的模式字符串原理大致相同痪枫,可以自行論證织堂。
kmp算法目標(biāo)字符串與模式字符串比較的代碼如下:

int KMP(const char *target , const char *pattern){
  size_t targetSize = strlen(target);
  size_t patternSize = strlen(pattern);
  if (patternSize > targetSize) { return -1; }
  int *t;
  t = calloc(patternSize + 1, sizeof(int));
  KMPPattern(pattern, t);
  int i = 0 , j = 0;
  while (j < targetSize) {
    while (i > -1 && target[j] != pattern[i]) {
      i = t[i];
    }
    i++;
    j++;
    if (i >= patternSize) {
      free(t);
      return (j - i);
    }
  }
  free(t);
  return -1;
}

結(jié)論

kmp算法由于先尋找模式字符串的內(nèi)部特征來降低與目標(biāo)字符串的匹配次數(shù),時(shí)間復(fù)雜度為O(m + n)奶陈,在進(jìn)行匹配之前需要先將模式字符串的失效函數(shù)計(jì)算出來易阳,在進(jìn)行匹配時(shí),如果匹配失敗吃粒,不需要將目標(biāo)字符串回溯潦俺,只需要將模式字符串的指針回溯值next[j]處從而提高效率。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市黑竞,隨后出現(xiàn)的幾起案子捕发,更是在濱河造成了極大的恐慌,老刑警劉巖很魂,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扎酷,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡遏匆,警方通過查閱死者的電腦和手機(jī)法挨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幅聘,“玉大人凡纳,你說我怎么就攤上這事〉圯铮” “怎么了荐糜?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)葛超。 經(jīng)常有香客問我暴氏,道長(zhǎng),這世上最難降的妖魔是什么绣张? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任答渔,我火速辦了婚禮,結(jié)果婚禮上侥涵,老公的妹妹穿的比我還像新娘沼撕。我一直安慰自己,他們只是感情好芜飘,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布务豺。 她就那樣靜靜地躺著,像睡著了一般燃箭。 火紅的嫁衣襯著肌膚如雪冲呢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天招狸,我揣著相機(jī)與錄音敬拓,去河邊找鬼。 笑死裙戏,一個(gè)胖子當(dāng)著我的面吹牛乘凸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播累榜,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼营勤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼灵嫌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起葛作,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤寿羞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后赂蠢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绪穆,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年虱岂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了玖院。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡第岖,死狀恐怖难菌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蔑滓,我是刑警寧澤郊酒,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站键袱,受9級(jí)特大地震影響猎塞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杠纵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望钩骇。 院中可真熱鬧比藻,春花似錦、人聲如沸倘屹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)纽匙。三九已至务蝠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間烛缔,已是汗流浹背馏段。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留践瓷,地道東北人院喜。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像晕翠,于是被迫代替她去往敵國(guó)和親喷舀。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容