KMP算法及其实现

KMP算法是一种常用的在字符串中寻找子串的算法,相比于最普通的一一比较的暴力法(BF法),KMP法在时间效率上明显更优。为了更好地分析KMP,首先简要介绍BF法。


BF法

假设要在长度为m的字符串text中查找长度为n的子串pattern(假设n小于m,否则显然是不可能找到的)。那么最传统的方法即是在text中开一个长度为n的“窗口”,如果这一窗口中字符串与pattern完全符合,则表示找到了子串所在的位置,此时窗口中第一个字符在text中的位置即是找到的子串位置。

说人话……BR法简单而言是这样:在text中找出以第0个字符为开始、长度为n的子串(也就是所谓的窗口),将这个子串与pattern逐字符地比较,若完全一致则为找到。如果不吻合,则在text中找出以第1个字符为开始、长度为n的子串,将这个子串与pattern逐字符地比较;在text中找出以第2个字符为开始、长度为n的子串,将这个子串与pattern逐字符地比较……以此类推,直到找到或者到达text末尾为止。

BF法每一次比较都要逐字符地比较text中抽出的子串和pattern,且每次比较失败(不吻合)时只把“窗口”向后移动一位,使得查找效率较低。


KMP法

上面提到了,BF法每次比较失败时都把窗口只向后移动一位,这导致了低效率。那么如果每次比较失败时把窗口多向后移动几位,那么效率自然就高了不少。那么移动几位呢?

假设用k表示每次比较失败时窗口向后移动的位数,则k=(pattern中第一个不吻合的字符的脚标i)-(next【i】)。算法的关键在于这个next!

简单想想可能会觉得如果在第t位比较失败了,则直接跳过前面的字符,把窗口向后移t位不就好了?考虑如下的情形:假设text为“abdabdabc……”,pattern为“abdabc”,开始时比较text[0]与pattern[0],吻合,再比较text[1]与pattern[1],吻合……一直到text[5]与pattern[5],不再吻合。按照我们在刚刚最简单的假设,是不是应该把窗口直接向后移动5位,将text从text[5]开始的子串与pattern继续比较?显然不对,我们明显发现text[4]开始的abdabc是符合pattern的,分明已经找到了才对!所以,我们需要前式中的next【i】。

回到我们的例子来讲解next究竟是什么,我们选择的pattern是“abdabc”,那么假设在pattern[4]处比对失败,那么因为pattern[4]前一个字符是“a”,而pattern的开头字符也是“a”,因此比起前一段所述的简单向后跳4位,还需要向前回退1位,因为从text中与pattern[3]对应的那个“a”,就有可能是符合pattern的那个窗口的开头字符。或者,假设我们之前一直比较成功,但到了pattern[5]处比对失败,那么因为pattern[5]前面的两个字符是“ab”而pattern开头的两个字符也是“ab”,因此此时的next应该是2,即需要先将窗口向后移5位,然后回退2位。

现在我们可以归纳出next【i】怎么计算了。Next【i】等于可以使pattern[0: i-1]这个子串中长度为n的前缀与长度为n的后缀相等的最大的n值。特例是next【0】,next【0】应为-1,因为k=(pattern中第一个不吻合的字符的脚标i)-(next【i】),此时(pattern中第一个不吻合的字符的脚标i)为0,为了让窗口仍然可以后移,需要将next【0】设为-1。

在本例中,pattern为“abdabc”,那么next为{-1, 0, 0, 0, 1, 2}。

下面就可以总结性地给出KMP算法的描述了:开始时,将text中从0开始、长度为n的窗口,与pattern比较,如若比较失败,把窗口向后移动k位,k=(pattern中第一个不吻合的字符的脚标i)-(next【i】),在之后每次的比较中可以跳过前next【i】的字符位比较(因为显然是一样的)……不断执行前述步骤,直到找到pattern或者到达text的末尾为止。


以下为我的C++实现:

#include<iostream>
#include<string>

using namespace std;

int* getNext(string instr) {
    int length = instr.length();
    int* next;
    next = new int[length];
    next[0] = -1;
    for(int i = 1; i < length; i++) {
        int max = 0;
        for(int j = 1; j < i-1; j++) {
            if(instr.compare(0, j, instr, i-j, j) == 0) {
                max = j;
            }
        }
        next[i] = max;
    }
    return next;
}

int KMP(string text, string pattern) {
    if(pattern.length() > text.length()) {
        return -1;
    }
    int* next;
    int ne = 0;
    next = getNext(pattern);
    for(int i = 0; i <= text.length() - pattern.length(); /*注意这里没有++*/) {
        int j;
        for(j = ne; j < pattern.length(); j++) {
            if(text[i+j] != pattern[j]) {
                break;
            }
        }
        if(j == pattern.length()) {
            return i;
        }
        i = i + j - next[j];
        if(next[j] == -1) {
            ne = 0;        
        } else {
            ne = next[j];
        }
    }
    return -1;
}

int main() {
    int result;
    string string1("ab abd ababcabcd"), string2("abcabcd");
    result = KMP(string1, string2);
    cout << result << endl;
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值