“浅析kmp算法”

“浅析kmp算法”

By 钟桓

9月 16 2014 更新日期:9月 16 2014

文章目录

  1. 1. 暴力匹配:
  2. 2. 真前缀和真后缀,部分匹配值
  3. 3. 如何使用部分匹配值呢?
  4. 4. 寻找部分匹配值
  5. 5. 拓展
    1. 5.1. 最小覆盖字串
  6. 6. 参考资料

首先,KMP是一个字符串匹配算法,什么是字符串匹配呢?简单地说,有一个字符串“BBC ABCDAB ABCDABCDABDE”,我想知道这个字符串里面是否有“ABCDABD”;我想,你的脑海中马上就浮现了一个简单的暴力算法,是的,它也有名字,叫做暴力匹配,就是从头开始进行匹配,如果不行的话,就从主字符串的下一个继续。看下面的图结合文字会更清晰些:


暴力匹配:

1

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

2

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

3

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

4

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

5

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

6

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。

虽然这样做可行,但是你有没有想过这样的效率很差,因为你要把”搜索位置”移到已经比较过的位置,重比一遍。


真前缀和真后缀,部分匹配值

上面说了,暴力匹配的效率是非常低下的,但是我们有什么办法让效率提升呢?让我们先来了解三个概念,“真前缀”和“真后缀”;这个比较好理解,看下面就可以理解了。

  - "A"的真前缀和真后缀都为空集,共有元素的长度为0;
  - "AB"的真前缀为[A],真后缀为[B],共有元素的长度为0;
  - "ABC"的真前缀为[A, AB],真后缀为[BC, C],共有元素的长度0;
  - "ABCD"的真前缀为[A, AB, ABC],真后缀为[BCD, CD, D],共有元素的长度为0;
  - "ABCDA"的真前缀为[A, AB, ABC, ABCD],真后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
  - "ABCDAB"的真前缀为[A, AB, ABC, ABCD, ABCDA],真后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
  - "ABCDABD"的真前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],真后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

从上面的例子可以体会到吧,真前缀就是从字符串第一个字符开始的所有字符串,但是不包括它自身;对于真后缀同理。

那么什么是部分匹配值呢?注意到上面提到的共有元素的长度了吗?部分匹配值的意思就是当前串的真前缀和真后缀中字符串相同的最大长度。“AB”的真前缀和真后缀中没有相同的,所以部分匹配值是0;“ABAB”的部分匹配值是2,因为真前缀中的“AB”和真后缀中的“AB”匹配,长度为2,所以部分匹配值是2。


如何使用部分匹配值呢?

让我们来看一些前面的例子,在“BBC ABCDAB ABCDABCDABDE”中匹配“ABCDABD”。

首先看一下ABCDABD的部分匹配表:

部分匹配表中的每一个值,对应的都是每一个字符为结尾的子串的部分匹配值。像“AB”,部分匹配值是0,所以对应的表里的值是0;“ABCDAB”,部分匹配值是2,所以对应的表里的值是2;

那么我们如何来用它呢?上面的暴力匹配我们说了,当ABCDABD的最后一个D和“ ”不匹配时,暴力匹配方式只会把ABCDABD右移一位,然后继续匹配。我们前面也说了,这样的方式没有充分利用一些信息。

那么我们该如何利用上面的信息呢?

比如前面我们说的情况,看下面的图:

前面的这个时候,我们只是让“ABCDABD”右移一位。但是有没有发现,其实前面已经匹配上的“ABCDAB”这一部分的信息都知道,所以我们知道“ABCDAB”右移一位依然无法匹配,这个时候,我们只需要考虑ABCDAB的真前缀和真后缀匹配最多,如果我们知道这个真前缀和真后缀,那么我们就知道如何移动了。只需要移动至真前缀和真后缀部分匹配即可。而这里就是需要考虑部分匹配值了。

为什么是这样呢?我们可以简单地证明一下。我们知道“ABCDAB”的部分匹配值,2,也就是说真前缀和真后缀最大的匹配长度是“AB”这一部分。我们只需要将“ABCDAB”的前缀的“AB”移动至和后缀的“AB”匹配。假设我们不移动到它们匹配,在前面部分也可能匹配,那么它们的部分匹配值应该更大,但是这里最大就是2了。所以,假设不成立。所以我们只需要将最长的 真前缀和真后缀 匹配即可。

匹配的时候,我们可以利用部分匹配值。

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

对于“ABCDAB”,部分匹配值2,6-2=4;所以将搜索词向后移动4位即可。

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

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

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

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


寻找部分匹配值

现在的问题是,我们如何来寻求这个部分匹配值,在上面的过程中,我们可以发现,只要我们知道部分匹配值了,就能够让匹配的速度加快。而对于部分匹配值,我们关心的其实就是那个搜索词。所以从搜索词入手。

我们定义这样一个数组next[],T标示匹配字符串,P标示搜索词。

那么next数组表示什么呢?看下面的表格:

搜索词 A B C D A B D
next -1 0 0 0 0 1 2

和上面的部分匹配表对比一下,你会发现,next数组就是 部分匹配值 整体向右移动了一位, 然后初始值赋值为 -1。

其实next数组也有含义,next[j]的值表示,当P[j] != T[i]时,指针 j 的下一步移动位置。

当j=0时不匹配怎么办?这个时候next[j]= -1;表示T需要左移1位。

所以当 P[j] != T[i] 时, 另 j = next[j] ,然后继续匹配。

当 P[j] == T[i] 时,i和j 分别都前进一位。

那么next数组该怎么求解呢?

当P[k] == P[i] 时,有 next[j+1] = next[j] +1;

当P[k] != P[i] 时,有 k = next[k]; 然后继续匹配。

如果 k == -1; 那么这个时候,表示P的第0字符都和现在的第i个字符不匹配,则 next[i] = 0; k++, i++;

所以,综上,便有了下面的程序。下面的getNext是获得next数组,KMP是进行匹配,下面的程序是poj3461 的示例程序。

import  java.util.Scanner;

public class Main{

    public int[] getNext(String P){
        int[] next = new int[P.length()];  // next 数组表示的是当 P[i]和P[k]不匹配时,k应该跳转到哪一个位置
                                            //这里的i时后缀指针,  k是前缀指针

        next[0]=-1;  // 因为开头的比较特殊,如果它不匹配,那么移动的应该是T,T应该左移,-1标示T左移

        int i=0,k=-1;

        while(i < P.length()-1)
        {
            if(k<0 || P.charAt(i) == P.charAt(k))
            {
                next[++i] = ++k;
            }else
                k = next[k];
        }

        return next;

    }

    public int KMP(String T, String P){

        int res=0;

        int[] next = getNext(P);

        int i=0,j=0;

        while(true)
        {

            if(i >= T.length())
                break;
            if( j==-1 || T.charAt(i) == P.charAt(j))
            {
                j++;
                if(j == P.length())
                {
                    res++;
                    j = next[j-1];
                }else
                    i++;
            }else
                j = next[j];

        }

        return res;
    }

    public void run(){
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        scan.nextLine();
        while(n>0){
            String P = scan.nextLine();
            String T = scan.nextLine();

            System.out.println(KMP(T,P));

            n--;
        }

    }

    public static  void main(String args[]){

        new Main().run();

    }
}

拓展

最小覆盖字串

最小覆盖子串(串尾多一小段时,用前缀覆盖)长度为n-next[n](n-pre[n]),n为串长。

证明分两部分:

1-长为n-next[n]的前缀必为覆盖子串。

当next[n]<n-next[n]时,如图a,长为next[n]的前缀A与长为next[n]的后缀B相等,故长为n-next[n]的前缀C必覆盖后缀B;

