KMP算法原理及实现分析

KMP算法分析

一、先来看看传统的暴力解法

1.1 BF算法设计思想

1、主串和模式串逐个字符进行比较

在这里插入图片描述

2、当出现**「字符串不相同」时,也就是「失配」**时,主串的比较位置重置为起始位置的下一个字符位置,模式串的比较位置重置为起始位置

在这里插入图片描述

1.2 BF算法的设计缺陷及解决方案

在BF算法中,每次失配都需要回溯指向上次比较起始字符的下一个字符。通过观察发现:在回溯的时候,已匹配似乎**「有一部分」没必要继续比较了,这样可以降低算法的「时间复杂度」**

在这里插入图片描述

2.1 KMP算法设计思路

在匹配过程中出现字符比较不相等时,**「主串 S」已比较的位置不回溯,「模式串 T」**比较的位置进行移动

在这里插入图片描述

在匹配过程中有一个难题需要解决:如何计算**「模式串 T」失配时的移动位数?经过三位牛人的研究,设计出了「部分匹配函数」**

2.2 部分匹配函数

部分匹配函数是KMP算法中最难以理解的部分。首先需要理解**「前缀」「后缀」**的概念。

· 前缀:指除了最后一个字符以外,一个字符串的全部头部组合

· 后缀:指除了第一个字符以外,一个字符串的全部尾部组合

下图分别是字符 c 的前缀和字符 a 的后缀

在这里插入图片描述

如何求出一个字符串的**[最大共有长度]**,下面以 **ababc **这个字符串举例

求出 c 这个字符的前缀与后缀

前缀后缀
ab
abab
ababab

那么由此可知 ababc这个字符串最大的 共有长度 就是 2 --ab

因此就可以知道 ababc 这个字符串所有字符的最大共有长度,可以得出一个next数组 [-1,0,0,1,2]

说明 数组的前两位是固定的 因为 a 这个字符无前缀与后缀,字符 b 虽然有,但是前缀与后缀不能达到最大长度1,这个前缀与后缀不能达到最大长度适应于所有字符。这就是为什么上面的表格没有将 前缀 abab与后缀abab列出来的原因。

那么来看一下 KMP算法的匹配过程

在这里插入图片描述

当发现ac不相等的时候,T串该如何移动,由上可知,T字符串的 next数组为[-1,0,1,2,3],发现字符c的最大共有长度为3,而此时 c 的后缀为 aaa, 前缀也是aaa,那是不是可以将T向前移动一个字符就可以了,就可以得到下图

在这里插入图片描述

红线框出的部分恰好就是失配时已匹配部分,“aaaa” 的最大共有元素为 “aaa”,这一部分字符就是不需要再重复进行比较直接跳过的字符

这里next数组代表当S串与T串匹配到的字符不相等时,T串该移动到的位置

  1. 当下标值是 -1时,代表 S串向前移动,T串在开头位置
  2. 当下标值>=0 表示 T串该跳到的位置,S串保持不动

说明next数组会因为语言下标的不同(比如lua和c),会有不一样的设定,后续看代码设定。

//求出 字符串 s2的 next
static int* getNextArray(string s2){
    int *next = NULL;
    int len = s2.length();
    if(len == 1){
        next = (int*)malloc(sizeof(int)*1);
        next[0] = -1;
        return next;
    }

    next = (int*)malloc(sizeof(int) * len);
    next[0] = -1;
    next[1] = 0;
    int i = 2; // 目前在哪个位置上求next数组的值
    int cn = 0;// 当前是哪个位置的值再和i-1位置的字符比较
    while(i < len){
        if(s2[i -1] == s2[cn]){// 配成功的时候
            next[i++] = ++cn;
        }else if(cn > 0){
            cn = next[cn];
        }else{
            next[i++] = 0;
        }
    }

    return next;
}

//获取s2串在s1串中第一次出现的位置 没找到返回 -1
static int getIndexOf(string s1,string s2){
    if(s2.length() < 1 || s1.length() < s2.length()){
        return -1;
    }

    int x = 0;
    int y = 0;
    int* next = getNextArray(s2);

    while(x < s1.length() && y <s2.length()){
        if(s1[x] == s2[y]){
            x++;
            y++;
        }else if(next[y] == -1){ //y == 0
            x++;
        }else{
            y = next[y];
        }
    }
    free(next);
    return y == s2.length() ? x - y : -1;
}

2.3 lua string.find() 寻找子串实现原理

//摘抄子 lua-5.1.4 lstrlib.c 文件
static int lmemfind (const char *s1, size_t l1,
                               const char *s2, size_t l2) {
    const char *start = s1;
    if (l2 == 0) return -1;  /* empty strings are everywhere */
    else if (l2 > l1) return -1;  /* avoids a negative `l1' */
    else {
        const char *init;  /* to search for a `*s2' inside `s1' */
        l2--;  /* 1st char will be checked by `memchr' */
        l1 = l1-l2;  /* `s2' cannot be found after that */
        while (l1 > 0 && (init = (const char *)memchr(s1, *s2, l1)) != NULL) { //寻找到第一个相等子串的问题
            init++;   /* 1st char is already checked */
            if (memcmp(init, s2+1, l2) == 0) //然后比较后续字符是否相等
                return (init-1) - start;
            else {  /* correct `l1' and `s1' to try again */
                l1 -= init-s1;
                s1 = init;
            }
        }
        return -1;  /* not found */
    }
}

