字符串处理

 在网上对于字符串处理的相关介绍很多,这也是各大公司常考的题型,主要是因为在信息处理中,字符串是最常见的结构,这样,字符串作为一种数据结构类型出现在越来越多的程序设计语言中,同时出现了相关的处理字符串的库;如<string.h>,MFC封装的string类CString,以及现在比较流行的BOOST库中的字符串处理算法等等。所以从基本的知识开始,逐步了解基本的字符串操作,也是自己最近对字符串知识了解的总结和备忘。

在本文中通过系统介绍字符串存储结构;字符串处理函数;字符串模式匹配算法。相应的给出代码实现。

字符串处理函数相关资源参考网站:(PS:相应的代码归其作者所有,使用请声明!)

http://blog.csdn.net/v_JULY_v/article/details/6417600

字符串模式匹配算法相关资源参考网站:(PS:相应的代码归其作者所有,使用请声明!)

http://sundful.iteye.com/blog/263847  (KMP详细讲解!)

        http://blog.csdn.net/v_july_v/article/details/6545192 (讲解详细!)

   http://www.searchtb.com/2011/07/%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%82%A3%E4%BA%9B%E4%BA%8B%EF%BC%88%E4%B8%80%EF%BC%89.html 

 (简洁明了!)


一.字符串结构

   在严蔚敏数据结构书中,将其结构分为:1.定长顺序存储。2.堆分配存储。3.块链存储。4.字典树存储结构(如:键树,Trie树),树结构存储字符串,主要用于错误单词的处理,这里不做介绍。


二.字符串常规处理函数:(主要是<string.h>中相关函数的实现)

这个的话可以参看string.h中的源码实现。像strstr,strlen,strcpy,strcmp,strcat,以及字符倒置,字符串前(后)移,(或者将这些操作结合一起考察)等基本的字符串操作函数各大公司招聘笔试面试常考的题。其中代码实现见参考连接。其他的操作在平时积累中再添加相应的字符串操作~!

(PS:如果有时间将其用类来封装实现之,待code~~~)

 /* 串的定长顺序存储表示 */
 #define MAX_STR_LEN 40 /* 用户可在255(1个字节)以内定义最大串长 */
 typedef char SString[MAX_STR_LEN+1]; /* 0号单元存放串的长度 */
 /* 串的堆分配存储 */
 typedef struct
 {
   char *ch; /* 若是非空串,则按串长分配存储区,否则ch为NULL */
   int length; /* 串长度 */
 }HString;
 /*串的块链存储表示 */
 #define CHUNK_SIZE 4 /* 可由用户定义的块大小 */
 typedef struct Chunk
 {
   char ch[CHUNK_SIZE];
   struct Chunk *next;
 }Chunk;
 typedef struct
 {
   Chunk *head,*tail; /* 串的头和尾指针 */
   int curlen; /* 串的当前长度 */
 }LString;

三.字符串模式匹配算法: (KMP算法的实现原理参考严蔚敏版本)


 1.KMP算法:(时间复杂度:最好O(n/m), 最坏O(n+m))

       模式串与文本串匹配:1.主串无需回溯,而模式串定位到next[j]与s[i]比较继续进行;2.主串与模式串第一个字符匹配,则主串前进下个字符s[i+1]与模式串开始位置继续进行比较匹配。这里关键是next函数的实现。

关于next函数的理论推导:

       1.在主串P和模式串S进行匹配时,存在S[i] != P[j]时,则前面字符串满足:P[0...j-1] = S[i-j...i-1]。

       2.假设模式串中存在第k+1个字符及P[k]与S[i]正在比较,则前面字符串满足: P[0...k-1] =S[i-k...i-1]。

       3.又因为P[j-k...j-1] = S[i-k...i-1], 所以结合上面的条件,推出:P[0...k-1] = P[j-k...j-1]

       于是乎将next函数定义为:(具体推导过程见严蔚敏数据结构书中的介绍P81,很详细~!这里只简单总结备忘)

                -1, 当j=0时
 next[j] =  Max{k|0<k<j,且P[0...k-1]=P[j-k...j-1]} 当此集合不为空时。
                0,  其他情况

 next函数的实现参考严蔚敏数据结构书中的介绍P83,是一个递推的推导过程(备忘)   

         KMP算法最大的特点是指示主主串的指针不需要回溯,整个匹配过程中,对主串仅需要从头到尾扫描一遍,这对处理从外设输入的庞大文件很有效,可以边读入边匹配,而无需回头重读。


