理解C语言(三) 字符串处理函数


1 字符串基础

字符串是一种重要的数据类型,有零个或多个字符组成的有限串行。

定义子串: 串中任意个连续的字符组成的子序列,并规定空串是任意串的子串,任意串也是其自身的子串,如字符串"adereegfb"中它本身、空串、诸如"ader"连续的字符串都是它的子串。子序列则不要求字符连续,但顺序要与主串保持一致,若有"abcd"与"ad"则两者的最长公共子序列为"ad"。在动态规划中计算最长公共子序列和最长公共子串中一定要能区分这两个概念!

在C语言中并没有显示的字符串类型,它有如下两种风格的字符串:

  • 字符串常量: 以双引号扩起来的字符序列,规定所有的字符串常量都由编译器自动在末尾添加一个空字符
  • 字符数组: 末尾添加了'\0'的字符数组,一般需要显示在末尾添加空字符。
char c1[]={'c','+','+'}; //末尾没有空字符
char c2[]={'c','+','+','\0'}; //末尾显示添加空字符
char c3="c++"; //末尾自动添加空字符

注意到通过字符数组初始化和字符串常量初始化并不完全相同的。因为字符串常量包含一个额外的空字符用于结束字符串,用它来初始化创建数组时,末尾会自动添加空字符。所以c1的长度是3,后两者的长度是4,并且字符数组c2和c3都被称为C风格字符串,而字符数组c1不是C风格字符串。

规定C风格的字符串都是以NULL空字符('\0')作为终结符结尾。由于它是字符串的终止符,但它本身并不是字符串的一部分,所以字符串的长度并不包括NULL字节,如strlen函数。而且C标准库中提供的各种字符串处理函数都要求提供的字符串或字符数组必须以空字符结束,否则会出现不可预料的结果。如:

char c[]={'c','+','+'};
printf("%d\n",strlen(c)); //结果输出为6,这是不正确的


2 标准库中的字符串处理函数

C标准库中头文件<string.h>定义了两组字符串函数(C++中用<string>表示)。

  • 第一组函数的名字以str开头,它主要处理以'\0'结尾的字符串,所以字符串内部不能包含任何'\0'字符。
  • 第二组函数的名字以mem开头,主要考虑非字符串内部含有零值的情形,它能够处理任意的字节序列,操作与字符串函数类似
  • 除了memmove函数外,其他函数都没定义重叠对象间的行为

为了提高程序在不同机器上的移植性,利用typedef定义新类型名,即typedef unsigned int size_t。 程序员必须要保证目标字符数组的空间能够足以存放结果字符串(有可能存在字符数组溢出的危险)

  • 字符串处理类

如下表为字符串处理函数说明,变量s,t的类型是char *, cs和ct的类型是const char *;n的类型为size_t,c的类型为int

  • 内存操作类

按照字节数组的方式操作对象,提供一个高效的函数接口(提供字节流的访问)。其中s,t类型是void * , cs,ct的类型是const void *; n类型为size_t,c类型为int

总结起来,头文件< string.h>实现了如下函数:

  • 长度计算、长度不受限和受限的复制、连接和比较版本的函数
  • 基础字符串查找(查找一个字符、一组字符和匹配一个子串)、高级字符串查找(查找子串前缀位置、返回token标记)
  • 处理任意字节序列的内存操作如复制、比较、查找和初始化等函数

2.1 手写字符串处理函数

A strlen/strcmp/strcpy/strcat等函数

代码实现和测试如下:

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

/***********************************
 *
 * 基本的字符串函数
 * strlen/strcmp/strcpy/strcat/strncmp/strncpy/strncat
 * strdup/strrev
 * atoi/strtod
 *
 **********************************/

size_t my_strlen(const char *src) {
    assert(src != NULL);
    /*方法1*/
    int len = 0;
    while(*src++ != '\0') {//函数退出条件是src='\0'但之后还进行了自增运算
        len++;
    }
    return len;
    /*方法2*/
    // const char *psrc = src;
    // while(*psrc++ != '\0') ;
    // return psrc - src - 1;
}

