《数据结构(C语言版)》学习笔记05 串

目录

一、基础知识

1.1 定义

1.2 存储方式

1.3 基本操作

1.3.1 初始化、创建、销毁、清空

1.3.2 判空、求长

1.3.3 比较

1.3.4 连接

1.3.5 求子串

1.3.6 查找

二、模式匹配

2.1 朴素模式匹配

2.2 KMP算法

2.3 改进的KMP算法


一、基础知识

1.1 定义

串(String)(或字符串)是由零个或多个字符组成的有限序列。

然后照例搬上ADT:

ADT String{

StrAssign(&T, chars)
    初始条件:chars是字符串常量。
    操作结果:生成一个其值等于chars的串T。
StrCopy(&T, S)
    初始条件:串S存在。
    操作结果:由串S复制得串T。
StrEmpty(S)
    初始条件:串S存在。
    操作结果:若S为空串,则返回true,否则返回false。
StrCompare(S, T)
    初始条件:串S和T存在。
    操作结果:若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。
StrLength(S)
    初始条件:串S存在。
    操作结果:返回S得元素个数,称为串的长度。
ClearString(&S)
    初始条件:串S存在。
    操作结果:将S清为空串。
Concat(&T, S1, S2)
    初始条件:串S1和S2存在。
    操作结果:用T返回由S1和S2联接而成的新串。
SubString(&Sub, S, pos, len)
    初始条件:串S存在,1<=pos<=StrLength(S)且0<=len<=StrLength(S)-pos+1。
    操作结果:用Sub返回串S的第pos个字符起长度为len的子串。
Index(S, T, pos)
    初始条件:串S和T存在,T是非空串,1<=pos<=StrLength(S)。
    操作结果:若主串S中存在和串T值相同的子串,则返回它在主串S中第pos个字符之后
             第一次出现的位置;否则返回值为0。

}

 这部分算是抽象数据结构,但是没有数据那部分,只写了基本操作,可以起到一定的辅助理解作用。

1.2 存储方式

串的存储方式主要有三种:

①定长顺序存储

其实就是顺序表中的静态内存分配方式,这里不展开了。

②堆分配存储

其实就是顺序表中的动态内存分配方式,malloc 函数分配的内存在堆(Heap)中,而静态内存分配的内存在栈(Stack)中。所以动态分配也叫堆分配存储。后面的代码主要针对这种存储方式实现。

③块链存储

首先块链存储是以链表为基础的,其次,如果一个结点只存一个字符的话,char 类型占 1 字节,Node * 类型的 next 指针占 4 字节,存储密度=1/(1+4)=20%,存储密度很低,虽然运算处理方便,然而存储占用量大。而块链表,则是在一个结点中存储多个字符(块),提高存储密度。

例如,以下是每个结点包含80个字符的块链字符串定义:

#define CHUNKSIZE 80

typedef struct Chunk{
    char ch[CHUNKSIZE];
    struct Chunk *next;
}Chunk;

typedef struct{
    Chunk *head, *tail;
    int len;
}LString;

1.3 基本操作

以下代码仅针对堆分配存储实现。

1.3.1 初始化、创建、销毁、清空

typedef struct{
    char *ch;
    int length;
}HString;

bool StrInit(HString &S){
    S.ch = NULL;
    S.length = 0;
    return true;
}

bool StrAssign(HString &T, char* chars){
    if(T.ch) free(T.ch);
    char *c = chars;
    int i;
    for(i=0; *c; i++, c++);
    if(!i){
        T.ch = NULL;
        T.length = 0;
    }
    else{
        T.ch = (char *)malloc((i+1)*sizeof(char));
        if(!T.ch) return false;
        for(int j=1; j<i+1; j++){
            T.ch[j] = chars[j-1];
        }
        T.length = i;
    }
    return true;
}

我这里擅自在教材的基础上加了个初始化函数,因为如果要传值的话,需要先清空原来的串,但是清空的手段只有 free。但是通过 HString 创建了一个字符串 S 后,S.ch 的内存是无法访问的,自然无法 free 了。事实也因此报过错,所以在赋值之前先初始化置空,就能保证赋值函数的正常运行了。

解释一下赋值函数 StrAssign:

目的是将一个字符串 chars 赋值给自定义的结构体 HString(Heap String),这就需要 chars 的串长,也是代码中的第一步:

