字符串匹配

字符串匹配

 

何为字符串匹配?

就是从目标文本字符串中找到目标字符串的过程

如下图所示:
在这里插入图片描述

字符串问题讨论可以分为讨论几个问题:

  1. P是否出现?
  2. 首次出现在哪里?
  3. 共有几次出现?
  4. 各出现在哪里?

本文着重讨论首次出现在哪里

 

一、蛮力算法(Brute Force算法)

算法

  • 自左向右,依次移动P串
  • 每一次比较,如果每个字符都匹配停止程序,返回比较首位置;如果发现任一位置不匹配,就继续将P移动到后一位置进行比较

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yHPy7Pns-1668485073721)(字符串匹配.assets/image-20221108115158154-16678868745421-16678868754523.png)]

 

版本一

#include <iostream>
#include <cstring>
using namespace std;
int BF1_match(char* T, char* P)
{
	int n = strlen(T);
	int m = strlen(P);
	int i = 0;
	int j = 0;
	while (i < n && j < m)
	{
		if (T[i] == P[j])
		{
			i++;
			j++;
		}
		else
		{
			j = 0;
			i -= j - 1;
		}
	}
	return i - j;
}

int main()
{
	char Text[] = "abcd";
	char Pattern[] = "cd";
	int ret = BF1_match(Text, Pattern);
	cout << ret << endl;
	system("pause");
	return 0;
}

我们来看看返回值可能的取值和它所代表的含义

可能返回值(以下图为例):0 - 10,返回值是从T串匹配成功开始的下标

如果返回值是0-6则代表匹配成功

如果返回值是7-10则匹配失败

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QiadwEBm-1668485073721)(字符串匹配.assets/image-20221108135555318.png)]

 

版本二

#include <iostream>
#include <cstring>
using namespace std;
int BF2_match(char* T, char* P)
{
	int n = strlen(T);
	int m = strlen(P);
	int i = 0;
	int j = 0;
	for (i = 0; i < n - m + 1; i++)
	{
		for (j = 0; j < m; j++)
		{
			if (T[i + j] != P[j])
			{
				break;
			}
		}
		if (j >= m)
		{
			break;
		}
	}
	return i;
}

int main()
{
	char Text[] = "abcd";
	char Pattern[] = "cd";
	int ret = BF2_match(Text, Pattern);
	cout << ret << endl;
	system("pause");
	return 0;
}

我们来看看返回值可能的取值和它所代表的含义

可能返回值(以下图为例):0 - 7,返回值是从T串匹配成功开始的下标

如果返回值是0-6,则代表匹配成功

如果返回值是7,则代表匹配失败

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VBUHGrzy-1668485073722)(字符串匹配.assets/image-20221108161817190.png)]

BF算法的时间复杂度: O(n * m)

 

二、KMP算法

KMP算法对于初始接触者会比较复杂,建议花几分钟把这个视频看看再来看这篇文章你会醍醐灌顶

我们可以看到BF算法效率十分低下,原因是重复一些非必要的比较,

主要原因有两个

  • 其一:重复多次比较之前已经匹配成功的元素
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hc7pQ4v9-1668485073722)(字符串匹配.assets/image-20221108210739643.png)]

    上图中遇到不匹配的字符,会将目标串回退,模式串复位,继续重复比较之前比较过的元素,我们是否可以优化为接下来直接比较j - 1和i,这就是KMP算法的优势

  • 其二:模板串只会一次移动一个单位

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tXLbBoIq-1668485073723)(字符串匹配.assets/image-20221108221155803.png)]
    上图中遇到不匹配的字符,会将目标串回退,模式串复位,但是很明显R和E、G不匹配,我们是否可以优化为接下来直接比较O和E呢,这也是KMP算法的优势

 

KMP算法的原理:

  1. 在匹配的过程中,目标串的指针不需要回溯,只回溯模式串的指针
  2. 如果模式串和目标串的前n个字符匹配成功,遇到匹配失败的字符时,模式串的指针回溯到第(匹配失败位置前的模式串的最长公共前后缀的长度+1)个元素,然后继续与目标串指针比较

