目录
一、基础知识
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 值的区别,才疏学浅故引用之。
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];
}
}
其实对于这一章,有很多一知半解的地方,基本上都是提出了自己的见解,以一种“相对”好理解的方式呈现出来了,当然肯定比不上各路大牛和老师们的讲解,望海涵。
==总结==