for(i=0; *c; i++, c++);

首先,这句代码在执行之前,先将 c 指向了 chars 的首地址,对 c 执行 ++ 操作其实是对 c 的地址执行 ++ 操作,每次 ++ 会向后走一个 sizeof(char),实现对字符串的遍历。

其次,终止条件 *c 其实不是 c==NULL,事实上是 *c=='\0',代表了字符串的末尾(但其实NULL和'\0'差别不大,见注)。

最后,for 循环执行完了,i 应该 chars 的最大下标+1,即 chars 的串长。

再之后的一个 if else 语句,则是判断当 i=0 时,意味着 chars 为空,但是空串也是串,不能不做操作;如果 i!=0,则按照 i+1 给自定义串分配内存空间。为什么是 i+1 呢,因为我的串下标是从 1 开始的,所以实际的数组长度 = 串长+1,下标为 0 的空间是不用的。

然后,便通过一个 for 循环给 S.ch 赋值即可。

注:关于几个 0 值的区别,才疏学浅故引用之。

0,‘0’,‘\0’,null的区别_hellosc01的博客-优快云博客_null和0的区别0:int型,表示数字0,ASCII码值为0;’\0’:char型,表示一个字符串结束的标志,不会显示,也不会单独存在,ASCII码值为0;null:代表空,ASCII码值为0 ;’0’:char类型,表示字符的内容为0,ASCII码值为48;从内存的角度看它们的区别:在计算机内存中,0、’\0’、null是一样的,值都是0。以数字的方式读取就是0,以字符的方式读取就是‘\0’,以某些其他方式读取就是null(null的定义跟编译器有关,有的编译器定义null可能不是0);而’0’在内存https://blog.youkuaiyun.com/sc179/article/details/108192225

bool DestroyString(HString &S){
    if(!S.ch) return false;
    free(S.ch);
    return true;
}

bool ClearString(HString &S){
    if(!S.ch) return false;
    S.length = 0;
    return true;
}

还有销毁和清空,老朋友了,每章每个数据结构都写…如果不理解请见线性表那篇:《数据结构(C语言版)》学习笔记2 线性表_奋起直追的vv的博客-优快云博客设计线性表的概念、操作、实现原理,具体说来,又分为顺序表、单链表(有头结点)、单链表(无头结点)、双向链表、循环单链表、静态链表和双循环链表(简述原理),另外还涉及不同表的特点及对比,由浅入深,比较全面。......https://blog.youkuaiyun.com/vv0610_/article/details/125472116

1.3.2 判空、求长

bool StrEmpty(HString S){
    return S.length==0?true:false;
}


int StrLength(HString S){
    return S.length;
}

1.3.3 比较

int StrCompare(HString S, HString T){
    for(int i=1; i<S.length+1 && i<T.length+1; i++){
        if(S.ch[i]!=T.ch[i]) return S.ch[i]-T.ch[i];
    }
    return S.length-T.length;
}

在开始之前要明确字符串比较的目的,即若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。其实说白了,如果两个字符串能加减,那么返回的就是 S-T。

众所周知啊,字符在计算机的存储方式是二进制编码,所以字符其实也是一个说,比如 '0' = 48,'a' = 97(ASCII)。那么 'a'-'0'=97-48=49,所以加减法的工作交给计算机完成就好了,我们只负责让他们减。

有了这个基础知识,就可以判断串的大小了,比如 "123" == "123",因为对应的每个字符的编码都一样大;"123" < "223",因为前者第一个字符的编码比后者小('1'<'2');"123" > "113",因为前者第二个字符的编码比后者大('2'>'1')。前面举的几个例子都是串长相等时的,而如果不相等,则有两种情况,不妨设 S.length > T.length,首先要做的还是从左到右挨个比较,如果有不相等的就直接返回值就好了;那如果比到了 T.length ,还都是全一样,这时候长的那个值更大(规定),即 "1234" > "123",哪怕长的那个串多出来的都是空格也可以,因为空格的ASCII是32,而短的那个在 '\0' = 0,32 > 0,这么解释就好理解了吧。

所以便有了以上代码,当短的那个串还没遍历完的时候,判断不等,返回 S.ch[i] - T.ch[i],如果前者大,返回值自然大于 0;如果前者小,返回值自然小于 0。 如果遍历完了,全等,那么返回 S.length - T.length,如果前者长,返回值自然大于 0;如果前者小,返回值自然小于 0。

1.3.4 连接

bool Concat(HString &T, HString S1, HString S2){
    if(T.ch) free(T.ch);
    T.ch = (char *)malloc((S1.length+S2.length+1)*sizeof(char));
    if(!T.ch) return false;
    for(int i=1; i<S1.length+1; i++){
        T.ch[i] = S1.ch[i];
    }
    for(int j=1; j<S2.length+1; j++){
        T.ch[j+S1.length] = S2.ch[j];
    }
    T.length = S1.length+S2.length;
    return true;
}

这个代码很容易理解吧,注意如果下标从 0 开始,那么分配的长度就是 S1.length + S2.length,如果下标从 1 开始,那么就得多 +1。然后两个串依次赋值给新串就好了。 

1.3.5 求子串

bool SubString(HString &Sub, HString S, int pos, int len){
    if(pos<1 || pos>S.length || pos+len-1>S.length) return false;
    if(Sub.ch) free(Sub.ch);
    if(!len){
        Sub.ch = NULL;
        Sub.length = 0;
    }
    else{
        Sub.ch = (char *)malloc((len+1)*sizeof(char));
        if(!Sub.ch) return false;
        for(int i=pos; i<pos+len; i++){
            Sub.ch[i-pos+1] = S.ch[i];
        }
        Sub.length = len;
    }
    return true;
}

需要注意三个条件:

①边界

pos+len-1>S.length

 首先函数的意思是,从主串 S 中的第 pos 个位置数 len 个字符,提取出来给 Sub,就是子串。

如果不好理解的话,直接带数就好了,假如 pos = 1, len = 2。那么要提取出来的就是 S.ch[1] 和 S.ch[2],注意这个 2 是怎么来的,从 pos = 1 的位置数 len = 2 个(包括 pos 位置自己),那要题去的子串的最后一个字符在主串中对应的下标就是 pos+len-1。而在本代码中自定义的字符串下标从 1 开始,最大的下标是 S.length。所以有 pos+len-1<=S.length 为合法。

至于教材上下标从 0 开始,判断条件为什么也是相同的呢。emm 其实还按上面的例子来说(pos = 1, len = 2),如果下标从 0 开始,提取出的是 S.ch[0] 和 S.ch[1],1 对应 pos+len-2 计算方法就变成了 pos-1+len-1<=S.length-1,结果上是一致的。

②循环终止条件

for(int i=pos; i<pos+len; i++)

因为下标是从 1 开始的,所以第 pos 个字符下标就是 pos,这是循环的起始条件;

而循环终止条件,写的是 i<pos+len,其实就是 i==pos+len-1,和前面说的一致。

③ 赋值下标

Sub.ch[i-pos+1] = S.ch[i];

S 中的第 i 个元素在 Sub 中的下标怎么算呢,带数,假如 i = 5,pos = 3,此时已经赋值了 i=3 和 i=4 两个位置的字符,该赋 i=5 这个位置,也即第 3 个字符,3 = 5 - 3 +1。即 S 中的第 i 个元素在Sub 中的下标为 i-pos+1。事实上,S 中的第 pos 个位置对应 Sub 中的第 1 个位置,此时 i-pos=0,循环一次后,i=pos+1,应该赋值给 Sub 中的第 2 个位置。这是我之前在队列里讨论过的经典从 1 到 10 有几个数的问题,变成了从 pos 到 i 有几个数的问题。

除了以上三个条件,其他需要注意的:

如果 Sub 本身有字符,先清空。

如果 len==0,直接给 Sub 赋空即可。 

1.3.6 查找

参见下面的模式匹配问题。

二、模式匹配

我严重怀疑我写不明白,尝试一下,毕竟自己学会的最好办法就是讲出来。 

首先声明,KMP算法中,仅包含next数组的手算方法,另附机算代码,不讲解机算原理(因为考试只考手算……对不住了)

2.1 朴素模式匹配

模式匹配,即查找,现在有模式串 T,要在主串 S 中返回 T 第一次出现的位置(第一个字符的下标)。

而朴素模式匹配,即暴力算法。

举一个书上的例子,S = "a b a b c a b c a c b a b",T = "a b c a c"。

该怎么找 T 在 S 中出现的位置呢?首先要做的第一件事,肯定是拿着 T 的第一个字符去和 S 中的字符比。

  • S:a b a b c a b c a c b a b
  • T:a

发现 T 的第一个字符匹配之后,就需要接着找 T 剩下的字符:

  • S:a b a b c a b c a c b a b
  • T:a b c

比到第三个字符发现不匹配了,这时候又要重新找 T 的第一个字符:

  • S:a b a b c a b c a c b a b
  • T:   a

显然不对,继续:

  • S:a b a b c a b c a c b a b
  • T:      a

又找到了,然后接着找 T 剩下的字符:

  • S:a b a b c a b c a c b a b
  • T:      a b c a c

很遗憾,T 最后一个字符匹配失败了,又得重新找 T 第一个字符,直接一步到位了:

  • S:a b a b c a b c a c b a b
  • T:               a

找到之后,在比较 T 剩下的字符:

  • S:a b a b c a b c a c b a b
  • T:               a b c a c

发现匹配了!然后返回 T 的第一个字符在 S 中出现的位置,本例中为 6。下面是书上的匹配示意图:

图自 《数据结构(C语言版)》

然后整理一下思路,首先需要有个指针 i 指向主串 S 中的第 i 个位置,还需要个指针 j 指向模式串 T 中的第 j 个位置(注意下标从 1 开始)。

i 是从前往后一直走的,但是如果和模式串不完全匹配,i 还要退回来。 比如上面的这一步:

  • S:a b a b c a b c a c b a b
  • T:      a b c a c

第一个 a 匹配时,i=3,j=1;第二个 b 匹配时 i=4,j=2;第三个 c 匹配时 i=5,j=3;第四个 a 匹配时 i=6,j=4;第五个 c 不匹配时,变成了这样:

  • S:a b a b c a b c a c b a b
  • T:         a  b c a c

此时 T 的第一个字符去匹配 S 的第 4 个字符了。于是我们发现了,假如当前在匹配 S 中的第 i 个字符和 T 中的第 1 个字符,记此时的位置 pos = i,那么如果匹配失败,下次重新开始的 i==pos+1。

现在将情况拓展为一般,假如当前在匹配 S 中的第 i 个字符和 T 中的第 j 个字符,并且匹配失败了,我们注意到 j 是从 1 开始的,所以从这次匹配开始,j 变大了 j-1,那么 i 也应该变大了 j-1,因为两个串的遍历比较是同步进行的。那么 i 的初始位置就为 i-(j-1) = i-j+1,下一次 i 的初始位置要在此基础上再加一,即 i-j+2。

所以就有了代码思路:

当匹配失败时,i = i-j+2;j = 1。

当 j > T.length 时,说明模式串遍历完了,也就是匹配成功了。

除此之外,还要将 i 控制在 S.length 之内。

int Index(HString S, HString T){
    int i=1, j=1;
    while(i<S.length+1 && j<T.length+1){
        if(S.ch[i]==T.ch[j]){
            i++, j++;
        }
        else{
            i = i-j+2;
            j = 1;
        }
    }
    if(j>T.length) return i-T.length;
    return 0;
}

 一千三百字不过十余行代码。

2.2 KMP算法

这里来一组新例子,依然是教材上的,S="a c a b a a b a a b c a c a a b c",T="a b a a b c a c"。

如果按照上面的方法来匹配,是这样的:

第一步:

  • S:a c a b a a b a a b c a c a a b c
  • T:a b 

第二步:

  • S:a c a b a a b a a b c a c a a b c
  • T:   a 

第三步:

  • S:a c a b a a b a a b c a c a a b c
  • T:      a b a a b

第四步:

  • S:a c a b a a b a a b c a c a a b c
  • T:         a

第五步:

  • S:a c a b a a b a a b c a c a a b c
  • T:            a b

第六步:

  • S:a c a b a a b a a b c a c a a b c
  • T:               a b a a b c a c

匹配完成。

但其实,在上面匹配的时候,我们会发现一个问题,有很多不必要的操作,再拎回来看一看:

第一步:

  • S:a c a b a a b a a b c a c a a b c
  • T:a b 

这一步看起来好像很合理;

第二步:

  • S:a c a b a a b a a b c a c a a b c
  • T:   a 

这一步也没毛病;

第三步:

  • S:a c a b a a b a a b c a c a a b c
  • T:      a b a a b

这一步合理合法合规;

