经典匹配算法: KMP、Sunday与ShiftAnd

news2025/1/17 13:57:48

本次介绍的三种算法的时间复杂度: 

基础概念:

图3
图1

单模匹配问题:单个模式串,比如我们要在一个长串母串S)中查找一个短串模式串T)是否出现过。

暴力匹配算法:

算法思想:用模式串去对齐母串的每一位,普通人能想到。

暴力匹配算法的作用是:让我们清楚地知道,这个世界上存在一种,虽然非常笨,但是能够正确地处理单模匹配问题的算法。所谓正确,其实意味着我们在匹配过程中,能够不重不会重复地和母串某一位进行匹配对齐不漏不会漏掉任何一次有可能找到模式串的匹配机会地去处理每一次匹配操作。为什么暴力匹配算法重要?因为我们应当意识到,不管我们对算法如何进行各种优化,我们优化以后的算法都要向暴力匹配算法一样,最起码做到两点:不重、不漏。即不管你怎么优化,在算法中都不能放弃任何一次有可能找到模式串的机会。

代码实现:

#include<iostream>
using namespace std;

// 暴力匹配算法
int brute_force(const char *text, const char *pattern) { // 返回母串中模式串出现的起始位置
    for (int i = 0; text[i]; ++i) {
        int flag = 1;
        for (int j = 0; pattern[j]; ++j) {
            if (text[i + j] == pattern[j]) continue;
            flag = 0;
            break;
        }
        if (flag == 1) return i; // 找到模式串,直接返回
    }
    return -1; // 遍历完了每一个位置,都没有找到。
}

#define TEST(func, s1, s2) { \
    printf("%s(\"%s\", \"%s\") = %d\n", #func, s1, s2, func(s1, s2)); \
}

int main() {
    char s1[100], s2[100];
    while (cin >> s1 >> s2) {
        TEST(brute_force, s1, s2);
    }
    return 0;
}

KMP算法:

在暴力匹配算法的基础上,我们要进行优化,首先得观察出来,暴力匹配算法究竟哪里比较浪费时间:

可以看到暴力匹配的时候,每次匹配失败都会往下一位进行对齐,但其实没有必要,下一次真正要对齐的是:模式串中,截止到我当前匹配成功的位置,最长的,前缀和后缀匹配的位置。

注意!这一点很关键:kmp将问题由从母串找信息,转变为到模式串里找信息。

图2

下一次就从这个匹配成功的后缀的起始位置开始就行了,跳过了中间那些注定会失败的部分: 

假设我们用 pre\_i 表示模式串中 \textup{i} 位的前缀,last\_j 表示模式串中 \textup{j} 位的后缀,那么以上过程可以看作对于图1中模式串中绿色部分,有如下的依次对比过程:

  1. 是否从第2位对齐,pre_4=last_4
  2. 是否从第3位对齐,pre_3=last_3
  3. 是否从第4位对齐,pre_2=last_2

最终发现只有从第4位开始对齐的时候,pre_2=last_2等式成立。于是下一次对齐的地方就是第四位,而且,前两位默认匹配成功,没必要再匹配了。

KMP算法的一般过程如下图所示,黄色是第一次匹配失败的位置,蓝色是最长前缀后缀匹配:

往下就只需要继续比较黄色区域部分,如果匹配不成功,就开始新一轮的KMP加速:Ta就变成新一轮的“绿色部分”,重复上面的过程,形成一个递归:

所以,所谓的优化方向其实就是,跳过一些,绝对不会产生答案的匹配位置。KMP做的就是这样一个加速。

代码实现:

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

// 提取kmp关键信息
void GetNext(const char *pattern, int *next) {
    next[0] = -1; // 如果文本串和模式串第一位就匹配失败,这个时候我应该向前跳到-1位。
    for (int i = 1, j = -1; pattern[i]; ++i) { // i指向的是第一个起冲突的位置,j指向的就是i的前一个,也就是模式串和母串匹配成功的位置
        while (j != -1 && pattern[j + 1] - pattern[i]) j = next[j]; // 黄色部分匹配失败,j指向下一个绿色部分中可能匹配成功的位置
        if (pattern[j + 1] == pattern[i]) j += 1; // 如果匹配成功, j就向后移动一位,绿色部分扩大一位
        next[i] = j;
    }
    return ;
}

