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


    先说说最基础的字符串的数组存储表示:
    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;
    }
  • 相关阅读:
    TreeView设置节点图标
    DELPHI 重命名文件名时 文件存在自动重命名
    ExtractNewFolderPath
    Delphi 记事本 TMemo
    Memo打印1
    Windows10和CentOS7双系统安装的一些小技巧
    正则表达式总结
    Java_得到GET和POST请求URL和参数列表
    【Restful】三分钟彻底了解Restful最佳实践
    Win7下U盘安装CentOS-7-x86_64-DVD-1503-01
  • 原文地址:https://www.cnblogs.com/yangyuliufeng/p/9989782.html
Copyright © 2020-2023  润新知