请将此算法原理结合上面两个实例看你会明白为什么要这么做
这里解释一下最长公共前后缀的长度
ababa为例
前缀:a,ab,aba,abab
后缀:a,ba,aba,baba
所以最长公共前后缀的长度为3

那么我们如何快速定位下面两张图的下一个j值(模式串回溯的位置)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nO6BjciX-1668485073723)(字符串匹配.assets/image-20221108224209616.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLHvIftZ-1668485073724)(字符串匹配.assets/image-20221108224216050.png)]对此我们可以构建一个next数组(大小为模式串的大小,因为它要为模式串中的每一个元素提供模式串回溯的位置),根据上文提出KMP算法原理可以算出每一个元素应该回溯的位置(这里是指第几个元素,但是我们是将字符串放在数组中,数组的下标是从0开始,所以还应该减一个1),例如next[j] = j - 1;next[4] = 1。

 

我们来看一个构建next数组的实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZSQ1PfTn-1668485073724)(字符串匹配.assets/image-20221108225420996.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5MobKfDk-1668485073725)(字符串匹配.assets/image-20221108232454330.png)]

这里只有一个疑问那就是next[0]应该怎么界定?

如果next[0]也按照上面的规则,那么也就是模式串需要回溯0,但这就相当于两个字符串中的指针都没有动,显然这么定义是有问题的,那么我们应该如何定义呢?

按照KMP算法我们定义next[0] = -1,至于为什么这么定义,你肯定很疑惑,不妨先记住它,在后面会解释这个原因(其实也就是当模式串中的指针为-1时,就将模式串和目标串的指针都+1,后移一位),通俗来讲next数组也就是记录的每一个元素前面的字串最大子串前后缀的个数

 

next数组构建

算法
  1. 首先将next[0]置为-1

  2. 采用递推的思想求解next[j]

    next[j + 1] <= next[j] + 1

    • 当且仅当P[j] == P[next[j]]时取等号
    • 不等于时,调整模式串的指针为t = next[t]
    • 直到t < 0 或者P[j] = P[next[j]]

接下来我们来剖析构造next数组的步骤:

  1. 我们可以确定next数组的第一二位一定分别是-1,0,后面求解每一位next值时,根据前一位进行比较
  2. 从第三位开始,将前一位与其next值对应的内容进行比较
  3. 如果相等,就在该位的next值就是前一位的next值加上1
  4. 如果不等,向前继续寻找next值对应的内容与前一位进行比较
  5. 直到找到某个位上内容的next值对应的内容与前一位相等为止
  6. 则这个位对应的next值+1即为需求的next值
  7. 如果找到第一位都没有找到与前一位相等的内容,那么求解的位上的next值为1

 

代码实现
#include <iostream>
#include <cstring>
using namespace std;
//创建next表
int* BuildNext(char* p)
{
	int m = strlen(p);
	int j = 0;
	int* Next = new int[m];
	Next[0] = -1;
	int t = Next[0];
	while (j < m - 1)
	{
		if (t < 0 || p[j] == p[t])
		{
			Next[++j] = ++t;
		}
		else
		{
			t = Next[t];
		}
	}
	return Next;
}

int main()
{
	char arr[] = "abab";
	int* ret = BuildNext(arr);
	cout << ret[0] << endl;
	cout << ret[1] << endl;
	cout << ret[2] << endl;
	cout << ret[3] << endl;
	system("pause");
	return 0;
}

我们来举一个实例来方便理解:

以下面这个数组为例,我们来看看程序运行的步骤,程序有m,j,t这三个重要的变量影响程序,我们接下来重点看看它们的取值之间有什么关联。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GIT19diQ-1668485073725)(字符串匹配.assets/image-20221109150504926.png)]

  1. m是字符串长度不会改变,我们重点来看看j和t这两个变量,首先将j指向数组的第一个位置,t为这个位置对应的next值
  2. 然后j指针指向第二个位置,t为第二个位置对应的next值,然后对比数组中j位置和t位置(0)是否相等,很显然不相等,将t设置为t的next值即为-1
  3. -1小于0,此时j指向第三个位置,t为第三个位置对应的next值,然后比对数组中j位置和t位置(0)是否相等,很显然不相等,将t设置为t的next值即为-1
  4. -1小于0,此时j指向第四个位置,t为第四个位置对应的next值,然后对比数组中j位置和t位置(0)是否相等,很显然相等,然后j指向第五个位置,t为第五个位置的next值(1)
  5. 此时j指向第五个位置,t为第五个位置对应的next值,然后对比数组中j位置和t位置(1)是否相等,很显然相等,然后j指向第六个位置,t为第六个位置的next值(2)
  6. 此时j指向第六个位置,t为第六个位置对应的next值,然后对比数组中j位置和t位置(2)是否相等,很显然不相等,将t设置为t的next值即为0,继续对比数组中的j位置和t位置(0)是否相等,很显然相等,然后j指向第七个位置,t为第七个位置对应的next值(1)
  7. 此时j指向第七个位置,t为第七个位置对应的next值,然后对比数组中j位置和t位置(1)是否相等,很显然不相等,将t设置为t的next值即为0,继续对比数组中的j位置和t位置(0)是否相等,很显然相等,然后j指向第八个位置,t为第八个位置对应的next值(1)
  8. 此时j指向第八个位置,t为第八个位置对应的next值,然后对比数组中j位置和t位置(1)是否相等,很显然相等,然后t指向第求个位置,t为第九个位置对应的next值(2)
  9. 接下来的过程和前面完全一样,这里就不一一列举了

 

KMP算法实现

#include <iostream>
#include <cstring>
using namespace std;
//创建next表
int* BuildNext(char* p)
{
	int m = strlen(p);
	int j = 0;
	int* Next = new int[m];
	Next[0] = -1;
	int t = Next[0];
	while (j < m - 1)
	{
		if (t < 0 || p[j] == p[t])
		{
			Next[++j] = ++t;
		}
		else
		{
			t = Next[t];
		}
	}
	return Next;
}
//KMP算法
int KMP_match(char* T, char* P)
{
	int* next = BuildNext(P);
	int n = strlen(T);
	int m = strlen(P);
	int i = 0;
	int j = 0;
	while (i < n && j < m)
	{
		if (j < 0 || T[i] == P[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}
	delete[] next;
	return i - j;
}

int main()
{
	char Text[] = "aabacabababcaaab";
	char Pattern[] = "ababc";
	int ret = KMP_match(Text, Pattern);
	cout << ret << endl;
	system("pause");
	return 0;
}

时间复杂度: O(n)

我们来举一个实例方便理解:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1n3qUENn-1668485073726)(字符串匹配.assets/image-20221109151116610.png)]

我们先把next数组表示出来next[5] = {-1, 0, 0, 1, 2}

这个程序中有i和j两个”数组指针“来控制程序的运行

  1. 首先i(0)指向目标串中的第一个元素,j(0)指向模式串中的第一个元素,如果T[i]和P[j]相等或者j<0,那么i和j都向后后退一步;如果不相等将j设置为j的next值,这里显然相等,所以i =1,j =1
  2. 此时i = 1,j = 1,很显然此时T[i]和P[j]不相等,那么就需要将j设置为next[j](0),那么此时i = 1, j = 0,对比T[i]和P[j],很显然相等,i和j都向后后退一步
  3. 此时i = 2, j = 1,很显然此时T[i]和P[j]相等,那么就继续将i和j都向后后退一步
  4. 此时i = 3, j = 2,很显然此时T[i]和P[j]相等,那么就继续i和j都后退一步
  5. 此时i = 4, j = 3, 很显然此时T[i]和P[j]不相等,那么就将j设置为j的next值(1)
  6. 此时i = 4, j = 1,很显然此时T[i]和P[j]不相等,那么就将j设置为j的next值(0)
  7. 此时i = 4, j = 0,很显然此时T[i]和P[j]不相等,那么就将j设置为j的next值(-1)此时将模式串中可能与T[4]相等的元素比较过了,那么只能使目标串向后移动一位,所以这就是为什么next数组中的第一位元素设为-1的原因了,当j < 0时,i和j都向后后退一步,j就指向了下个需要比较的位置,i变为0复原
  8. 此时i = 5, j = 0,很显然此时T[i]和P[j]相等,所以都向后退一个单位一直到i = 9, j = 4才出现不相等,那么就将j设置为j的next值(2)
  9. 此时i = 9, j = 2,很显然此时T[i]和P[j]相等,所以都向后退一个单位直到i = 11, j = 4
  10. 此时i = 11, j = 4,然后显然此时T[i]和P[j]相等,所以都向后退一个单位
  11. 此时i = 12, j = 5,此时j就等于m了,循环停止,返回i - j(7)即是匹配成功或失败首字符匹配位置

 

