1 串

1.1 串的存储结构

  • 定长顺序存储
typedef struct{
    char str[maxSize+1]; //加1的空间是为了保存'\0'
    int length; //长度
}Str;
  • 变长分配存储表示
typedef struct{
    char *ch; //在分配时使用malloc动态分配长度为length的空间
    int length;
}Str;

2.2 串的基本操作

  • 赋值操作
int strassign(Str &str, char *ch)
{
    //该函数将指针ch所指的串赋值给str
    //需要执行的步骤包括:释放原数据,检查ch的长度,逐一赋值
    //对于串而言,赋值必须是一个一个字符进行,而不能整体赋值(用string类型则不同,这里不讨论)
}
//具体调用
strassign(str, "hello");
  • 取串长操作
  • 串比较操作(串比较实际上是字典序比较(ASCLL))
  • 串连接操作(将两个串拼接成一个串)
  • 求字串操作(求串中给定位置开始长度为len的字串)
  • 串清空操作

2.3 串的模式匹配算法KMP

对一个串中某子串的定位操作称为串的模式匹配

  • 简单模式匹配算法

简单模式匹配算法的思想为单纯地顺序推进,每推进一个位置进行一次比较,若本位字符相等则比较下一位字符是否相等,若不相等则子串整体向后移动一个单位重新开始进行比较直至子串中所有的字符都完成匹配,否则失败。

int index(Str str, Str substr){
    int i=1, j=1, k=1;
    while(i<str.length && j<substr.length){
        if(str[i]==substr[j])
        {
            ++i;
            ++j;
        }else{
            j=1;
            k++;
            i=k;
        }
    }
    if(j>substr.length) //如果j大于子串的长度说明上面的循环是因为子串已经匹配完成而退出的
        return k; //用k标记子串在主串中的位置
    return 0;
}
  • KMP算法(难)

进步状态:当匹配受阻时,进步状态指找到了下一个能够完成对当前受阻字符前所有字符的匹配工作的状态(有点绕)
更加直观的解释是:正如我们在前面的普通方法中所了解到的,假设子串的长度为l,在位置k时我们已经完成了对子串的前m个字符的匹配,但是第m+1个字符匹配错误;如果按普通方法进行,那么我们将子串整体向前推进一格,同时重新开始子串的匹配(从第一个字符开始);而“进步状态”的思想则是直接将子串推进到下一次有可能进行匹配的状态,这样节省了大量的匹配时间,那么难点就来到了如何构建这样一个“跳转表”。

字符匹配的过程实际上就是一次一次寻找下一个进步状态的过程,直到完成全部的串匹配工作

next数组:next [ j ]数组中的元素标记了当 j 号元素发生阻塞时下一个进步状态的“位置”,直接实现跳转即可

如何求next数组?

void getnext(Str substr, int next[]){
    int i=1,j=0; //串从数组下标1的位置开始存储,因此初值为1
    next[1]=0; //标志第一种特殊情况,即第一个字符不匹配
    while(i<substr.length){
        if(j==0||substr.ch[i]==substr.ch[j]){
  //当j=0时表示没有相同的串,需要从第一个字符开始比较
  //如果substr.ch[i]==substr.ch[j]成立那么说明当i+1位置阻塞时,可以直接跳转到j+1的位置进行比较
            ++i;
            ++j;
            next[i] = j;
        }else{
            j=next[j];//如果不匹配那么就要“迭代寻找”,即跳转到j不匹配时的下一个进步状态
        }
    }
}
  • KMP算法
int KMP(Str str, Str subStr, int next[]){//KMP算法的实现
    int i=1, j=1;//串从数组下标1开始存储存
    while(i<=str.length && j<=subStr.length){
        if(j==0 || str.ch[i] == subStr.ch[j]){//j==0表示第一位就不匹配,发生了第一种特殊情况
            ++i;
            ++j;
        }else{
            j = next[j];
        }
    }
    if(j>subStr.length){
        return i-str.length;//返回子串的位置
    }else
        return 0;
}

2.4 KMP算法的改进

改进思路: 在KMP算法中可能存在大量的重复比较(如字符串“AAAAAB”,在next数组中next[5]保存的是4,next[ 4 ]保存的是3……,但其实他们都是重复的)

具体改进方法:将next数组进行改进,判断各个值是都会造成“浪费比较”,得到nextval数组

求nextval的一般步骤:

  • 当j=1时,nextval[j]=0,作为特殊标记
  • 当next数组所指值与当前值不相等时,nextval赋值为所指值
  • 当next数组所指值与当前值相等时,nextval赋值为所指值的nextval值

注意: 这里是 推进 的思想,核心有两点:第一,求nextval的过程是从第一个字符开始进行确认的;第二,后续比较的记录,已经被记录在了nextval数组中,无需重复;

代码实现:

void getnextval(Str substr, int nextval[]){
    int i=1, j=0;//字符串从下标位置1开始存储,因此初始值为1
    nextval[1] = 0;//标志第一种特殊情况
    while(i<=substr.length){
        if(j==0||substr[i]==substr[j]){
            ++i;
            ++j;//到这里已经确认了next数组中的值,即next[i]==j;
            if(substr[i]!=substr[j]){//如果所指值和当前值不相等,满足nextval要求,直接赋值
                nextval[i]=j;
            }else{//不满足要求
                nextval[i]=nextval[j];
            }
        }else{
            j=nextval[j];
        }
    }
}

2.5 知识点

  • 串的两种最基本的存储方式:顺序存储方式和链式存储方式
  • 串的长度指“字符元素的个数”,数组的长度等于串的长度加一(保存‘\0’)
  • KMP算法中的next以及nextval数组中的值来自于子串本身,而与所要进行匹配的串无关
  • KMP算法实现累计匹配(不考虑子串重叠情况)
int KMP(Str str, Str substr){
    int i=1,j=1,sum=0;
    while(i<=str.length){//只要没有完全扫描完就不停止进行匹配
        if(j==0||str[i]==str[j]){
            ++i;
            ++j;
        }else{
            j=next[j];
        }
        if(j>str.length){//循环对j的值进行判断,只要确定已经完成了一次子串的匹配,就重新进行匹配(j=0)
            j=1;
            ++sum;
        }
    }
    return sum;//返回总匹配次数
}