实现用KMP算法匹配字符串

问题

有一个字符串“ABCABABCABDA”,问该字符串里面是否包含“ABCABD”,是的话请求出下标位置。

思路

一、简单的想法
最容易想到的方法应该就是让target的第一个字符和pattern的第一个字符比较,如果不相等,则让pattern后移一位,直到target有一个字符,与pattern的第一个字符相同为止。接着比较target和pattern的下一个字符,如果在pattern匹配完之前,target有一个字符与pattern匹配不相等,那么就让pattern整个后移一位,再从头逐个比较。显然,这样效率会很差。比如这道题,我们画个图来模拟匹配过程。
《实现用KMP算法匹配字符串》
当匹配到pattern的第6个字符D时,和A不相同。所以就让pattern后移一位,从第一个字符A与target的第二个字符B比较。可见每次字符不匹配时都要回溯到开始位置的这种做法时间开销会比较大。

二、KMP算法
那么又要怎么做才能有好的效率呢?
我们看看第一次匹配失败的情况,前面已经成功匹配了5个字符,我们从图可以很容易看出,如果将pattern直接移到target的第4个字符而不是只后移一位,那么这个时候又已经匹配成功了2个字符,即AB,这样就提高了效率。如下图:
《实现用KMP算法匹配字符串》
那么,这其中是有什么联系吗?
我们来看看pattern这个字符串ABCABD,在第一次已经匹配的前5个字符里,有个特点,就是前后有2个字符是重复的,即AB。所以通过这个前后重复的字符个数,我们就能知道在target里最有可能匹配pattern的下个新的位置。
KMP算法就是利用了pattern本身的信息提出来的!
(一)生成部分匹配表
首先,我们需要根据pattern来生成一张部分匹配表。先了解概念“前缀”和“后缀”。
“前缀”指除了最后一个字符以外,一个字符串的全部头部组合。
“后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
所以,“部分匹配值”就是“前缀”和“后缀”的最长共有元素的长度。以本例的pattern(“ABCABD”)为例加深认识。

  • “A”的前缀和后缀都为空集,共有元素的长度为0
  • “AB”的前缀为[A],后缀为[B],共有元素的长度为0
  • “ABC”的前缀为[A,AB],后缀为[BC,C],共有元素的长度为0
  • “ABCA”的前缀为[A,AB,ABC],后缀为[BCA,CA,A],共有元素为“A”,长度为1
  • “ABCAB”的前缀为[A,AB,ABC,ABCA],后缀为[BCAB,CAB,AB,A],共有元素为“AB”,长度为2
  • “ABCABD”的前缀为[A,AB,ABC,ABCA,ABCAB],后缀为[BCABD,CABD,ABD,BD,D],共有元素的长度为0

    所以部分匹配表就是:
    《实现用KMP算法匹配字符串》

那么这张表该怎么用程序生成呢?
我们可以用迭代的方式。假设matchTable[n]是部分匹配表(注意数组下标是从0开始的),对于pattern的前i-1个已知部分匹配值的字符,如果匹配值为count,则对于pattern的第i个字符,有如下可能:

  1. pattern[i] == pattern[count],此时matchTable[i]=count+1=matchTable[i-1]+1。
  2. pattern[i] != pattern[count],此时只能在pattern的前count个字符组成的字串中找出最大的且能与pattern[i]匹配的部分匹配值

具体代码见最后。

(二)KMP算法匹配过程
有了部分匹配表,我们就能根据下面的公式来算出pattern向后移动的位数。

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

所以完整的匹配过程是:
《实现用KMP算法匹配字符串》

可见,这样大大减少了比较次数,提高了效率。
最后,附上具体的实现代码。

代码

//
// Created by huxijie on 17-3-13.
// KMP算法的实现

#include <iostream>
#include <string>
using namespace std;

//生成样式字符串的部分匹配表
int *generateMatchTable(const string& pattern,int *matchTable) {
    int length = pattern.length();

    if (matchTable == NULL) {
        return NULL;
    }

    int count = 0;   //部分匹配值,表示目前“前缀”和“后缀”的最长共有元素的个数,0表示没有,以此类推
    matchTable[0] = 0;

    //迭代求pattern前i个字符的部分匹配值
    for (int i = 1; i < length; ++i) {
        count = matchTable[i - 1];

        //对于pattern[i]!=pattern[count]的情况,此时只能在pattern的
        //前count个字符组成的字串中找出最大的且能与pattern[i]匹配的部分匹配值
        while (count >= 1 && pattern[i] != pattern[count]) {
            count = matchTable[count-1];     //注意数组下标是从0开始的
        }

        //匹配成功,则在上一个部分匹配值的基础上加1
        if (pattern[i] == pattern[count]) {
            matchTable[i] = count + 1;
        } else {    //否则直接置为0,表示“前缀”和“后缀”没有共有字符
            matchTable[i] = 0;
        }
    }

    return matchTable;

}

//结合部分匹配表进行字符串匹配过程
int kmpMatch(const string& target,const string& pattern) {
    //求出部分匹配表
    int *matchTable = new int[pattern.length()];
    matchTable = generateMatchTable(pattern,matchTable);
    const int targetLength = target.length();
    const int patternLength = pattern.length();
    int targetIndex = 0;
    int patternIndex = 0;

    while (targetIndex < targetLength && patternIndex < patternLength) {
        if (target[targetIndex] == pattern[patternIndex]) {
            targetIndex++;
            patternIndex++;
        } else if (0 == patternIndex) {
            targetIndex++;
        } else {
            patternIndex = matchTable[patternIndex-1];
        }
    }

    delete (matchTable);

    if (patternIndex == patternLength) {
        return targetIndex - patternIndex;
    } else {
        return -1;
    }

}

int main() {
    string target = "ABCABABCABDA";
    string pattern = "ABCABD";
    cout << kmpMatch(target, pattern);

    return 0;
}
    原文作者:KMP算法
    原文地址: https://blog.csdn.net/ancientmoondjay/article/details/61926296
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