算法之美-KMP快速串匹配

23-41-58.jpg

串匹配算法也称作模式匹配算法,就是在目标字符串中查找子字符串,常用于文本搜索、入侵检测等领域,将目标字符串定义为T(t0,t1,... tn-1),将模式字符串定义为P(p0,p1...pm-1)。下面将来逐步讲解算法之美中的KMP算法。

1.BF算法

最简单的做法就是遍历目标字符串中的每一个字符与模式字符串中的字符进行逐一比较,不相同的时候保持目标字符串的索引不变模式字符串的索引加1,在进行逐个字符的比较,这种方式也称作BF(Brute Force)算法,如下图所示:

23-51-09.jpg

由此可见,BF算法的时间复杂度为O((n-m)*m),该方法简单、直观,但是由于每遇到一次失配都要将目标字符串的索引回溯,显然效率很低。

2.MP算法

MP算法是由詹姆斯·莫里斯和沃恩·普莱特在1970年提出的一种快速匹配算法。该算法的主要特点是当失配情况发生时,目标字符串的索引不需要回溯,利用模式字符串的内部特征可以完全避免目标字符串的回溯,这样可以极大的提高检索效率。先举一个简单的例子对于目标字符串ababcababd与模式字符串ababd来说:

23-22-43.jpg

第一次失配发生在t[i = 4] = cp[j = 4] = d 处如果按照BF检索的话,下一次比较时字符串T和字符串P的起始位置分别为1和0,但是这样做完全没有必要,通过观察模式字符串可以发现,ababd中前四个字符具有相同的特征就是a[0]a[1] = a[2]a[3],那么下次匹配时,就不需要将目标字符串的索引回溯,我们只需要将模式字符串的索引位置变为j = 2就可以进行下轮的比较:

23-31-25.jpg

此时发现t[4] != p[2],需要比较t[4]与p[0]得到如下图:

23-33-14.jpg

发现t[4] != p[0],然后将i加1,再与p进行比较:

23-35-38.jpg

逐个比较之后发现完全相等。
MP算法就是在对字符串进行匹配之前,先求出模式字符串中各个字符间的关系(由于模式字符串在进行匹配之前就可以确定),然后依据此关系与目标字符串进行匹配。记录模式字符串P中各个字符之间的关系的函数也叫作字符串的失效函数。让我们先来看看这个失效函数,先举一个简单的例子:目标字符串为aaaaab与模式字符串aaab的比较,当第一轮比较到i=3时,失配发生了,此时t[i] = ap[i] = b不相等,由于模式字符串在i=3之前的字符都与目标字符串的字符相同即:t[0]t[1]t[2] = p[0]p[1]p[2],而且模式字符串中的前三个字符也完全相同即:p[0]=p[1]=p[2]那么下次比较的位置我们能准确的定位:目标字符串t[3]与模式字符串p[2]进行比较,因为t[1] = p[0] ;t[2] = p[1],所以如果从t[1]开始逐个与模式字符串中的字符比较就会造成浪费。失效函数是记录当失配发生时,模式字符串索引应该跳转的位置。
<b>我们用f(i)记录当失配发生时,模式字符串应该回退的索引位置,则定义如下:对于长度为n的字符串,位于第m位的字符如果存在如下关系:p[0]...p[k] = p[m-k]...p[m],且满足该条件的k最大,那么f[m]=k,若不存在f(i) = 1,其中n > m , m > k。</b>,当遇到失配发生时,我们只需要将模式字符串的指针移动到f(m-1) + 1处,而不需要移动目标字符串的指针。
可以简单的论证一下当存在匹配模式时(读者可以自行论证当不存在模式匹配时的情况):对于目标字符串T和模式字符串P(我们假设都从下标1开始),当T[i + 1 ... i + k - 1]的k-1个元素与P[1 ... k-1]相同,但是T[i+k] 与 P[k]比较时失配,如果f(k - 1) = 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]了,所以,当P[k]失配时,我们将下次比较的位置定位在P[f(k-1)+1],我们假设next[k] = f(k-1)+1,当失配在第k个字符串发生时,next[k]表示下次比较时,模式字符串的索引值。
下面的C代码就是求取模式字符串的next函数如下:

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。首先看看简单的1、2、3这几个步骤,如果我们要求解的是aaaaa这样字符全部相同的模式字符串,自然而然,随着字符索引的增加,next[i]根据定义也应该是递增+1的,这三步很好理解。再来看看4、5步,如果向前遍历的过程中x[i]!=x[j],由于i总比j大,需要将j回溯,回溯的位置就是mpArr[j](其实就是f(k-1)+1),在进行比较,如果j = -1(mpArr[0])时,执行1、2、3步。
对于字符串aaabc执行的结果为

image.png

目标字符串与模式字符串比较代码如下:

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算法相似,唯一的不同点就是,f(m)不仅要满足p[0]...p[k-1] = p[m-k]...p[m-1],同时要满足条件P[k]!=P[m]。KMP是在MP算法上的优化,在与目标字符串比较时,T[i ... i + k - 1]的k-1个元素与P[0 ... k-1]相同,但是T[i+k] 与 P[k]比较时失配,按照MP算法的话,模式字符串应该回溯到f(k-1)+1这个位置,如果 P[f(k-1)+1]与P[k]相同的话,再次与目标字符串比较,肯定会失败。我们用kmpNext[]数组来记录KMP算法下失配时模式串的偏移:

  1. 如果kmpNext[j] = -1 表示P[j] = P[0],且P[j]前面k个字符与P开头的k个字符不等,或者相等但是P[j]!=P[k]
  2. 如果kmpNext[j] = k 表示模式字符串P中,字符P[j]前面k个字符与模式字符串开头k个字符相同,且 P[j]!=P[k]
  3. 其它情况kmpNext[j] = 0
    kmp计算kmpNext[]的算法实现如下:
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,那么根据定义可知道,kmpNext[] = [-1,-1,-1,-1],这就是var[i]==var[j]这个判断做的事情,但是如果是字符串aaaab,那么kmpNext[] = [-1,-1,-1,-1,3],最后的3就是else里做的事情,复杂的模式字符串原理大致相同,可以自行论证。
kmp算法目标字符串与模式字符串比较的代码如下:

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;
}

结论

kmp算法由于先寻找模式字符串的内部特征来降低与目标字符串的匹配次数,时间复杂度为O(m + n),在进行匹配之前需要先将模式字符串的失效函数计算出来,在进行匹配时,如果匹配失败,不需要将目标字符串回溯,只需要将模式字符串的指针回溯值next[j]处从而提高效率。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容