字符串查找 - 模拟实现strstr 、BF算法 、 KMP算法

文章目录

前言

一、模拟实现库函数strstr

二、BF算法

三、KMP算法

总结


前言

路漫漫其修远兮,吾将上下而求索。


一、模拟实现库函数strstr

Tips:此处采用利用指针+字符串末尾'\0' 的判断,当然你可以利用数组的下标;

库函数strstr 的原型:

char * strstr ( const char *str1, const char * str2);

strstr 的工作原理分析:在主串中寻找字串;可以在str1 中找到str2 ,便返回str1 中找到str2 位置的首字符的地址;如若找不到,便会返回NULL;

显然,想要一个字符串(str1)中找到另一个字符串(str2),首先得在主串(str1)中找到其(str2)首字符,然后再将其”身体“部分一一比对;如果到了str2 ”尾部‘都相同,就说明能在str1 中找到字符串str2 ; 而如若,在比对“身体”部分的时候,发现存在不同得地方(字符),就说明在str1 中找的这个头不对 (前提是,这个“头”在主串str1 的范围中),那便换一个头往后,再继续比对“身体"部分……

显然,查找是一个循环的过程;

在上述代码中,我们需要一个指针在str1 中找"头" ,并且需要两个指针分别在 str1 ,str2 中比对”身体“部分;且在存在”头”找错的情况,所以要将用来比对身体部分的指针进行”刷新“;

代码如下:

#include<stdio.h>
#include<assert.h>

char* my_strstr(const char* str1, const char* str2)
{
	assert(str1 && str2);
	const char* p = str1;//“头”
	const char* s1 = str1;//工具指针
	const char* s2 = str2;//工具指针
	//在str1 的范围内
	while (*p)
	{
		//刷新工具指针
		s1 = p;
		s2 = str2;
		//当头相同时,比较“身体”;当然得在合理范围内
		while ((*s1 == *s2) && *s1 && *s2)
		{
			s1++;
			s2++;
			//身体相同时,当到str2 到达尾部,说明能在str1 中找到str2
			if (*s2 == '\0')
			{
				return (char*)p;
			}
		}
		//出了“身体”相等的循环,说明身体不相等-->换“头”
		p++;
	}
	//等到了这里还没有返回值,说明在str1 中找不到str2 
	return NULL;
}
int main()
{
	char ch1[] = "accbcbbbcccdbbb";
	char ch2[] = "ccd";
	char* tmp = my_strstr(ch1, ch2);
	printf("%s\n", tmp);

	return 0;
}

代码运行情况如下:

二、BF算法

其实上面模拟实现库函数strstr 中就很“暴力”,BF算法也是如此,跟模拟实现strstr 的逻辑一样;但是有两种思考逻辑;

一是,利用指针+'\0' ;二是,利用数组的下标和字符串长度;

为了更好衔接KMP算法,此处利用数组的下标来实现BF算法;

分析:跟模拟实现strstr 的思想很像,只不过是将专门指向“头”的指针的功能让工具指针也具备罢了;

图解如下:

代码如下:(模拟strstr 的味道极重)


//BF算法
#include<stdio.h>
#include<assert.h>
#include<string.h>

int BF(const char* str, const char* sub)
{
	assert(str && sub);//因为要对str1 srt2 进行解引用操作,所以要避免str1 str2 为空指针
	int len_str = strlen(str);
	int len_sub = strlen(sub);
	int i = 0;//负责str 的下标
	int j = 0;//负责 sub 的下标

	//在查找的范围内
	while (i < len_str && j < len_sub)
	{
		j = 0;//每一次的刷新

		while (str[i] == sub[j] && i <len_str && j< len_sub)
		{
			i++;
			j++;
			
		}

		if (j >= len_sub)
		{
			return 1;
		}
		i = i - j + 1;

	}
	//出了循环就说明没有找到
	return 0;
}

int main()
{
	char ch1[] = "accbcbbbcccdbbb";
	char ch2[] = "ccd";
	int ret = BF(ch1, ch2);
	if (ret)
		printf("能找到\n");
	else
		printf("找不到\m");

	return 0;
}

代码输出结果如下:

改进的BF代码:

如下:

//BF算法
#include<stdio.h>
#include<assert.h>
#include<string.h>