/**
 * 切记不可用*s++ == *t++
 * 因为不相等时时还会继续比较一次,自增运算应放在循环体内
 */
int my_strcmp(const char *s,const char *t) {
    assert(s != NULL && t != NULL);
    while(*s == *t) {
    	if(*s == '\0')
    		return 0;
    	s++;
    	t++;
    }
    // return ((*(unsigned char *)s > *(unsigned char *)t) > 0)? 1: -1;
    return ((*s - *t) > 0)?1:-1;
}

int my_strncmp(const char *s,const char *t,size_t n) {
	assert(s != NULL && t != NULL);
	while(n-- && *s == *t) { //条件用n判断但之后n减少了1
		if(n == 0 && *s == *t)
			return 0;
		s++;
		t++;
		
	}
	return ((*s - *t) > 0)? 1: -1;
}


/**
 * 要求src和dst不重叠,且dst有足够空间
 */ 
char *my_strcpy(char *dst,const char *src) {
    if(src == dst) return dst;
    assert(src != NULL && dst != NULL);
    char *pdst = dst;
    while(*pdst++ = *src++);
    //*pdst = '\0'; //该代码可以忽略
    return dst;
}

char *my_strncpy(char *dst,const char *src,size_t n) {
	assert(src != NULL && dst != NULL);
	char *pdst = dst;
	while(n-- > 0 && *src != '\0')
		*pdst++ = *src++;
	*pdst = '\0'; //切记勿忘
	return dst;
}


char *my_strcat(char *dst,const char *src) {
	assert(src != NULL && dst != NULL);
	char *pdst = dst;
	while(*pdst) pdst++;
	while((*pdst++ = *src++) != '\0');
	//*pdst = '\0'; //该行可以忽略
	return dst;
}


char *my_strncat(char *dst,const char *src,size_t n) {
	assert(src != NULL && dst != NULL);
	char *pdst = dst;
	while(*pdst) pdst++ ; 
	while(n-- && (*pdst++ = *src++)) ;
	*pdst = '\0';
	return dst;
}

/*字符串拷贝到新位置,需要配合free使用*/
char *my_strdup(const char *src) {
	if(src == NULL) return NULL;
	/*先计算字符串长度*/
	size_t len = my_strlen(src);
	char *new_addr = malloc(len + 1);
	char *res = new_addr;
	while((*new_addr++ = *src++) != '\0');
	return res;
	// 测试
	// char *str = my_strdup("hello world!");
	// printf("%s\n",str);
	// free(str);
}

char *my_strrev(char *src) {
	assert(src != NULL);
	char *s = src;
	char *t = src + my_strlen(src) - 1;
	while(s < t) {
		*s ^= *t;
		*t ^= *s;
		*s ^= *t;
		s++;t--;
	}
	return src;
	// 测试
	// char s[] = "hello";
	// printf("%s\n",my_strrev(s)); //不能使用字符串,因为字符串是常量,无法修改
}

/**
 * 字符串转整数即atoi
 * 如果第一个非空格字符存在,从数字或者正负号开始做类型转换,
 * 检测到非数字或者结束符时停止转换返回相应整数;否则溢出时就返回0
 * 判断字符串是不是数字,类似于strtod
 */
int my_atoi(const char *src) {
	const char *p = src;
	while(*p && (*p == ' ' || *p == '\t' || *p == '\n') ) p++;
	long long res = 0;
	bool flag = false;
	bool valid = false;
	if(*p == '+')
		p++;
	else if(*p == '-') {
		p++;
		flag = true;
	}
	/*检测到非数字字符时停止转换,返回整形数否则返回0*/
	for(;*p && (*p >= '0' && *p <= '9');p++) {
		int sign = (flag == true)?-1:1;
		res = res * 10 + sign * (*p - '0');
		if((flag && res < 0x80000000) || (!flag && res > 0x7fffffff)) {
			res = 0;
			break;
		}
	}
	if(*p == '\0') {
		valid = true;
	}
	return (int)res;
}