三、KMP算法优化

你可能会有疑问,为什么这么高效率的算法还可以优化?但是的确可以,看看下面这个例子你就会明白了,当1和0匹配不上时,**j指针会回退到2,但是0和之前匹配失败的0有什么不一样吗?**而且它不只是会这样操作一次,它会一直回退到0,然后i的指针才会+1,这样是十分没有必要的,对此的解决方法我们只需要把next数组稍稍改变一下成为新的wellnext数组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Teo6r30T-1668485073726)(字符串匹配.assets/image-20221109171144384.png)]

wellnext数组如何构建呢?

其实不难,只需要在构建next数组的同时,多加一个条件判断那就是回退回去的那个元素是否与当前元素相等如果相等那就没有必要回退到那个位置,而是回退到那个位置的next位置,所以将wellnext值设置为那个位置的next位置

 

代码实现

#include <iostream>
#include <cstring>
using namespace std;
//创建next表
int* BuildNextwell(char* p)
{
	int m = strlen(p);
	int j = 0;
	int* Next = new int[m];
	Next[0] = -1;
	int t = Next[0];
	while (j < m - 1)
	{
		if (t < 0 || p[j] == p[t])
		{
			j++;
			t++;
			Next[j] = (p[j] != p[t] ? t : Next[t]);
		}
		else
		{
			t = Next[t];
		}
	}
	return Next;
}
//KMP算法
int KMP_match(char* T, char* P)
{
	int* next = BuildNextwell(P);
	int n = strlen(T);
	int m = strlen(P);
	int i = 0;
	int j = 0;
	while (i < n && j < m)
	{
		if (j < 0 || T[i] == P[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}
	delete[] next;
	return i - j;
}

int main()
{
	char Text[] = "aabacabababcaaab";
	char Pattern[] = "ababc";
	int ret = KMP_match(Text, Pattern);
	cout << ret << endl;
	system("pause");
	return 0;
}

根据代码我们不难看出,实际只是改变next数组

我们来看一个创建新的wellnext数组的实例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j8Uh6yt7-1668485073727)(字符串匹配.assets/image-20221109193934799.png)]

原来next数组应该是next[] = {-1, 0, 1, 2, 3, 4}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K28ii8NN-1668485073728)(字符串匹配.assets/image-20221109180116721.png)]

  1. j指针指向第一个位置,t为这个位置对应的next值
  2. t < 0,j指向第二个位置,t++(0),判读P[j]和P[t]是否相等,显然这里是相等的,所以第二个位置的wellnext值为不能为t,而应该为next[t]所以为-1
  3. 此时j指向第二个位置,t为这个位置的next值(0),判断P[j]和P[t]是否相等,显然这里是相等的,那么j就会指向第三个位置,t为这个位置的next值(1),判断P[j]与P[t]是否相等,显然这里是相等的,所以第三个位置的wellnext值不能为t,而应该next[t]所以为-1
  4. 此时j指向第三个位置,t为这个位置的next值(1),判断P[j]和P[t]是否相等,显然这里是相等的,所以j会指向第四个位置,t为这个位置next值(2),判断P[j]与P[t]是否相等,显然这里是相等的,所以第四个位置的wellnext值不能为t, 而应该next[t]所以为-1
  5. 此时j指向第四个位置,t为这个位置的next值(2),判断P[j]和P[t]是否相等,显然这里是相等的,所以j会指向第五个位置,t为这个位置next值(3),判断P[j]与P[t]是否相等,显然这里是相等的,所以第五个位置的wellnext值不能为t,而应该为next[t]所以为-1
  6. 此时j指向第五个位置,t为这个位置next值(3),判断P[j]与P[t]是否相等,显然是相等的,所以j会指向第六个位置,t为这个位置next值(4),判断P[j]与P[t]是否相等,显然这里是相等的,所以第六个位置的wellnext值不能为t,而应该为next[t]所以为-1

 