当next[n]>n-next[n]时,如图b,将原串X向后移n-next[n]个单位得到Y串,根据next的定义,知长为next[n]的后缀串A与长为前缀串B相等,X串中的长为n-next[n]的前缀C与Y串中的前缀D相等,而X串中的串E又与Y串中的D相等……可见X串中的长为n-next[n]的前缀C可覆盖全串。

2-长为n-next[n]的前缀是最短的。

如图c,串A是长为n-next[n]的前缀,串B是长为next[n]的后缀,假设存在长度小于n-next[n]的前缀C能覆盖全串,则将原串X截去前面一段C,得到新串Y,则Y必与原串长度大于next[n]的前缀相等,与next数组的定义(使str[1..i]前k个字母与后k个字母相等的最大k值。)矛盾。得证!有人问,为什么Y与原串长大于next[n]的前缀相等?由假设知原串的构成必为CCC……E(E为C的前缀),串Y的构成必为CC……E(比原串少一个C),懂了吧!

一个字符串A(1 <= |A| <= 1000000)可以写成某一个子串B重复N次所得,记为A = B^N,求最大的N。

算法分析:

令L = |A|,容易发现,用KMP自匹配后L - p[L]即得到最小覆盖子串的长度。

下面我们要证明一个问题:一个字符串的覆盖子串长度,一定是它的最小覆盖子串长度的倍数。

设最小覆盖子串长度d整除L, 假设存在u > d满足u整除L且d不整除u。

易得,Ai = A(i + d),Ai = A(i + u),则A(i + d) = A(i + u),即Ai = A(i + u - d),不断进行可得到A_i = A(i + u - kd)(k为正整数)。

因为d不整除u,那么必然存在k使得0 < u - kd < d,与d是最小循环子串长度矛盾。

所以,最小覆盖子串长度若为L的约数则得解否则输出1。时间复杂度O(L)。

最小覆盖字串的例题 poj2406 , 代码可以参考以下:

import  java.util.Scanner;

public class Main{

    public int getNext(String P){
        int[] next = new int[P.length()+10];  // next 数组表示的是当 P[i]和P[k]不匹配时,k应该跳转到哪一个位置
                                            //这里的i时后缀指针,  k是前缀指针
        next[0]=-1;  // 因为开头的比较特殊,如果它不匹配,那么移动的应该是T,T应该左移,-1标示T左移

        int i=0,k=-1;

        while(i < P.length())
        {
            if(k<0 || P.charAt(i) == P.charAt(k))
            {
                next[++i] = ++k;
            }else
                k = next[k];
        }

        return P.length()-next[P.length()];
    }

    public void run(){
        Scanner scan = new Scanner(System.in);
        while(scan.hasNext()){
            String P = scan.nextLine();

            if(P.charAt(0)==‘.‘)
                break;

            int t = getNext(P);
            int len = P.length();

            if(len%t == 0)
            {
                System.out.println(len/t);
            }else
                System.out.println(1);
        }

    }

    public static  void main(String args[]){

        new Main().run();

    }
}

参考资料

时间: 2024-08-12 18:07:13

“浅析kmp算法”的相关文章

KMP算法浅析

背景: KMP算法之所以叫做KMP算法是因为这个算法是由三个人共同提出来的,就取三个人名字的首字母作为该算法的名字.其实KMP算法与BF算法的区别就在于KMP算法巧妙的消除了指针i的回溯问题,只需确定下次匹配j的位置即可,使得问题的复杂度由O(mn)下降到O(m+n). KMP算法的思想就是:在匹配过程称,若发生不匹配的情况,如果next[j]>=0,则目标串的指针i不变,将模式串的指针j移动到next[j]的位置继续进行匹配:若next[j]=-1,则将i右移1位,并将j置0,继续进行比较.

hiho 1015 KMP算法 &amp;&amp; CF 625 B. War of the Corporations

