数据结构一周目:第四章 - 串

109 阅读8分钟

前言

Hi, 我是Rike,欢迎来到我的频道~

本篇为大家带来的是,C语言版数据结构笔记,第四章-串。串是一种比较特殊的结构,它将零个或多个字符组合成为一个有序的字符序列,在不同语言中的表示和处理方式会有很大的不同。本篇会讲述关于C语言是如何处理串的,以及经典的模式匹配算法。

希望各位多多支持~


一、基础知识

串是由零个或多个字符组成的有限序列,是一种特殊的线性表,字符串中的字符之间也具有"一对一"的逻辑关系。串的最后一个字符 \0 作为编译器识别串结束的标记。

数据结构中,根据串中存储字符的数量及特点,对一些特殊的串进行了命名:

  • 空串:存储 0 个字符的串,例如 S = ""(双引号紧挨着)。

  • 空格串:只包含空格字符的串,例如 S = " "(双引号包含 3 个空格)。

  • 子串和主串:假设有两个串 a 和 b,如果 a 中可以找到几个连续字符组成的串与 b 完全相同,则称 a 是 b 的主串,b 是 a 的子串。

    例如,若 a = "shujujiegou",b = "shuju",由于 a 中也包含 "shuju",因此串 a 和串 b 是主串和子串的关系;

需要注意的是,空格串和空串不同,空格串中含有字符,只是都是空格而已。另外,只有串 b 整体出现在串 a 中,才能说 b 是 a 的子串,比如 "shujiejugou" 和 "shuju" 就不是主串和子串的关系。

串存储结构的具体实现

存储一个字符串,数据结构包含以下 3 种具体存储结构:

  1. 定长顺序存储:实际上就是用普通数组(又称静态数组)存储;

  2. 堆分配存储:用动态数组存储字符串;

  3. 块链存储:用链表存储字符串;

    第三种存储不在描述

二、存储结构

(一)定长顺序存储

串的定长顺序存储结构,可以简单地理解为采用 "固定长度的顺序存储结构" 来存储字符串,因此限定了其底层实现只能使用静态数组。

使用定长顺序存储结构存储字符串时,需结合目标字符串的长度,预先申请足够大的内存空间。

 typedef struct
 {
     char str[maxsize+1];//maxsize为穿的最大长度
     /*串除了本身存储的数据元素外,还有最后一位的结束标记符,因此最大长度需+1*/
     int length;
 }Str;

不同参考书对串的结构体定义有所不同。在本资料里,给串尾加上 ' \0 ' 结束标记,同时其也包含在 length 中。

(二)堆分配存储

即,变长分配存储(动态数组存储)。

 typedef struct
 {
     char *ch;//指向动态分配存储区首地址的字符指针
     int length;//串长度
 }Str;
 
 /*相关操作*/
 Str S;
 S.length = L;
 S.ch = (char*)malloc((L+1)*sizeof(char));
 S.ch[length范围内的位置] = 某字符变量;
 某字符变量 = S.ch[length范围内的位置];
 free(S.ch);

这种存储方式在使用时,需要 malloc 和 free 函数动态申请和释放空间,在串处理应用程序中更为常用。

三、串操作

(一)赋值操作

 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=NULL;
         str.length=0;
         return 1;
     }
     else
     {
         str.ch=(char*)malloc(sizeof(char) * (len+1));
         if(str.ch==NULL)
             return 0;
         else
         {
             c=ch;
             for(int i=0;i<=len;++i,++c)//此处用“<=”是将‘/0’复制到新串做结束标记
                 str.ch[i]=*c;
             str.length=len;
             return 1;
         }
     }
 }
 /*函数使用格式*/
 strassign(str,"输入的字符");

(二)取串长度

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

(三)串比较

 /*一般比较的是ASCII码值或串长度*/
 int strCompare(Str s1, Str s2)
 {
     for(int i=0;i<s1.length && i<s2.length;++i)
         if (s1.ch[i]!=s2.ch[i])
             return s1.ch[i] - s2.ch[i];
     return s1.length - s2.length;
 }

(四)串连接

 /*将两个串收尾相接,合并成一个字符串0*/
 int concat(Str& str, Str str1, Str str2)
 {
     if(str.ch)
     {
         free(str.ch);
         str.ch=NULL;
     }
     str.ch=(char*)malloc(sizeof(char)*(str1.length+str2.length+1));
     if(!str.ch)
         return 0;
     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 1;
 }

(五)求子串

 /*从给定串中某一位置开始到某一位置结束的操作*/
 int subString(Str& substr, Str str, int pos, int len)
 {
     if(pos<0||pos>=str.length||len<0||len>str.length-pos)
         return 0;
     if(substr.ch)
     {
         free(substr.ch);
         substr.ch=NULL;
     }
     if(len==0)
     {
         substr.ch=NULL;
         substr.length=0;
         return 1;
     }
     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 1;
     }
 }

(六)清空串

 int clearString(Str& str)
 {
     if(str.ch)
     {
         free(str.ch);
         str.ch=NULL;
     }
     str.length=0;
     return 1;
 }

四、BF 算法

普通模式匹配算法,其实现过程没有任何技巧,就是简单粗暴地拿一个串同另一个串中的字符一一比对,得到最终结果。

 int index(Str str,Str substr)
 {
     int i=1,j=1,k=1;//串从数组下标1位置开始存储,因此初值为1
     while(i<=str.length && j<=substr.length)
     {
         if(str.ch[i] == substr.ch[j])
         {
             ++i;++j;
         }
         else
         {
             j=1;
             i=++k;//匹配失败,i从主串的下一位置开始,k中记录了上一次的起始位置
         }
     }
     if(j>substr.length)
         return k;
     else return 0;
 }

五、kmp 算法

(一)目的

快速的从主串找出想获取的子串。

(二)求 next[j]值的方法

通过消除主串指针的回溯来提高匹配效率。

以下两种方法,数组下标从 0 或 1 开始,建议数组下标从 1 开始。

  • 下标从 0开始:

    • next[ 0 ] = -1
    • next[ 1 ] = 0
    • next[ j ] = 从 1 到 j 的最大公共前后缀
  • 下标从 1开始:

    • next[ 1 ] = 0
    • next[ 2 ] = 1
    • next[ j ] = 从 1 到 j - 1 的最大公共前后缀

1、方法一

(1)在主串与模式串 t 匹配部分(相同的部分)字符串中,有 n 个相同的公共前后缀。

匹配部分的两端有某两个子串完全相同,分别称为前缀和后缀

(2)定义一个 next[ j ] 数组(1 < j < m,m 为 t 长度)。

作用:当 t 中第 j 个字符发生不匹配时,应从 next[ j ] 开始处的字符开始重新与主串比较。 ( j 前部分为公共前后缀)

(3)将 t 以从前向后的顺序分别给出数组的下标,并输入 next[ ] 数组。

(4)当 next[ ] 中第 j 个字符发生不匹配时,找出 j 前所有字符的最大长度 L 且相同的前缀与后缀,此时 L 为 next[ j ] 的值。

记: next[ j ] = L + 1

其中,

  • j:t 的下标;
  • L:第 j 个字符前字符串的公共前后缀最大长度(不包括字符串本身)

2、方法二

本质:根据前一位进行递归比较,最后 next[j] = next[k] +1 或 next[j] = 1。

设:下标 j,字符 t,next 值,将这三位看为一组数据。

一组数据包含三个值。例:A 组数据:Aj 、At 、Anext

(1)将第 j 位的前一位数据组作为 A,将其字符记为“ 比较字符 At ”。寻找与 Anext值相等的数组下标的数据组,记为“ 比较组 B ”。

(2)判断

  • 当 At = Bt ,则 next[ j ] 等于 B 组中的 next 值加 1 ,即: next[ j ] = Bnext +1。

  • 当 At != Bt ,则寻找与 Bnext 相等下标的数据组,该组数据为新的比较组 C 。

    • 当 At = Ct ,则 next[j] 等于 C 组中的 next 值加 1,即:next[ j ] = Cnext +1。
    • 当 At != Ct ,则再次寻找,直到寻找到一组数据 M 中 Mnext 所对应下标数据中的字符 t 与 At 相等,则 next[ j ] = Mnext +1 。

若找到第一位其字符 t 与 At 都不匹配,则 next[ j ] = 1。

在这个过程中,只有比较字符 At 不需要变动,比较组会递归地向前查询判断。

(三)代码

1、求 next 数组

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

2、KMP 算法

 int KMP(Str str, Str substr, int next[])
 {
     int i=1,j=1;//串从数组下标1位置开始存储,因此初值设1
     while(i<=str.length && j<=substr.length)
     {
         if(j==0||str.ch[i]==substr.ch[j])
         {
             ++i;
             ++j;
         }
         else
         {
             j=next[j];
         }
     }
     if(j>substr.length)
         return i-substr.length;
     else
         return 0;
 }

六、kmp 算法的改进

(一)求解 nextval 数组的一般步骤

设第 j 位的数据组作为 A ,其字符记为“ 比较字符 At ”,求其 nextval[ j ]。

寻找与 At值相等的数组下标的数据组,记为“ 比较组 B ”。

  1. 当 j = 1 时,nextval[ j ] = 0 ,作为特殊标记。

    可以确定 nextval 数组的第一位与 next 数组第一位相同。

  2. 当 j >= 2 时,比较 At 与 Bt 是否相同:

    1. 若相同,则 nextval [ j ] 值等于 B 的 nextval 值,即:nextval [ j ] = Bnextval 。
    2. 若不同,则 nextval [ j ] 值等于自身的 next 值,即:nextval [ j ] = Anext 。

不再进行向前方递归查询

1、下标从 0 开始

2、下标从 1 开始

(二)Code

 void getNextval(Str substr, int nextval[])
 {
     int j = 1, t = 0;//下标从1开始,所以j=1
     nextval[1] = 0;
     while(j < substr.length)
     {
         if(t==0 || substr.ch[j]==substr.ch[t])
         {
             if(substr.ch[j+1] != substr.ch[t+1])
                 nextval[j+1] = t+1;
             else
                 nextval[j+1] = nextval[t+1];
             ++j; ++t;
         }
         else
             t = nextval[t];
     }
 }

参考资料

个人学习记录,若有侵权,请留言联系

  • 2022 天勤计算机考研高分笔记-数据结构
  • 2022 王道计算机考研复习指导-数据结构
  • 解学武数据结构与算法教程(C 语言版):data.biancheng.net/