四、BM算法

自右向左依次比对字符

我们来看一个实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p5xhzOhR-1668485073728)(字符串匹配.assets/image-20221109224712868.png)]

从最后面名和道开始匹配,不匹配,则在模式串中寻找道这个字符的下标位置,如果找到了,就用这个下标位置的元素与目标串中的道进行匹配,然后继续从右往左匹配;如果没有找到,则将目标串整体移过匹配失败的位置,模式串的指针回溯到首位置

  1. 名与道不匹配,在模式串中搜索道,没有找到,整体后移
  2. 名与道不匹配,在模式串中搜索道,没有找到,整体后移
  3. 常与可不匹配,在模式串中搜索可, 没有找到,整体后移
  4. 最后一次匹配成功

我们将以上从右向左不匹配的情况模式串中的那个字符称为坏字符

例如下图中y就是一个坏字符

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WoQMDC9G-1668485073728)(字符串匹配.assets/image-20221109230620052.png)]

在模式串中寻找X,返回X的下标

以下为在模式串中寻找X的情况:

  1. 如果在模式串中找到X,就返回它的下标,并将它与目标串相匹配,重新继续从右向左查找

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OLi2ssjE-1668485073729)(字符串匹配.assets/image-20221109231123884.png)]

  2. 如果在模式串找到多个X,就应该返回其中最靠右者,如此可避免回溯

  3. 如果在模式串中没有找到X,就返回哨兵(-1)与KMP相似,这样就可以在目标串中向后移动一位查找

  4. 如果在模式串中找到的X数值大于j就没有必要对齐,直接在目标串中向后移动一位查找

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1S8eIUlM-1668485073729)(字符串匹配.assets/image-20221109231811677.png)]

 

BC表构建

算法
  1. 先将整个BC表设置为通配符-1
  2. 依次遍历模式串,将其中最后出现的那个元素的下标存储到BC表中

 

代码实现
int* bc_Build(char* pattern)
{
	int* bc = new int[256];
	int sz = strlen(pattern);
	for (int i = 0; i < 256; i++)
	{
		bc[i] = -1;
	}
	for (int j = 0; j < sz; j++)
	{
		bc[pattern[j]] = j;
	}
	return bc;
}

 

此时我们来分析一下最好情况和最坏情况

  • 最好情况:

    如下图就是最好情况,O(n / m)

    在这里插入图片描述

  • 最坏情况

    如下图就是最坏情况,O(n * m)

    在这里插入图片描述

由上面的最坏情况我们可以看出此时仍然有待改进

 

我们再来看一个更具一般性的例子

在这里插入图片描述

  1. C与H失配,查询BC表,在模式串中找到了c,可是它下标太过靠后所以模式串整体右移
  2. C与H失配,查询BC表,在模式串中找到了H,可是它下标太过靠后所以模式串整体右移
  3. A与H失配,查询BC表,在模式串中找到了A,将他们对齐,匹配成功

在上述过程中,我们是否能够直接从第一步跳到第三步,接下来就来看看GS策略

GS表构建

事先构建一个表单,上面记录每次需要移动的距离,但是直接构建GS表有一定的难度,具体实现的时候我们会使用一个叫做suffix表的辅助表

构建suffix表