void test() {
	if(my_strlen("") == strlen(""))
		printf("test successful!\n");
	else
		printf("test failed\n");
	if(my_strlen("hello") == strlen("hello"))
		printf("test successful!\n");
	else
		printf("test failed\n");
  	if(my_strlen("hello world") == strlen("hello wrold"))
		printf("test successful!\n");
	else
		printf("test failed\n");
}



void test1() {
	if(my_strcmp("3357","3367") == strcmp("3357","3367"))
		printf("test successful!\n");
	else
		printf("test failed\n");
	if(my_strcmp("hello","hi") == strcmp("hello","hi"))
		printf("test successful!\n");
	else
		printf("test failed\n");
	if(my_strcmp("help","hello") == strcmp("help","hello"))
		printf("test successful!\n");
	else
		printf("test failed\n");
	if(my_strcmp("help","help") == strcmp("help","help"))
		printf("test successful!\n");
	else
		printf("test failed\n");
	if(my_strncmp("hello","hi",1) == strncmp("hello","hi",1))
		printf("test successful!\n");
	else
		printf("test failed\n");

	if(my_strncmp("hello","hi",1) == strncmp("hello","hi",1))
		printf("test successful!\n");
	else
		printf("test failed\n");
	if(my_strncmp("help","help",2) == strncmp("help","help",2))
		printf("test successful!\n");
	else
		printf("test failed\n");
}


void test2() {
	char dst[20];
	char dst1[20];
	//strcmp(my_strcpy(dst,"hello"),strcpy(dst1,"hello")) == 0;
	if(strcmp(my_strncpy(dst,"hello",3),strncpy(dst1,"hello",3)) == 0)
		printf("test successful!\n");
	else
		printf("test failed\n");
}

void test3() {
	char dst[20] = {'h','e','\0'};
	//strcmp(my_strncat(dst,"world",3),"hewor") == 0;
	if(strcmp(my_strcat(dst,"world"),"heworld") == 0)
		printf("test successful!\n");
	else
		printf("test failed\n");
}

int main() {
	typedef void (*func)();
	func test_func[] = {test,test1,test2,test3};
	int len = sizeof(test_func) /sizeof(test_func[0]);
	for(int i = 0; i < len;i++)
		test_func[i]();
    return 0;
}

实现时要注意一个细节,指针的自增运算与循环条件结束的问题。例如为什么在strcat实现中使用该函数遍历到字符串结束符会出现问题。这是因为该语句首先判断*pdst != '\0',不论是否满足条件都会执行指针的自增运算,也就是说都执行到了'\0'字符的下一个位置了,如果还需要利用到尾字符这样就出错了。

再例如while(*s++ = *t++);: 它首先是*s = *t;然后再判断*s != '\0';若满足继续循环,否则退出循环。但是务必注意的是不论继续循环与否接下来要执行s +=1,t += 1;

B 字符串查找函数

有以下几个查找函数:

  • strchr/strrchr/strspn/strcspn/strpbrk
  • strstr/strtok
char *my_strchr(const char *src,int ch) {
	assert(src != NULL);
	const char *psrc = src;
	while(*psrc != '\0' && *psrc != ch) {
		psrc++;
	}
	return (*psrc == '\0')? NULL : (char *)psrc;
}

char *my_strrchr(const char *src,int ch) {
	if(src == NULL) return NULL;
	const char *rsrc = src;
	while(*rsrc!= '\0') rsrc++;
	for(--rsrc; *rsrc != ch;--rsrc) {
		if(rsrc == src)
			 return NULL;
	}
	return (char *)rsrc;
}


/*BF算法的指针版本*/
char *my_strstr(const char *cs,const char *ct) {
	assert(cs != NULL && ct != NULL);
	const char *s = cs;
	const char *t = ct;
	for(; *cs != '\0';cs++) {
		for(s = cs,t = ct;*t != '\0' && *s == *t;s++,t++);
		if(*t == '\0')
			return (char *)cs;
	}
	return NULL;
}

/*BF算法的数组版本*/
char *my_strstr1(const char *cs,const char *ct) {
	assert(cs != NULL && ct != NULL);
	int len1 = my_strlen(cs);
	int len2 = my_strlen(ct);
	for(int i = 0; i <= len1 - len2;i++) {
		int j = 0;
		while(j < len2 && cs[i + j] == ct[j]) j++;
		if(j == len2)
			return (char *)(cs + i);
	}
	return NULL;
}

void get_next(const char *pat,int *next,int n) {
	int j = -1;
	next[0] = -1;
	for(int i = 1; i < n;i++) {
		while(j != -1 && pat[j + 1] != pat[i])  j = next[j]; //回退到匹配尾字符的位置
		if(pat[j + 1] == pat[i]) j++;
		next[i] = j; //更新当前位置的next值
	}
}


int kmp_strstr(const char *src,const char *pat) {
	int slen = my_strlen(src);
	int plen = my_strlen(pat);
	int *next = malloc(sizeof(int) * plen);
	get_next(pat,next,plen);

	int j = -1;
	for(int i = 0; i < slen;i++) {
		while(j > -1 && pat[j + 1] != src[i])  j = next[j];
		if(pat[j + 1] == src[i]) j++;
		if(j == plen - 1) { //表明模式串最后一个字符被匹配
			free(next);
			return i - j;
		}
	}
	free(next);
	return -1;
}

void test4() {
	printf("%s\n",my_strstr("hello","ell"));
	printf("%s\n",my_strstr1("damnyouif you have passwd","ave"));
	printf("%d\n",kmp_strstr("hello","ell"));
	printf("%d\n",kmp_strstr("damnyouif you have passwd","ave"));
	printf("%d\n",kmp_strstr("hello","st"));
}

在子串匹配的算法中,使用暴力查找算法时间复杂度为O(m*n),KMP算法的时间复杂度为O(m+n)。解释下KMP算法的思路: 先计算next数组(部分匹配表),再基此表在文本串进行查找匹配。

举一个例子,例如模式串ABCDABD不匹配...ABCDABE...。考虑到前六个字符ABCDAB是匹配的,且B的匹配值是2(说明有个前缀和后缀相同,记最长的前缀长度,本例中是后缀串AB和前缀串AB),则文本串中后面的AB无需再次比较了,因为模式串有前缀AB和它匹配,于是模式串移动4位。

那么在KMP算法中这种移位是怎么实现的?

  1. 首先我们构造一个next数组,得到该模式串以每个字符结尾其前缀串和后缀串的最长匹配下标。 next数组可转化为给定一个字符数组,求该字符串的最大相等k前缀和k后缀,求出这个最大的k

  2. 也就是说我们判断的是pat[j+1](前缀字符)与pat[i](当前字符)是否匹配。这其实是一个动态规划问题,即已知dp[0..i-1]求出dp[i]。即当pat[j+1] == pat[i]时匹配了dp[i]=j+1;不相等呢回退指针看看有没有匹配的字符或者到达不匹配的标志

C memset/memchr/memcmp/memcpy/memmove等函数

内存操作函数是按照字符数组的方式操作对象,注意这几个函数中的size大小均为对应类型的大小乘以元素的个数。下面重点讲述以下几个函数:

memset : 为内存块做初始化工作,一般是在对定义的字符数组初始化为某个字符或者其他类型的默认值。

char buf[10];
memset(buf,'c',10); //将buf数组中的元素都设置成字符'c',用法正确

char buf[] = "hello,world";
memset(buf,'0',strlen(buf)); //同上,针对字符串用的是strlen

struct record {
	char name[16];
	int seq;
} ;
//快速清空结构体元素和数组,用法正确
struct record r1;
strcut recodr arr[10];
memset(&r1,0,sizeof(struct record));
memset(&arr,0,sizeof(struct record) * 10);

int a[10];
memset(buf,0,sizeof(int) * 10);//将int数组初始化为0,用法正确但不建议使用,直接初始化
memset(buf,1,sizeof(int) * 10);//用法错误,这是对40个字节进行赋值,并非每个元素是1

可以看到几个特点

  • 初始化字符数组时用strlen,初始化其他类型时用sizeof。这是因为sizeof返回数组或类型为其分配好的空间大小,并不关心里面存储的数据;而strlen值关心存储的数据内容,并且字符串长度不包括结束符
  • memset一般是用来对较大结构体或数组进行初始化或清除操作,使用对应类型默认值

memcpy : 从源起始位置拷贝n个字节到目标的起始位置,用于复制任意可读写类型的空间。

它不允许两内存区域出现重叠。相比strcpy只能用于复制字符串来说,memcpy是可以复制任何内容;并且它的复制指定了拷贝的字节长度,操作更安全。那么当src和dst以任何形式出现了重叠,就会出现数据覆盖的问题,这样的结果是未定义的,如:

  1. dst目的地址空间在src源地址空间右面(src + n > dst
  2. src目的地址空间在dst源地址空间右面(dst + n > src)

那么如何处理呢,这就要用到memmove函数了

memmove : 它可以保证源串在被覆盖之前将重叠区域的字节先拷贝到目标区域中(先处理重叠部分即可)。

它的思路是:

  1. 若dst小于src有重叠时(dst+n > src),仍从头开始复制
  2. 若src小于dst有重叠时(src+n > dst),从尾部开始复制

代码实现如下:

void *my_memset(void *src,int c,size_t n) {
	assert(src != NULL);
	char *psrc = (char *)src;
	while(n--) {
		*psrc++ = c;
	}
	return src;
}

/*返回c在cs的前n个字符第一次出现的位置,找不到返回NULL*/
void *my_memchr(const void *src,int c,size_t n) {
	assert(src != NULL);
	char *psrc = (char *)src;
	while(n--) {
		if(*psrc == c)
			return psrc;
		psrc++;
	}
	return NULL;
}

int my_memcmp(const char *cs,const char *ct,size_t n) {
	assert(cs != NULL && ct != NULL);
	while(n-- && *cs == *ct) {
		if(n == 0 && *cs == *ct)
			return 0;
		cs++;
		ct++;
	}
	return *cs - *ct;
}

/*将串src中的n个字符拷贝到dst中,以字节流方式处理,按字符数组的方式操作对象*/
void *my_memcpy(void *dst,const void *src,size_t n) {
	assert(src != NULL && dst != NULL);
	char *psrc = (char *)src;
	char *pdst = (char *)dst;
	while(n--) {
		*pdst++ = *psrc++;
	}
	return dst;
}


void *my_memmove(void *dst,const void *src,size_t n) {
	assert(src != NULL && dst != NULL);
	char *psrc = (char *)src;
	char *pdst = (char *)dst;
	if(dst > src && dst - src < n) {
		while(n--) {
			*(pdst + n) = *(psrc + n);
		}
	} else {
		while(n--) {
			*pdst++ = *psrc++;
		}
	}
	return dst;
}


void test5() {
	char s1[] = "Hello,world!";
	char s2[] = "Hello,worl!";
	if(my_memcmp(s1,s2,sizeof(s2)) == memcmp(s1,s2,sizeof(s2)))
		printf("test successful!\n");
	else
		printf("test failed\n");
}

void test6() {
	char str[] = "memmove can be very useful......";
	char str1[] = "memmove can be very useful......";
	if(strcmp(my_memmove(str + 20,str + 15,11),memmove(str1 + 20,str1 + 15,11)) == 0)
		printf("test successful!\n");
	else
		printf("test failed\n");
}

以上为字符串处理的完整实现,如有问题欢迎指正_


3 字符串的实际应用

3.1 字符串包含和逆置问题

3.1.1 串的模式匹配算法

3.1.2 字符串移位包含问题

3.1.3 翻转单词中的顺序

3.2 字符串的转换、删除、替换

3.2.1 字符串转换思路

3.2.2 字符串删除思路

3.2.3 字符串的替换

3.2.4 统计字符串次数问题

3.3 字符串的排列、组合

3.3.1 字符串排列

3.3.2 字符串组合

3.3.3 next_perm和prev_perm

3.4 字符串回文问题

3.4.1 最长回文子串

http://chuansongme.com/n/183586

3.4.2 回文分割

http://chuansongme.com/n/181884

3.4.3 最少插入字符

http://chuansongme.com/n/202680

3.5 总结


posted @ 2015-04-21 11:18  charlesxiong  阅读(25457)  评论(0编辑  收藏  举报