在网上对于字符串处理的相关介绍很多,这也是各大公司常考的题型,主要是因为在信息处理中,字符串是最常见的结构,这样,字符串作为一种数据结构类型出现在越来越多的程序设计语言中,同时出现了相关的处理字符串的库;如<string.h>,MFC封装的string类CString,以及现在比较流行的BOOST库中的字符串处理算法等等。所以从基本的知识开始,逐步了解基本的字符串操作,也是自己最近对字符串知识了解的总结和备忘。
在本文中通过系统介绍字符串存储结构;字符串处理函数;字符串模式匹配算法。相应的给出代码实现。
字符串处理函数相关资源参考网站:(PS:相应的代码归其作者所有,使用请声明!)
字符串模式匹配算法相关资源参考网站:(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;
}