[c++]字符串匹配kmp算法

7 篇文章 0 订阅

重新梳理了下kmp算法


kmp的核心是一个next数组
先看一下这个next的使用方法

//简单写一个字符串查找的函数
//在s中找第一个ss
//没找到的话返回-1
//找到的话返回开头字符的index
//
//函数里的注释在后面都会看到相应的解释

int find(string& s, string& ss){
    vector<int> next;
    getNext(ss, next);//分析模式串得到next数组
    int i=1,j=1;//注意!!!迭代变量都是从1开始
    while(j <= s.size()){
        if(i==0 || ss[i-1] == s[j-1]){
            if(i >= ss.size()) return j-ss.size();
            j++;i++;//两个相等则对比下一个(或者特殊状态i==0)
        }else{
            i = next[i];//匹配失败,j不动,i通过next跳到下一个位置
        }
    }
    return -1;    
}

step 1 kmp原理

kmp的原理是:对模式串分析,得到一个next数组,用于指导匹配失败时候模式串回溯的新位置。

例子1:
    j     (原字符串的索引)
abababxxx (原字符串)
ababc     (模式串)
    i     (模式串的索引)

普通的暴力匹配的话:ij都会回到起点,然后模式串往后走一位,变成如下情形
 j
abababxxx
 ababc
 i
 
 理论上,ij已经走过的位置,是已经知道了它的信息,那么回溯的时候是能够精准跳到某一个位置。kmp算法设计为回溯时候j位置不变,只改变模式串的位置,保证了效率的提高,如下
    j
abababxxx
  ababc
    i
可以发现的是模式串新跳到的位置其实i并不是开头,而且i前面的也都是刚好匹配的,这就是kmp效率高的原因。

那么他为啥能这么跳呢?

通过第一个例子可以看出,跳之前,已经匹配了的是abab,其中存在“最长的 相同的 前缀和后缀 字串”,就是ab。其他的例如abcab的是ab,ababa的是aba。

因此,跳之前已经匹配了的部分(例子里的abab)的后缀,等于,该部分的前缀,也就是跳之后的已匹配状态的串(i前面的ab)

所以对比两个状态,看得出跳的前后j的位置不变,i跳到了之前的那个前缀的后一位,而next数组的含义也正是“next[跳前的i] = 跳后的i”。

然后我们的目的就变成了“求模式串中,从头开始的每一个字串中,“最长的 相同的 前缀和后缀 字串”的长度”,也就是next[某位置] = 这个位置求的长度 。

这里有一个特殊的点,如果匹配失败时只有i回溯,那么i在第一个位置就失败,j就应该去下一位,然后i也从头开始。所以有一个取巧的方式,就是下标都从1开始。这样的话,在开头的find函数里的next回溯的位置全部大于零,而0位特殊状态,是需要让j++ i=1的一种“回溯”,
而这种状态刚好可以并在j++i++的分支里,所以if的判断里有一个特殊状态。

step 2

要解决新目标,先来一个最粗暴的方式:

“遍历模式串的每一个字符(k,初始化为1),并让其(j,初始化为k)与模式串的第一个字符(i,初始化为0)对比。

  • 如果相等,那么记录next[j]=i(只表含义,ij的值并不严谨),i++j++继续对比。
  • 如果不等,那么,j位置之前的字串的“最长的 相同的 前缀和后缀 字串”的长度”为i自增的次数,然后回溯到k继续下一次遍历”。

那么回溯这里很慢能不能优化一下呢?能,办法就是我们需要一个next数组指导匹配失败之后回述的位置,,,嗯禁止套娃。

所以,,这里要递归?不不不,其实会有一点点记忆化搜索的影子,因为这里要用的next数组不是要嵌套个什么函数去获取,而是直接用的,为什么能直接用,下面来证明一下。

这里可以看成是模式串和自己进行匹配,此处的原串中的索引位置j,此处的模式串种索引位置为i,初始化应为j2,i1(如果两个都从第一个开始就完全相等没有什么用的)。

在这只有两种操作,ij自增,或者i通过next找下一个i,而在粗暴的方式中可以看到next数组是通过j找到i。有没有可能出现next[x]==x的情况,x位置匹配失败,然后下一个从x开始,显然是不存在的,所以每通过next找一次,位置就会更靠前,所以j是严格恒大于i的。而且next是通过j进行赋值的,在我们的算法中j只会出现自增,所以可以保证我们在使用next找i的下一个位置时,next[i]都是之前已经生成好了的

所以可以看出这里的next是可以使用的。

step 3 试着写写

于是我们来尝试着写一下getNext函数

/* 不可用
void getNext(string& s, vector<int>& next){
    //首先初始化 注意下标都是从1开始
    next.resize(s.size() + 1);
    int j = 2, i = 1; //!!!伏笔1,这里在后面会做修改
    next[1] = 0; //第一个匹配失败应为特殊状态
    
    //然后写循环,退出条件是j走到末尾
    //我们先写简单的分支2
    while(j < s.size()){
        //分支1:相等或者特殊情况 
        //这里有一点要注意
        //因为我们的下标从1开始的,在取字符时候要减去1
        if(i==0 || s[i-1] == s[j-1]){
            //然后在相等的时候记录next,并且ij自增
            next[j+1] = i+1; //!!!遗留问题,为什么是这样看下一个例子
            ++j;++i;
        }
        // 分支2:不等
        else{
            //不相等的时候i要通过next数组得到新的i
            i = next[i];
        }
    }
}
*/
例子2:

在生成next数组时候走到其中分支1的情况:
      j       j=4
a b a b c         (A行)
    a b a b c     (B行)
      i       i=2
此时知道的是s[i-1] == s[j-1],根据B行得知某字串有前缀ab,根据A行得知该字串有后缀ab,因此我们根据j的值知道该子串长度为4,这个子串也是我们前文花大量精力要找的具有特殊性质的子串,所以也是j+1位匹配失败时候我们可以进行跳转。

然后我们再来看一下使用next的情况:
            t       t=n
... a b a b x ...   
    a b a b c       
            k       k=5
这里为了区分两个场景不同的下标做了更改
此时为k等于5匹配失败的情况,也就是上面情况的“j+1位匹配失败”,可以通过观察看出下一跳的位置为:
            t       t=n
... a b a b x ...  
        a b a b c   
            k       k=3
k由5跳到3,而3是什么,是这个特殊的子串里的前缀的下一位,还原回例子2最开头的图里,应该就是i+1

在例子2中 j=4 i=2 时对应的跳转是 5->3
所以遗留问题处,记录next的代码应该为:
next[j+1]=i+1

最后我们来看初始化的时候,j=2,但是记录第一个的next是3,next[2]没有被记录。这里可以利用i==0的特殊状态,将初始化改为 j=1,i=0将next[2]记录进去

step 4 成品

好了,这下我们可以写出来正确的getNext函数:

void getNext(string& s, vector<int>& next){
    int j = 1, i = 0;
    next.resize(s.size() + 1);
    next[1] = 0;
    while(j < s.size()){
        if(i==0 || s[i-1] == s[j-1]){
            next[j+1] = i+1;
            ++j;++i;
        }
        else{
            i = next[i];
        }
    }
}
// 至此基础版的kmp就完成了,配合最开始的find函数一起看一下
int find(string& s, string& ss){
    vector<int> next;
    getNext(ss, next);
    int i=1,j=1;
    while(j <= s.size()){
        if(i==0 || ss[i-1] == s[j-1]){
            if(i >= ss.size()) return j-ss.size();
            j++;i++;
        }else{
            i = next[i];
        }
    }
    return -1;    
}

step 5 进阶

上面的next数组还能进行优化,我们来看一个例子

例子3:
          j
... a a a x a ...
    a a a a b c
          i

我们来手写一下他的next数组:
(写每一位时就计算它之前的子串的特殊前缀的长度 + 1)
string:   a a a a b c
next[]: 0 0 1 2 3 4 1
index : 0 1 2 3 4 5 6 

根据next数组下一跳的位置是:
          j
... a a a x a...
      a a a a b c
          i
实际人为跳转的位置是:
            j
... a a a x a...
            a a a a b c
            i
而我们观察发现其实应该直接跳回开头让j自增了,但是它还是在next里面一步一步找才能找到开头,所以要把这里优化掉。

其实可以优化的原因是因为模式串中有相同的字符,因为在某一次的循环里面,已经判断为 s[i-1] != s[j-1] ,然后i通过next跳转(写为i_new)。

  • 如果s[i-1] == s[i_new-1], 那么它跳转了再判断也还是不相等,需要再跳下一个
  • 如果s[i-1] != s[i_new-1], n那么跳转了之后他是有可能会相等的所以不能再跳了
    所以在给next数组赋值时候,要看一下这个跳转关系的两者是否相同
    其实这里有一点并查集优化的味道