第四步:

  • S:a c a b a a b a a b c a c a a b c
  • T:         a

问题出现了。第三步的时候,已经匹配了 a b a a b 这五个字符,这就意味着在主串中的对应位置也是 a b a a b,否则不会匹配到第六个字符才失败,并且第六个字符肯定不是 c,否则 a b a a b c 就匹配上了。

但此时第四步的匹配还是从 b 这个字符开始,本身在第三步中就已经知道了它是 b,不可能匹配的,干嘛还要进行这一步呢?

第五步:

  • S:a c a b a a b a a b c a c a a b c
  • T:            a b

第五步同理,此时虽然匹配到第二个字符,但是在第三步中已经知道了 S 中第六个字符一定是 a,不可能和 b 匹配,干嘛还要进行这一步呢?

于是我们发现了,在朴素模式匹配中,浪费了很多步骤去匹配我们已知不可能匹配的字符,那有没有一种办法能跳过这些多余步骤呢?KMP算法他来了。

那最佳情况是什么样的呢?在上面的分析中,我们通过模式串匹配失败时候的情况,分析了主串中必然是或者必然不是某个字符,那我们拿着前面例子的结果,尝试分析一下:

第一步,匹配到第二个字符失败:

  • S:x x x x x x x x x x x x x x x x x
  • T:a b 

匹配到第二个字符才失败,这就意味着主串的第一个字符肯定是 a,第二个字符肯定不是b。于是有了:

  • S:a !b x x x x x x x x x x x x x x x
  • T:a  b 

此时,第二步是必要的,因为我只知道了第二个字符不是 b,但不知道它是不是 a。

第二步,匹配到第一个字符失败:

  • S:a x x x x x x x x x x x x x x x x
  • T:   a 

那么可以确定了,第二个字符肯定也不是 a。所以还得费力再跑一个循环。

第三步,匹配到第四个字符失败:

  • S:a x x x x x x x x x x x x x x x x
  • T:      a b a a b c

匹配到第六个字符才失败,意味着前五 个字符匹配成功了,肯定是 a b a a b,第六个肯定不是 c。于是有了:

  • S:a x a b a a b !c x x x x x x x x x
  • T:      a b a a b  c

现在我们会发现,如果把 T 右移:

  • S:a x a b a a b !c x x x x x x x x x
  • T:         a b a a b  c

肯定不匹配,再右移:

  • S:a x a b a a b !c x x x x x x x x x
  • T:             a b a a b  c

还是不匹配,因为第二个字符 b 对应主串的位置肯定不是 b,再右移,这一步不能省了。

第四步:

  • S:a x a b a a b !c x x x x x x x x x
  • T:               a b a a b  c

事实上,这一步就匹配成功了。

所以,我们在决定下一次开始匹配的位置的时候,其实和主串是无关的,并且仅和模式串有关,因为每次匹配成功或失败,都可以通过模式串来判断主串中是否是或者不是某个字符。

那么是怎么决定下一次重新开始的位置的呢?

再看一下:

  • S:a c a b a a b a a b c a c a a b c  i=1,j=1 开始
  • T:a b                                               i=2,j=2 失败
  • S:a c a b a a b a a b c a c a a b c  i=2,j=1 开始
  • T:   a                                               i=2,j=1 失败
  • S:a c a b a a b a a b c a c a a b c   i=3,j=1 开始(初始为 i=2,j=0,然后 i++,j++)
  • T:      a b a a b c                              i=8,j=6 失败
  • S:a c a b a a b a a b c a c a a b c   i=8,j=3 开始
  • T:               a b a a b c a c               匹配成功

于是可以总结出几个表面的规律,比如 i 要么变大要不不变,不会像朴素模式匹配那样回头了,并且每次有字符匹配成功,i 会自加;如果第一个字符就匹配失败,i 也要自加。而且我们还可以发现,模式串每次开始的位置 j 并不一样,和具体失败的位置有关,于是我们尝试着写一个数组用来保存这种规律。next[i] = j 代表在第 i 个位置匹配失败下一次匹配从 j 开始。于是我们记录一下上面的规律

  • next    1 2 3 4 5 6 7 8 
  • next[j] 0 1          3

很遗憾上面的例子只能写出三个位置,可能会有疑问在于为什么 next[1] = 0,看一下接下来的解释。

对于模式串 abaabcac 来说,

假如第一个位置匹配失败了,那么也就是说主串中对应的第一个位置肯定不是 a:

S:x

T:a

开始时 i=1,j=1,匹配失败时 i=1,j=1。我们的目的是让 j = next[1],并且下一次循环 j 还要从 T 的第 1 个位置开始。所以假设我们先让 next[1]=1。

假如第二个位置匹配失败了,那么主串对应的第一个位置是 a,第二个位置未知,但肯定不是 b:

S:a x

T:a b

开始时 i=1,j=1,匹配失败时 i=2,j=2。我们的目的是让 j = next[2],并且由于主串第二个字符未知,下一次循环 j 还要从 T 的第 1 个位置开始,并且 i 位置不变,next[2]=1。

假如第三个位置匹配失败了,那么主串对应的前两个位置是 a b,第三个未知但肯定不是 a:

S:a b x

T:a b a

开始时 i=1,j=1,匹配失败时 i=3,j=3。我们的目的是让 j = next[3],并且由于主串第三个字符未知,且第二个字符不是 a,下一次循环 j 还要从 T 的第 1 个位置开始,并且 i 位置不变,next[3]=1。

假如第四个位置匹配失败了,那么主串对应的前三个位置是 a b a,第四个未知但肯定不是 a:

S:a b a x

T:a b a a

开始时 i=1,j=1,匹配失败时 i=4,j=4。我们的目的是让 j = next[4],但是主串第四个位置只知道不是 a,但不知道是不是 b,所以下次循环应该这么开始(如下),此时 i 位置不变,并且模式串的第一个 a 不用匹配了,下一次循环 j 从 T 的第 2 个位置开始即可,所以 next[4]=2。

S:a b a x

T:      a b

假设第五个位置匹配失败了,那么主串对应的前四个位置是 a b a a,第五个未知但肯定不是 b:

S:a b a a x

T:a b a a b

开始时 i=1,j=1,匹配失败时 i=5,j=5。我们的目的是让 j = next[5]。应该不用多解释了,下次循环应该这么开始(如下),模式串的第一个 a 不用匹配了,此时 i 位置不变,下一次循环 j 从 T 的第 2 个位置开始即可,所以 next[5]=2。

S:a b a a x

T:          a b

假设第六个位置匹配失败了,那么主串对应的前五个位置是 a b a a b,第六个未知但肯定不是 c:

S:a b a a b x

T:a b a a b c

开始时 i=1,j=1,匹配失败时 i=6,j=6。我们的目的是让 j = next[6]。下次循环应该这么开始(如下),模式串的前两个 a b 不用匹配了,此时 i 位置不变,下一次循环 j 从 T 的第 3 个位置开始即可,所以 next[6]=3。

S:a b a a b x

T:          a b a

我们会发现,除了第一个位置就匹配失败的时候,i 总是随着模式串匹配过程在增加,也就是说代码中必然会有 i++ 这个过程发生,并且还是和 j++ 同步进行的。于是我们分析几种情况:

如果当前字符匹配成功:i++,j++;

如果当前字符匹配失败:j=next[j],i 还留在原地。但是当第一个字符就匹配失败的时候,i 的原地是真的原地,其他时候都是发生了变化的。也就是为了保证 i 每次都能向后移动一个字符(除了匹配失败的时候),必须要在第一个字符匹配失败的时候或者有字符匹配成功的时候让 i++,但是又字符匹配成功的时候 j 也得 ++。

所以,当 next[1]=1 的时候,下一次循环 i++,j++,相当于主串第 i 个元素和模式串第 2 个元素匹配了,多走了一个,当然可以通过代码来处理,但是通过把 next[1] 设置为 0,两个一起++,又省了三行代码…(个人理解)

int IndexKMP(HString S, HString T, int next[]){
    int i=1, j=1;
    while(i<S.length+1 && j<T.length+1){
        if(j==0 || S.ch[i]==T.ch[j]){
            i++, j++;
        }
        else{
            j = next[j];
        }
    }
    if(j>T.length) return i-T.length;
    return 0;
}

相比于朴素模式匹配,只改了 else 中对于 i 的回溯操作,以及用上了 next 数组,增加了 if 的判断条件而已。

最后再说一下怎么算 next 数组,上面是为了举例说明的,如果是为了快速算出来,不妨按照以下程序进行:

假设模式串为 "a a a a b"

