KMP算法简介

KMP算法,是由Knuth,Morris,Pratt共同提出的模式匹配算法,其对于任何模式和目标序列,都可以在线性时间内完成匹配查找,而不会发生退化,是一个非常优秀的模式匹配算法。本文就对该算法进行基本的介绍,由于水平有限,解释不恰当的地方,欢迎指出谢谢。

在KMP算法中,对于每一个模式串我们会事先计算出模式串的内部匹配信息,在匹配失败时最大的移动模式串,以减少匹配次数。

比如,在简单的一次匹配失败后,我们会想将模式串尽量的右移和主串进行匹配。右移的距离在KMP算法中是如此计算的:在已经匹配的模式串子串中,找出最长的相同的前缀和后缀,然后移动使它们重叠。

KMP算法对于朴素匹配算法的改进是引入了一个跳转表next[]数组。以模式字符串ABABABB为例,其跳转表为:

index 0 1 2 3 4 5 6
substr A B A B A B B
next -1 0 0 1 2 3 4

求解过程如下:候选串即为最长的相同的前缀和后缀串
(1)当 j 等于 0 的时候发生不匹配,即模式串第一个字符与主串i位置不匹配,应将i跳过当前位置,从下一个位置和模式串第一个字符继续比较,此时将 next[0] 设置为-1来表示特殊情况;
(2)当 j 等于 1 时发生不匹配,此时匹配的子串 S 为“A”,候选串只能是空串,下次匹配还是从模式串的下标0开始比较,即 next[1] 设为0;
(3)当 j 等于 2 时发生不匹配,此时匹配的子串 S 为“AB”,候选串只能是空串,下次匹配还是从模式串的下标0开始比较,即 next[2] 设为0;
(4)当 j 等于 3 时发生不匹配,此时匹配的子串 S 为“ABA”,候选串为是“A”,因此 next[3] 设为1;
(5)当 j 等于 4 时发生不匹配,此时匹配的子串 S 为“ABAB”,候选串为是“AB”,因此 next[4] 设为2;
(6)当 j 等于 5 时发生不匹配,此时匹配的子串 S 为“ABABA”,候选串为是“ABA”和“A”,选择长度大的子串“ABA”,因此 next[5] 设为3;
(7)当j等于6时发生不匹配,此时匹配的子串 S 为“ABABAB”,候选串为是“ABAB”和“AB”,选择长度大的子串“ABAB”,因此 next[6] 设为4;

为了更进一步的说明next的求解:
再以模式串ababaaababaa为例:
next例子

以主串为ABABCABCABABABBC, 模式串为ABABABB为例的匹配过程

index 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
substr A B A B C A B C A B A B A B B C
one A B A B A(2) B B
two A B A(0) B A B B
three A B A(0) B A B B
four A B A B A B B

通过模式串的4次移动,完成了对目标串的模式匹配。这里以匹配的第一步为例,当模式串(从0计数)在第4个位置处不匹配,next[4] = 2, 所以从模式串的第2个位置从新与不匹配处比较,得到第二步。当第二步中的不匹配位置的next值0时,表示第一个字符匹配不成功,则i向后移动一个,模式串从头开始匹配,得到第三步。
在整个匹配过程中,无论模式串如何向后滑动,目标串的输入字符都在不会回溯,直到找到模式串,或者遍历整个目标串都没有发现匹配模式为止。

关于求解 next 数组的方法:
如何利用 next[n] 求解 next[n+1],减少重复计算呢?
求解 next[n+1] 的时候,由上边的分析可知,此时存在两个子串 a 和 b 是匹配的,即模式串中 0 到 k-1 的子串(对应 n 时的前缀)和子串 n-k 到 n-1 (对应 n 时的后缀)是相互匹配的,下面分两种情况求解 next[n+1]:
第一种情况:下标 k 处的字符与 n 处的字符匹配,则 0 到 k 位置的子串与 n-k 到 n 位置的子串匹配(例如 n+1 的情况(k=2)为:ABABA),显然有 next[n+1] = k + 1 = next[n] + 1
第二种情况:下标 k 处的字符与 n 处的字符不匹配,这样我们为了消除将0 到 k的子串与n处的不匹配,需要向前移动到 next[n] 处。假设 next[k] = k’,相当于找到了S串中的这么一对子串0~k’-1 和 n- (k’ - 1) 到n是匹配的,如果 k’ 处字符和 n 处字符匹配,这利用情况 1)中的办法求得 next[n+1] , 否则用同样的办法借助之前求得的 next 数组值来继续处理。(这一部分我也不是很懂?)

Java代码如下:

import java.util.Scanner;
public class KMPOfJava {

    public static void main(String[] args) {
        KMPOfJava kmp = new KMPOfJava();
        Scanner in = new Scanner(System.in);
        String str = "";
        String substr = "";
        int next[];
        while (in.hasNext()){
            str = in.nextLine();
            substr = in.nextLine();
            next = kmp.getNext(substr);
            System.out.println(kmp.KMP(str, substr, next));
            kmp.printNext(next);
        }
    }
    /**
     * 测试用例:
     * 主串:    ABABCABCACBAB
     * 模式串:ABCAC
     * 模式串:ABABAB
     * 模式串:ABABABB
     * 主串:    abbababaaababaa
     * 模式串:ababaaababaa 
     * */
    /** KMP算法 */
    public int KMP(String str, String substr, int next[]){
        int i = 0, j = 0;
        while (i < str.length() && j < substr.length()){
            if (str.charAt(i) == substr.charAt(j)){//匹配
                ++i;
                ++j;
            }else{//不匹配
                j = next[j];//取出next数组对应j处的值
                if (j == -1){//如果为第一个位置
                    j = 0;//从模式串的头部开始比较
                    ++i;//主串后移一个位置
                }
            }
        }
        if (j == substr.length())
            return i - substr.length();
        else return -1;

    }

    /** 得到模式串的next数组 */
    public int[] getNext(String substr){
        int i = 0, j = -1;
        //定义next数组,长度为模式串的长度
        int next[] = new int[substr.length()];
        //设置next初始位置为-1
        next[0] = -1;
        //求解next其他值(当前字符串的最长的相同的前缀和后缀)
        while (i < substr.length() - 1){
            //第一种情况:k处字符与n-k处字符匹配
            if (j == -1 || substr.charAt(i) == substr.charAt(j)){
                ++i;
                ++j;
                next[i] = j;
            }else{//第二种情况:k处字符与n-k处字符不匹配
                j = next[j];
            }
        }
        return next;
    }

    /**打印next函数 */
    public void printNext(int next[]){
        for (int n : next){
            System.out.print(n + " ");
        }
        System.out.println();
    }
}
posted @ 2016-04-27 16:26  zsper  阅读(269)  评论(0编辑  收藏  举报