int kmp(const char *text, const char *pattern) {
    int n = strlen(pattern);
    int *next = (int *)malloc(sizeof(int) * n); // 当前位置匹配不成功的时候,模式串要向前跳到第几位
    GetNext(pattern, next); // 根据模式串的信息,预处理出KMP加速信息
    for (int i = 0, j = -1; text[i]; i++) { // i指向当前匹配的每一位,j指向当前匹配位置之前匹配成功的那一位。
        while (j != -1 && text[i] - pattern[j + 1]) j = next[j];
        if (text[i] == pattern[j + 1]) j += 1;
        if (pattern[j + 1] == 0) return i - j; // 匹配成功了模式串的最后一位,返回母串中的起始位置
    }
    return -1; // 匹配失败
}

#define TEST(func, s1, s2) { \
    printf("%s(\"%s\", \"%s\") = %d\n", #func, s1, s2, func(s1, s2)); \
}

int main() {
    char s1[100], s2[100];
    while (cin >> s1 >> s2) {
        TEST(kmp, s1, s2);
    }
    return 0;
}

算法思想升华:上面的过程可以抽象化成这样一个过程:给一个字符,j 变一下,状态变一下,这本质上就是状态机是在做状态之间的转换。

因为kmp算法每一次只需要拿到文本串的一个字符,那么kmp算法就可以处理基于流数据的单模匹配问题。因为kmp一开始没有必要存储文本串的全量信息。

想象一种问题场景:两台计算机之间的信息通讯,A机器每次会给B机器传输一个字符,假设B机器就是用来过滤敏感词的。但是A机器发给B机器的文章到底有多长,不知道,究竟什么时候结束也不知道。类似这种场景就是流数据。我们需要在通讯的过程中过滤出来敏感词,敏感词就是模式串。

Sunday算法:

黄金对齐点位:

当模式串失配的情况发生了,模式串整体向后平移一位,此时最后一位d对应到e,可知这个e一定会出现在模式串中,于是从后向前查找,找到e最后出现的位置。将模式串在e这个位置和母串对齐,对齐之后,再重头开始比较:

此时发现第一位又失配了,于是模式串再整体向后移动一位,最后一位对应到a,可知a一定出现在模式串当中,于是从后向前找到最后一个a出现的位置,并与母串对齐,再重头进行匹配。

此时,发现能匹配完全,算法结束。

sunday算法要求我们预处理的信息:每一种字符在模式串中最后出现的位置。

如果文本串中,出现了模式串中根本没出现过的字符,那么模式串应该向后移动整个模式串的长度。

sunday算法适合处理在一篇文章中,查找一个单词。如果每次都向后这样子跳啊,sunday算法的时间复杂度最好能到达O(n/m),m是单词的长度,n是文章的长度。

所以sunday算法在实际场景中是远优于kmp,远优于暴力匹配的。

代码实现:

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

#define TEST(func, s1, s2) { \
    printf("%s(\"%s\", \"%s\") = %d\n", #func, s1, s2, func(s1, s2)); \
}

int sunday(const char *text, const char *pattern) {
    #define BASE 256
    int n = strlen(text), m, last_pos[BASE];
    for (int i = 0; i < 256; i++) last_pos[i] = -1;
    for (m = 0; pattern[m]; ++m) last_pos[pattern[m]] = m; // 当前字符出现在第n位
    for (int i = 0; i + m <= n; i += (m - last_pos[text[i + m]])) {
        int flag = 1;
        for (int j = 0; pattern[j]; ++j) {
            if (text[i + j] == pattern[j]) continue;
            flag = 0;
            break;
        }
        if (flag) return i; 
    }
    return -1;
}

int main() {
    char s1[100], s2[100];
    while (cin >> s1 >> s2) {
        TEST(sunday, s1, s2);
    }
    return 0;
}

shift-and算法:

首先,将模式串处理成类似下边的二进制数据:

怎么解释这段信息呢?以第一个a字符为例:a字符出现在0和3两个位置。那么d[a]转换成二进制以后,左边是低位,右边是高位,就等于十进制数 9。

同理可以算得d[c] = 4、d[d] = 32、d[e] = 18

第二步:设定一个P值,表示以文本串当前位置为最后一个字符,能够匹配成功模式串的前几位。

P值的转移:当新输入一个字符s[i]的时候,加入P原来表示能够匹配第1位和第4位,那么P<<1就表示,能够匹配第2位和第5位,当然新进来这个字符也有可能匹配成功前1位,所以要或上1。

当然最后是否能够匹配成功还是要&上d[s[i]]

代码实现:

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

#define TEST(func, s1, s2) { \
    printf("%s(\"%s\", \"%s\") = %d\n", #func, s1, s2, func(s1, s2)); \
}