void getNext(string& s, vector<int>& next){
    int j = 1, i = 0;
    next.resize(s.size() + 1);
    next[1] = 0;
    while(j < s.size()){
        if(i==0 || s[i-1] == s[j-1]){
            // 跳转关系是 j+1 > i+1
            // 取两者字符 s[j]  s[i]
            if(s[j] != s[i]){ 
                //若不等,与之前的操作相同
                next[j+1] = i+1;
            }
            else {
                //若相等,把i+1跳到的位置赋值到这
                next[j+1] = next[i+1];
            }
            ++j;++i;
        }
        else{
            i = next[i];
        }
    }
}
//江湖中将这种优化过的数组称为nextval,只要将这个函数使用next->nextval全局替换一下就可以。

参考链接:
https://www.bilibili.com/video/BV1ZJ4119752?p=1


以前写的太垃圾了都懒得看了。。。


本来是想写一个split函数,暴力比较的,后来写完后加上了kmp算法(忽略一大堆头文件…在别的代码里写的…)

#include <stdio.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <netdb.h>
#include <errno.h>

#include<string>
#include<vector>
#define MAX_EVENT 20
#define READ_BUF_LEN 10
using namespace std;
// kmp算法:
// 当字符串匹配了一部分之后发现失败,可以根据已经匹配成功的部分推倒出短串往后移动的位置,
// 避免了短串每次匹配失败只后移一位.
// 所以可以先分析一遍短串,确定在短串每个idx失败的时候短串往后移的距离,
// 之后在该处失败时就可以直接移动短串,跳过中间的循环
//
//
// _getKmpVec()
// 传入字符串及长度
// 返回该字符串的 kmp数组 (ret)
// kmp数组含义为 idx:val
// 在短串idx位置失败的时候,移动短串到下一个新位置之后,有val个是已经匹配成功的(此处并不是要移动的距离)
bool _getKmpVec(const char* str,const int len, int* ret){
//  printf("=====\n");
    for(int i = 0, cnt = 0; i < len; i++){ // 默认值cnt为0, 表示当前匹配成功的个数
//      printf("%d %c   %d %c\n",i ,str[i], cnt ,str[cnt] );
        if(i == 0)
        {
        }
        else if ( str[cnt] == str[i] )  // 数组第一位为0
        {
            cnt++;
        }
        else
        {
            cnt = 0;
        }
        ret[i] = cnt;
    }
//  printf("%s\n",str);
//  for(int i=0;i<len;i++)
//      printf("%d",ret[i]);
//  printf("\n");
    return true;
}
int _find(const char* str1, const char* str2, const int len1, const int len2, int* kmp_vec)
{
//  printf("=====find\n");
    for(int i = 0, cnt = 0 ; i < len1; )
    {
//      printf("str1:%c idx:%d    str2:%c idx:%d | ", str1[i] ,i,  str2[cnt],cnt);
        if(str1[i] == str2[cnt]) // 如果当前项匹配成功
        {
            cnt++; //要匹配的短串的idx 增加
            i++;
            if(cnt >= len2) // str2完全匹配
            {
//              printf("retu  %d  %d\n" ,i,cnt);
                return i-cnt;
            }
//          printf("succ  %d  %d\n" ,i,cnt);
        }
        else // 当前匹配失败,找到下一个位置
        {
            if(cnt != 0) // 当前有匹配成功的
                cnt = kmp_vec[cnt]; //更新已经成功匹配到的数量,新短串位置跟原来的长串位置比
            else //当前短串就没有成功的,该动长串了
                i++;
//          printf("fail  %d  %d\n" ,i,cnt);
        }
    }
    return -1;
}
int find(const char* str1, const char* str2, const int len1, const int len2, int* kmp_vec)
{
    if(str1 == NULL || str2 == NULL) return -1;
    if(len1 <= 0 || len2 <= 0) return -1;
    if(len1 < len2) return -1;
    return _find(str1, str2, len1, len2, kmp_vec);
}
bool split(const string& ori_str, const string& sub_str, vector<string>& ret_vec)
{
    const char* str1 = ori_str.c_str();
    const char* str2 = sub_str.c_str();
    int len1 = ori_str.length();
    int len2 = sub_str.length();
    int len = 0;
    int kmp_vec[len2];
    _getKmpVec(str2, len2, kmp_vec);
    while( (len = find(str1, str2, len1, len2, kmp_vec)) != -1 ){
//      printf("find:%d\n", len);
        string tmp_str(str1, len);
        ret_vec.push_back(tmp_str);
        len1-=len+len2;
        str1+=len+len2;
    }
    if(len1 != 0){
        string tmp_str(str1, len1);
        ret_vec.push_back(tmp_str);
    }
    return true;
}

int main()
{
    string str1 = "ababcabcacbab";
    string str2 = "abcab";

    printf("%s %s:\n", str1.c_str(), str2.c_str());
    vector<string> vec;
    split(str1, str2, vec);
    for(auto &it : vec)
    {
        printf("%s\n", it.c_str());
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值