每一个元素记录的位置为以这个元素为边界,与模式串后缀匹配的最大长度,特殊的最后一个位置与模式串匹配的最大长度即为模式串本身的长度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CIMbOnoV-1668485073730)(字符串匹配.assets/image-20221110101623639.png)]

代码实现
int* suffix_Build(char* pattern)
{
	int sz = strlen(pattern);
	int* suffix = new int[sz];
	int num = 0;
	suffix[sz - 1] = sz;
	for (int i = sz - 2; i >= 0; i++)
	{
		for (num = 0; num <= i && pattern[i - num] == pattern[sz - 1 - num]; num++);
		suffix[i] = num;
	}
	return suffix;
}

接下来我们进行手动填表,来感受一下算法的流程

  1. 首先将最后一个位置填表为模式串的长度(15)
  2. i此时指向13这个位置,一直循环14次,直到将0号位置填充完毕
  3. 进入第二层循环,这层循环用num来控制循环条件、
  4. 首先num = 0 < i = 13 && pat[13 - 0] != pat[15 - 0 - 1],suffix[13] = 0
  5. i = 12,num = 0 < i = 12 && pat[12 - 0] != pat[15 -1 - 0],suffix[12] = 0
  6. i = 11, num = 0 < i = 11 && pat[11 - 0] != pat[15 - 1 - 0],suffix[11] = 0
  7. i = 10,num = 0 < i = 10 && pat[10 - 0] != pat[15 -1 - 0], suffix[10] = 0
  8. 一直到i = 8 ,num = 0 < i = 8 && pat[8 - 0] == pat[15 - 1 - 0], num = 1
  9. i = 8, num = 1 < i = 8 && pat[8 - 1] == pat[15 - 1 - 1], num = 2
  10. i = 8, num = 2 < i = 8 &&pat[8 - 2] == pat[15 - 1 - 2], num = 3
  11. i = 8, num = 3 < i =8 && pat[8 - 3] == pat[15 - 1 - 3], num = 4
  12. i = 8, num = 4 < i = 8 && pat[8 - 4] != pat[15 - 1 -4], num = 4
  13. suffix[8] = 4
  14. 接下来就不一一演示了,相信到这里你应该十分清楚这个填表过程了

 

构建GS表,分三种情况:

  • 模式串中有子串匹配上好后缀

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iacgcm4m-1668485073730)(字符串匹配.assets/image-20221110134603929.png)]

  • 模式串中没有子串匹配上好后缀,但找到了一个最大前缀

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WmAe22WJ-1668485073730)(字符串匹配.assets/image-20221110134740166.png)]

  • 模式串中没有子串匹配上好后缀,也找不到一个最大前缀

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1V0qT0gF-1668485073731)(字符串匹配.assets/image-20221110134920036.png)]

给出具体实现之前实现之前,我们来思考一个问题如果同时存在符合条件的串中子串和前缀字串,为了防止回溯,我们应该让一次移动的距离小一点,应该先优先匹配下标较大的元素

 

代码实现
int* gs_Build(char* pattern)
{
	int sz = strlen(pattern);
	int lastindex = sz - 1;
	int* suffix = suffix_Build(pattern);
	int* gs = new int[sz];
	//找不到对应的字串和前缀
	for (int i = 0; i < sz; i++)
	{
		gs[i] = sz;
	}
	//找前缀
	for (int i = lastindex; i >= 0; i--)
	{
		if (i + 1 == suffix[i])
		{
			for (int j = 0; j < lastindex - i; j++)
			{
				gs[j] = lastindex - i;
			}
		}
	}
	//找后缀
	for (int i = 0; i < lastindex; i++)
	{
		gs[lastindex - suffix[i]] = lastindex - i;
	}
	return gs;
}

接下来我们来看一个实例方便理解:

在这里插入图片描述
在这里插入图片描述

  • 上图为这个模式串的suffix表,接下来我们进行第一个循环(对应移动整个模式串)的填表

    很简单就是将每个位置填表为15

  • 上图为这个模式串的suffix表,接下来我们进行第二个循环(对应找到了一个最大前缀)的填表

    1. i = 14, suffix[14] = 15 , i + 1 = 15, j = 0 < 14 - 14
    2. i = 13, suffix[13] = 0 != 13 + 1, i–
    3. i = 12, suffix[12] = 0 != 12 + 1, i–
    4. i = 11, suffix[12] = 0 != 11 + 1, i–
    5. 一直到i = 2
    6. i = 2, suffix[2] = 3, i + 1 = 3, j = 0 < 14 - 2 gs[0] == 15, gs[0] = 12,j++
      • j = 1 < 14 - 2, gs[1] ==15, gs[1] = 12, j++
      • j = 2 < 14 - 2, gs[2] == 15, gs[2] = 12,j++
      • j = 3 < 14 - 2, gs[3] == 15, gs[3] = 12, j++
      • j = 4 < 14 - 2, gs[4] == 15, gs[4] = 12, j++
      • 直到j = 11 ,gs[11] == 15, gs[11] = 12,j++
      • j = 12 == 14 - 2
    7. i = 1 ,suffix[1] = 0 , i + 1 = 2
    8. i = 0, suffix[0] = 0, i + 1 = 1
  • 上图为这个模式串的suffix表,接下来我们进行第三个循环(对应找到一个串中子串)的填表

    1. i = 0,lastindex = 14, suffix[0] = 0, lastindex - i = 14, lastindex - suffix[0] = 14, gs[14] = 14

    2. i = 1, lastindex = 14, suffix[1] = 0,lastindex - i = 13, lastindex - suffix[1] = 14, gs[14] = 13

    3. i = 2, lastindex = 14, suffix[2] = 3, lastindex - i = 12, lastindex - suffix[2] = 11, gs[11] = 12

    4. i = 3,lastindex = 14, suffix[3] = 0, lastindex - i = 11, lastindex - suffix[3] = 14, gs[14] = 11

    5. i = 4,lastindex = 14, suffix[4] = 0, lastindex - i = 10, lastindex - suffix[4] = 14, gs[14] = 10

    6. i = 5, lastindex = 14, suffix[5] = 0, lastindex - i = 9, lastindex - suffix[5] = 14, gs[14] = 9

    7. i = 6,lastindex = 14, suffix[6] = 0, lastindex - i = 8, lastindex - suffix[6] = 14, gs[14] = 8

    8. i = 7,lastindex = 14, suffix[7] = 9,lastindex - i = 7, lastindex - suffix[7] = 14, gs[14] = 7

    9. i = 8, lastindex = 14, suffix[8] = 4, lastindex - i = 6, lastindex - suffix[8] = 10, gs[10] = 6

    10. i = 9, lastindex = 14, suffix[9] = 0, lastindex - i = 5, lastindex - suffix[9] = 14, gs[14] = 5

    11. i = 10, lastindex = 14, suffix[10] = 0,lastindex - i = 4, lastindex -suffix[10] = 14, gs[14] = 4

    12. i = 11, lastindex = 14, suffix[11] = 0, lastindex - i = 3, lastindex - suffix[11] = 14, gs[14] = 3

    13. i = 12, lastindex = 14, suffix[12] = 0, lastindex - i = 2, lastindex - suffix[12] = 14, gs[14] = 2

    14. i = 13, lastindex = 14, suffix[13] = 0, lastindex - i = 1, lastindex - suffix[13] = 14, gs[14] = 1

 

代码实现

#include <iostream>
#include <cstring>
using namespace std;
int* bc_Build(char* pattern)
{
	int* bc = new int[256];
	int sz = strlen(pattern);
	for (int i = 0; i < 256; i++)
	{
		bc[i] = -1;
	}
	for (int j = 0; j < sz; j++)
	{
		bc[pattern[j]] = j;
	}
	return bc;
}

int* suffix_Build(char* pattern)
{
	int sz = strlen(pattern);
	int* suffix = new int[sz];
	int num = 0;
	suffix[sz - 1] = sz;
	for (int i = sz - 2; i >= 0; i--)
	{
		for (num = 0; num <= i && pattern[i - num] == pattern[sz - 1 - num]; num++);
		suffix[i] = num;
	}
	return suffix;
}

