目录

  • 前言
  • 1. BF算法
    • 1.1 画图分析
    • 1.3 BF 算法的时间复杂度
  • 2. KMP 算法
    • 2.1 KMP 算法和 BF 算法 的区别
      • 2.1.1 为什么主串不回退?
    • 2. 2 next 数组
    • 2.2.3 next数组总结
  • 3.代码
    • 3.1 伪代码分析
    • 3.2 求 next 数组
      • 3.2.1 next数组的优化
  • 4. 完整代码

前言

本文章是基于比特大博哥在B站讲解 KMP 算法的一篇总结,
视频链接:
KMP讲解

字符串匹配问题:
字符串匹配问题,就是在一个长字符串S中,选择一个子字符串D的位置
比如字符串 char S[] = "ecfabctgabcdlghe";
字符串 char D[] = "abcd";
如何才能高效地在S中寻找到包含字符串D起始的位置?
对此,产生了各种各样的算法。
下面,主要介绍BF和KMP算法。

1. BF算法

BF算法,即暴力(Brute Force)算法。
一种比较低效的算法,但是思路很简单。
大致就是先从字符串S的第一位与字符串D的第一位做比较,
如果两个第一位都相等,则继续比下去,
比较字符串S的第二位和字符串D的第二位
直到字符串D走完为止。
字符串D如果能顺利走完,则说明字符串S的第一位就是我要找的起始位置。如果比较的过程中不相等,则字符串S要从第二位开始与字符串D的第一位继续比较。一直进行下去,直到找到。
如果字符串S走完了,说明字符串S中不包含字符串D
如果字符串D走完了,说明字符串S中包含了字符串D,返回S刚开始比较的位置。
文字描述很难理解,我们画个图就明白了。

1.1 画图分析

首先,拿字符串S的第0位开始与字符串D的第0位比较,
因为两者一开始就不一样,所以字符串S要从下一位开始与字符串D比较。
此时 i++

字符串S中 i 指向的字符仍然和J指向的字符不一样
i继续往后走
i++

仍然不一样
i继续往后走
i++

此时,i 和 j 指向的字符一样了,
那么字符串D 的 j 可以同时和 i 继续往后比对了。

i++ ,j++

i++,j++

注意了!
重点来了

此时 i 和 j 所指向的位置不相等了。
所以 j 要跳回到0位置
那么 i 要跳回到两者开始比较前的后一个位置。
代码怎么写呢?
j = 0 ;这个简单
对于 i 来说,两者比较前的位置是哪?
是不是 i - j ? 6 - 3 = 3
因为一共走了3步嘛,也就是 j 走的步数。
那么 i 要跳回到两者开始比较前的后一个位置代码就是i = i - j + 1;
然后我们继续比对。

两者跳回到相应位置,继续比对
这里我们 i 一直往后走,都和 j 不一样,我们画图上省略几步。
.
.
.

此时 i 和 j 相等了,可以继续比较了。
i++;
j++;



重点又来了!!!
此时 j 再往后走一步,就说明字符串 D 走完了,说明字符串 S 中包含 D

此时 J 走完了,那么字符串 S 包含 字符串 D 的起始位置也就是i - J = 12 - 4;

到此,字符串 S 包含 字符串 D 情况分析完毕。
.
.
如果字符串 S 不包含字符串 D 会出现什么情况呢?
思考一下,再往下看
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
你的想法不错!
就是 字符串 S 比字符串 D 先走完!这种情况不包含
直接上代码!