2.4 比较 kmp 与 lua string.find()的效率

//g++ kmp.cpp -o kmp

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include<cstring>
#include <sys/time.h>
#include<vector>

using namespace std;

static vector<string> allStr;
static vector<string> allMatch;

static int* getNextArray(string s2);

long long getCurrentTime()  
{  
    struct timeval tv;  
    gettimeofday(&tv,NULL);  
    return tv.tv_sec * 1000 + tv.tv_usec / 1000;  
} 

static int getIndexOf(string s1,string s2){
    if(s2.length() < 1 || s1.length() < s2.length()){
        return -1;
    }

    int x = 0;
    int y = 0;
    int* next = getNextArray(s2);

    while(x < s1.length() && y <s2.length()){
        if(s1[x] == s2[y]){
            x++;
            y++;
        }else if(next[y] == -1){ //y == 0
            x++;
        }else{
            y = next[y];
        }
    }
    free(next);
    return y == s2.length() ? x - y : -1;
}

//求出 字符串 s2的 next
static int* getNextArray(string s2){
    int *next = NULL;
    int len = s2.length();
    if(len == 1){
        next = (int*)malloc(sizeof(int)*1);
        next[0] = -1;
        return next;
    }

    next = (int*)malloc(sizeof(int) * len);
    next[0] = -1;
    next[1] = 0;
    int i = 2; // 目前在哪个位置上求next数组的值
    int cn = 0;// 当前是哪个位置的值再和i-1位置的字符比较
    while(i < len){
        if(s2[i -1] == s2[cn]){// 配成功的时候
            next[i++] = ++cn;
        }else if(cn > 0){
            cn = next[cn];
        }else{
            next[i++] = 0;
        }
    }

    return next;
}


static string getRandomString(int possibilities, int size) {
    int ranLen = (rand() % size) + 1;
    // char* ans = (char*)malloc(ranLen);
    string res(ranLen,'\0');
    for(int i = 0;i < ranLen;i++){
        res[i] = (char)(int)(rand() % possibilities + 'a');
    }
    res[ranLen] = '\0';
    return res;
}


static int lmemfind (const char *s1, size_t l1,
                               const char *s2, size_t l2) {
    const char *start = s1;
    if (l2 == 0) return -1;  /* empty strings are everywhere */
    else if (l2 > l1) return -1;  /* avoids a negative `l1' */
    else {
        const char *init;  /* to search for a `*s2' inside `s1' */
        l2--;  /* 1st char will be checked by `memchr' */
        l1 = l1-l2;  /* `s2' cannot be found after that */
        while (l1 > 0 && (init = (const char *)memchr(s1, *s2, l1)) != NULL) {
            init++;   /* 1st char is already checked */
            if (memcmp(init, s2+1, l2) == 0)
                return (init-1) - start;
            else {  /* correct `l1' and `s1' to try again */
                l1 -= init-s1;
                s1 = init;
            }
        }
        return -1;  /* not found */
    }
}

static void testTime(){
    vector<int> memFindVec;
    vector<int> kmpFindVec;
    long long startTime = getCurrentTime();
    for(int i = 0;i < allStr.size();i++){
        string& str = allStr[i];
        string& match = allMatch[i];
        memFindVec.push_back(lmemfind(str.c_str(),str.length(),match.c_str(),match.length()));
    }

    cout << "memFindTime:"<< getCurrentTime() - startTime<<endl;

    startTime = getCurrentTime();
    for(int i = 0;i < allStr.size();i++){
        string& str = allStr[i];
        string& match = allMatch[i];
        kmpFindVec.push_back(getIndexOf(str, match));
    }
    cout << "kmpFindTime:"<< getCurrentTime() - startTime<<endl;

    for(int j = 0;j < memFindVec.size();j++){
        if(memFindVec[j] != kmpFindVec[j]){
            cout << allStr[j] << "   " << allMatch[j]<<endl;
            cout << "error:"<< " j: " << j << "   " << memFindVec[j] << "  " << kmpFindVec[j]<<endl;
            break;
        }
    }
    cout << "test success"<<endl;
}


static void randomString(){
    int possibilities = 5;
    int strSize = 20;
    int matchSize = 5;
    int testTimes = 5000000;
    for(int i = 0; i < testTimes;i++){
        allStr.push_back(getRandomString(possibilities,strSize));
        allMatch.push_back(getRandomString(possibilities,matchSize));
    }
}


int main(){
    srand((unsigned)time(NULL));
    randomString();
    testTime();
    // {
    //     string str = "bacebaccdcbedacca";
    //     string match = "cd";
    //     cout << lmemfind(str.c_str(),str.length(),match.c_str(),match.length())<<endl;
    // }

    return 0;
}

2.5 结论

由上述案例代码可知, 基于特定的样本, string.find() 的效率还是远远高于kmp的,只能说 c的库函数还是厉害,kmp虽然号称O(N),但是会省去很多常数项的东西,具体怎么达到O(N),可以看看算法导论 32.4章节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值