int shift_and(const char *text, const char *pattern) {
    int code[256] = {0};
    int n = 0;
    for (n = 0; pattern[n]; ++n) code[pattern[n]] |= (1 << n);
    int p = 0;
    for (int i = 0; text[i]; i++) {
        p = (p << 1 | 1) & code[text[i]]; // p状态转移
        if (p & (1 << (n - 1))) return i - n + 1; 
    }
    return -1;
}

int main() {
    char s1[100], s2[100];
    while (cin >> s1 >> s2) {
        TEST(shift_and, s1, s2);
    }
    return 0;
}

时间复杂度为O(n)

shift-and算法适用于下面这个问题场景:

每个位置允许出现多个字符,可以由表中的信息体现出来。

并且shift-and算法也能诠释自动机的思想:每进来一个字符,P的状态就转变一下。所以shift-and算法也能处理流数据单模匹配问题,并且实际中比kmp更加高效。

int GetNextP(char ch, int *code, int p) {
    return (p << 1 | 1) & code[ch]; // p状态转移
}
int shift_and(const char *text, const char *pattern) {
    int code[256] = {0};
    int n = 0;
    for (n = 0; pattern[n]; ++n) code[pattern[n]] |= (1 << n);
    int p = 0;
    for (int i = 0; text[i]; i++) {
        p = GetNextP(text[i], code, p);
        if (p & (1 << (n - 1))) return i - n + 1; 
    }
    return -1;
}

自动机的思想对于算法思维的培养是至关重要的,是一大类的算法思维,因为自动机的本质,就是图灵机,图灵机的本质,就是当代计算机的运行原理。故自动机囊括了所有程序运行的本质原理,这意味着自动机的思想对于我们写程序来说,它的意义远高于我们对于排序算法的学习,远高于对于搜索算法的学习,远高于我们对于任何一种其他算法的学习,这就是自动机思想对于专业的编程人员、对于编码思维锻炼的重要意义所在。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1016122.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IDEA下使用Spring MVC

<?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 http://ma…

56资源网系统源码搭建知识付费-含源码

内置了上万条数据资源 大致功能&#xff1a; 支持免费与付费&#xff08;增加了插件付费插件&#xff09;支持侧边栏支持添加各类型广告&#xff08;你所能用到的基本都有&#xff09;.支持网盘下载模块支持所有页面自定义支持文章页三方跳转支持添加页面支持自定义采集&#…

nginx配置指南

nginx.conf配置 找到Nginx的安装目录下的nginx.conf文件&#xff0c;该文件负责Nginx的基础功能配置。 配置文件概述 Nginx的主配置文件(conf/nginx.conf)按以下结构组织&#xff1a; 配置块功能描述全局块与Nginx运行相关的全局设置events块与网络连接有关的设置http块代理…

Python Opencv实践 - 视频文件写入(格式和分辨率修改)

参考资料&#xff1a; python opencv写视频——cv2.VideoWriter()_cv2.cv.videowriter(_翟羽嚄的博客-CSDN博客 import cv2 as cv import numpy as np#1. 打开原始视频 video_in cv.VideoCapture("../SampleVideos/Unity2D.mp4") video_width int(video_in.get(c…

优化器的使用

代码示例&#xff1a; import torch import torchvision from torch import nn from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter# 加载数据集转化为Tensor…

腾讯mini项目-【指标监控服务重构】2023-08-22

今日已办 50字项目价值和重难点 项目价值 通过将指标监控组件接入项目&#xff0c;对比包括其配套工具在功能、性能上的差异、优劣&#xff0c;给出监控服务瘦身的建议 top3难点 减少监控服务资源成本&#xff0c;考虑性能优化如何证明我们在监控服务差异、优劣方面的断言…

ubuntu 22.04运行opencv4的c++程序遇到的问题

摘要&#xff1a;本文介绍一下在ubuntu系统中&#xff0c;运行一个最简单的opencv4程序都出问题的解决方法&#xff0c;并对其基本原理作简单阐述。解决问题的方法有很多&#xff0c;本文只提供其中一种。 opencv版本是4.2.0&#xff0c;ubuntu版本是20.04 查询opencv版本的指…

Aztec.nr:Aztec的隐私智能合约框架——用Noir扩展智能合约功能

1. 引言 前序博客有&#xff1a; Aztec的隐私抽象&#xff1a;在尊重EVM合约开发习惯的情况下实现智能合约隐私 Aztec.nr&#xff0c;为&#xff1a; 面向Aztec应用的&#xff0c;新的&#xff0c;强大的智能合约框架使得开发者可直观管理私有状态基于Noir构建&#xff0c;…

写一篇nginx配置指南