next[1] 无脑写 0。

假设第2个位置匹配失败了,第二个位置及其后的字符未知,此时 i 指着 2 那个位置(红色):

  • a x x x x 
  • a a a a b

把模式串右移,让模式串对应位置前的字符全部匹配(字面上的位置,不是数组下标什么的,红色标的是位置):

  • a x x x x 
  •    a a a a b

此时是匹配的红色位置对应模式串中第一个位置,所以 next[2]=1。

假设第3个位置匹配失败了,第三个位置及其后的字符未知,此时 i 指着 4 那个位置(红色):

  • a a x x x 
  • a a a a b

把模式串右移,让模式串对应位置前的字符全部匹配:

  • a a x x x 
  •    a a a a b

此时要匹配的红色位置对应模式串中第二个位置,所以 next[3]=2。

假设第4个位置匹配失败了,第四个位置及其后的字符未知,此时 i 指着 5 那个位置(红色):

  • a a a x x  
  • a a a a b 

把模式串右移,让模式串对应位置前的字符全部匹配:

  • a a a x x 
  •    a a a a b

此时要匹配的红色位置对应模式串中第三个位置,所以 next[4]=3。

假设第5个位置匹配失败了,第五个位置及其后的字符未知,此时 i 指着 6 那个位置(红色):

  • a a a a x  
  • a a a a b 

把模式串右移,让模式串对应位置前的字符全部匹配:

  • a a a a x  
  •    a a a a b 

此时要匹配的红色位置对应模式串中第四个位置,所以 next[5]=4。

下面是求 next 数组的代码:

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

2.3 改进的KMP算法

上面求得的 next 数组还可以进一步优化,比如我们再再再再再举个例子:

  • S:a a b a a a a b
  • T:a a a a b

上面已经求了这个模式串的 next 数组:

  • j:         1 2 3 4 5 
  • next[j]:0 1 2 3 4

来试算一下:

第一步:

  • S:a a b a a a a b
  • T:a a a a b

第三个位置不匹配,i=3,j=next[3]=2。

第二步:

  • S:a a b a a a a b
  • T:   a a a a b

第二个位置不匹配,i=3,j=next[2]=1。

第三步:

  • S:a a b a a a a b
  • T:      a a a a b

第一个位置不匹配,i=3,j=next[1]=0。此时 i++,j++。

第四步:

  • S:a a b a a a a b
  • T:         a a a a b

事实上,从第一步开始,我们就已经知道了主串的第三个位置不是 a,第二步还是要用 a 去和它比,第三步依然还是要用 a 去和它比,显然,这是不希望看到的。

那怎么优化呢?直接从第一步跳到第四步就好了,让 next 数组一步到位。即:如果第 j 个位置匹配失败了,下一次要去 k = next[j] 这个位置,如果模式串第 j 个字符和第 k 个字符一样,那么就让 next[j] = next[k]。比较抽象啊,举个例子,新数组叫 nextval,nextval[1] 还是无脑写 0。

  • j:             1 2 3 4 5 
  • T:            a a a a b
  • next[j]:     0 1 2 3 4
  • nextval[j]:0

从左往右看,我们发现 T[2] = T[1],所以让 nextval[2] = nextval[next[2]],即 nextval[2] = nextval[1] = 0:

  • j:             1 2 3 4 5 
  • T:            a a a a b
  • next[j]:     0 1 2 3 4
  • nextval[j]:0 0

从左往右看,我们发现 T[3] = T[2],所以让 nextval[3] = nextval[next[3]],即 nextval[3] = nextval[2] = 0,以此类推:

  • j:             1 2 3 4 5 
  • T:            a a a a b
  • next[j]:     0 1 2 3 4
  • nextval[j]:0 0 0 0 4

最后附上代码:

void getNextval(HString T, int nextval[]){
    int i=1, j=0;
    nextval[1] = 0;
    while(i<T.length+1){
        if(j==0 || T.ch[i]==T.ch[j]){
            i++, j++;
            if(T.ch[i]!=T.ch[j]) nextval[i] = j;
            else nextval[i] = nextval[j];
        }
        else j = nextval[j];
    }
}

其实对于这一章,有很多一知半解的地方,基本上都是提出了自己的见解,以一种“相对”好理解的方式呈现出来了,当然肯定比不上各路大牛和老师们的讲解,望海涵。

==总结==

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值