算法----BF算法KMP算法

2024-04-26 15:44
文章标签 算法 kmp bf

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

请想象一个情景:

当你脑海中突然浮现出一个词,你该怎么去找到这个词的有关内容?

打开我们浏览器的搜索框,输入你想的这个词,然后点击Enter。浏览器就会自动搜索与该词匹配的内容。

这个过程实际上可以简化成以下形式:

有一个文本串S,一个模式串P(也叫子串),现在要查找PS中的位置。

我们今天所讨论的两个算法就是有关该过程的算法。

事实上,对于检索,无非就是两个字符串的匹配过程,模式串是你想要匹配的串,主串是你搜索所在串。

针对模式串中的一个个字符与主串进行匹配,

匹配成功则继续往后匹配;

匹配失败则跳过该串段继续匹配,直到主串中出现与模式串完全相同的串段,此时则成功找到。

所以检索无非就是模式匹配的过程。

BF算法和KMP算法是较为著名的模式匹配算法,接下来作出详细介绍。

BF算法

BF算法(Brute-Force)也称为暴力算法,其核心原理是逐个比较文本串和模式串的字符,如果匹配失败,则通过向右移动模式串的位置,再次进行比较。

算法步骤

我们设主串和模式串中的字符位置分别为ij

  • 如果当前字符匹配成功(即T[i] == P[j]),则i++,j++,继续匹配下一个字符;
  • 如果当前字符匹配失配(即T[i]! = P[j]),则令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0

举例:

假设我们有一个文本串T为:“ABCDABCDABCE”,以及一个模式串P为:“ABCE”,我们要在文本串T中查找是否存在模式串P。

首先,我们将文本串T和模式串P在一条直线上对齐:

文本串T:  ABCDABCDABCE
模式串P:  ABCE

然后,我们从文本串T的第一个字符开始和模式串P的第一个字符比较:

第一次比较:'A’和’A’相等。

文本串T:  ABCDABCDABCE
模式串P:  A

第二次比较:'B’和’B’相等。

文本串T:  ABCDABCDABCE
模式串P:  AB

第三次比较:'C’和’C’相等。

文本串T:  ABCDABCDABCE
模式串P:  ABC

第四次比较:'D’和’E’不相等,出现失配。

文本串T:  ABCDABCDABCE
模式串P:  ABCE

在匹配失败后,我们将模式串P向右移动一位,重新从文本串T的当前位置和模式串P的第一个字符开始比较:

文本串T:  ABCDABCDABCE
模式串P:   A

第一次比较:'A’和’B’不相等。出现失配。

那么再将模式串P向右移动一位,重新从文本串T的当前位置和模式串P的第一个字符开始比较:

文本串T:  ABCDABCDABCE
模式串P:    A

第一次比较:'A’和’C’不相等。出现失配。

继续类似的比较过程,直到文本串T遍历完毕。

代码演示

根据上述过程我们可以写出BF算法的代码:

int ViolentMatch(char* s, char* p)
{int sLen = strlen(t);int pLen = strlen(p);int i = 0;int j = 0;while (i < sLen && j < pLen){if (t[i] == p[j]){//①如果当前字符匹配成功(即T[i] == P[j]),则i++,j++    i++;j++;}else{//②如果失配(即T[i]! = P[j]),令i = i - (j - 1),j = 0    i = i - j + 1;j = 0;}}//匹配成功,返回模式串p在文本串s中的位置,否则返回-1if (j == pLen)return i - j;elsereturn -1;
}

我们发现这种确实可以被称作暴力解法:无论后面的元素是否匹配,模式串P都会回溯到它的第一个字符开始重新比较,而例如在第二次重新匹配的过程中,实际上是必定失配的,从而又要继续回溯再重新比较,属实暴力且死板。

时间复杂度

BF算法的时间复杂度取决于文本串T的长度为n,模式串P的长度为m。在最坏情况下,BF算法需要在文本串T的每个位置上都尝试匹配模式串P,因此时间复杂度为O(n*m)

在实际情况下,BF算法的效率并不高,特别是当文本串T和模式串P的长度很大时。对于较长的文本串和模式串,BF算法的时间复杂度可能会导致性能问题。

那么有没有另外一种解法,可以避免不必要的i回溯,而只移动j即可呢?这样所需要消耗的时间就会大大减少。

答案就是KMP算法。

KMP算法

KMP算法的核心思想是利用模式串自身的特点来加速匹配过程,避免重复匹配。

算法步骤

我们设主串和模式串中的字符位置分别为ij

  • 如果当前字符匹配成功(即T[i] == P[j]),步骤与暴力匹配法相同,则i++,j++,继续匹配下一个字符;
  • 如果当前字符匹配失配(即T[i]! = P[j]),则根据最大长度表计算需要移动的位数。

这里由于匹配成功的情况与前面BF相同,所以我们只对匹配失败进行讨论。

公式

需要移动的位数=已匹配的字符数-失配字符的上一位字符对应的最大长度

这里我们理解一下具体的步骤,以及为什么是上述的公式。

为什么是算出最大长度的相同前缀和后缀

因为当最大长度的前缀和后缀相同的时候,移动已匹配的字符数-最大长度即可保证不会多移动或者少移动。如图:

在这里插入图片描述

蓝色部分的字符个数即是我们需要移动的字符个数,而黄色部分的字符个数即是最大相同长度,而黄色+蓝色部分即是

已匹配的字符个数

所以有:已匹配的字符个数=最大相同长度+需要移动的字符个数,从而得出:

需要移动的字符个数=已匹配的字符个数-最大相同长度

注意:我们要找的是相同前后缀的最长长度,注意一定是要最长的。并且不能是字符串本身。(如果是本身便没有意义了)

最大长度表是什么

上述过程我们所提到的最大长度表指的是模式串中最大长度的相同前缀和后缀,在公式中的最大相同长度就是对照该表得出的。

在这里插入图片描述

如何求最大长度表呢?在具体的代码中,我们需要使用一个函数来求出最大长度表,并且在具体的算法实现中,让其对应求出所需要移动的字符个数。这个时候我们就需要用到next数组


next数组

首先我们最需要知道的是:next数组的作用就是求出最大长度表。

最大长度表并不是使用next数组求出来的对照表,而是指的是next数组本身。next数组存储的是最大长度表,用于帮助算法快速定位匹配位置;

而由于数组的初始下标为0的限制,在书写上两者会有以下的差异:

next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。(因为数组的初始下标为0)

也就是 j-next[j]

在这里插入图片描述

问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。

next的具体求法

针对主串和模式串来进行字符匹配,而p[k]是主串正在被匹配的子字符串的元素,p[j]是正在进行匹配的模式串的元素。

  • 如果模式串的第i个字符和第j个字符相等(即p[k ] == p[j]),则next[j + 1] = next[j] + 1 = k + 1,同时i和j都向后移动一位。

也就是说此时:t(1)t(2)…t(k)=t(j-k+1)t(j-k+2)…t(j)

那么next[j + 1] = next[j] + 1 = k + 1。代表此字符前的模式串中,有长度k+1 的相同前缀后缀。

  • 如果模式串的第i个字符和第j个字符不相等(即p[k ] ≠ p[j]),则next[ j + 1 ] = next[k] + 1,否则继续递归前缀索引k = next[k],而后重复此过程。 也就是说此时正在匹配的字符失配,而算出的next[ j + 1 ] = next[k] + 1就是最大长度。

注意:主串是永远不动的,动的一直都是子串也就是模式串,也就是说i永不递减,只有j会递减。

next数组的代码使用递推求解,好处在于会不断利用已掌握的信息来避免重复的运算。

针对上述的字符不相同的情况,我们对此进行更详细的解答。

首先我们应该以一种类动态规划的思想去思考这个问题:动态规划中,我们会利用已知的子问题来解决更大规模的问题,避免重复的计算;而在KMP中,next数组存储了模式串中的最大长度,这个最大长度会帮助我们跳过一些不必要的比较,这个在后面会提到。

接下来,我们进行模拟。

当匹配不成功时:

查找是否存在更短的共同前后缀,如果找到了,则重新从此处再做一次KMP算法

例如,有以下字符串:

A  B  A  C  A  B  A  B
0  1  2  3  4  5  6  7

扫描到 6 号位的 A 时,最长公共前后缀是 ABA;而扫描到 7 号位的 B 时,ABAC 和 ABAB 不匹配了,即原来的最长公共前后缀失配。

这时候我们要做的事情就是,找上一次匹配中次长的公共前后缀,看与 7 号位的 B 拼接起来是否能匹配。因为我们的目的就是为了继续匹配,但是由于最长的已经用过了,所以就找次长的。

这时候,注意到上一次扫描中 0 ~ 2 位的 ABA 是和 4 ~ 6 位的 ABA 完全相同的,所以考察上一次匹配中次长的公共前后缀,只能在考察上一次匹配中的最长公共前后缀中寻找,也就是说,只能考察 ABA 中更短的 BA、A 是否是次长的,而这直接在前面一个 ABA 中考察就行(因为前后两个ABA是一样的)。(我们的目的就是要找最长公共前后缀)

所以我们把 ABA C ABA 的中间部分(C)和后缀(ABA)直接抛弃,等效于一个串 ABA(也就是前缀)与 B 拼接成 ABAB。这样再来计算第 7 位的 B 的 next 值,等价于计算 ABAB 第 3 位的 B 的 next 值。

代码演示
void GetNext(char* p,int next[])
{int pLen = strlen(p);next[0] = -1;int k = -1;int j = 0;while (j < pLen - 1){//p[k]表示前缀,p[j]表示后缀if (k == -1 || p[j] == p[k]) {++k;++j;next[j] = k;}else {k = next[k];}}
}

这样我们就能计算出我们需要的next值,从而得到最大长度表,然后再将该表代入到需要移动字符数的计算公式中,继续匹配下去。

当然

接下来根据KMP算法的主要核心以及next数组举例介绍流程:

假设我们有文本串 T = "ABCABABCABD" 和模式串 P = "ABCABD"。我们需要在文本串 T 中找到模式串 P 的出现位置。

步骤1: 构建next数组

首先,我们需要为模式串 P 构建一个next数组。这个数组用于在不匹配的情况下,告诉我们应该从模式串的哪个位置重新开始匹配。

对于模式串 P = "ABCABD",我们如下构建next数组:

  1. 初始化:next[0] = -1 (当模式串的第一个字符不匹配时,我们没有更早的位置可以回退到)
  2. P[1] 开始,比较前后缀:
    • j = 0next[1] = 0 (没有相同的前后缀)
    • j = 1,比较 P[0]P[1],不相同,next[2] = 0
    • j = 2,比较 P[0]P[2],不相同,next[3] = 0
    • j = 3,比较 P[1]P[3],相同,next[4] = 1
    • j = 4,比较 P[2]P[4],相同,next[5] = 2
    • j = 5,比较 P[3]P[5],不相同,next[6] = 0

最终的next数组为 [-1, 0, 0, 0, 1, 2, 0].

步骤2: 使用next数组进行匹配

现在我们使用next数组来匹配文本串 T 和模式串 P

T = ABCABABCABD
P = ABCABD
  1. 初始位置:i = 0, j = 0
    • T[0] = P[0] (A = A),匹配,i = 1, j = 1
    • T[1] = P[1] (B = B),匹配,i = 2, j = 2
    • T[2] = P[2] (C = C),匹配,i = 3, j = 3
    • T[3] = P[3] (A = A),匹配,i = 4, j = 4
    • T[4] = P[4] (B = B),匹配,i = 5, j = 5
    • T[5] = P[5] (C ≠ D),不匹配,根据 next[5] = 2j = 2
  2. 继续匹配:
    • T[5] = P[2] (C = C),匹配,i = 6, j = 3
    • T[6] = P[3] (A = A),匹配,i = 7, j = 4
    • T[7] = P[4] (B = B),匹配,i = 8, j = 5
    • T[8] = P[5] (D = D),匹配,i = 9, j = 6(模式串匹配完成)

匹配成功,模式串 P 在文本串 T 中的起始位置为 3 (从0开始计数)。

通过这个例子,我们可以看到KMP算法如何有效地使用next数组来避免不必要的比较,从而加快字符串匹配的过程。

nextval(next的扩展优化)

请看以下举例

T = AAABAAAAB
P = AAAAB

当i=4,j=4时,我们发现此时的字符不匹配,那么根据next[j]的指示我们还需进行:i=4不变,分别j=3、j=2、j=1的三次比较。

但是实际上模式串中的13以及第4个字符全都相等,因此将13这3个字符再去和主串的第4个字符比较实际上是和j=4时的情况是一样的,是会失配的。

在这里插入图片描述

那么我们应该如何避免并跳过这种重复的比较,直接进行i=5、j=1的比较呢?

这里则可以对next进行优化得到nextval

//优化过后的next 数组求法
void GetNextval(char* p, int next[])
{int pLen = strlen(p);next[0] = -1;int k = -1;int j = 0;while (j < pLen - 1){//p[k]表示前缀,p[j]表示后缀  if (k == -1 || p[j] == p[k]){++j;++k;//较之前next数组求法,改动在下面4行if (p[j] != p[k])next[j] = k;   //之前只有这一行else//因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]next[j] = next[k];}else{k = next[k];}}
}

这个代码就完美地规避了上述讲到的问题。nextval可以实现跳跃到一个更远的位置进行匹配,从而减少不必要的比较次数。


接下来的代码是整个KMP算法的代码

代码演示

int KmpSearch(char* s, char* p)
{int i = 0;int j = 0;int sLen = strlen(s);int pLen = strlen(p);while (i < sLen && j < pLen){//①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++    if (j == -1 || s[i] == p[j]){i++;j++;}else{//②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]    //next[j]即为j所对应的next值      j = next[j];}}if (j == pLen)return i - j;elsereturn -1;
}

时间复杂度

KMP算法的时间复杂度分析如下:

  1. 构建next数组的时间复杂度:构建next数组的时间复杂度是O(m),其中m是模式串的长度。

  2. 匹配过程的时间复杂度:在KMP算法中,匹配过程的时间复杂度主要取决于文本串的长度n。在匹配过程中,每次失配时,根据next数组回退到一个更早的位置重新开始匹配,而不会重复比较已经匹配过的字符。因此,匹配过程的时间复杂度是O(n)。

综合以上两点,KMP算法的总体时间复杂度为O(m + n),其中m是模式串的长度,n是文本串的长度。相比于朴素的字符串匹配算法的**O(m*n)**时间复杂度,KMP算法通过利用next数组的特性,在匹配过程中避免了不必要的比较,从而实现了更高效的字符串匹配。

KMP的使用场景

总的来说,KMP算法适用于需要快速匹配模式串的场景,特别是在文本串较长、模式串较短的情况下。为什么呢?我们参照KMP的时间复杂度,O(m + n)m是模式串的长度,n是文本串的长度:

  • 长文本串(针对n):KMP算法适用于处理长文本串,因为它能够在匹配过程中避免不必要的比较,从而减少比较次数,提高匹配效率。
  • 短模式串(针对m):KMP算法在处理短模式串时效果显著,因为其时间复杂度不会随着模式串长度的增加而大幅增加。
  • 需要多次匹配:如果需要在同一文本串中多次匹配同一个模式串,KMP算法可以提高效率,因为构建好的next数组可以被重复利用。

常用用途

  • 字符串搜索:KMP算法常用于在文本串中搜索特定的模式串,例如搜索关键字、词语等。

  • 文本处理:在文本处理领域,KMP算法可以用于文本匹配、替换等操作。

  • 编译器设计:在编译器的词法分析阶段,KMP算法用于匹配词法单元,如关键字、标识符等。

  • 网络协议:在网络协议中,KMP算法可以用于匹配特定的模式,例如在URL匹配、数据包匹配等场景中。
    *是文本串的长度:

  • 长文本串(针对n):KMP算法适用于处理长文本串,因为它能够在匹配过程中避免不必要的比较,从而减少比较次数,提高匹配效率。

  • 短模式串(针对m):KMP算法在处理短模式串时效果显著,因为其时间复杂度不会随着模式串长度的增加而大幅增加。

  • 需要多次匹配:如果需要在同一文本串中多次匹配同一个模式串,KMP算法可以提高效率,因为构建好的next数组可以被重复利用。

常用用途

  • 字符串搜索:KMP算法常用于在文本串中搜索特定的模式串,例如搜索关键字、词语等。
  • 文本处理:在文本处理领域,KMP算法可以用于文本匹配、替换等操作。
  • 编译器设计:在编译器的词法分析阶段,KMP算法用于匹配词法单元,如关键字、标识符等。
  • 网络协议:在网络协议中,KMP算法可以用于匹配特定的模式,例如在URL匹配、数据包匹配等场景中。

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



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

相关文章

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

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

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

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

如何通过Golang的container/list实现LRU缓存算法

《如何通过Golang的container/list实现LRU缓存算法》文章介绍了Go语言中container/list包实现的双向链表,并探讨了如何使用链表实现LRU缓存,LRU缓存通过维护一个双向... 目录力扣:146. LRU 缓存主要结构 List 和 Element常用方法1. 初始化链表2.

golang字符串匹配算法解读

《golang字符串匹配算法解读》文章介绍了字符串匹配算法的原理,特别是Knuth-Morris-Pratt(KMP)算法,该算法通过构建模式串的前缀表来减少匹配时的不必要的字符比较,从而提高效率,在... 目录简介KMP实现代码总结简介字符串匹配算法主要用于在一个较长的文本串中查找一个较短的字符串(称为

通俗易懂的Java常见限流算法具体实现

《通俗易懂的Java常见限流算法具体实现》:本文主要介绍Java常见限流算法具体实现的相关资料,包括漏桶算法、令牌桶算法、Nginx限流和Redis+Lua限流的实现原理和具体步骤,并比较了它们的... 目录一、漏桶算法1.漏桶算法的思想和原理2.具体实现二、令牌桶算法1.令牌桶算法流程:2.具体实现2.1

Python中的随机森林算法与实战

《Python中的随机森林算法与实战》本文详细介绍了随机森林算法,包括其原理、实现步骤、分类和回归案例,并讨论了其优点和缺点,通过面向对象编程实现了一个简单的随机森林模型,并应用于鸢尾花分类和波士顿房... 目录1、随机森林算法概述2、随机森林的原理3、实现步骤4、分类案例:使用随机森林预测鸢尾花品种4.1

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