您的位置:首页 > 其它

KMP算法

2016-05-23 22:14 239 查看


前言

前几天,突然听到一位刚刚面试完应聘者的同事吐槽到“现在的程序员基本功怎么这么差,连一个简单的KMP算法都搞不定,还好意思开那么高的薪水"。听到这里,笔者默默的翻出《数据结构》,打开google。本文正是在这样的背景下对KMP算法的复习与整理。


简介

该算法是一种改进的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此称之为KMP算法。此算法可以在O(n+m)的时间数量级上完成串的模式匹配操作。


思想

举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?



首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索字符串"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。



因为B与A不匹配,搜索字符串再往后移。



就这样,直到字符串有一个字符,搜索字符串的第一个字符相同为止。



接着比较字符串和搜索字符串的下一个字符,还是相同。



直到字符串有一个字符,与搜索字符串对应的字符不相同为止。



这时,最自然地方式就是将搜索字符串整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。其算法时间复杂度即为O(m*n)。



一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的关键思想就是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。



怎么做到这一点呢?可以针对搜索字符串,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。



已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

右移位数 = 已匹配的字符数 - 对应的部分匹配值


6-2=4, 则将搜索字符串后移4位。



因为空格与C不匹配,搜索字符串还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索字符串向后移2位。



因为空格与A不匹配,继续后移一位。



逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索字符串向后移动4位



逐位比较,直到搜索字符串的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索字符串向后移动7位,这里就不再重复了。


部分匹配表的生成

从上面的匹配过程,我们发现部分匹配表是KMP算法的关键所在,解下来让我们看一下部分匹配表是如何生成的。

首先,我们需要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

字符串“string”为例,则“string”的前缀即为: “s", "st", "str", "stri", "strin"。其后缀即为: "g", "ng", "ing", "ring", "tring"。

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,



字符串前缀后缀部分匹配值
A空集空集0
ABAB0
ABCA, ABC, BC0
ABCDA, AB, ABCD, CD, BCD0
ABCDAA, AB, ABC, ABCDA, DA, CDA, BCDA,1
ABCDABA, AB, ABC, ABCD, ABCDAB, AB, DAB, CDAB, BCDAB2
ABCDABDA, AB, ABC, ABCD, ABCDA, ABCDABD, BD, ABD, DABD, CDABD, BCDABD0
"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索字符串移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。




实现

在KMP算法中有个数组,叫做前缀数组,也有的叫next数组,每一个子串有一个固定的next数组,它记录着字符串匹配过程中失配情况下可以向前多跳几个字符,当然它描述的也是子串的对称程度,程度越高,值越大,当然之前可能出现再匹配的机会就更大。next数组的求法是KMP算法的关键,但是理解next数组并不是一件轻松的事情。

由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:



而且,根据这个表可以得出下述结论

失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值

上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。

给定字符串“ABCDABD”,可求得它的next 数组如下:



把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单!

换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:



根据最大长度表求出了next 数组后,从而有

右移位数 = 失配字符所在位置 - 失配字符对应的next 值


而后,你会发现,无论是基于《最大长度表》的匹配,还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的。

接下来,咱们来写代码求下next 数组。

基于之前的理解,可知计算next函数的方法可以采用递推,如果对于值k,有p0 p1, ..., pk-1 = pj-k pj-k+1, ..., pj-1,相当于next[j-1] = k。那么对于pattern的前j 个序列字符,得

若pattern[k] == pattern[j],则next[j] = next(j-1) + 1 = k + 1
若pattern[k ] ≠ pattern[j],相当于在字符p[k]之前不存在前缀"p0 p1, …, pk-1"跟后缀“pj-k pj-k+1, …, pj-1"相等,那么是否可能存在另一个值t<k,使得p0 p1, …, pk-1 = pj-t pj-t+1…pj-1成立呢?这个t 显然应该是next[k],因为这相当于一个"利用next函数值进行T串和T串的匹配"问题。

求next数组如下:

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

static inline void getNext(const char *pattern, int *next, int pattern_len)

{

int i = 0;

int j = -1;

next[0] = -1;

while (i < pattern_len - 1)

{

if (j == -1 || pattern[i] == pattern[j])

{

++i;

++j;

if (pattern[i] != pattern[j]) //正常情况

next[i] = j;

else //特殊情况,这里即为优化之处。考虑下aaaab, 防止4个a形成012在匹配时多次迭代。

next[i] = next[j];

}

else

{

j = next[j];

}

}

}

static inline bool match(const char *src, const char *pattern)

{

bool is_match = true;

int src_index = 0;

int pattern_index = 0;

int src_len = strlen(src);

int pattern_len = strlen(pattern);

//创建next数组,并初始化

int *next = (int *)malloc(pattern_len * sizeof(int));

getNext(pattern, next, pattern_len);

//匹配主循环体

while (pattern_index < pattern_len && src_index < src_len)

{

//若对应位置字符匹配则右移1位,否则移动pattern

if (pattern_index == -1 || src[src_index] == pattern[pattern_index])

{

src_index++;

pattern_index++;

}

else

{

pattern_index = next[pattern_index];

}

}

//若pattern_index未达到串尾,表明pattern未完成匹配。否则即是完成匹配

if (pattern_index >= pattern_len)

{

is_match = true;

}

else

{

is_match = false;

}

return is_match;

}

int main(void)

{

char src[] = "aaaabacdeg";

char pattern[] = "aabacd";

bool res = match(src, pattern);

printf("res: %d\n", (int)res);

return 0;

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: