理解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 總結



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM