当前位置: 首页 > news >正文

写给自己的KMP——C++版本

文章目录

  • 一、前言
  • 二、暴力搜索
  • 三、KMP算法
  • 四、关键点记录
  • 五、总结

一、前言

又翻到了这个算法,一个常用的子串(子数组)匹配算法,看一遍学一遍,学一遍忘一遍,反反复复,不过每次回忆起来所用的时间越来少了,其本质上就是在暴力搜索的基础上加上 next 数组加速匹配,算法的关键在于 next 数组的理解和求解方法。

不想画图,缺少图解的算法很难给初学者讲清楚,所以本文也仅仅是个人的笔记而已,用于记录算法中关键点、帮助回忆或者理解其中的一些关键因素,如果想从头学习 KMP,还是去搜索其他资料吧,相关的内容有很多,有些文章写的很详细的。

今天的示例代码用C++来写,上一版的自己写的KMP我查了一下是C语言版本的,初看起来已经有点费劲了,随着时间的推移,我决定根据理解再写一次,写完才发现,和之前的风格判若两人。

二、暴力搜索

在原字符串中搜索模式串,最容易想到的就是暴力搜索,匹配则向后移动,不匹配则原串回溯,模式串归0,代码很容易实现,列举如下:

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

int violence_find(string s, string p) {
    int m = s.size(), n = p.size(), i = 0, j = 0;

    while (i < m && j < n) {
        if (s[i] == p[j]) { // match character
            ++i;
            ++j;
        } else { // mismatch
            i -= j - 1;
            j = 0;
        }
    }

    return j == n ? i - j : -1;
}

int main() {
    string s("abdfdjfdkekfdaa5gsdsf");
    string p("fdkekfd");

    cout << violence_find(s, p) << endl;
    return 0;
}

典型的 O(MN) 解法,这种解法慢就慢在原字符串的回溯上,也就是语句 i -= j - 1; 的效果,当出现失配时,原字符串之前的匹配几乎“白费”,每次最多移动一个字符,而 KMP 算法决定利用之前的“努力成果”。

三、KMP算法

在 KMP 算法中先利用模式串构建一个 next 数组,当出现失配情况时根据模式串前缀和后缀情况,最大程序利用已经匹配的部分来达到加速查找的目的,只需要求一个 next 数组,其他部分和暴力匹配的代码很像:

int kmp_tmp(string s, string p) {
    int m = s.size(), n = p.size(), i = 0, j = 0;

    vector<int> next = std::move(gen_next(p));

    while (i < m && j < n) {
        if (s[i] == p[j]) {
            ++i;
            ++j;
        } else { // mismatch
            j = next[j];
            if (j == -1) {
                ++i;
                j = 0;
            }
        }
    }

    return j == n ? i - j : -1;
}

和暴力搜索的代码对照下,只有 else 中语句块不太一样,这个 i 只前进不后退了,其实这个里的 j == -1 语句可以合并到判定相等的 if 语句块中,完成 KMP 代码如下:

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

vector<int> gen_next(string p) {
    int n = p.size(), i = 0, j = -1;
    vector<int> next(n, -1);

    while (i < n - 1) {
        if (j == -1 || p[i] == p[j]) {
            ++i;
            ++j;
            next[i] = j;
        } else j = next[j]; // mismatch, move j
    }

    return next;
}

int kmp(string s, string p) {
    int m = s.size(), n = p.size(), i = 0, j = 0;

    vector<int> next = std::move(gen_next(p));

    while (i < m && j < n) {
        if (j == -1 || s[i] == p[j]) {
            ++i;
            ++j;
        } else j = next[j]; // mismatch, move j
    }

    return j == n ? i - j : -1;
}

int main() {
    string s("abdfdjfdkekfdaa5gsdsf");
    string p("fdkekfd");

    cout << kmp(s, p) << endl;
    return 0;
}

四、关键点记录

  1. next[i] 中记录的实际上是 p[0,i-1] 这个字符串中所有前缀和所有后缀交集中最长字符串的长度,比如'fdkekfd' 这个字符串所有前缀和所有后缀交集中最长字符串是 'fd',其长度是2。

  2. 字符串的前缀和后缀不包括字符串本身。

  3. next[0] 初始化成-1仅仅是一个编程技巧,你可以初始化成任意值,只要你分辨出是失配的情况即可,这里初始成 -1 正好可以和 s[i] == p[j] 这种情况合并,所以初始化成 -1 会常用一点。

  4. 在 KMP 算法中原串索引 i 比较傲娇,它只前进不会回溯,这也是 KMP 速度快的一个主要原因。

  5. 当出现失配时,模式子串的前缀和后缀有重合,可以直接移动模式串的前缀到刚刚匹配的后缀部分,但要记住如果没有重合的前缀和后缀,失配时移动模式串的速度会更快,这里容易弄反。

五、总结

  • KMP 算法的关键是求解 next 数组,是一个被称为部分匹配表(Partial Match Table)的数组
  • KMP 算法相比暴力匹配时间复杂度提升到了O(N+M),但是并不是最优秀的字符串匹配算法
  • 想要更快或者选择更合适的算法可以了解下从模式串的尾部开始匹配的 BM算法,以及从模式串的头部开始匹配的 Sunday算法

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

想要看到更高的风景,除了让自己跳的更高以外,还可以选一个更高的平台站上去。找到一个2米高的平台并努力爬上去,远比你原地起跳2米要容易的多~

相关文章:

  • C++中一些可以在偷懒时直接使用的函数
  • protobuf中SerializeToString和SerializePartialToString的区别
  • linux文件权限简单备忘知识点
  • 使用AddressSanitizer搭配addr2line查找C/C++内存泄漏问题
  • C++对我来说简直就是星辰大海,为了避免翻船,我选择从小河沟出发
  • cpplint中filter参数的每个可选项的含义
  • 手把手搭建一个redis集群
  • 换个角度来看看C++中的左值、右值、左值引用、右值引用
  • C/C++中的数据类型转换()/static_cast/dynamic_cast/const_cast/reinterpret_cast
  • C++11中std::move和std::forward到底干了啥
  • 使用box2dweb做一个下落的小球,宝宝玩的不亦乐乎
  • C++中使用std::sort自定义排序规则时要注意的崩溃问题
  • 从一个小题中的应用来体会下std::tie的便利之处
  • Floyd-Warshall——仅用4行代码就能解决多源最短路径问题的算法
  • Dijkstra——通过不断松弛来解决单源最短路径问题的算法
  • ES6简单总结(搭配简单的讲解和小案例)
  • KMP算法及优化
  • Mac转Windows的拯救指南
  • QQ浏览器x5内核的兼容性问题
  • React 快速上手 - 07 前端路由 react-router
  • spring security oauth2 password授权模式
  • 开源地图数据可视化库——mapnik
  • 聊聊redis的数据结构的应用
  • 如何学习JavaEE,项目又该如何做?
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • 在Docker Swarm上部署Apache Storm:第1部分
  • ###STL(标准模板库)
  • #include
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (3)选择元素——(14)接触DOM元素(Accessing DOM elements)
  • (4)事件处理——(2)在页面加载的时候执行任务(Performing tasks on page load)...
  • (Git) gitignore基础使用
  • (八)c52学习之旅-中断实验
  • (二十五)admin-boot项目之集成消息队列Rabbitmq
  • (简单有案例)前端实现主题切换、动态换肤的两种简单方式
  • (排序详解之 堆排序)
  • (学习日记)2024.01.19
  • (一)Dubbo快速入门、介绍、使用
  • (一)UDP基本编程步骤
  • (中等) HDU 4370 0 or 1,建模+Dijkstra。
  • (转)GCC在C语言中内嵌汇编 asm __volatile__
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .net core 调用c dll_用C++生成一个简单的DLL文件VS2008
  • .NET Framework 3.5中序列化成JSON数据及JSON数据的反序列化,以及jQuery的调用JSON
  • .Net mvc总结
  • .Net Web窗口页属性
  • .net 反编译_.net反编译的相关问题
  • .NET 中使用 TaskCompletionSource 作为线程同步互斥或异步操作的事件
  • .net 桌面开发 运行一阵子就自动关闭_聊城旋转门家用价格大约是多少,全自动旋转门,期待合作...
  • .NET(C#) Internals: as a developer, .net framework in my eyes
  • .Net6使用WebSocket与前端进行通信
  • .NET教程 - 字符串 编码 正则表达式(String Encoding Regular Express)
  • .net开发引用程序集提示没有强名称的解决办法
  • .NET使用HttpClient以multipart/form-data形式post上传文件及其相关参数
  • []AT 指令 收发短信和GPRS上网 SIM508/548