字符串的模式匹配:KMP算法

来源:互联网 发布:python recv超时 编辑:程序博客网 时间:2024/06/07 06:26

  KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。其实KMP算法与BF算法的区别就在于KMP算法巧妙的消除了指针i的回溯问题,只需确定下次匹配j的位置即可,使得问题的复杂度由O(mn)下降到O(m+n)。

与BF算法的比较

  普通的匹配算法BF中,目标串和模式串的索引值都需要重新回到起点再次进行匹配,所以该算法的时间复杂度较高,达到O(m*n)。KMP算法的核心思想是寻找模式串本身的特征,在此基础上达到目标串不回溯,模式串有规律回溯的目的,以减少回溯比较的次数,降低时间复杂度,达到O(m+n)。但是这是建立在提高空间复杂度的基础上的。
  BF算法:时间复杂度O(m*n);空间复杂度O(1)
  KMP算法:时间复杂度O(m+n);空间复杂度O(n)

算法思想

  KMP算法的核心是寻找模式串本身的规律。在该算法中表现为反映该规律的next数组。next数组的作用是在每次失配时,模式串根据next数组对应位置上的值回溯模式串索引的位置。next数组的求法也是KMP算法的精华所在。
  
  next数组有如下定义:

(1) next[j] = -1: j = 0
(2) next[j] = max(k): 0 < k < j 且 P[0,k-1]=P[j-k, j-1];
(3) next[j] = 0 其他

  该定义的意义就是next数组的首位均为-1。在其他位置上,该位置之前的子串存在与该位置之后的同等长度的子串,我们称之为前后缀(如在a b c a b中,前缀和后缀均为”ab”。)。当没有任何相同的前后缀的情况下,next值为0。否则next值为k。k可以理解为子串的长度。
  
  再举个例子来看一下设么是前缀和后缀:

字符串:”test”
前缀为: t, te, tes
后缀为:est, st, t

  next值举例:

P  a   b  a   b  a  
j   0   1  2   3   4
next -1   0  0   1  2

   我们来分析一下next值的产生过程:

“a”的前缀和后缀都为空集,共有元素的长度为0;
“ab”的前缀为[a],后缀为[b],共有元素的长度为0;
“aba”的前缀为[a, ab],后缀为[ba, a],共有元素的长度0;
“abab”的前缀为[a, ab, aba],后缀为[bab, ba, b],共有元素的长度为0;
“ababa”的前缀为[a, ab, aba, abab],后缀为[baba, aba, ba, a],共有元素为”a”,长度为1;

   由next数组的定义可知,我们可以递推出如下结论:

(1) next[0] = -1
假设next[j]=k, 即P[0, k-1]==P[j-k, j-1]
(2) 若P[j] == P[k],则有P[0, k]==P[j-k, j],很显然,next[j+1]=next[j]+1=k+1;
(3) 若P[j] != P[k],则可以把其看做模式匹配的问题,即匹配失败的时候,k值如何移动,显然k=next[k]。

  那么具体的实现可以是(JAVA):

    static void getNext(char[] p){        int[] next = new int[p.length];        next[0] = -1;        int j = 0;        int k = - 1;        while(j < p.length - 1){            // 匹配的情况下,p[j]==p[k]            if (k == -1 || p[j] == p[k]) {                j++;                k++;                next[j] = k;            } else{                //p[j]!=p[k]                k = next[k];            }         }    }

  或者可以使用直接求解(java):

    static void getNext(char[] p) {        int i, j, temp;        int[] next = new int[p.length];        for (i = 0; i < p.length; i++) {            if (i == 0) {                next[i] = -1; // next[0]=-1            } else if (i == 1) {                next[i] = 0; // next[1]=0            } else {                temp = i - 1;                for (j = temp; j > 0; j--) {                    if (equals(p, i, j)) {                        next[i] = j; // 找到最大的k值                        break;                    }                }                if (j == 0)                    next[i] = 0;            }        }    }    // 判断p[0...j-1]与p[i-j...i-1]是否相等    static boolean equals(char[] p, int i, int j) {        int k = 0;        int s = i - j;        for (; k <= j - 1 && s <= i - 1; k++, s++) {            if (p[k] != p[s])                return false;        }        return true;    }

  KMP算法的思想就是:我们的原则还是从左向右匹配, 在匹配过程称,若发生不匹配的情况,如果next[j]>=0,则目标串的指针i不变,将模式串的指针j移动到next[j]的位置继续进行匹配;若next[j]=-1,则将i右移1位,并将j置0,继续进行比较。
  
  KMP的匹配过程:

搜索词:      abcdabd匹配值(next):00001201、字符串和模式串(搜索词)对齐bbc abcaab abcdabcdabdecabcdabd2、字符串的第一个字符为b与搜索词的第一个字符a不匹配。搜索词后移一位bbc abcaab abcdabcdabdec abcdabd3、因为b与a不匹配,搜索词再往后移。bbc abcaab abcdabcdabdec  abcdabd4、就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。bbc abcaab abcdabcdabdec    abcdabd5、接着比较字符串和搜索词的下一个字符,还是相同, 直到字符串有一个字符,与搜索词对应的字符不相同为止。即搜索词的d与字符串的空格不符。这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。bbc abcaab abcdabcdabdec     abcdabd6、一个基本事实是,当空格与d不匹配时,你其实知道前面六个字符是"abcdab"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。查表可知,最后一个匹配字符b对应的"部分匹配值"2,因此按照下面的公式算出向后移动的位数: 移动位数 = 已匹配的字符数 - 对应的部分匹配值, 即 6 - 2 等于4,所以将搜索词向后移动4位。bbc abcaab abcdabcdabdec        abcdabd7、因为空格与c不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2"ab"),对应的"部分匹配值"0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。bbc abcaab abcdabcdabdec          abcdabd    8、因为空格与a不匹配,继续后移一位。           bbc abcaab abcdabcdabdec           abcdabd    9、逐位比较,直到发现c与d不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。bbc abcaab abcdabcdabdec               abcdabd     10、逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。                         

  代码实现如下(java):

    static int KMPMatch(char[] T, char[] p) {        int i = 0;        int j = 0;        int next[] = getNext(p);## 标题 ##        while (i < T.length) {            if (j == -1 || T[i] == p[j]) {                i++;                j++;            } else {                j = next[j]; // 消除了指针i的回溯            }            if (j == p.length)                return i - p.length;        }        return -1;    }
0 0
原创粉丝点击