#include <iostream>
#include <cstring>
#include <cassert>
//BF算法
int BF(const char* source, const char* dest)
{assert(source && dest); //断言一下,避免空指针int len_source = strlen(source); //计算 字符串 S 的长度int len_dest = strlen(dest); // 计算 字符串 D 的长度int i = 0, j = 0;  //图中的 i 和 j while (i < len_source && j < len_dest) // 当两者其中一个走完时,结束循环{if (source[i] == dest[j]) // 如果比较的位置字符相等,就继续比较{i++;j++;}else  // 否则 重新开始 比较{i = i - j + 1; //如果不相等就重新找字符串 S 下一个位置匹配j = 0;}}// 如果 j 先走完 字符串 D 的长度,就说明字符串 S 中包含 字符串 D if (j >= len_dest) {return i - j; //返回起始位置}return -1; // 否则返回 -1
}
int main()
{printf("%d\n", BF("abcdefg", "fg"));//5printf("%d\n", BF("abcdefg", "fga"));//-1printf("%d\n", BF("abcdefgabcd", "abcd"));//0return 0;
}

1.3 BF 算法的时间复杂度

BF算法 最坏的情况下,就是未找到
比如
在 字符串S1 :abcabcabcabcf (记录长度为M)中 找
字符串 S2:abcd (记录长度为N)
第1次比较花了 N 次,发现不一样,重新在 S2 第1位比
第2次比较花了 N 次,发现不一样,重新在 S2 第2为比
第M次比较花了N次,发现都不一样,
所以时间复杂度为
O(M * N)

2. KMP 算法

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n) ----------百度百科

上面说的next函数,其实是一个next数组,记住这个next数组,后面有大用!

2.1 KMP 算法和 BF 算法 的区别

首先,要理解 KMP 算法之前,要知道
KMP 和 BF 的区别就是 主串的 i 不会回退,并且 j 也不会回到 0 位置

2.1.1 为什么主串不回退?

假设上图中在 i = 2 位置处匹配失败了,就算回退到 主串 1 位置也是没有必要的,因为 主串 的1位置处字符也和 子串 0 位置的 a 不一样

再来看下面这张图

第一次匹配时,在 5 位置处 匹配失败了
此时,我们想一想,如果是我们人为移动的话,
j 下一次移动到哪合适?
思考一下
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
答案时 j 移动到 2 位置 处最合适
为什么呢?来分析一下
如果我们按照 BF 的解法,第一次匹配失败,i 要回退到 1 为位置处,
j 要回退到 0 位置处

此时,匹配又失败了。i 到 2 位置处 匹配

又匹配失败了,i 要到 3 位置处匹配

OK,这次 主串 3 位置处 与 子串 0 位置处匹配成功了,比较进行下去

直到这里,又失败了,此时按照 BF 算法 ,i 要回退到 4 位置,j 要回退到0。
好,现在想一下刚才我们问的问题,第一次匹配失败时,如果是我们人为地移动 j 移动到哪合适?
是不是 现在 j 现在 待在的子串 的 2 位置处?
既然我第一次i 和 j 同时 ++ 时匹配失败,第二次 i 和 j 同时 ++ 时匹配也失败了,那么我第二次这个匹配过程是可以省略掉的,就是直接在第一次 i 和 j 同时 ++ 匹配失败时,i 不回退,j 不退回到 0 位置,而是回退这个 2 位置。

现在,我们观察一下,这两个字符串。

可以发现这 i 前面的 两个字符,和 子串 0 1两个位置处字符是一样的,
而且 j 既然能走到 5 位置,说明 主串 的 3 4 位置处 和我子串的 3 4 位置也是一样的
而 子串中 括起来的两个方框,长度分别是2,(2 + 2)/ 2 = 2,刚好是我 j 回退的最佳位置
这是不是可能说明这是一个特性,让 j 回退时,回退到这么一个位置就好了呢?当然,这可能也是个巧合,我们继续往下看。

我们的目的始终都是: i 不回退,j 回退到一个特定的位置。
既然想让 i 不回退,那么我们就要在主串中找到和子串匹配的一部分。
j 回退到什么位置也是我们要求的

2. 2 next 数组

还记得 KMP 定义里的next数组吗

这个next数组里面要存的就是我们 j 在哪个位置匹配失败所要回退的位置,
比如我们刚才在 5 位置处匹配失败,就要回退到 2 位置,记录下来
就是 next[j] = k,k是我们要求的。
现在,直接告诉你结论,k怎么求
1.找到匹配成功部分的两个相等的真子串(不包含本身)长度,一个下标 0 开始,另一个以 j - 1 下标结尾。
2.不管时什么数据,我们规定next[0] = -1;next[1] = 0;
现在我们来求一下next 数组里各个k
现在我们来求这个子串的next数组

