数据结构与算法-字符串与字符串匹配算法

先说说最基础的字符串的数组存储表示:
C语言中顺序串的存储分配可分为两种:
(1)静态分配的数组表示:
#define maxSize256
typedef char SeqString[maxSize];
长度定义为256,实际只能存储255个字符(最后用“”表示串值终结)
如需要记录字符串当前实际字符个数,则:
#define maxSize256
typedef struct{
   char ch[maxSize];
   int curLength;
} SeqString;
这种字符串表示简单,但存储空间是在程序编译时静态分配的,放满再存会产生溢出。数组空间不能扩展,程序中止。
(2)动态分配的数组表示:
可以使用new、delete等动态存储管理的函数,根据实际需要动态的分配和释放字符串的存储空间。
这样定义的顺序串类型也有两种形式:
typedef char *SeqString;
或者
#define MaxSize256
typedef struct{
   char *ch;
   int curLength;
} SeqString;
在创建字符串时可用new操作动态分配该字符串的存储空间
ch = new char[maxSize];
if(ch==NULL) exit(1);
这种存储方式处理简单,但预先定义了数组大小,不能适应扩展空间需要
#define maxSize256
typedef struct{
   char *ch;
   int maxSize;
   int curLength;
} SeqString;
在初始化时进行动态存储分配,对结构定义中所有数据成员赋值
void initString(SeqString s){
    s.ch = new char[defaultSize];
    if(s.ch==NULL) exit(1);
    s.ch[0] = ‘0’;
    s.maxSize = defaultSize;
    s.curLength = 0
}
当数组空间放满,对其进行成倍扩充
void overflowProcess(){
   char *newAddress = newchar[2*maxSize];
   if(newAddress==NULL) {cerr<<“Memory Allocation Error”<<endl;exit(1);}
   intn = maxSize = 2*maxSize;
   char *srcptr = ch;
   char *desptr = newAddress;
   while(n—) *desptr++ = *srcptr++;
   delete []ch;
   ch = newAddress;
}
 
string与string.h的区别:
标准C中是不存在string类型的,string是标准C++扩充字符串操作的一个类。而C++的string类操作对象是string类型字符串,该类重装了一些运算符,添加了一些字符串操作成员函数,使得操作字符串更加方便。有的时候我们要将string串和char*串配合使用,所以也会涉及到这两个类型的转化问题。
标准C中有string.h这个头文件,string.h这个头文件中定义了一些我们经常用到的操作字符串的函数,如:strcpy、strcat、strcmp等等,但是这些函数的操作对象都是char*指向的字符串。 
 
<string.h>常用方法:
对于char型指针,在声明时可以直接赋值
char ch[] = "abc";
我试了一下,这么赋值即使没指定大小,但字符串的大小实际还是已经决定好了。
注意:对于char数组型变量,在非声明的时候不能直接赋值,只能通过ch[i]慢慢修改了
  • 字符串复制
char* strcpy(char *string1 , char *string2)
把字符串string2的内容复制给string1,如果string1有内容则会被覆盖。
如果string1长度更大,则只覆盖前面部分。
如果string2长度更大,则string1会变成和string2一样。
char* strncpy(char *string1 , char *string2 , int n)
只复制前n个字符的部分复制,会把前n个字符覆盖掉。
char* strdup(char *string1)
为字符串string1分配内存空间,返回值为指向该内存开始地址点指针,
即拷贝字符串string1的一个副本,由函数返回值返回。这个副本有自己的内存空间,和string1不相干
  • 字符串连接
char* strcat(char *string1 , char *string2)
连接字符串string2到string1后面。string2原内容保持不变。
char* strncat(char *string1 , char *string2 , int n)
将特定数量字符连接到另一字符串后面
  • 在给定字符串中搜索指定字符
char* strchr(char *string1 , char ch)
试了一下,第二个参数可以是’a’,也可以是char ch
返回指向字符ch的首指针。若搜索失败则返回NULL
char* strrchr(char *string1 , char ch)
在给定字符串中搜寻某个指定字符最后一次出现的地址
unsigned long strcspn(char *string1 , char *string2)
在给定字符串中搜寻某一个指定字符第一次出现的位置,从0开始计数。(即这个字符串前面还有多少个字符)
注意:第二个参数必须是char型指针
char* strpbrk(const char *string1 , const char *string2)
在两个字符串中寻找首次出现的共同字符,返回该字符在string1中的地址
char* strstr(const char *string1 , const char *string2)
在第一个字符串周搜索第二个字符串,返回它在第一个字符串中的地址。
  • 计算字符串的长度strlen
unsigned long strlen(cons tchar *string1)
  • 字符串比较大小
int strcmp(char *string1 , char *string2)
返回结果为正时,说明第一个字符串>第二个字符串
 
<string>常用方法:
int main(){
    string str;  
    str = "Hello world";   // 给str赋值为"Hello world"
    char cstr[] = "abcde";  //定义了一个C字符串
    string s1(str);       
    string s2(str,6);     //将str内,开始于位置6的部分当作s2的初值(从0开始)
    string s3(str,6,3);  //将str内,开始于6且长度最多为3的部分作为s3的初值
    string s4(cstr);   //将C字符串作为s4的初值
    string s5(cstr,3);  //将C字符串前3个字符作为字符串s5的初值。
    string s6(5,'A');  //生成一个字符串,包含5个'A'字符
    string s7(str.begin(),str.begin()+5); //区间str.begin()~str.begin()+5内的5个字符作为初值
    return 0;
}
此外可以直接对string对象进行赋值
str = “abc”;
str = str + “abc”;
str += “abc”;
可用下列函数来获得string的一些特性:
int capacity()const;    //返回当前容量(即string中不必增加内存即可存放的元素个数)
int max_size()const;    //返回string对象中可存放的最大字符串的长度
int size()const;        //返回当前字符串的大小
int length()const;       //返回当前字符串的长度
bool empty()const;        //当前字符串是否为空
void resize(int len , char c);  //把字符串当前大小置为len,多去少补。第二个参数c可以省略,表示补的时候拿c补充
注意:length()函数返回字符串的长度,这个数字应该和size()返回的数字相同
size_type find(const basic_string &str , size_type index);  //返回str在字符串中第一次出现的位置(从index开始查找)
size_type find(const char *str , size_type index);  
size_type find(const char *str , size_type index , size_type length);  //搜索长度为length
size_type find(char ch , size_type index);  // 返回字符ch在字符串中第一次出现的位置(从index开始查找)
这些string类的查找函数,都有唯一的返回类型size_type,即一个无符号整数。
若查找成功,返回按查找规则找到的第一个字符或子串的位置(从0开始,相当于该字符前面有多少个字符)
若查找失败,返回npos,npos定义如下:
static const size_type npos = -1;
因此查找字符串A是否包含子串B,不是用 strA.find(strB) > 0 而是 strA.find(strB) != string:npos 。
rfind()与find()很相似,差别在于查找顺序不一样,rfind()是从指定位置起向前查找,直到串首(index还是从前往后数的)
find_first_of( )在源串中从位置pos起往后查找,只要在源串中遇到一个字符,该字符与目标串中任意一个字符相同,就停止查找
find_first_not_of( )在源串中从位置pos开始往后查找,直到遇到某个字符与目标串中的任意一个字符都不相同,才停止查找
find_last_of( )和find_last_not_of( )从指定位置起向前查找
此外,还有一些常用函数:
string &insert(int p , const string &s);  //在str[p]位置插入字符串s
string &replace(int p , int n , const char *s); //删除从str[p]开始的n个字符,然后在str[p]处插入串s
string &erase(int p , int n);  //删除str[p]开始的n个字符,返回修改后的字符串
string substr(int pos = 0 , int n = npos)const;  //返回pos开始的n个字符组成的字符串
void swap(string &s2);    //交换当前字符串与s2的值
string &append(const char *s);   //把字符串s连接到当前字符串结尾
void push_back(char c)   //当前字符串尾部加一个字符c
const char *data()const;   //返回一个非null终止的c字符数组,用于string转const char*。它返回的数组不以空字符终止,
const char *c_str()const;  //返回一个以null终止的c字符串,用于string转const char*
 
几种常见的字符串匹配算法:
#include <cstring>
#include <cstdio>
void search(char *pat,char *txt){
    int M=strlen(pat);
    int N=strlen(txt);
    for(int i=0; i<N-M; i++){
        int j;
        for(j=0; j<M; j++)
            if(txt[i+j]!=pat[j]) break;
        if(j==M) printf("Pattern found at index %d /n",i);
    }
}
外层循环执行N-M+1次,内层循环执行M次,时间复杂度为O((N-M+1)*M)
2.KMP(Knuth-Morris-Pratt)算法
若求到next[q]=k,则前q+1个字符组成的字符串,相同的最长前缀和最长后缀长度为k+1
next[0]一定为-1(只有第一个字符的字符串,不存在相同的最长前缀和最长后缀)
求到next[q]=k后,若第k+2个字符ptr[k+1]和第q+2个字符是一样的,那么next[q+1]=k+1
如果是不一样的,k就变成next[k](next[k]必定是小于k的),直到k最后一直循环到-1为止,再比较若第k+2个字符ptr[k+1]和第q+2个字符是否是一样的
搜索的时候,如果出现str[s_i]和ptr[p_i]不一样,如果这时候p_i还是0那么自然s_i继续往前就行了;如果P_i已经不是0了,s_i就不用往前了,因为重合的那部分字符串前缀后缀有一部分是一样的,直接p_i=next[p_i-1]+1即可
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void cal_next(char *ptr, int *next, int plen) {
    next[0]=-1;     
    int k=-1;
    for (int q=1; q<=plen-1; q++) {
        while (k>-1 && ptr[k+1]!=ptr[q]) k=next[k];
        if (ptr[k+1]==ptr[q]) k=k+1; 
        next[q]=k;
    }
}
int KMP(char *str, int slen, char *ptr, int plen, int *next){   //在str中寻找ptr
    int s_i=0, p_i=0;
    while (s_i<slen && p_i<plen){
        if (str[s_i]==ptr[p_i]){
            s_i++;
            p_i++;
        }
        else {
            if(p_i==0) s_i++;
            else p_i=next[p_i-1]+1;
        }
    }
    return (p_i==plen)?(s_i-plen):-1;
}
看到好多博客上说k = next[k]可以用k--代替,但这样是不对的。
例如acceaccc,求到next[6]=2后,用这种方法会求出next[7]=1,实际上next[7]=-1
因为相同的最长前缀和最长后缀长度为k+1,并不代表这k+1个长度的字符串随便从前从后取一串都是匹配的。只有从前从后取next[k]+1长度的字符串,才是一定前后匹配的。如,最大acceacc长度为3的字符串前后匹配都是acc,并不代表长度为2的字符串ac和cc是前后匹配的。只有next[2]长度的字符串才是一定前后匹配的。
3.BM(Boyer-Moore)算法
int bmMatch(const string & text, const string & pat){
    int *bc = getBc(pat);
    int *gs = getGs(pat);
    //patAt指向了当前pat和text对齐的位置
    int patAt = 0;
    //cmp指向了当前比较的位置
    int cmp;
    const size_t PATLASTID = pat.length() - 1;
    const size_t patLen = pat.length();
    const size_t textLen = text.length();
    while (patAt + patLen <= textLen){
        //如果匹配成功,cmp就会来到-1的位置上
        //patAt + cmp 指向了text上当前比较的字符
        for (cmp = PATLASTID; cmp >= 0 && pat[cmp] == text[patAt + cmp]; --cmp);     
        if (cmp == -1)
            break;
        else{
            patAt += max(gs[cmp], cmp - bc[text[patAt + cmp]]);
        }
    }
    delete []bc;
    delete []gs;
    return (patAt + patLen <= textLen)? patAt : -1;
}
int *getBc(const string& pattern){
    //坏后缀情况下建立bc表
    int *bc = new int[256];  //256是字符表的规模大小(ACSII)
    int len = pattern.length();
    for (int i = 0; i < 256; ++i) bc[i] = -1;   //坏字符不存在时,为-1  
    for (int i = 0; i < len; ++i) bc[pattern[i]] = i;
    return bc;
}
int *suffixes(const string& pat){
    //好后缀情况下构建gc表记录每次需要移动的距离比较困难  
    const int len = pat.length();
    int num;
    int *suff = new int[len];  //辅助表suffix[i]=x表示以i为边界向左,与模式串后缀匹配的最大长度
    suff[len - 1] = len;
    for (int i = len - 2; i >= 0; —i){
        for (num = 0; num <= i && pat[i-num] == pat[len-num-1]; ++num);
        suff[i] = num;
    }
    return suff;
}
int *getGs(const string& pat){
    //构建gc[i]表,记录遇到好后缀时模式串需要移动的距离,i表示好后缀左侧第一个坏字符
    const int len = pat.length();
    const int lastIndex = len - 1;
    int *suffix = suffixes(pat);
    int *gs = new int[len];
    for (int i = 0; i < len; ++i) gs[i] = len; //情况一:找不到对应的子串和前缀
    //找前缀
    for (int i = lastIndex; i >= 0; --i) //情况二:存在我们想要的前缀
        if (suffix[i] == i + 1)
            for (int j = 0; j < lastIndex - i; ++j)
                if (gs[j] == len) gs[j] = lastIndex - i;
    for (int i = 0; i < lastIndex; ++i) gs[lastIndex - suffix[i]] = lastIndex - i; //情况一:找中间的匹配子串
    delete []suffix;
    return gs;
}
原文地址:https://www.cnblogs.com/yangyuliufeng/p/9989782.html