字符串匹配算法之BF与KMP算法

2024-04-08 23:52
文章标签 算法 字符串 匹配 kmp bf

本文主要是介绍字符串匹配算法之BF与KMP算法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

BF算法(暴力匹配算法)

KMP算法

核心思想:

next数组

next数组的优化


BF算法(暴力匹配算法)

#include <assert.h>
int BF(const char* str, const char* sub)
{assert(str != NULL && sub != NULL);if (str == NULL || sub == NULL){return -1;}int i = 0;int j = 0;int strLen = strlen(str);int subLen = strlen(sub);while (i < strLen && j < subLen){if (str[i] == sub[j]){i++;j++;}else{i = i - j + 1;  //主串从上次开始匹配的下一个位置开始匹配j = 0; //子串每次从头开始匹配}}if (j >= subLen){return i - j; //返回子串在主串中首次出现的首位置的下标}return -1; //没有匹配返回-1
}int main()
{printf("%d\n", BF("ababcabcdabcde", "abcd")); //5printf("%d\n", BF("ababcabcdabcde", "abcde")); //9printf("%d\n", BF("ababcabcdabcde", "abcdef")); //-1return 0;
}

KMP算法

核心思想:

KMP算法是在BF算法基础上的优化,BF算法中匹配不成功时i和j都要回退,而KMP算法的核心就是匹配失败时,i不回退,而j回退到特定的位置(不一定是0号位置了),然后继续匹配!

问题:为啥i可以不回退, j也不一定非要回退到0号位置??

有了上面的分析,我们的目标就已经很明确了,就是要计算子串的每个位置匹配失败时,j应该回退到哪一个位置,于是就有了我们的next数组!

next数组

next[i] 记录的就是子串匹配到 i 位置时 匹配失败后 i 应该回退的下标

而next数组如何求解呢??? 比如next数组中 i 下标对应的值是几呢??? 那我们只需要看子串中 [0, i-1]这段区间前缀 和 后缀相等的字符串的最长长度, 假如是k, 那么next[i] = k

子串的第一个位置和第二个位置匹配失败时,该位置之前绝不可能用公共子串,所以next[0]和next[1]都应该是0, 但是为了方便后续代码处理,我们将next[0]置成-1

我们现在已经明白了next数组的做用和求法,但问题是上面的next数组是我们肉眼观察求得的,可是计算机并没有上帝视角,如何编程求得next数组呢???

假设已知next[i] = k, 我们能否求得next[i+1]等于多少呢??? 如果可以求出来,由于next[0]和next[1]都是已知的,所以我们只需要从第三个位置开始for循环即可求得next数组~

问题: 已知next[i] = k, 求next[i+1] = 多少??

1.假设p[i] == p[k], 那么next[i+1] = k+1   (p是patten, 指的是子串, 也叫模式串)

证明:

next[i] = k, 说明 p[0] 到 p[k-1] 与 p[x] 到 p[i-1] 这两段字符串是一样的,根据两段字符串长度一样可以求得x,  (k-1-0)+1 = (i-1-x)+1, 求得x=i-k, 所以 p[0]...[k-1] == p[i-k]...p[i-1]

而p[i] == p[k], 则 p[0]...[k] == p[i-k]...p[i], 即 next[i+1] = k+1

2.假设p[i] != p[k], 此时需要做的就是让k回退即可,只需要让k = next[k],一直回退到 p[i] == p[k], 此时next[i+1] = k+1了!!!

如果回退到了0位置,p[i] 仍然不等于 != p[k],  k再 = next[k], k就 == -1了, 此时如果再判断 p[i] == p[k] 就会越界! 所以如果k == -1了,表明next[i+1]也就是0,刚好是k+1, 这就是为啥把next[0]设置成-1的原因, 然后i++, k++继续填充next数组即可

到此为止,我们就把kmp算法的核心都讲解完了,重点就是kmp算法的核心思想与next数组的原理与求法

#include <assert.h>
#include <stdio.h>
void GetNext(int* next, const char* sub)
{int lensub = strlen(sub);next[0] = -1;next[1] = 0;int i = 2;//下一项int k = 0;//前一项的Kwhile (i < lensub)//next数组还没有遍历完{//注意,讲解原理时我们假设已知next[i],求next[i+1], 但写代码时next[i]是我们要求解的,因此已知next[i-1]if ((k == -1) || sub[k] == sub[i - 1]) {next[i] = k + 1;i++;k++;}else{k = next[k]; //k回退}}
}int KMP(const char* s, const char* sub, int pos) //pos表示从主串的pos位置开始匹配
{int i = pos;int j = 0;int lens = strlen(s);int lensub = strlen(sub);int* next = (int*)malloc(lensub * sizeof(int));//和子串一样长assert(next != NULL);GetNext(next, sub);while (i < lens && j < lensub){if ((j == -1) || (s[i] == sub[j])) {i++;j++;}else{//如果子串的第一个位置字符就和主串不匹配,那么j就会直接变成-1,然后进入if, 此时让j++来到0号位置,i++来到下一个位置继续匹配即可j = next[j]; }}free(next);if (j >= lensub){return i - j;}else{return -1;}
}int main()
{const char* str = "ababcabcdabcde";const char* sub = "abcd";printf("%d\n", KMP(str, sub, 0)); //5return 0;
}
next数组的优化

如果子串中,有大量的重复元素时,next数组就可以优化,因为假设6号下标匹配失败,回退到next[6]也就是5号下标, 此时字符仍然是'a', 依旧会匹配失败,还需要继续回退!!!

 所以我们可以将next数组优化成nextval数组,nextval数组可以根据next数组求得

1.回退到的位置和当前字符一样,就写回退那个位置的nextval值

2.回退到的位置和当前字符不一样,就写当前字符原来的next值

这篇关于字符串匹配算法之BF与KMP算法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/886697

相关文章

C#数据结构之字符串(string)详解

《C#数据结构之字符串(string)详解》:本文主要介绍C#数据结构之字符串(string),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录转义字符序列字符串的创建字符串的声明null字符串与空字符串重复单字符字符串的构造字符串的属性和常用方法属性常用方法总结摘

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

python中字符串拼接的几种方法及优缺点对比详解

《python中字符串拼接的几种方法及优缺点对比详解》在Python中,字符串拼接是常见的操作,Python提供了多种方法来拼接字符串,每种方法有其优缺点和适用场景,以下是几种常见的字符串拼接方法,需... 目录1. 使用 + 运算符示例:优缺点:2. 使用&nbsjsp;join() 方法示例:优缺点:3

java字符串数字补齐位数详解

《java字符串数字补齐位数详解》:本文主要介绍java字符串数字补齐位数,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java字符串数字补齐位数一、使用String.format()方法二、Apache Commons Lang库方法三、Java 11+的St

C++字符串提取和分割的多种方法

《C++字符串提取和分割的多种方法》在C++编程中,字符串处理是一个常见的任务,尤其是在需要从字符串中提取特定数据时,本文将详细探讨如何使用C++标准库中的工具来提取和分割字符串,并分析不同方法的适用... 目录1. 字符串提取的基本方法1.1 使用 std::istringstream 和 >> 操作符示

C语言字符函数和字符串函数示例详解

《C语言字符函数和字符串函数示例详解》本文详细介绍了C语言中字符分类函数、字符转换函数及字符串操作函数的使用方法,并通过示例代码展示了如何实现这些功能,通过这些内容,读者可以深入理解并掌握C语言中的字... 目录一、字符分类函数二、字符转换函数三、strlen的使用和模拟实现3.1strlen函数3.2st

Java反转字符串的五种方法总结

《Java反转字符串的五种方法总结》:本文主要介绍五种在Java中反转字符串的方法,包括使用StringBuilder的reverse()方法、字符数组、自定义StringBuilder方法、直接... 目录前言方法一:使用StringBuilder的reverse()方法方法二:使用字符数组方法三:使用自

Golang中拼接字符串的6种方式性能对比

《Golang中拼接字符串的6种方式性能对比》golang的string类型是不可修改的,对于拼接字符串来说,本质上还是创建一个新的对象将数据放进去,主要有6种拼接方式,下面小编就来为大家详细讲讲吧... 目录拼接方式介绍性能对比测试代码测试结果源码分析golang的string类型是不可修改的,对于拼接字