#1015 : KMP算法 时间限制:1000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho是一对好朋友,出生在信息化社会的他们对编程产生了莫大的兴趣,他们约定好互相帮助,在编程的学习道路上一同前进. 这一天,他们遇到了一只河蟹,于是河蟹就向小Hi和小Ho提出了那个经典的问题:“小Hi和小Ho,你们能不能够判断一段文字(原串)里面是不是存在那么一些……特殊……的文字(模式串)?” 小Hi和小Ho仔细思考了一下,觉得只能想到很简单的做法,但是又觉得既然河蟹先生这么说了,就

KMP算法详解

这几天学习kmp算法,解决字符串的匹配问题,开始的时候都是用到BF算法,(BF(Brute Force)算法是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果.BF算法是一种蛮力算法.)虽然也能解决一些问题,但是这是常规思路,在内存大,数据量小,时间长的情况下,还能解决一些问题,但是如果遇到一些限制时间和内存的字符串问

KMP算法

1 /* next数组是KMP算法的关键,next数组的作用是:当模式串T和主串S失配 2 * ,next数组对应的元素指导应该用T串中的哪一个元素进行下一轮的匹配 3 * next数组和T串相关,和S串无关.KMP的关键是next数组的求法. 4 * 5 * ——————————————————————————————————————————————————————————————————— 6 * | T | 9 | a | b | a | b | a | a | a | b | a | 7

KMP算法解决字符串出现次数

比如主串为:"1001110110" 子串为:"11" 则出现位置分别为:3 4 7 //KMP算法 2015.6.7 #include<iostream> #include<stdlib.h> using namespace std; int main() { char *s = "1001110110"; char *p = "11"; int ar[20] = { 0 }; //next ar[0

串模式匹配之BF和KMP算法

本文简要谈一下串的模式匹配.主要阐述BF算法和KMP算法.力求讲的清楚又简洁. 一 BF算法 核心思想是:对于主串s和模式串t,长度令为len1,len2,   依次遍历主串s,即第一次从位置0开始len2个字符是否与t对应的字符相等,如果完全相等,匹配成功:否则,从下个位置1开始,再次比较从1开始len2个字符是否与t对应的字符相等.... BF算法思路清晰简单,但是每次匹配不成功时都要回溯. 下面直接贴代码: int BF_Match(char *s, char *t) { int i=0,

跳跃表,字典树(单词查找树,Trie树),后缀树,KMP算法,AC 自动机相关算法原理详细汇总

第一部分:跳跃表 本文将总结一种数据结构:跳跃表.前半部分跳跃表性质和操作的介绍直接摘自<让算法的效率跳起来--浅谈"跳跃表"的相关操作及其应用>上海市华东师范大学第二附属中学 魏冉.之后将附上跳跃表的源代码,以及本人对其的了解.难免有错误之处,希望指正,共同进步.谢谢. 跳跃表(Skip List)是1987年才诞生的一种崭新的数据结构,它在进行查找.插入.删除等操作时的期望时间复杂度均为O(logn),有着近乎替代平衡树的本领.而且最重要的一点,就是它的编程复杂度较同类

字符串的KMP算法替换

1 #include<iostream> 2 #include<string> 3 using namespace std; 4 5 6 7 class myString 8 { 9 private: 10 string mainstr; 11 int size; 12 void GetNext(string p,int next[]); 13 int KMPFind(string p,int next[]); 14 public: 15 myString(); 16 //~myS

算法 - KMP算法

1 解决问题 从一个字符串中查找子串,如果存在返回字串在字符串中的位置. 示例: 字符串(T):"BBC ABCDAB ABCDABCDABDE" 子串( P):"ABCDABD" 通过算法查找字串P在字符串T中的位置为15(从0开始). 2 暴力算法 思路: 循环T,从T的每个字符开始子字串P匹配. 代码: int strstr(char iTarget[], int iTLen, char iPattern[], int iPLen) { for (int i