数据结构—串

数据结构中提到的串,即字符串,由 n 个字符组成的一个整体( n >= 0 )。这 n 个字符可以由字母、数字或者其他字符组成。

特殊的串

空串:含有零个字符的串。例如:S = “”(双引号中没有任何东西),一般直接用 Ø 表示。
空格串:只包含空格的串。注意和空串区分开,空格串中是有内容的,只不过包含的是空格,且空格串中可以包含多个空格。例如,a = ” ”(包含3个空格)。
子串与主串:串中任意个连续字符组成的字符串叫做该串的子串,包含子串的串称为主串。

例如:a = ”BEI”,b = ”BEIJING”,c = ”BJINGEI” 。对于字符串 a 和 b 来说,由于 b 中含有连续的字符串 a ,
所以可以称 a 是 b 的子串,b 是 a 的主串;而对于 c 和 a ,虽然 c 中也含有 a 的全部字符,但不是连续的 “BEI” ,所以串 c 和 a 没有任何关系。

子串在主串中的位置:对于串 a = ”BEI” 来说,首字符 ‘B’ 在串 b 的位置为 1 ,所以子串 a 在主串 b = “BEIJING” 中的位置是 1。

子串在主串中的位置和字符在数组中的存放位置不同,子串在主串的位置从 1 开始数。
两个串相等的标准:如果两个串的串值完全相同,那么这两个串相等。

串的三种存储结构存

储串的结构有三种:
1 定长顺序存储;
2 堆分配存储;
3 块链存储。

定长顺序存储

采用固定长度的数组(即静态数组)存储串。
例如:char a[7] = "abcdfg";
此方式存储串时,需要预估串的长度提前申请足够的存储空间。目标串如果超过了数组申请的长度,超出部分会被自动舍弃(称为“截断”)。
例如:char a[3] = "abcdfg";//实际上数组中只存储了 “abc” ,后边的被截断。堆分配存储

采用动态数组存储串。

在C语言中,存在着一个被称之为“堆”的自由存储区,用 malloc 函数和 free 函数管理,malloc 函数负责申请空间,free 函数负责释放空间。
例如:

char * a = (char*)malloc(5*sizeof(char));//创建 a 数组,动态申请5个 char 类型数据的存储空间

使用堆分配存储的优势在于:当发现申请的空间不够用时,可以通过 realloc() 函数重新申请更大的存储空间。

例如:a = (char*)realloc(a, 10*sizeof(char));//前一个参数指申请空间的对象;第二个参数,重新申请空间的大小

使用 malloc 函数申请的存储空间,不会自动释放,需要程序员调用 free() 函数手动释放。如果不手动释放,当程序执行彻底结束,由操作系统进行回收。

例如:free(a);//释放动态数组a申请的空间

举一个完整的例子,连接串 “abc” 和 “defg” 变为 “abcdefg” ;

#include 
#include 
#include 

int main()
{
    char * a1=NULL;
    char * a2=NULL;
    
    a1=(char*)malloc(3*sizeof(char));
    strcpy(a1, "abc");//将字符串“abc”复制给a1
    
    a2=(char*)malloc(3*sizeof(char));
    strcpy(a2, "defg");
    
    int lengthA1=strlen(a1);
    int lengthA2=strlen(a2);
    if (lengthA1
image.png

注:在程序中,我们给 a1 和 a2 赋值的时候,使用了 strcpy 复制函数。在这里不能直接用:a1 = ”abc”这种方式,
如果你这样做,程序编译会出错,告诉你,没有 malloc 的空间不能 free 。

原因是: strcpy 函数是将字符串复制到申请的存储空间中,而直接赋值是字符串存储在别的内存空间(本身是一个常量,放在常量区)中,
更改了指针 a1 和 a2 的指向,也就是说,之前动态申请的存储空间虽然申请了,结果还没用呢就丢了。

块链存储

块链存储,其实就是借用链表的存储结构来存储串。一般情况下使用单链表就足够了,而且不需要增设头结点。

在构建链表时,每个结点可以存放一个字符,也可以存放多个字符。


数据结构—串_第1张图片
块链存储.png

链表中最后一个结点的数据域不一定全被串值占满,通常会补上 “#” 或者其他特殊的字符和字符串中的字符区分开。

每个结点设置字符数量的多少和存储的串的长度、可以占用的存储空间以及程序实现的功能相关。

如果串包含数据量很大,但是可用的存储空间有限,那么就需要提高空间利用率,相应地减少结点数量(因为多一个节点,就多申请一个指针域的空间)。

而如果程序中需要大量地插入或者删除数据,如果每个节点包含的字符过多,操作字符就会变得很麻烦,为实现功能增加了障碍。

总结

在平时编写程序,经常会用到例如:char *a = ”abcd”;这种方式表示字符串,和上面三种存储方式最主要的区别是:这种方式用于表示常量字符串,只能使用,不能对字符串内容做修改(否则程序运行出错);而以上三种方式都可以对字符串进行删改的操作。

例如:

#include 
int main() {
    char* a="abcd";
    a[1]='b';
    return 0;
}

程序编译可以通过,运行失败,改成下面堆分配存储的方式就对了:

#include 
#include 
#include 
int main() {
    char * a=(char*)malloc(4*sizeof(char));
    strcpy(a, "abcd");
    a[1]='e';
    printf("%s",a);
    return 0;
}
image.png

三种存储表示方式中,最常用的是堆分配存储,因为它在定长存储的基础上通过使用动态数组,避免了在操作串时可能因为申请存储空间的不足而丢失字符数据;和块链存储方式相比,结构相对简单,更容易操作。


BF算法(普通模式匹配算法)

判断两个串之间是否存在主串与子串的关系,这个过程称为串的模式匹配。

在串的模式匹配过程,子串 T 通常被叫做“模式串”。

普通的模式匹配(“BF”算法)

判断两个串是否存在子串与主串的关系,最直接的算法就是拿着模式串,去和主串从头到尾一一比对,这就是“BF”算法的实现思想。

将提供的模式串(例如 “abcac” )从主串的第一个字符开始,依次判断相同位置的字符是否相等,如果全部相等,则匹配成功;

反之,将子串向后移动一个字符的位置,继续与主串中对应的字符匹配。

算法运行过程:(图中,i 和 j 表示匹配字符在数组中的位置下标)


数据结构—串_第2张图片
image.png

如图所示,第一次匹配,模式串和主串匹配到第三个字符时,匹配失败;模式串向右移动一个字符的位置,

还是从第一个字符 ‘a’ 和主串的第二个字符 ‘b’ 相匹配,匹配失败;模式串继续后移一个字符的位置,继续匹配。

#include 
#include 
int sel(char * S,char *T){
    int i=0,j=0;
    while (i
image.png
“BF”算法的时间复杂度

“BF” 算法在最理想的情况下的时间复杂度为O(m)( m 是模式串的长度,也就是第一次匹配就成功的情况)。
一般情况下,"BF"算法的时间复杂度为O(n+m)(n是主串的长度,m是模式串的长度)。
最坏的情况下的时间复杂度为O(nm)(例如主串 S 为“000000000001”,模式串 T ”001”,每次匹配时,
直到匹配最后一个元素,才得知匹配失败,运行了 n
m 次)。

总结

“BF”算法在进行模式匹配时,从主串的第一个字符开始,每次失败,模式串向后移动一个字符的位置,继续匹配,无脑式操作。但是整个算法受测试数据的影响非常大,在解决实际问题时,由于数据量庞大,时间复杂度往往会很高。所以,对在“BF”算法的基础上,对其做了改进,形成“KMP”算法。


KMP算法(快速模式匹配算法)

普通模式匹配算法,大体思路是:模式串从主串的第一个字符开始匹配,每匹配失败,主串中记录匹配进度的指针 i 都要进行 i-j+1 的回退操作(这个过程称为“指针回溯”),同时模式串向后移动一个字符的位置。一次次的循环,直到匹配成功或者程序结束。

"KMP"算法相比于"BF"算法,优势在于:

  • 在保证指针 i 不回溯的前提下,当匹配失败时,让模式串向右移动最大的距离;
  • 并且可以在O(n+m)的时间数量级上完成对串的模式匹配操作;

故,"KMP"算法称为“快速模式匹配算法”。

模式串向右移动距离的计算

在模式串和主串匹配时,各有一个指针指向当前进行匹配的字符(主串中是指针 i ,模式串中是指针 j ),在保证 i 指针不回溯的前提下,如果想实现功能,就只能让 j 指针回溯。

j 指针回溯的距离,就相当于模式串向右移动的距离。 j 指针回溯的越多,说明模式串向右移动的距离越长。

计算模式串向右移动的距离,就可以转化成:当某字符匹配失败后, j 指针回溯的位置。

对于一个给定的模式串,其中每个字符都有可能会遇到匹配失败,这时对应的 j 指针都需要回溯,具体回溯的位置其实还是由模式串本身来决定的,和主串没有关系。

模式串中的每个字符所对应 j 指针回溯的位置,可以通过算法得出,得到的结果相应地存储在一个数组中(默认数组名为 next )。

计算方法是:对于模式串中的某一字符来说,提取它前面的字符串,分别从字符串的两端查看连续相同的字符串的个数,在其基础上 +1 ,结果就是该字符对应的值。

每个模式串的第一个字符对应的值为 0 ,第二个字符对应的值为 1 。

例如:求模式串 “abcabac” 的 next 。前两个字符对应的 0 和 1 是固定的。

对于字符 ‘c’ 来说,提取字符串 “ab” ,‘a’ 和 ‘b’ 不相等,相同的字符串的个数为 0 ,0 + 1 = 1 ,所以 ‘c’ 对应的 next 值为 1 ;
第四个字符 ‘a’ ,提取 “abc” ,从首先 ‘a’ 和 ‘c’ 就不相等,相同的个数为 0 ,0 + 1 = 1 ,所以,‘a’ 对应的 next 值为 1 ;
第五个字符 ‘b’ ,提取 “abca” ,第一个 ‘a’ 和最后一个 ‘a’ 相同,相同个数为 1 ,1 + 1 = 2 ,所以,‘b’ 对应的 next 值为 2 ;
第六个字符 ‘a’ ,提取 “abcab” ,前两个字符 “ab” 和最后两个 “ab” 相同,相同个数为 2 ,2 + 1 = 3 ,所以,‘a’ 对应的 next 值为 3 ;
最后一个字符 ‘c’ ,提取 “abcaba” ,第一个字符 ‘a’ 和最后一个 ‘a’ 相同,相同个数为 1 ,1 + 1 = 2 ,所以 ‘c’ 对应的 next 值为 2 ;
所以,字符串 “abcabac” 对应的 next 数组中的值为(0,1,1,1,2,3,2)。
上边求值过程中,每次都需要判断字符串头部和尾部相同字符的个数,而在编写算法实现时,对于某个字符来说,可以借用前一个字符的判断结果,计算当前字符对应的 next 值。

具体的算法如下:
模式串T为(下标从1开始):“abcabac”
next数组(下标从1开始): 01

第三个字符 ‘c’ :由于前一个字符 ‘b’ 的 next 值为 1 ,取 T[1] = ‘a’ 和 ‘b’ 相比较,不相等,继续;由于 next[1] = 0,结束。 ‘c’
对应的 next 值为1;(只要循环到 next[1] = 0 ,该字符的 next 值都为 1 )

模式串T为: “abcabac”
next数组(下标从1开始):011

第四个字符 ’a‘ :由于前一个字符 ‘c’ 的 next 值为 1 ,取 T[1] = ‘a’ 和 ‘c’ 相比较,不相等,继续;
由于 next[1] = 0 ,结束。‘a’ 对应的 next 值为 1 ;

模式串T为: “abcabac”
next数组(下标从1开始):0111

第五个字符 ’b’ :由于前一个字符 ‘a’ 的 next 值为 1 ,取 T[1] = ‘a’ 和 ‘a’ 相比较,相等,结束。
‘b’ 对应的 next 值为:1(前一个字符 ‘a’ 的 next 值) + 1 = 2 ;

模式串T为: “abcabac”
next数组(下标从1开始):01112

第六个字符 ‘a’ :由于前一个字符 ‘b’ 的 next 值为 2,取 T[2] = ‘b’ 和 ‘b’ 相比较,相等,所以结束。
‘a’ 对应的 next 值为:2 (前一个字符 ‘b’ 的 next 值) + 1 = 3 ;

模式串T为: “abcabac”
next数组(下标从1开始):011123

第七个字符 ‘c’ :由于前一个字符 ‘a’ 的 next 值为 3 ,取 T[3] = ‘c’ 和 ‘a’ 相比较,不相等,继续;
由于 next[3] = 1 ,所以取 T[1] = ‘a’ 和 ‘a’ 比较,相等,结束。‘a’ 对应的 next 值为:1 ( next[3] 的值) + 1 = 2 ;

模式串T为: “abcabac”
next数组(下标从1开始):0111232

算法实现:

void Next(char*T,int *next){
    int i=1;
    next[1]=0;
    int j=0;
    while (i

注意:在此程序中,next 数组使用的下标初始值为 1 ,next[0] 没有用到(也可以存放 next 数组的长度)。
而串的存储是从数组的下标 0 开始的,所以程序中为 T[i-1] 和 T[j-1]。

基于next的KMP算法的实现

先看一下 KMP 算法运行流程(假设主串:ababcabcacbab,模式串:abcac)。

第一次匹配:


数据结构—串_第3张图片
image.png

匹配失败,i 指针不动,j = 1(字符‘c’的next值);

第二次匹配:


数据结构—串_第4张图片
image.png

相等,继续,直到:

数据结构—串_第5张图片
image.png

匹配失败,i 不动,j = 2 ( j 指向的字符 ‘c’ 的 next 值);

第三次匹配:


数据结构—串_第6张图片
image.png

相等,i 和 j 后移,最终匹配成功。

使用普通算法,需要匹配 6 次;而使用 KMP 算法,则只匹配 3 次。

实现代码:

int KMP(char * S,char * T){
    int next[10];
    Next(T,next);//根据模式串T,初始化next数组
    int i=1;
    int j=1;
    while (i<=strlen(S)&&j<=strlen(T)) {
        //j==0:代表模式串的第一个字符就和指针i指向的字符不相等;S[i-1]==T[j-1],如果对应位置字符相等,两种情况下,指向当前测试的两个指针下标i和j都向后移
        if (j==0 || S[i-1]==T[j-1]) {
            i++;
            j++;
        }
        else{
            j=next[j];//如果测试的两个字符不相等,i不动,j变为当前测试字符串的next值
        }
    }
    if (j>strlen(T)) {//如果条件为真,说明匹配成功
        return i-(int)strlen(T);
    }
    return -1;
}
代码整合
#include 
#include 
void Next(char*T,int *next){
    int i=1;
    next[1]=0;
    int j=0;
    while (istrlen(T)) {//如果条件为真,说明匹配成功
        return i-(int)strlen(T);
    }
    return -1;
}

int main() {
    int i=KMP("ababcabcacbab","abcac");
    printf("%d",i);
    return 0;
}
image.png
升级版的next

注意:KMP 算法的关键在于 next 数组的确定,其实对于上边的KMP算法中的next数组,不是最精简的,还可以简化。

例如:

模式串T:a b c a c
    next  :0 1 1 1 2

在模式串“abcac”中,有两个字符 ‘a’,我们假设第一个为 a1,第二个为 a2。在程序匹配过程中,如果 j 指针指向 a2 时匹配失败,那么此时,主串中的 i 指针不动,j 指针指向 a1 ,很明显,由于 a1==a2,而 a2!=S[i],所以 a1 也肯定不等于 S[i]。

为了避免不必要的判断,需要对 next 数组进行精简,对于“abcac”这个模式串来说,由于 T[4] == T[next[4]] ,所以,可以将next数组改为:

模式串T:a b c a c
    next  :0 1 1 0 2

这样简化,如果匹配过程中由于 a2 匹配失败,那么也不用再判断 a1 是否匹配,因为肯定不可能,所以直接绕过 a1,进行下一步。

实现代码:

void Next(char*T,int *next){
    int i=1;
    next[1]=0;
    int j=0;
    while (i

使用精简过后的 next 数组在解决例如模式串为“aaaaaaab”这类的问题上,会减少很多不必要的判断次数,提高了KMP算法的效率。

例如:精简前为 next1,精简后为 next2:

模式串:a a a a a a a b
  next1:0 1 2 3 4 5 6 7
  next2:0 0 0 0 0 0 0 7
总结

KMP 算法,之所以比 BF 算法快的根本原因在于:KMP 算法其实也和 BF 算法一样,都是从主串开头开始匹配,但是在匹配过程中,KMP算法记录了一些必要的信息。根据这些信息,在后续的匹配过程中,跳过了一些无意义的匹配过程。

你可能感兴趣的:(数据结构—串)