首先,按照规则,next[0] = -1,next[1] = 0

我们继续求 2 位置时的 k
从 0 位置开始, j - 1 = 2 - 1 = 1 结尾
也就是找从 a 开始,b 结尾两个相等的真子串,
可以看到这是一个子串,不是两个,所以
next[2] = 0;

我们继续求 3 位置的 k
从 0 位置开始,j - 1 = 3 - 1 = 2 结尾
也就是从a 开始,c 结尾两个相等的真子串
可以看到,并不存在,所以
next[3] = 0;

我们继续求 4 位置的 k

从 0 位置开始,j - 1 = 4 - 1 = 3 位置结尾 也就是从a 开始,a 结尾的两个相等的真子串 可以看到括起来的部分
前一个自身开始,自身结尾 后一个也自身开始,自身结尾
长度为1,所以next[4] = 1;

我们继续求 5 位置的 k

找 a 开始 b 结尾的两个相等的真子串
长度为 2 ,所以
next[5] = 2;

我们继续求 6 位置的 k
找 a 开始,a 结尾的两个相等的真字串
长度为1
所以next[6] = 1;
我们继续求 7 位置的 k
找 a 开始 ,a 结尾的两个相等的真子串
长度为 2
所以 next[7] = 2

我们继续求 8 位置的 k
找从 a 开始 c 结尾的两个相等的真子串
长度为3
所以 next[8] = 3

我们继续求 9 位置的 k
找 从a 开始 a 结尾的两个相等的真子串
长度为4
所以next[9] = 4

我们继续求 10 位置的 k
找从 a 开始 b 结尾的两个相等的真子串
长度为 5
所以next[10] = 5

next 数组我们就求完了。
记住next数组里求的是我们 在位置 j 处匹配失败时要退回的 k 处
上面的方法都是我们手动去求的
那我们在代码上怎么体现?
先不急着看代码,我们分析一下

现在假设我们next[j] = k;
就是 j 位置匹配失败时,我们 j 要退回到 k 处

J 现在在 8 位置处,我们求 9 位置为什么等于 next[9] = 4?

如下图分析一下,
8 位置处,我们的 k = 3
是不是说明了刚才我们求的时候
从a 开始 c 结尾的两个相等的真子串就是我们下面画横线的两个
我们肉眼看可以看出来。

也就是说,有这么一个公式

p[0]~p[k-1] = p[x]~p[j-1]......(1)
表示第0个位置到第k-1个位置的字符  等于  第x到第j-1个位置的字符
这个x是我们要求的
按照上面的公式,我们求一下x
利用数组之间的长度
k - 1 - 0 = j - 1 - x;
可以轻易得出 x = j - k;......(2)
所以 将(2)代入(1),
得 p[0]~p[k-1] = p[j-k]~p[j-1]  ......(3)
代入验证一下
p[0]~p[2] = p[5]~p[7],正确!!!
我们回过来,我们要求的是 9 位置为什么 k = 4
再假设,如果 p[k] = p[j] ......(4)
那么由(3),就可以有
p[0]~p[k] = p[j-k]~p[j]......(5)
我们一开始假设
k = next[j]
对应了p[0]~p[k-1] = p[j-k]~p[j-1]; ......(3)那么刚才推出来的 p[0]~p[k] = p[j-k]~p[j] ......(5)
可以对应
k + 1 = next[j + 1]
也就是说,有了
next[i] = k 和 p[k] = p[j] 这两个假设
那么就能对应出
k + 1 = next[j+1]
我们要求的 9 的位置为什么是 4 也就求出来了。
因为p[3] = p[8] = a;
所以 3 + 1 = next[ 8 + 1]
所以next[9] = 4
那如果 p[j] != p[k]
我们的 next[i+1] 怎么求呢?
看一下下面这个例子