2.BM算法:(时间复杂度:最好O(n/(m+1)), 最坏O(n*m)?)

          后缀匹配,是指模式串的比较从右到左,模式串的移动也是从左到右的匹配过程,经典的BM算法其实是对后缀蛮力匹配算法的改进。为了实现更快移动模式串,BM算法定义了两个规则,坏字符规则最好后缀规则,分别得到坏字符表和最好后缀表。利用好后缀和坏字符可以大大加快模式串的移动距离。

BM算法则综合坏字符表和最好后缀表,模式串定位位到母串给定位置,移动的最大值。


字符串模式匹配算法实现代码:(代码归其作者所有,使用请声明~!)

/*===============================================================================
@editor:weedge
@date:2011/08/08
@comment:
	字符串模式匹配算法:
	1.前缀蛮力匹配,
	2.优化得到KMP算法(主串无需回溯,而模式串定位到p[next[j]]位置与s[i]继续匹配比较),模式串的比较从左到右,模式串的移动是从左到右的匹配过程。
	3.后缀缀蛮力匹配,
	4.以及坏字规则匹配,
	5.最好后缀规则匹配,
	6.最后综合2者得到BM算法,模式串的比较从右到左,模式串的移动也是从左到右的匹配过程。
	相关资源参考网站:(PS:相应的代码归其作者所有,使用请声明!)
	http://blog.csdn.net/v_july_v/article/details/6545192 (介绍详细)
	http://www.searchtb.com/2011/07/%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%82%A3%E4%BA%9B%E4%BA%8B%EF%BC%88%E4%B8%80%EF%BC%89.html (简洁明了) 

================================================================================*/

#include <iostream>
using namespace std;

#define NEXT

//=========================前缀蛮力匹配O(M*N)========================================
/*======================================================================
	前缀蛮力匹配算法的代码(以下代码从linux源码string.h中抠出),
	模式串和母串的比较是从左到右进行(strncmp()),
	如果找不到和模式串相同的子串,则从左到右移动模式串,距离为1(s++)。
=========================================================================*/
char* str_str(register const char *s, register const char *p)
{
	register size_t len = strlen(p);
	if (len==0) return (char*)s;
	while(*s!=*p || strncmp(p,s,len))
	{//首字母不相等,或者首字母相等,但是整个字符串不相等,那么母串中下个字符开始再进行匹配,即模式串从左向右移动一位。
		if (*s++ == '\n')
		{
			return (char*) NULL;
		}
	}
	return (char*)s;
}

/*=============================================================
int search_forward(char const*, int, char const*, int)(严蔚敏版本)
查找出模式串patn在主串src中第一次出现的位置
plen为模式串的长度
返回patn在src中出现的位置,当src中并没有patn时,返回-1
=============================================================*/
int search_forward(char const* src, int slen, char const* patn, int plen)
{
	int i = 0, j = 0;
	while( i < slen && j < plen )
	{
		if( src[i] == patn[j] )  //如果相同,则两者++,继续比较
		{
			++i;	
			++j;           
		}
		else
		{
			//否则,指针回溯,重新开始匹配
			i = i - j + 1;  //退回到最开始时比较的位置
			j = 0;
		}
	}
	if( j >= plen )
		return i - plen;  //如果字符串相同的长度大于模式串的长度,则匹配成功
	else
		return -1;
}

//===========================================KMP O(M+N) 最好:O(N),最坏:O(N/M)==================================================

/*==================================================================
求next数组各值的函数:
(严蔚敏版本改版,严蔚敏版本是从数组下标1开始,而数组下标0中的数为字符串长度)
其next函数定义如下:
			-1, 当j=0时
next[j] =	Max{k|0<k<j,且P[0...k-1]=P[j-k...j-1]} 当此集合不为空时。
			0,  其他情况
======================================================================*/
void get_next(char const* ptrn, int plen, int* nextval)
{
	int i = 0; 
	nextval[i] = -1;
	int j = -1;
	while( i < plen-1 )
	{
		if( j == -1 || ptrn[i] == ptrn[j] )   //循环的if部分
		{
			++i;
			++j;
			nextval[i] = j; 
		}
		else                                 //循环的else部分
			j = nextval[j];
	}
}