int* gs_Build(char* pattern)
{
	int sz = strlen(pattern);
	int lastindex = sz - 1;
	int* suffix = suffix_Build(pattern);
	int* gs = new int[sz];
	//找不到对应的字串和前缀
	for (int i = 0; i < sz; i++)
	{
		gs[i] = sz;
	}
	//找前缀
	for (int i = lastindex; i >= 0; i--)
	{
		if (i + 1 == suffix[i])
		{
			for (int j = 0; j < lastindex - i; j++)
			{
				if (gs[j] == sz)
				{
					gs[j] = lastindex - i;
				}
			}
		}
	}
	//找后缀
	for (int i = 0; i < lastindex; i++)
	{
		gs[lastindex - suffix[i]] = lastindex - i;
	}
	return gs;
}

int BM_match(char* Text, char* Pattern)
{
	int* bc = bc_Build(Pattern);
	int* gs = gs_Build(Pattern);
	int Tsz = strlen(Text);
	int Psz = strlen(Pattern);
	int Plastindex = Psz - 1;
	int tp = 0;					//目标串和模式串的对齐位置
	int cmp;					//此时正在比对元素模式串中的指针
	while (tp + Psz <= Tsz)
	{
		for (cmp = Plastindex; cmp >= 0 && Text[cmp + tp] == Pattern[cmp]; cmp--);
		if (-1 == cmp)
		{
			break;
		}
		else
		{
			tp += max(gs[cmp], cmp - bc[Text[tp + cmp]]);
		}
	}
	delete[] bc;
	delete[] gs;
	return (tp + Psz <= Tsz) ? tp : -1;
}

int main()
{
	char Text[] = "abcaadefggggsersgsetset";
	char Pattern[] = "gggg";
	int ret = BM_match(Text, Pattern);
	cout << ret << endl;
	system("pause");
	return 0;
}

时间复杂度: O(n)

 

五、BF、KMP和BM算法性能比较

下面这张图就可以直观的描述三种算法的性能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kHpZ9sMX-1668485073731)(字符串匹配.assets/image-20221110232705301.png)]

最好情况最坏情况
BF算法O(n + m)O(n * m)
KMP算法O(n + m)O(n + m)
BM算法O(n / m)O(n + m)

从上面的比较可以看出BM算法无论是在最好情况亦或是最坏情况效率相比于其他两种算法都是较优的

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Python中,有多种方法可以进行字符串匹配,具体的选择取决于你的需求和场景。以下是几种常见的字符串匹配方法: 1. 使用`==`运算符进行匹配:你可以使用`==`运算符来判断两个字符串是否相等。例如: ```python str1 = "hello" str2 = "world" if str1 == str2: print("字符串匹配成功") else: print("字符串匹配失败") ``` 2. 使用`in`关键字进行匹配:你可以使用`in`关键字来判断一个字符串是否包含另一个字符串。例如: ```python str1 = "hello world" str2 = "world" if str2 in str1: print("字符串匹配成功") else: print("字符串匹配失败") ``` 3. 使用`find()`方法进行匹配:`find()`方法可以用来查找一个子字符串在原字符串中的位置。如果找到了子字符串,它会返回子字符串的起始位置;如果没有找到,它会返回-1。例如: ```python str1 = "hello world" str2 = "world" index = str1.find(str2) if index != -1: print("字符串匹配成功,起始位置为", index) else: print("字符串匹配失败") ``` 4. 使用正则表达式进行匹配:正则表达式是一种强大的字符串匹配工具,可以用来匹配更加复杂的字符串模式。Python提供了`re`模块来支持正则表达式操作。例如,你可以使用`re.search()`函数来查找一个字符串中是否存在匹配某个模式的子字符串: ```python import re str1 = "hello world" pattern = r"world" match = re.search(pattern, str1) if match: print("字符串匹配成功") else: print("字符串匹配失败") ``` 这些是一些常见的字符串匹配方法,你可以根据具体的需求选择合适的方法来进行字符串匹配

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值