int BF(const char* str, const char* sub)
{
	assert(str && sub);
	int len_str = strlen(str);
	int len_sub = strlen(sub);
	int i = 0;//负责str 的下标
	int j = 0;//负责 sub 的下标

	//在查找的范围内
	while (i < len_str && j < len_sub)
	{
		if (str[i] == sub[j])
		{
			i++;
			j++;
		}
		else
		{
			//刷新
			//非常巧妙,当j = 0,而 str[i] != sub [j]--> i = i +1;相当于i 自增了;
 			i = i - j + 1;
			j = 0;
		}
	}
	if (j >= len_sub)
	{
		return 1;
	}
	return 0;
}

int main()
{
	char ch1[] = "accbcbbbcccdbbb";
	char ch2[] = "ccd";
	int ret = BF(ch1, ch2);
	if (ret)
		printf("能找到\n");
	else
		printf("找不到\n");

	return 0;
}

代码运行结果如下:

三、KMP算法

在BF算法中,需要遍历str中的所有字符来找sub 中的首字符;这不太高效,显然KMP就是优化BF算法的一种算法;

KMP算法是一种字符串匹配算法,由D.E.Knuth.J.H.Morris和V.R.Pratt提出,因此被称为克努特-莫里斯-普拉特操作(简称KMP算法)。该算法的核心是利用匹配失败后的信息,尽量减少使用子串与主串的匹配次数,即在匹配成功得串中找与字串其前段字符匹配的串,以达到快速匹配的目的;具体实现是通过一个数组:next 数组,每次匹配失败后j 回退的下标的信息在next 数组中(此next 数组中会包含字串的局部匹配信息);想要得到数组需要通过写函数计算得到;

KMP算法的实现涉及到前缀和后缀相等的部分的最大长度的计算,以及如何利用这个信息来优化匹配过程。这个过程虽然看起来复杂,但通过逐步的学习和理解,可以逐渐掌握其精髓。

KMP算法的时间复杂度 为O(m+n)

KMP与BF 的不同所在:

1、BF算法中, i 会返回到上一次匹配开始的下一个坐标( i = i - j +1);而在KMP算法中,i 始终不会回退,只会前进

2、在BF算法中,j 在每一次匹配失败后会返回到其起始位置即下标为0 的位置上;但是在KMP算法中,j 返回到下标为几 的位置上(j 会返回到特殊位置上),next 数组说了算,即存在 next[ j ] = k ;

在上图中,你可以发现,str [ i ]  前面的 abca 与 sub [ j ] 前面的 abca 是相同的而在字串中,前面两个ab 等于后面两个ab ,所以可以得到主串中靠近 str[i] 的ab 与字串中第一个ab 相同;

见下图,主串中的 i 不变,而只需要比较str [ i ] 是否与sub[ j ](此 j 是移动之后的j ) 相等 ;如果相等,则 i 与 j 便继续向前(右)比对;如若不相等,则说明此 i 前面的部门字符串可以舍弃,而 j 便会回到 下标为0 的位置上;

相当于,主串中的 i 的位置不变,但是得在这部分字符(从i 与sub 比对得开头到有差异这段)中找找看有没有与字串相同字串的部分字符段(限制:一个必须为下标为0 的位置开始,另一个必须以 j-1 的下标结尾),有的话,j 便移动到特定位置上;没有的话,j 便回归到下标为0 的位置上;通俗的讲就是,在比较这个比对过程中其最左边的字符段有没有和最右边得字符串相同的 (本质上也是在找合适可以匹配的开头,不过相较于暴力的BF一个一个地找,KMP就更加灵活,为字串中每一个可能不会匹配成功地字符对应了相应地返回坐标);

在下图中,j 回退到了下标为2  的位置上,因为在字串中,找到以两个相等的真字串,其中一个以下标为0 开始,另外一个以下标为 j-1 结束;而此真字串的长度放在字串的开头,就可以被看作已经被比对过,自然 j = 2 ;此时:

之前我们说过,在KMP 算法中,j 回退到的位置由next 数组决定的;在以上描述中,我们视乎可以看到一些计算next 数组中元素的苗头;即,在已经匹配的字符串中找两个真字串,其中一个字串必须以下标为0的字符开头,另外一个字串必须以 j-1 对应位上的字符结尾(对位置的强制要求);若存在这两个真字串(不包含本身),那么j 回退的位置就为此真字串的长度,这样说的原因是因为下标为0的字段的后面的坐标就是此字段的长度;