此时p[j] != p[k]
我们的 next [6] = 1 怎么求呢?

既然我们 p[j] = p[k]
才能求出下一位,我们就想办法让
p[j] != p[k]
让它变成 p[j] = p[k]
怎么变?
思考一下。就两个字。
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
答案是 回退!

next[2] = 0,那我们就回退到 0 位置

此时 p[k] = p[j]
所以满足我们要的条件了,
所以 next[j + 1] = k + 1
next[5 + 1] = 0 + 1 = 1;
next[6] = 1
可以求出来了!!!

2.2.3 next数组总结

总结一下
1.当 p[k] = p[j] 时
可以直接求next[j+1] = k + 12.当p[k] != p[j] 时
我们就回退!
回退到 p[k] = p[j] 为止

3.代码

3.1 伪代码分析

先看一下伪代码
//和上面的BF算法代码,差不多类似。
//先不管next数组怎么求
int KMP(const char* source, const char* dest,int pop)
{ // source 表示主串,dest表示子串,pop表示从母串哪个位置开始求,一般为0位置assert(source && dest);//防止空指针int len_source = strlen(source);//计算长度int len_dest = strlen(dest);int i = pop; // 遍历主串int j = 0;//遍历子串while (i < len_source && j < len_dest){if (j == -1 || source[i] == dest[j])  {i++;j++;}else{j = next[j];}}if (j >= len_dest) // 当子串先走完说明主串包含子串,返回位置{                  //不明白i - j的可以看看上面的BF算法分析return i - j;}return -1; //否则说明不主串不包含子串,返回-1
}
//看一下这段代码
if (j == -1 || source[i] == dest[j])  {i++;j++;}
else{j = next[j];}
//source[i] == dest[j] 就是上来先比较一下,都一样就继续往后比
// j == -1 什么意思?
//就是当子串dest[0] != source[0];
//进入else
//那么 j = next[0] = -1;
//而 j = -1 会导致数组越界,所以我们要加上 j == -1 时j++

3.2 求 next 数组

//先malloc一下next数组
int* next = (int*)malloc(sizeof(int) * len_dest);
//再写求next的函数
//注意,这里的写法和我们刚才分析的有所差异
//但是大体思路一样
void GetNext(int* next, const char* dest)
{int len_dest = strlen(dest);next[0] = -1;next[1] = 0;int j = 2;//当前j 下标int k = 0; //前一项的kwhile (j < len_dest){if((k == -1) || dest[k] == dest[j - 1]){next[j] = k + 1;j++;k++;}else{k = next[k];}}
}
//看一下这段代码
if((k == -1) || dest[k] == dest[j - 1])
{next[j] = k + 1;j++;k++;
}
else
{k = next[k];
}

j 从 2 位置开始

我们刚才分析的时候,如下图

我们是知道了,j 下标,
才得出 p[j] = p[k]
才知道了 next[j + 1] = k + 1;而我们这里

j 是没求的
所以相当于我们求的是 j - 1 下标
所以我们现在的 j 相等于原来的 j + 1

之前分析的图(如下图)相对于我们现在要求的图来说是这样的
我们还不知道next[8] = 3

所以相当于 p[j - 1] = p[k]
所以p[8-1] = p[k]p[7]   =  p[k] ,现在 k 在位置2上所以p[7] = p[2] = 'c'
也就是说next[j] = k + 1;
所以公式由原来的 p[j] = p[k]
变成了 p[j - 1] = p[k]
所以next[j] = k + 1;
int j = 2;//当前j 下标
int k = 0; //前一项的k
void GetNext(int* next, const char* dest)
{int len_dest = strlen(dest);next[0] = -1;next[1] = 0;int j = 2;//当前j下标int k = 0; //前一项的kwhile (j < len_dest){//满足dest[k] = dest[j-1]就进来if((k == -1) || dest[k] == dest[j - 1]){next[j] = k + 1; // 如刚才分析的那样j++;k++;}else   //否则就回退,注意回退的时候是有可能退到-1的//因为next[0] = -1,所以上面的if语句有 k == -1时就进来k++;//所以某一个位置上 next[j] = -1 + 1 = 0;{k = next[k];}}
}

