在网上对于字符串处理的相关介绍很多,这也是各大公司常考的题型,主要是因为在信息处理中,字符串是最常见的结构,这样,字符串作为一种数据结构类型出现在越来越多的程序设计语言中,同时出现了相关的处理字符串的库;如<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; }