再次回顾一下,next 数组是用来保存字串中某个位置匹配失败后所要回退的位置(下标);即存在next [ j ] = k; j 为字串当前匹配失败的位置,k 为 j 匹配失败后所要回退的下标;

接下来我们来计算一下以上示例中其字串的 next 数组是如何计算得到的 

规则:

1、找到匹配成功部分的两个相等的真字串(不包含其本身),一个以下标为0的字符作为开头,另一个以下标为 j - 1 的字符作为结尾

2、不管是什么数据 next [0] = -1; next[1] = 0; (在不同的资料中next数组中的第一个元素和第二个元素为1 和 0 , 其实影响也不是很大);

过程如下图所示:

原理是如此得到的,那么推广到代码让计算机通过代码根据不同的字串而得到对应的next 数组呢?

其实很简单,如上图;

假设我们知道当前下标为j 时对应下标数组中next 中的元素,而去求 next[j+1];next[j] = k; k 的意思是当子串在 j 下标时与主串匹配失败,字串便回退到下标为 k 的位置上去;而字串回退到该下标的原理是,在字串与主串匹配成功的部分字段中找此段(匹配成功的部分字)结尾部分有没有和前部分字符段相同的部分,已达到 i 不变避免在主串中错过合适的开头;如果找得到,j 便回退到找到找到的串的后面下标处;如果找不到,j 会继续回退,直到 j  回退为0,则说明在此匹配成功的字符段中没有与子串开头字符相匹配的字段,所以可以舍弃,i 在主串不动的位置开始重新与子串进行匹配,j即 回退到下标为0 的位置上进行“刷新”;

代码如下:

#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>

void GetNext(const char* sub, int* next)
{
	//已经拍过空
	int lensub = strlen(sub);
	//规定
	next[0] = -1;
	next[1] = 0;
	int j = 2;//从下标为2 开始计算
	int k = 0; //因为 next[j-1]=0; 所以 k= 0,k 代表的是 j 前一个下标对应的next数组的值
 
	while (j < lensub)//数组next 所需要计算的元素个数
	{
		if ((sub[j - 1] == sub[k])||(k==-1))
		{
			next[j] = k + 1;
			j++;//调整
			k++;
		}
		else
		{
			//不相等便继续回退
			k = next[k];//但是如果 next[k]==-1;就代表着k 要回到下标为0 的位置上;
		}
	}
}

int KMP(const char* str, const char* sub, int pos)
{
	assert(str && sub);//因为要对str sub 进行解引用操作,就要保证str sub 不为空指针
	int i = pos;//在主串下标为 pos 的位置开始查找子串
	int j = 0;
	int lenstr = strlen(str);
	int lensub = strlen(sub);
	if (lenstr == 0 || lensub == 0)
		return -1;
	if (pos < 0 || pos >= lenstr)
		return -1;

	int* next = (int*)malloc(lensub * sizeof(int));//next 数组在内存中开辟的空间
	assert(next);//排空
	//调用函数计算next 数组中元素的值
	GetNext(sub, next);//next数组是根据子串来计算得到的

	//得到next 数组,便可以实现查找
	while (i < lenstr && j < lensub)//在合理的范围之内
	{
		if ((str[i] == sub[j])|| (j==-1))
		{
			i++;//调整
			j++;
		}
		else//不相同的话,i 只会前进而不会回退,j 会根据 next 数组而回退到一定下标的位置上
		{
			j = next[j];//next 数组下标为0 的元素为-1,其本质是想让j 回退到下标为0的位置上;
		}
	}
	//记得释放空间
	free(next);
	//出了此范围有两种情况,一是 i >= lenstr ;二是 j >= lensub ;
	if(j >= lensub)
	{
		return i - j;
	}
	//否则就是没有找到
	return -1;
}

int main()
{
	//KMP算法
	char ch1[] = "abbcdabccdabcdabcaddd";
	char ch2[] = "abca";
	int ret = KMP(ch1, ch2, 0);
	if (ret == -1)
		printf("没有找到\n");
	else
	    printf("在主串下标为%d处找到了子串\n", ret);

	return 0;
}

代码运行结果如下:


总结

BF算法其实和模拟实现strstr 的逻辑很像, 或者说就是一样的感觉~

但是KMP 算法中 在主串负责管理下标 的 i 只会前进而不会回退; 而 j会根据next 数组中的对应的值而回退到特定位置上,即存在 next[j]= k ; 

  • 18
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值