nginx.conf配置 找到Nginx的安装目录下的nginx.conf文件&#xff0c;该文件负责Nginx的基础功能配置。 配置文件概述 Nginx的主配置文件(conf/nginx.conf)按以下结构组织&#xff1a; 配置块功能描述全局块与Nginx运行相关的全局设置events块与网络连接有关的设置http块代理…

AIGC专栏6——通过阿里云与AutoDL快速拉起Stable Diffusion和EasyPhoto

AIGC专栏6——通过阿里云与AutoDL快速拉起Stable Diffusion和EasyPhoto 学习前言Aliyun DSW快速拉起&#xff08;新用户有三个月免费时间&#xff09;1、拉起DSW2、运行Notebook3、一些小bug AutoDL快速拉起1、拉起AutoDL2、运行Notebook 学习前言 快速拉起AIGC服务 对 用户体…

CAN Driver

CAN Driver 前言&#xff1a;CAN驱动针对的是微控制器内部的CAN控制器&#xff0c;它可以实现以下功能&#xff1a; 对CAN控制器进行初始化&#xff1b; 发送和接收报文&#xff1b; 对报文的数据和功能进行通知&#xff08;对接收报文的指示、对发送报文的确认&#xff09…

基于SSM+Vue的人力资源管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

交叉编译工具链-Ubuntu 安装说明

交叉编译工具链-Ubuntu 安装说明 【实验目的】 了解交叉编译工具链的安装方法与使用方法 【实验环境】 1、 ubuntu 14.04 发行版 【注意事项】 1、实验步骤中以“$”开头的命令表示在 ubuntu 环境下执行 【实验步骤】 1、安装交叉编译工具链 在 ubuntu 下打开一个终端并进入到家…

yamot:一款功能强大的基于Web的服务器安全监控工具

关于yamot yamot是一款功能强大的基于Web的服务器安全监控工具&#xff0c;专为只有少量服务器的小型环境构建。yamot只会占用非常少的资源&#xff0c;并且几乎可以在任何设备上运行。该工具适用于Linux或BSD&#xff0c;当前版本暂不支持Windows平台。 比如说&#xff0c;广…

elasticsearch11-实战搜索和分页

个人名片&#xff1a; 博主&#xff1a;酒徒ᝰ. 个人简介&#xff1a;沉醉在酒中&#xff0c;借着一股酒劲&#xff0c;去拼搏一个未来。 本篇励志&#xff1a;三人行&#xff0c;必有我师焉。 本项目基于B站黑马程序员Java《SpringCloud微服务技术栈》&#xff0c;SpringCloud…

Prometheus+Grafana可视化监控【ElasticSearch状态】

文章目录 一、安装Docker二、安装ElasticSearch(Docker容器方式)三、安装Prometheus四、安装Grafana五、Pronetheus和Grafana相关联六、安装elasticsearch_exporter七、Grafana添加ElasticSearch监控模板 一、安装Docker 注意&#xff1a;我这里使用之前写好脚本进行安装Docke…

Linux学习之平均负载的概念和查看方法

先理解一下平均负载的含义&#xff1a; 平均负载是指单位时间内&#xff0c;系统处于可运行状态和不可中断状态的进程数&#xff0c;也可以看成平均活跃进程数。 可运行状态的进程&#xff1a; 正在使用CPU或者正在等待CPU处理的进程&#xff0c;ps 命令看到的&#xff0c;处于…

AO天鹰优化算法|含源码(元启发式算法)

-----------------------往期目录------------------ 1、灰狼优化算法 文章目录 天鹰优化器一、第一种搜索方法二、第二种搜素方法三、第三种搜素方法四、第四种搜索方法 代码实现 天鹰优化器 Aquila Optimizer&#xff08;AO&#xff09;&#xff0c;灵感来自Aquila在捕捉猎物…

Mysql001:(库和表)操作SQL语句

目录&#xff1a; 》SQL通用规则说明 SQL分类&#xff1a; 》DDL&#xff08;数据定义&#xff1a;用于操作数据库、表、字段&#xff09; 》DML&#xff08;数据编辑&#xff1a;用于对表中的数据进行增删改&#xff09; 》DQL&#xff08;数据查询&#xff1a;用于对表中的数…

开源日报 0825 | 简化开发过程,提升Swift应用性能的扩展工具库

OpenZeppelin/openzeppelin-contracts Stars: 22.8k License: MIT OpenZeppelin Contracts 是一个用于安全智能合约开发的库。它建立在社区验证过的代码基础上&#xff0c;具有以下主要功能&#xff1a; 实现了 ERC20 和 ERC721 等标准。灵活的基于角色的权限控制方案。可重…