字符函数和字符串函数的模拟实现及KMP算法

求字符串长度

strlen

函数功能

求一个字符串的长度,计算的是’\0’之前出现的字符个数

函数参数

size_t strlen( const char *string );
# size_t 是函数的返回类型
# char* string 是函数参数

模拟实现

size_t my_strlen(char *str)
{
	int count = 0;
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}

在这里插入图片描述

注意事项

1.strlen的结束标志是’\0’,因此要求传给strlen的参数必须带’\0’,否则strlen的结果就是随机值。(因为strlen会一直向后访问,直到找到’\0’。可以说是一个大情种了(手动滑稽))。

2.strlen的返回值是无符号整形,如果用两个strlen相减的话得到的结果始终是非负数。

长度不受限制的字符串函数

strcpy

函数功能

字符串拷贝,将一个字符串的内容拷贝到另一个字符串中(包括’\0’)

函数参数

char* strcpy(char * destination, const char * source );
# char* 是函数的返回值,返回的是目标空间的起始地址
# source 是要拷贝的字符串
# destination 是目标空间

模拟实现

char* my_strcpy(char* dest, char* src)
{
    assert(dest&&src);//assert断言,保证dest和src不为空指针,头文件为#include<assert.h>
	char* ret = dest;//函数要求返回目标空间的起始地址
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

while循环里的语句是将src指向的内容拷贝到dest当前指向的位置,再用拷贝的结果来作为判断,最后对两个指针进行自增操作。

在这里插入图片描述

注意事项

源字符串必须要是以"\0"结束

目标空间要足够大

目标空间必须可修改。

strcat

函数功能

字符串追加,在一个字符串的末尾追加另一个字符串(包含’\0’)

函数参数

char * strcat ( char * destination, const char * source );
# char* 是函数的返回值,返回的是目标空间的起始地址
# source 是要追加的字符串
# destination 是目标空间

模拟实现

char* my_strcat(char* dest, char* src)
{
    assert(dest&&src);
	char* ret = dest;
	//先找到dest的末尾,再字符串拷贝

	while (*dest != '\0')
	{
		dest++;
	}

	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

在这里插入图片描述

注意事项

1.源字符串必须以’\0’结束

2.目标空间必须足够大,能够放下追加后的字符

3.目标空间必须是可修改的

4.strcat不能自己给自己追加。(因为自己给自己追加会覆盖掉末尾的’\0’,导致死循环)

strcmp

函数功能

字符串比较,比较两个字符串大小,如果相等则往后移,直到’\0’结束

函数参数

int strcmp ( const char * str1, const char * str2 );
# int 函数返回值
# char* str1  char* str2 用于比较的两个字符串

#返回值
>0 : str1 大于 str2;
=0 : str1 等于 str2;
<0 : str1 小于 str2

模拟实现

int my_strcmp(const char* str1, const char* str2)
{
	assert(str1 && str2);
	//如果相等就往后比较
	while (*str1 != '\0' && *str2 != '\0' && *str1 == *str2)
	{
		//相等的情况在这里,如果进来循环并且有一个为\0,那就说明两个字符串都到结尾了
		if (*str1 == '\0')
			return 0;

		str1++;
		str2++;
	}

	/*if (*str1 > *str2)
		return 1;
	else
		return -1;*/
	return(*str1 - *str2);//第一个字符串大于第二个字符串,则返回大于0的数字,第一个字符串小于第二个字符串,则返回小于0的数字。既然它们不相等,那么直接作差就能得到大小
}

在这里插入图片描述

注意事项

字符在内存中是以ASCII码值存储的,因此比较的也是每一对字符的ASCII码值

长度受限制的字符串函数

由于strcpy,strcat,strcmp等字符串函数不受长度的限制,容易造成越界的问题存在安全隐患。因此C语言还给我们提供了另外几种相对安全的字符串函数,即strncpy,strncat,strncmp。它们比原字符串函数多了一个参数,这个参数是用于指定操作的字节数。因为受到长度的限制,不会无脑梭哈,因此也相对更安全。(不是绝对安全,毕竟我要写bug谁也拦不住(斜眼笑))。

strncpy

函数功能

字符串拷贝,把一个字符串中指定的字节数的内容拷贝到另一个字符串中

函数参数

char * strncpy ( char * destination, const char * source, size_t num );
# char* 是函数的返回值,返回的是目标空间的起始地址
# source 是要拷贝的字符串
# destination 是目标空间
# num 是要拷贝的字节数

模拟实现

char* my_strncpy(char* dest, const char* src, size_t num)
{
	assert(dest && src);
	char* ret = dest;  //记录目标空间的起始地址
	while (num--)
	{
		*dest = *src;
		if (*dest == '\0')  //如果*dest为0,说明将src的结束字符赋给了dest,这种情况下源字符串的长度小于num
		{
			while (num--)
			{
				//如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。(与库函数的实现方式保持一致)
				*dest++ = 0;
			}
			break;
		}
		dest++;
		src++;
	}
	*dest = '\0';  //在dest的末尾补上结束字符
	return ret;
}

在这里插入图片描述

在这里插入图片描述

注意事项

可以看到,如果源字符串中的内容不够num个,会把用’\0’来补足。

strncat

函数功能

字符串追加,将一个字符串中num个字节的内容拷贝到另一个字符串的末尾,并在末尾补充’\0’。

函数参数

char * strncat ( char * destination, const char * source, size_t num );
# char* 是函数的返回值,返回的是目标空间的起始地址
# source 是要追加的字符串
# destination 是目标空间
# num 是要追加的字节数

模拟实现

char* my_strncat(char* dest, const char* src, size_t num)
{
	assert(dest && src);
	char* ret = dest;  //记录目标空间的起始地址

	//找到dest末尾
	while (*dest != '\0')
	{
		dest++;
	}

	//strncpy
	while (num--)
	{
		*dest = *src;
		if (*dest == '\0')  //如果*dest为0,说明将src的结束字符赋给了dest,这种情况下源字符串的长度小于num
		{
			//如果源字字符串的长度小于num,则只复制到终止空字符的内容。(与库函数的实现方式保持一致)
			return ret; //直接返回
		}
		dest++;
		src++;
	}
	*dest = '\0';  //则在dest的末尾补上结束字符
	return ret;
}

在这里插入图片描述

注意事项

如果源字符串的内容少于num个字节,也只复制到终止空字符的内容,也就是说并不会再去补’\0’。

strncmp

函数功能

字符串比较,比较两个字符串中前num个字节内容的大小

函数参数

int strncmp ( const char * str1, const char * str2, size_t num );
# int 函数返回值
# char* str1  char* str2 用于比较的两个字符串
# num 要比较的字节数

#函数返回值
>0 : str1 大于 str2;
=0 : str1 等于 str2;
<0 : str1 小于 str2

模拟实现

int my_strncmp(const char* str1, const char* str2, int num)
{
	assert(str1 && str2);
	for(int i=0;i<num;i++)//库中使用的是for循环,因此我们也用for循环
	{
		if (*str1 == *str2)
		{
			str1++; 
			str2++;
		}
		else
		{
			return(*str1 - *str2);
		}
	}
	return 0;
}

在这里插入图片描述

注意事项

比较的是每一对字符中的ASCII码值。

对前num个字节逐一比较,除非遇到’\0’或者不相等字符。

字符串查找函数

strstr

函数功能

查找子串,在一个字符串中查找是否包含该子串

函数参数

char * strstr ( const char *str1, const char * str2); 
# char* 函数返回值,返回字符串中子串的起始地址,若找不到,则返回NULL
# char* str1 要搜索的字符串;
# char* str2 子串

模拟实现

分析:

在这里插入图片描述

代码实现:

char* my_strstr(const char* str1, const char* str2)
{
	assert(str1 && str2);
	//根据分析,至少要三个指针

	const char* s1 = str1;
	const char* s2 = str2;
	const char* p = str1;

	while (*p)
	{
		s1 = p;//
		s2 = str2;
		while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2)
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
			return (char*)p;

		p++;//p记录的是上次匹配的起始位置,匹配失败要从上次的位置的下一个位置开始匹配
	}

	return NULL;
}

总结

每次匹配失败后,子串回到起始位置,主串回到上次匹配的起始位置的下一个位置。

注意事项:被查找的主串和子串都不能为空串,且都要以"\0"结尾。如果查找成功则返回主串中子串所在位置的地址,查找失败则返回空指针。

KMP算法

strstr每次匹配失败子串都要回到起始位置,主串则是这个不行那就换下一个位置。这样的效率就很低,因为主串中的每一个位置都被尝试过,而子串也被遍历了很多遍。而KMP的一个优化就在于,匹配失败后主串的位置保持不动,而子串的位置回到一个特定位置(可能是起始位置也可能不是)。

引入

在这里插入图片描述

那么假设能在已经成功匹配的字符串中找到一个最大程度相同的串,那么如何确定子串回退的位置?

下面是两个例子:

在这里插入图片描述

这两个例子看起来好像很凑巧,哎,就是凑巧,就是玩~。其实关于子串回退的位置,KMP给定了一个next数组用于保存子串在某个位置匹配失败后应该回退的位置。

next数组

用next[ j ]=k 来表示子串在某个位置匹配失败应该回退的位置。其中 j 是子串匹配失败的位置,而 k 是子串在j位置匹配失败后应该回退的位置。

k值的求法

1.找到匹配成功部分两个相等的真子串(不包含本身),一个以下标0开始,一个以下标 “j-1 ”结束。

2.不管什么样的数据,规定:next[ 0 ]=-1,next[ 1 ]=0.

举例求next数组:

在这里插入图片描述

解析:

在这里插入图片描述

注意事项

这里说的相同的字符串,可以有部分内容是重叠的。但必须要是从 0 下标开始到 “j-1 ”下标结束。

k的值是以能找到的最大程度相同字符串的长度为准。

此时我们已经学会如何求next数组了,那么假设我们知道next[ i ]=k,我们如何求next[ i+1]?

这里就要用一点数学来推导了:

在这里插入图片描述

这里很凑巧的是子串的P[ i ]==P[ k ],那么如果子串的P[ i ]!=P[ k ]我们又如何知道next[ i+1 ]的值呢?

在这里插入图片描述

如果P[ i ] ! = P[ k ],那么可以说,当前的k位置的元素一定不是我们要找的,因此还要继续回退,即 k = next[ k ] ,也就是将next[ k ]的值赋给next,直到p[ i ] == p[ k ]。

值得注意的是,这些推导都是在next[ i ] = k的前提下才存在的。

next数组就是KMP算法的精髓,因为next数组的存在,子串不必每次都从头再来,主串也不必走回头路。学会了next数组的求法,就已经是掌握了KMP思想,接下来就是代码实现了。

代码实现

KMP代码:

//str代表主串
//sub代表子串
//pos代表从主串的pos位置开始查找

int KMP(char* str, char* sub, int pos)
{
	assert(str && sub);

	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(sizeof(int) * lenSub);
	assert(next);

	GetNext(sub,next, lenSub);
	int i = pos;//遍历主串
	int j = 0;//遍历子串

	while (i < lenStr && j < lenSub)
	{
		if (j==-1||str[i] == sub[j])//如果第一次匹配就失败,但next[0]=-1,此时j为-1,但其实只需要回退到0位置,sub[-1]越界为了避免这种情况,当j为-1时,直接放进去++。
		{
			i++;
			j++;
		}
		else
		{
			//匹配失败,i不回退,j回退到next[j]的位置
			j = next[j];
		}
	}
	if (j >= lenSub)
		return i - j;

	return -1;

}

这里需要留意的一点就是:规定next数组下标为0处的值为负一,但是子串从负一位置开始访问就是越界了。因此当 j 为负一时应该直接进入循环。

在这里插入图片描述

GetNext实现:

void GetNext(char* sub, int* next,int lenSub)
{
	next[0] = -1;
	next[1] = 0;
	int i = 2;
	int k = 0;//是钱一项的k

	while (i < lenSub)
	{
		if(k==-1||sub[i - 1] == sub[k])//本来是sub[i]==sub[k],但是这个i是已经自增一次后得到的i
		{
			next[i] = k + 1;
			i++;
			k++;
		}
		else
		{
			k = next[k];//sub[i-1]!=sub[k],要继续回退,此时的k应该等于当前k位置对应的next数组的值

		}
	}

}

这里同样有需要注意的地方:之前是已知next[ i ]的值求next[ i+1 ]。但这里是已知next[ i-1 ]求next[ i ]的值。

在这里插入图片描述

完整代码:


void GetNext(char* sub, int* next,int lenSub)
{
	next[0] = -1;
	next[1] = 0;
	int i = 2;
	int k = 0;//是钱一项的k

	while (i < lenSub)
	{
		if(k==-1||sub[i - 1] == sub[k])//本来是sub[i]==sub[k],但是这个i是已经自增一次后得到的i
		{
			next[i] = k + 1;
			i++;
			k++;
		}
		else
		{
			k = next[k];//sub[i-1]!=sub[k],要继续回退,此时的k应该等于当前k位置对应的next数组的值

		}
	}

}

int KMP(char* str, char* sub, int pos)
{
	assert(str && sub);

	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(sizeof(int) * lenSub);
	assert(next);

	GetNext(sub,next, lenSub);
	int i = pos;//遍历主串
	int j = 0;//遍历子串

	while (i < lenStr && j < lenSub)
	{
		if (j==-1||str[i] == sub[j])//如果第一次匹配就失败,但next[0]=-1,此时j为-1,但其实只需要回退到0位置,sub[-1]越界为了避免这种情况,当j为-1时,直接放进去++。
		{
			i++;
			j++;
		}
		else
		{
			//匹配失败,i不回退,j回退到next[j]的位置
			j = next[j];
		}
	}
	if (j >= lenSub)
		return i - j;

	return -1;

}

int main()
{
	printf("%d\n", KMP("ababcabcdabcde", "abcd", 0));//5
	printf("%d\n", KMP("ababcabcdabcde", "abcdef", 0));//-1
	printf("%d\n", KMP("ababcabcdabcde", "ab", 0));//0
	return 0;
}

在这里插入图片描述

注意:返回值是子串在主串中的位置。

内存操作函数

前面的strcpy,strcat,strcmp,strncpy,strncat,strncmp,strstr等都是字符串函数,也就是说它们只能对字符串进行操作。如果要对其他类型操作就需要学习内存操作函数。常见的内存操作函数有memcpy,memmove,memcmp,memset。因为内存操作函数可以操作的类型很多,我们不会知道使用者会给我们传什么类型的参数,因此我们在实现时通常将函数参数设定为泛型指针,在实现功能时以字节为单位。

memcpy

函数功能

内存拷贝,将一块内存中的num个字节拷贝到另外一块内存中。常用来处理空间不重叠的数据拷贝。

函数参数

void * memcpy ( void * destination, const void * source, size_t num );
# void* 函数返回值,返回dest内存空间的地址;
# void* destination 目标内存空间地址;
# void* source 源空间地址;
# size_t num 要拷贝的字节数;

模拟实现

void* my_memcpy(void* dest, const void* src, size_t num)
{
	assert(dest && src);
	char* ret = dest;
	//要将src中num个字节拷贝到dest中,因为不确定num的大小,所以我们一个字节一个字节的拷贝是最正确的选择

	while (num--)
	{
		*(char*)dest = *(char*)src;

		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return ret;
}

在这里插入图片描述

注意事项

在C语言标准中,memcpy只用来拷贝非重叠空间的数据。如果拷贝重叠空间的内容,由于mempy是由前向后拷贝的,这就会导致数据覆盖覆盖的情况发生,最后得到的结果不是我们想要的。C语言为了处理重叠的数据空间,给定了memmove函数。

memove

函数功能

内存移动,将一块内存数据的num个字节的内容向指定的位置移动。常用来处理重叠空间的数据拷贝。

函数参数

# memmove 函数的参数和 memcpy 函数完全相同
void * memmove ( void* destination, const void * source, size_t num );

模拟实现

memove的参数与memcpy的参数相同,那么memmove是如何实现重叠空间的拷贝的呢?

前面提到memcpy是从前向后拷贝的,而memmove之所以能处理重叠空间的拷贝,是因为memmove会根据情况的不同选择由前向后拷贝还是从后向前拷贝。

在这里插入图片描述

总结:src < dest就从前向后拷贝,其他都从后向前拷贝

代码实现:

void* my_memmove(void* dest, const void* src, size_t num)
{
	assert(dest && src);
	char* ret = dest;
	//总结就是分两种情况,src<dest就从前向后拷贝,其他都从后向前拷贝

	if (dest < src)
	{
		while (num--)
		{
			*(char*)dest = *(char*)src;
			
			dest = (char*)dest+1;
			src = (char*)src + 1;
		}
	}
	else
	{
		//从后向前拷贝,是指从最后一个字节开始,起始位置+(--num)就是最后一个字节

		while (num--)
		{

			*((char*)dest + num) = *((char*)src + num);//num在递减,每次加num就能直接实现从最后一个字节往前移动
		}
	}
}

在这里插入图片描述

memset

函数功能

内存设置,把一块内存中num个字节的内容设定为指定的数据。

函数参数

void *memset( void *dest, int c, size_t count );
# void* 函数返回值,返回目标空间的地址;
# int c 函数参数,指定你想要初始化的数据;
# size_t count 函数参数,指定初始化的字节数

模拟实现

void* my_memset(void* dest, int k, size_t num)
{
	assert(dest);

	char* ret = dest;
	//将dest的num个字节全部初始化为k

	while (num--)
	{
		*(char*)dest = (char)k;

		dest = (char*)dest + 1;
	}
	return ret;

在这里插入图片描述

memcmp

函数功能

内存比较,比较两块内存中前num个字节内容的大小。

函数参数

int memcmp ( const void * ptr1, const void * ptr2, size_t num );
# int 函数返回值;
# void* ptr1 void* ptr2 要比较的两块内存;
# size_t num 要比较的字节数;

#函数返回值
>0 : ptr1 大于 ptr2;
=0 : ptr1 等于 ptr2;
<0 : ptr1 小于 ptr2;··

模拟实现

int my_memcmp(const void* str1, const void* str2, size_t num)
{
	assert(str1 && str2);
	int count = 0;
	while (count < num&& *(char*)str1 == *(char*)str2)
	{
		count++;
		str1 = (char*)str1 + 1;
		str2 = (char*)str2 + 1;
	}

	if (count == num)
	
		return 0;

	else

		return (*(char*)str1 - *(char*)str2);
}

在这里插入图片描述

  • 7
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

别动我的饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值