/*==================================================================
修正后的求next数组各值的函数:

======================================================================*/
void get_nextval(char const* ptrn, int plen, int* nextval)
{
	int i = 0; 
	nextval[i] = -1;
	int j = -1;
	while( i < plen-1 )
	{
		if( j == -1 || ptrn[i] == ptrn[j] )   //循环的if部分
		{
			++i;
			++j;
			//修正的地方就发生下面这4行
			if( ptrn[i] != ptrn[j] ) //++i,++j之后,再次判断ptrn[i]与ptrn[j]的关系
				nextval[i] = j;      //之前的错误解法就在于整个判断只有这一句。
			else					//后面出现相同的字符可以直接跳过,所以next赋值等于前面的next值。
				nextval[i] = nextval[j];
		}
		else                                 //循环的else部分
			j = nextval[j];
	}
}

/*===================================================================
int KMPSearch(char const*, int, char const*, int, int const*, int pos)  
KMP模式匹配函数:(严蔚敏版本改版)
输入:src, slen主串
输入:patn, plen模式串
输入:nextval KMP算法中的next函数值数组
输出:成功返回位置,否则为-1
===============================================================*/
int KMPSearch(char const* src, int slen, char const* patn, int plen, int const* nextval, int pos)
{
	int i = pos;
	int j = 0;
	while ( i < slen && j < plen )
	{
		if( j == -1 || src[i] == patn[j] )
		{
			++i;
			++j;
		}
		else
		{
			j = nextval[j];          
			//当匹配失败的时候直接用p[j_next]与s[i]比较,而母串无需回溯。
		}
	}
	if( j >= plen )
		return i-plen;
	else
		return -1;
}


//===========================后遍历蛮力匹配O(M*N)=======================================
/*==========================================================
int search_reverse(char const*, int, char const*, int)
模式串的比较从右到左,模式串的移动也是从左到右的匹配过程
成功返回位置,否则为-1
=========================================*/
int search_reverse(char const* src, int slen, char const* patn, int plen)
{
	int s_idx = plen, p_idx;  	
	if (plen == 0)		
		return -1;
	while (s_idx <= slen)//计算字符串是否匹配到了尽头		
	{	
		p_idx = plen;	
		while (src[--s_idx] == patn[--p_idx])//开始匹配		
		{		
			//if (s_idx < 0)	
				//return -1;	
			if (p_idx == 0)		
			{     	
				return s_idx;		
			}	
		}	
		s_idx += (plen - p_idx)+1;	//主串位置s_idx向后回溯plen-p_index+1位到比较前的下个位置,相当于模式串从左向右移动一位。
	}	
	return -1;	
}


//==========================BM 最好:O(N/M), 最坏:O(M*N)================================

/*=============================================================
  函数:void BuildBadCharacterShift(char *, int, int*)
目的:根据好后缀规则做预处理,建立一张好后缀表shift[256](字符所表示的范围:0-255)
参数:  
pattern => 模式串P	
plen => 模式串P长度	  
shift => 存放坏字符规则表,长度为的int数组		
返回:void			  
=======================================================================*/
void BuildBadCharacterShift(char const* pattern, int plen, int* shift)
{
	for( int i = 0; i < 256; i++ )	
		*(shift+i) = plen;
	while ( plen >0 )		
	{	
		*(shift+(unsigned char)*pattern++) = --plen;		
	}	
}

/*=============================================================
search_badcharacter函数:采用坏字规则(坏字表)来查找出模式串patn在主串src中第一次出现的位置
	这里出现两种情况:
	1.坏字符没出现在模式串中,这时可以把模式串移动到坏字符的下一个字符,继续比较。
	2.坏字符出现在模式串中,这时可以把模式串第一个出现的坏字符和母串的坏字符对齐,当然,这样可能造成模式串倒退移动。
	return patn在src中出现的位置,当src中并没有patn时,返回-1
具体参考:
http://www.searchtb.com/2011/07/%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%82%A3%E4%BA%9B%E4%BA%8B%EF%BC%88%E4%B8%80%EF%BC%89.html#comment-627
==============================================================*/
int search_badcharacter(char const* src, int slen, char const* patn, int plen, int* shift)
{	
	int s_idx = plen, p_idx;  
	int skip_stride;	
	if (plen == 0)		
		return -1;
	while (s_idx <= slen)//计算字符串是否匹配到了尽头	
	{	
		p_idx = plen;	
		while (src[--s_idx] == patn[--p_idx])//开始匹配		
		{	
			//if (s_idx < 0)	
			//Return -1;	
			if (p_idx == 0)		
			{     		
				return s_idx;		
			}	
		}
		skip_stride =  shift[(unsigned char)src[s_idx]];
		s_idx += (skip_stride>plen-p_idx ? skip_stride: plen-p_idx)+1;	
	}	
	return -1;	
}

/*=================================================================
  函数:void BuildGoodSuffixShift(char *, int, int*) 
目的:根据最好后缀规则做预处理,建立一张好后缀表(这个貌似有点难度)
参数:
pattern => 模式串P
plen => 模式串P长度
shift => 存放最好后缀表数组
返回:void
具体参考:
http://blog.csdn.net/v_july_v/article/details/6545192 
==============================================================*/
void  BuildGoodSuffixShift(char const* pattern, int plen, int* shift)
{	
	shift[plen-1] = 1;            // 右移动一位	
	char end_val = pattern[plen-1];
	char const* p_prev, const* p_next, const* p_temp;
	char const* p_now = pattern + plen - 2;            // 当前配匹不相符字符,求其对应的shift
	bool isgoodsuffixfind = false;                    // 指示是否找到了最好后缀子串,修正shift值
	for( int i = plen -2; i >=0; --i, --p_now)		
	{	
		p_temp = pattern + plen -1;	
		isgoodsuffixfind = false;
		while ( true )		
		{		
			while (p_temp >= pattern && *p_temp-- != end_val);          // 从p_temp从右往左寻找和end_val相同的字符子串	
			p_prev = p_temp;       // 指向与end_val相同的字符的前一个	
			p_next = pattern + plen -2;             // 指向end_val的前一个
			// 开始向前匹配有以下三种情况	
			//第一:p_prev已经指向pattern的前方,即没有找到可以满足条件的最好后缀子串
			//第二:向前匹配最好后缀子串的时候,p_next开始的子串先到达目的地p_now, 
			//需要判断p_next与p_prev是否相等,如果相等,则继续住前找最好后缀子串	
			//第三:向前匹配最好后缀子串的时候,p_prev开始的子串先到达端点pattern, 这个可以算是最好的子串
				  
			if( p_prev < pattern  && *(p_temp+1) != end_val )         // 没有找到与end_val相同字符	
				break;

			bool  match_flag = true;        //连续匹配失败标志	
			while( p_prev >= pattern && p_next > p_now )		
			{
				if( *p_prev --!= *p_next-- )		
				{	
					match_flag = false;      //匹配失败	
					break;	
				}
			}
		
			if( !match_flag )
				continue;          //继续向前寻找最好后缀子串
			else	
			{	
				//匹配没有问题, 是边界问题
				if( p_prev < pattern || *p_prev != *p_next)	
				{
					// 找到最好后缀子串
					isgoodsuffixfind = true;
					break;	
				}
				// *p_prev == * p_next  则继续向前找	
			}	
		}
		shift[i] = plen - i + p_next - p_prev;
		if( isgoodsuffixfind )
			shift[i]--;               // 如果找到最好后缀码,则对齐,需减修正
	}
}

/*==============================================================
  search_goodsuffix:
  采用最好后缀匹配规则(利用最好后缀表)来查找出模式串patn在主串src中第一次出现的位置
 如何根据好后缀规则移动模式串,shift(好后缀)分为三种情况:
  1.模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠左边的子串对齐。
  2.模式串中没有子串匹配上后后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可。
  3.模式串中没有子串匹配上后后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。此时,直接移动模式到好后缀的下一个字符。
  return patn在src中出现的位置,当src中并没有patn时,返回-1	
具体参考:
http://www.searchtb.com/2011/07/%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%82%A3%E4%BA%9B%E4%BA%8B%EF%BC%88%E4%B8%80%EF%BC%89.html#comment-627
========================================================================================*/
int search_goodsuffix(char const* src, int slen, char const* patn, int plen, int* shift)
{
	int s_idx = plen, p_idx;  
	int skip_stride;
	if (plen == 0)	
		return -1;
	
	while (s_idx <= slen)//计算字符串是否匹配到了尽头	
	{	
		p_idx = plen;
		while (src[--s_idx] == patn[--p_idx])//开始匹配	
		{	
			//if (s_idx < 0)	
				//return -1;
			if (p_idx == 0)	
			{     	
				return s_idx;	
			}
		}
		skip_stride =  shift[p_idx];
		s_idx += skip_stride +1;
	}
	return -1;
}

