第四章—串

串数据类型的定义

串的定义

串是由0个或多个字符组成的有限序列。串中字符的个数称为串的长度,含有0个元素的串叫空串。在C语言中,可以用以下语句定义一个名为str的串。

char str[] = "abcdef";

说明:串通常用一个字符数组来表示。从这个角度来讲,数组str内存储字符为’a’、‘b’、 ‘c’、‘d’、‘e’、‘f’、’\0’,其中’\0’作为编译器识别串结束的标记,而串内字符为’a’、‘b’、 ‘c’、‘d’、‘e’、‘f’,因此数组str的长度为7,而串的长度为6。

串中任意连续的字符组成的子序列称为该串的子串,包含子串的串称为主串,某个字符在串中的序号称为这个字符的位置。通常用子串第一个字符的位置作为子串在主串中的位置。要注意的是,空格也是串字符集合的一个元素,由一个或者多个空格组成的串称为空格串(注意:空格串不是空串)。

串的逻辑结构与线性表类似,串是限定了元素为字符的线性表,从操作集上讲,串与线性表有很大的区别,线性表的操作主要针对表内的某一个元素,而串操作主要针对串内的一个子串。

串的存储结构

  1. 定长顺序存储表示

一般不采用char str[] = "abcdef";定义并初始化一个串,原因是仅仅以’\0’作为串结束的标记在求串长时需要扫描整个串,时间复杂度为O(n);不如额外定义一个变量存储串长,这样求串长就变成了时间复杂度为O(1)的操作。

typedef struct string{
	char str[maxSize+1]; //str数组的长度为maxSize+1,是因为多出了一个\0
	int length;
}Str;
  1. 变长分配存储表示

变长分配存储表示方法的特点是,在程序执行过程中根据需要动态分配,其结构体的定义

typedef struct str{
    char *ch; //指定动态分配存储区首地址的字符指针
    int length; //串长度
}Str;

这种存储方式在使用时,需要用函数malloc()来分配一个长度为length,类型为char的连续存储空间,分配的空间可以用函数free()释放掉。用函数malloc()分配存储空间如果成功,则返回一个指向起始地址的指针,作为串的基地址,这个地址由ch指针来指向;如果分配失败,则返回NULL。

串的基本操作

操作定义

//以空串构造字符串
Str* initStr();
//赋值操作,赋值成功返回1。
int strassign(Str* str,char* ch);
//取串长度
int strlength(Str str);
//串比较操作,比较方式:通过ASCII码比较,如果相等,则找下一个。
int strcompare(Str str1,Str str2);
//串连接操作
Str* concat(Str str1,Str str2);
//求子串操作,从pos开始,len长度的子串
Str* substring(Str str,int pos,int len);
//串清空操作
int cleanstring(Str *str);

操作实现

Str* initStr(){
    Str* str = (Str*) malloc(sizeof(Str));
    str->ch = nullptr;
    str->length = 0;
    return str;
}

int strassign(Str* str,char* ch){
    if(str->ch){ //释放原先串的空间
        free(str->ch);
    }
    int len = 0;
    char *c = ch;
    while(*c){ //求ch串的长度
        ++len;
        ++c;
    }
    if(len == 0){ //如果ch是一个空串,直接返回空串
        str->ch = nullptr;
        str->length = 0;
        return 1;
    }else{
        str->ch = (char*) malloc(sizeof(char) * (len + 1));
        //取len+1,为了多分配一个空间存放\0
        if(str->ch == nullptr){
            return 0;
        }else{
            c = ch;
            for (int i = 0; i <= len; ++i,++c) {
                //循环的结束符号是<=,len+1,为了将ch的\0复制到新串
                str->ch[i] = *c;
                str->length = len;
            }
            return 1;
        }
    }
    return 0;
}

int strlength(Str str){
    return str.length;
}

int strcompare(Str str1,Str str2){
    for (int i = 0; i < str1.length && i < str2.length; ++i) {
        if(str1.ch[i] != str2.ch[i]){
            return str1.ch[i] - str2.ch[i];
        }
    }
    return str1.length - str2.length;
}

Str* concat(Str str1,Str str2){
    Str *str = (Str*)malloc(sizeof(Str));
    str->ch = (char*) malloc(sizeof(char) * (str1.length+str2.length));
    int i = 0;
    while(i < str1.length){
        str->ch[i] = str1.ch[i];
        ++i;
    }
    int j = 0;
    while(j <= str2.length){
        str->ch[i+j] = str2.ch[j];
        ++j;
    }
    str->length = str1.length + str2.length;
    return str;
}

Str* substring(Str str,int pos,int len){
    if(pos<0 || pos>=str.length || len<0 || len>str.length-pos){
        return nullptr;
    }
    Str *substr = (Str*)malloc(sizeof(Str));
    if(len == 0){
        substr->ch = nullptr;
        substr->length = 0;
        return substr;
    }else{
        substr->ch = (char*)malloc(sizeof(char) * (len+1));
        int i = pos;
        int j =0;
        while(i<pos+len){
            substr->ch[j] = str.ch[i];
            ++i;
            ++j;
        }
        substr->ch[j] = '\0';
        substr->length = len;
        return substr;
    }
}

int cleanstring(Str *str){
    if(str->ch){
        free(str->ch);
        str->ch = nullptr;
    }
    str->length = 0;
    return 1;
}

字符串模式匹配算法

对于一个串中某子串的定位操作称为串的模式匹配,其中待定位的子串称为模式串。

一般的算法

算法的基本思想:从主串的第一个位置和模式串的第一个字符串开始比较,如果相等,则继续逐一比较后序字符,否则从主串的第二个字符开始,再重新用上一步的方法与模式串中的字符作比较。依次类推,知道比较完模式串中的所有字符,若匹配成功,则返回模式串在主串中的位置;若匹配不成功,则返回一个可区别于主串所有位置的标记,如“0”。

int index(Str str,Str substr){
    int i = 0, j = 0,k = i;
    while (i < str.length && j < substr.length){
        if(str.ch[i] == substr.ch[j]){
            ++i;
            ++j;
        }else{
            j = 1;
            i = ++k;
        }
    }
    if(j == substr.length){
        return k;
    }else{
        return -1;
    }
}

KMP算法

以ABCABCDHIJK中找出ABCE,串中的位置指针i,j来说明,第一个位置下标以0开始,称为第0位。如果是人为来寻找的话,肯定不会再把i移动回第1位,因为主串匹配失败的位置(i=3)前面除了第一个A之外再也没有A了,为什么能知道主串前面只有一个A?因为已经知道前面三个字符都是匹配的。 移动过去肯定也是不匹配的!有一个想法,i可以不动,只需要移动j即可。

上面的这种情况还是比较理想的情况,我们最多也就多比较了再次。但假如是在串“SSSSSSSSSSSSSA”中查找“SSSSB”,比较到最后一个才知道不匹配,然后i回溯,这个的效率是显然是最低的。

当匹配失败时,j要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的。 如果用数学公式来表示是这样的P[0 ~ k-1] == P[j-k ~ j-1]。

该规律是KMP算法的关键,KMP算法是利用待匹配的子串自身的这种性质,来提高匹配速度。该性质在许多其他中版本的解释中还可以描述成:若子串的前缀集和后缀集中,重复的最长子串的长度为k,则下次匹配子串的j可以移动到第k位(下标为0为第0位)。将这个解释定义成最大重复子串解释。

这里面的前缀集表示除去最后一个字符后的前面的所有子串集合,同理后缀集指的的是除去第一个字符后的后面的子串组成的集合。举例说明如下:

在“aba”中,前缀集就是除掉最后一个字符’a’后的子串集合{a,ab},同理后缀集为除掉最前一个字符a后的子串集合{a,ba},那么两者最长的重复子串就是a,k=1;

在“ababa”中,前缀集是{a,ab,aba,abab},后缀集是{a,ba,aba,baba},二者最长重复子串是aba,k=3;

在“abcabcdabc”中,前缀集是{a,ab,abc,abca,abcab,abcabc,abcabcd,abcabcda,abcabcdab},后缀集是{c,bc,abc,dabc,cdabc,bcdabc,abcdabc,cabcdabc,bcabcdabc},二者最长重复的子串是“abc”,k=3;

当j为0时,如果这时候不匹配,怎么办? j已经在最左边了,不可能再移动了,这时候要应该是i指针后移。所以在代码中才会有next[0]=-1;这个初始化。

所以,整个KMP的重点就在于当某一个字符与主串不匹配时,应该知道j指针要移动到哪?这里直接讲述计算机如何进行处理。

  1. 找出前缀pre,设为pre[0~m];
  2. 找出后缀post,设为post[0~n];
  3. 从前缀pre里,先以最大长度的s[0~m]为子串,即设k初始值为m,跟post[n-m+1~n]进行比较:
  4. 如果相同,则pre[0~m]则为最大重复子串,长度为m,则k=m;
  5. 如果不相同,则k=k-1;缩小前缀的子串一个字符,在跟后缀的子串按照尾巴对齐,进行比较,是否相同。

根据上面的求解过程,我们知道子串的j位前面,有j个字符,前后缀必然少掉首尾一个字符,因此重复子串的最大值为j-1,因此知道下一次的j指针最多移到第j-1位。

求next数组

怎么求这个(这些)k呢?因为在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当T[i] != P[j]时,j指针的下一个位置。另一个非常有用且恒等的定义,因为下标从0开始的,k值实际是j位前的子串的最大重复子串的长度。请时刻牢记next数组的定义,下面的解释是死死地围绕着这个定义来解释的。

void getNext(Str substr,int next[]){
    int j = 0,t = 0;
    next[0] = -1;
    while (j<substr.length-1){
        if(t == -1 ||substr.ch[j] == substr.ch[t]){
            next[j+1] = t+1;
            ++t;
            ++j;
        }else{
            t = next[t];
        }
    }
}

有了next数组下面就是KMP算法

int KMP(Str str,Str substr){
    int next[100];
    getNext(substr,next);
    int i=0,j=0;
    while (i < str.length && j < substr.length){
        if(j==-1 || str.ch[i] == substr.ch[j]){
            ++i;
            ++j;
        }else{
            j = next[j];
        }
    }
    if(j == substr.length){
        return i - substr.length;
    }else{
        return 0;
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值