3.2.1 next数组的优化

我们的next数组还能再次优化,来看下面这个例子

当我的 j 在 5 位置处匹配失败,是不是要根据最下面的next数组,是不是要先回退到 4 位置,如何到了 4 位置要 回退到 3 位置,然后一直回退一直回退,到 0 位置,回退 -1 位置。

那我能不能一步回退到 -1?

优化后如下

while ( i < len_dest){if (dest[i] == dest[next[i]]){next[i] = next[next[i]];}i++;}

4. 完整代码

//KMP算法
void GetNext(int* next, const char* dest)
{int len_dest = strlen(dest);next[0] = -1;next[1] = 0;int j = 2;//当前j下标int k = 0; //前一项的kwhile (j < len_dest){//满足dest[k] = dest[j-1]就进来if ((k == -1) || dest[k] == dest[j - 1]){next[j] = k + 1; // 如刚才分析的那样j++;k++;}else   //否则就回退,注意回退的时候是有可能退到-1的//因为next[0] = -1,所以上面的if语句有 k == -1时就进来k++;//所以某一个位置上 next[j] = -1 + 1 = 0;{k = next[k];}}int i  = 2;while ( i < len_dest)  // 优化next数组{   //如果回退的位置和当前位置的字符一样if (dest[i] == dest[next[i]]){   //就写回退位置那个next值next[i] = next[next[i]];}i++;}
}
// source 表示主串,dest表示子串,pop表示从母串哪个位置开始求,一般为0位置
int KMP(const char* source, const char* dest,int pop)
{assert(source && dest);int len_source = strlen(source);int len_dest = strlen(dest);int i = pop; // 遍历主串int j = 0;//遍历子串int* next = (int*)malloc(sizeof(int) * len_dest);GetNext(next, dest);while (i < len_source && j < len_dest){if ((j == -1) || source[i] == dest[j]){i++;j++;}else{j = next[j];}}
//source[i] == dest[j] 就是上来先比较一下,都一样就继续往后比
// j == -1 什么意思?
//就是当子串dest[0] != source[0];
//进入else
//那么 j = next[0] = -1;
//而 j = -1 会导致数组越界,所以我们要加上 j == -1 时j++if (j >= len_dest){return i - j;  //如果子串先走完,说明主串包含子串,返回 i - j的位置}                //不明白的可以看看上面BF算法分析return -1;
}//否则说明主串不包含子串,返回 -1
int main()
{printf("%d\n", KMP("rgabcdkkk", "abcd", 0)); // 2printf("%d\n", KMP("rgabcdkkk", "r", 0)); // 0printf("%d\n", KMP("rgabcdkkk", "kg", 0)); // -1return 0;
}

【算法篇-字符串匹配算法】BF算法和KMP算法相关推荐

  1. Algorithm:C++语言实现之字符串相关算法(字符串的循环左移、字符串的全排列、带有同个字符的全排列、串匹配问题的BF算法和KMP算法)

    Algorithm:C++语言实现之字符串相关算法(字符串的循环左移.字符串的全排列.带有同个字符的全排列.串匹配问题的BF算法和KMP算法) 目录 一.字符串的算法 1.字符串的循环左移 2.字符串 ...

  2. BF算法和KMP算法

    给定两个字符串S和T,在主串S中查找子串T的过程称为串匹配(string matching,也称模式匹配),T称为模式.这里将介绍处理串匹配问题的两种算法,BF算法和KMP算法. BF算法 (暴力匹配 ...

  3. 若S作主串,P作模式串,试分别写出利用BF算法和KMP算法的匹配过程。

    目   录 题目: 百度文库-答案: (1) (2) MOOC标准答案: (1) (2) mooc答案-截图: 数据结构(C语言版)-严蔚敏2007 题目: 设字符串S='aabaabaabaac', ...

  4. 字符串匹配—BF算法和KMP算法

    BF算法 本章重点是是KMP算法,但是由于其较难理解,先从更简单朴素的BF算法开始. 其思路非常简单 也就是,对这样两个字符串(称短的为模式串,长的为主串): 让主串和模式串的每个字符逐个匹配,如果从 ...

  5. 串的BF算法和KMP算法个人总结

    子串(模式串)的定位操作通常称作串的模式匹配 其中包含最初始的BF算法(Brute-Force)即简单匹配算法或者称作朴素的模式匹配算法,利用穷举法的思路 另一种就是改进后的KMP算法,还有对KMP算 ...

  6. 数据结构之字符串匹配算法(BF算法和KMP算法)

    字符串匹配算法: 就是给定两个串,主串(s)和子串(sub), 查找子串是否在主串里面,如果找到便返回子串在主串中第一个元素的位置下标,否贼返回-1,. 在这里我 们讨论的时候主要用字符串来举例实现. ...

  7. 模式串匹配的BF算法和KMP算法

    KMP是三位大牛:D.E.Knuth.J.H.Morris和V.R.Pratt同时发现的.为了解决模式匹配问题,也即寻找模式串(子串)在主串中第一次出现的位置,若模式串在主串中不存在则返回-1. 简单 ...

  8. BF算法和KMP算法实现

    https://blog.csdn.net/xxibgdrgn_liqian_a_/article/details/80690593

  9. 数据结构与算法之美笔记——基础篇(下):图、字符串匹配算法(BF 算法和 RK 算法、BM 算法和 KMP 算法 、Trie 树和 AC 自动机)

    图 如何存储微博.微信等社交网络中的好友关系?图.实际上,涉及图的算法有很多,也非常复杂,比如图的搜索.最短路径.最小生成树.二分图等等.我们今天聚焦在图存储这一方面,后面会分好几节来依次讲解图相关的 ...

最新文章

  1. 图像转换 之 方形图转化扇形图
  2. python日期函数引用_Python pandas.to_datetime函数方法的使用
  3. Python的os.walk()方法详细讲解
  4. 直流UPS与传统UPS系统节能分析
  5. 异常处理_月隐学python第19课
  6. 清华大学《操作系统》(十一):处理机调度
  7. 让机器读懂视频:亿级淘宝视频背后的多模态AI算法揭秘
  8. SQLite的SQL语法
  9. 合肥师范学院计算机操作系统期末考试题,2005级操作系统期末试卷A卷及答案
  10. 解决进入XP系统无法响应故障
  11. STL源码剖析 第5章 关联式容器
  12. 高等代数(第三版)北大(参考答案)
  13. VB制作控件之图片命令按钮➀
  14. 最适合写python程序的软件
  15. 蛋白质结构数据预处理基础(个人笔记)
  16. canvas学习(html5)画画
  17. 联想原装系统OEM系统联想出厂系统联想原装系统Lenovo ThinkPad ThinkBook出厂预装系统原厂系统
  18. 计算机的基本组成 教案反思,《计算机系统组成》教学反思
  19. HDU 5117 Fluorescent
  20. perf + 火焰图用法 小结

热门文章

  1. 张一鸣讲述字节跳动7年:诞生于民宅,大力出奇迹
  2. 蓝牙协议HFP,HSP,A2DP,AVRCP等
  3. flink mysql sink_聊聊flink的sink
  4. Smartbi分页报表设置,轻松实现报表分页展示
  5. 支付宝启用人工智能客服 能力媲美大学生
  6. Unity3D学习记录(实现一个简单的Galgame)(二)
  7. 网络编程 三次握手、四次挥手
  8. 打印的时候计算机出现蓝屏,在电脑打印时电脑总是会蓝屏或重启怎么处理
  9. 【OpenGL】一、Visual Studio 2019 创建 Windows 桌面程序 ( Visual Studio Installer 安装 C++ 桌面开发库 | 创建桌面程序 )
  10. php mysql三_PHP 和 MySQL 基础教程(三)