/*========================================================================
	函数:int* BMSearch(char *, int , char *, int, int *, int *)
	目的:判断文本串T中是否包含模式串P
	说明:综合坏字符表bad_shift和最好后缀表good_shift,模式串定位位到母串给定位置,移动的最大值。
	参数:
	src => 文本串T
	slen => 文本串T长度
	ptrn => 模式串P
	pLen => 模式串P长度
	bad_shift => 坏字符表
	good_shift => 最好后缀表
	返回:
	int - 1表示匹配失败,否则返回位置					  
================================================*/
int BMSearch(char const*src, int slen, char const*ptrn, int plen, int const*bad_shift, int const*good_shift)
{
	int s_idx = plen;  
	if (plen == 0)	
		return 1;
	
	while (s_idx <= slen)//计算字符串是否匹配到了尽头	
	{	
		int p_idx = plen, bad_stride, good_stride;	
		while (src[--s_idx] == ptrn[--p_idx])//开始匹配		
		{		
			//if (s_idx < 0)	
				//return -1;
			
			if (p_idx == 0)		
			{     	
				return s_idx;	
			}	
		}
		
		// 当匹配失败的时候,向前滑动
		bad_stride = bad_shift[(unsigned char)src[s_idx]];      //根据坏字符规则计算跳跃的距离
		good_stride = good_shift[p_idx];                                 //根据好后缀规则计算跳跃的距离
		s_idx += ((good_stride > bad_stride) ? good_stride : bad_stride )+1;//取大者	
	}
	return -1;	
}


int main()
{
	char *strS = "ababcabcacbab";
	char *strP = "abcac";

	/*test str_str*/
	cout<<"/*test str_str*/"<<endl;
	cout<<str_str(strS,strP)<<endl;

	/*test search_forward*/
	cout<<"/*test search_forward*/"<<endl;
	cout<<search_forward(strS,strlen(strS),strP,strlen(strP))<<endl;

	/*test KMPSearch*/
	cout<<"/*test KMPSearch*/"<<endl;

	int *nextval = new int[strlen(strP)];
#ifdef NEXT
	get_next(strP,strlen(strP),nextval);
	cout<<"get_next:"<<endl;
#else
	get_nextval(strP,strlen(strP),nextval);
	cout<<"get_nextval:"<<endl;
#endif
	int i;
	for (i=0; i<strlen(strP); i++)
	{
		cout<<nextval[i]<<" ";
	}
	cout<<endl;

	int kmp = KMPSearch(strS,strlen(strS),strP,strlen(strP),nextval,0);//主串从开始位置进行查找匹配。
	cout<<kmp<<endl;

	delete []nextval;

	/*test search_reserve*/
	cout<<"/*test search_reserve*/"<<endl;
	cout<<search_reverse(strS,strlen(strS),strP,strlen(strP))<<endl;

	/*test search_badcharacter*/
	cout<<"/*test search_badcharacter*/"<<endl;

	int *BadCharacterShift = new int[1<<8];//一个字符8位,2^8.
	BuildBadCharacterShift(strP,strlen(strP),BadCharacterShift);
	cout<<"BuildBadCharacterShift:"<<endl;
	char *strTemp = strP;
	for (i=0; i<strlen(strP); i++)
	{
		cout<<*(BadCharacterShift + (unsigned char)*strTemp++)<<" ";
	}
	cout<<endl;

	int bc = search_badcharacter(strS,strlen(strS),strP,strlen(strP),BadCharacterShift);
	cout<<bc<<endl;

	//delete []BadCharacterShift;

	/*test search_goodsuffix*/
	cout<<"/*test search_goodsuffix*/"<<endl;

	int *GoodSuffixShift = new int[strlen(strP)];
	BuildGoodSuffixShift(strP,strlen(strP),GoodSuffixShift);
	cout<<"BuildGoodSuffixShift:"<<endl;
	for (i=0; i<strlen(strP); i++)
	{
		cout<<GoodSuffixShift[i]<<" ";
	}
	cout<<endl;

	int gs = search_goodsuffix(strS,strlen(strS),strP,strlen(strP),GoodSuffixShift);
	cout<<gs<<endl;

	//delete []GoodSuffixShift;

	/*test BMSearch*/
	cout<<"/*test BMSearch*/"<<endl;

	int bm = BMSearch(strS,strlen(strS),strP,strlen(strP),BadCharacterShift,GoodSuffixShift);
	cout<<bm<<endl;

	delete []BadCharacterShift;
	delete []GoodSuffixShift;

	system("pause");
	return 0;
}






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值