C语言数据结构第四章—— KMP算法
get 到的东西:
学习一个算法的思路,流程是得先明白算法的原理,看懂它的代码,知道它的逻辑链,然后自己动手复现一遍,最后改进代码达到类似的高效效率就差不多了√
KMP算法简单来说就是
- 想要主串读取不会倒回去读,发现实现这个想法的话需要知道匹配表PMT,知道前缀后缀的概念。
- 想怎么去实现这个next表,用自己和自己比。
关于创造next表,其实可以知道它的原理之后自己动手实现一遍,看代码可以看不懂,但是实现起来就行,只要坚持 i 不后退就行。
实现流程
这个函数分为两个步骤
先做这个吧,可以验证有没有写对(可以先写PMT就是value的数组,然后在录入数组的方式和加一下前缀就行)(下次试试直接写next,从PMT到next的转变要改东西,可能会漏)
0. 先做value数组,再延后输入就可以变成next数组了,下边的 next都是 value!
1. 用两个自己相比较,指向主串的(上面那串)的位置是i,子串(下面那串)的位置是j
2. 先定义放到数组位置的是当前位置的PMT(包括该位置),此时i和j位置也是指针位置
3. 把第一个next[0]赋好值,然后开始根据知道的算法用代码一步一步的写出来
第一步实现的是:
- 从第一个值开始比较,
- 如果相等的话就移动两个指针,同时记next是现在下边指针的位置,把指针再往后移,
- 还有在主串读完之后再结束循环,所以加了一个for
应该再加个不相等的情况,
不相等的情况下还要分两种情况
- j 的位置是0时,此时不相等那就是PMT=0了 ,value赋值给0,然后i到下一位,j不变,继续比
- j 的位置不是0时,可以先往前移动j的位置, 移动到0到j位置的PMT的位置(这个比较重要,容易忘,最好画图了解),看看j往前移了之后的j到0能不能成为匹配的前缀。如果不行的话再往前移。j = nextj-1(第几个和数组下标的关系)
3.好像对了?!我去把数组改成next了
next里表示的是当前位置之前的(不包括当前位置)的字母的KMP值,可以在录入时先让i+1,再录入
然后改next[0] = -1,next[1] = 0,其它正常(最后一位不用了?先看下下边的主函数需不需要)
在每次输入next的值时做个输出,看看实时的结果
printf("number %d next is %d\n", i, next[i]);
ok,是输出函数出了问题,因为往后移动了一位,所以next数组会比KMP多一个数,先这样吧,改一下输出函数,继续写主串子串相比了
.
-
- 字符的含义a,b是主串和子串
i,j是主串,子串的指针位置 - i,j都是从第一个开始匹配,如果相等的话,那就继续往下匹配,
- 一直匹配到j匹配完了,那就是有b是a的子串了
- 一直匹配到i匹配完了,j没完,那也没有,而且整个匹配过程结束
- 所以循环的跳出条件是i = strlen(A) || j = strlen(B)
- 字符的含义a,b是主串和子串
- 在j结束时, 不管i有没有结束,都是匹配成功。
- i结束时,如果j结束了,那还是匹配成功,所以i的优先度高于j。出循环后用j来判断是否匹配成功。
- 继续想匹配条件
- 不匹配时,i不回去,这时候就要用到之前定好的i数组了。j往前退回到next[j]+1(next[j]个相等的,然后往后移以为和i比)的位置,
- 如果 j = 0了还不相等(这时候就是next[0] = -1的作用了,让next[j]+1 = 0 ),那这个i的位置是没有了,然后走下一个,下一个从j=0开始比
3.好像好了,测试一下?
输出的改成 i - j + 1,判断i,j出来时的含义就可
完事√(不过没有把公式揉在一起,再说吧)
C语言代码——实现部分操作
// 实现BF和KMP算法用 2020/2/19-2020/2/20
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Size 5
typedef struct list_str2{ //str2 的结构体再上一层的定义
char *str2;
int max; //申请到空间的最大值
}List_str2;
void Init_str2(List_str2 *plist_str2);
void getNext(char * p, int * next);
void Print_next(int* next);
void KMP(char* a, char *b, int* next);
int main()
{
int next[100];
List_str2 Primary_str;
List_str2 Substr;
printf("输入字符给主串,一直到输入为#时停止输入\n");
Init_str2(&Primary_str);
printf("输入字符给子串,一直到输入为#时停止输入\n");
Init_str2(&Substr);
getNext( Substr.str2 , next);
KMP(Primary_str.str2, Substr.str2, next);
Print_next(next);
printf("Hello world!\n");
return 0;
}
void Init_str2(List_str2 *plist_str2) //初始化并第一次输入str2
{
plist_str2->str2 = (char *)malloc(Size * sizeof(char));
plist_str2->max = Size;
// printf("输入字符给str2,一直到输入为#时停止输入\n");
char input_data;
int count = -1;
do{
scanf("%c", &input_data);
// printf("看看scanf读到了啥:%c\n", input_data);
if(input_data!= '#')
{
count ++; //结束的count是当前最后一位输入的data的数组坐标
(plist_str2->str2)[count] = input_data;
}
if(count==plist_str2->max-1) //如果数据空间快不够用了,就申请新的
{
plist_str2->str2 = (char *)realloc(plist_str2->str2, (Size + plist_str2->max) * sizeof(char *));
plist_str2->max += Size;
}
}while(input_data!='#');
fseek(stdin, 0, SEEK_END);//清除缓存区
(plist_str2->str2)[count+1] = '\0'; //加一个字符串结束的标志
}
void getNext(char * p, int * next)//得到字符串的next函数,不过暂时是多一位的
{
int i, j;
next[0] = -1;
next[1] = 0;
i = 1, j = 0;
for( ; i+1 <= strlen(p); ) //i在没循环完时会一直重复
{
if( p[i] == p[j] )
{
i++;
next[i] = j + 1; //next i 要的值是子串的第几个,所以用子串+1
// printf("number %d next is %d\n", i, next[i]);
j++;
}
else
{
if( j==0){ //子串第零个和主串不一样,那就没救了,这个位置的PMT是0,然后主串后移
i++;
next[i] = 0;
// printf("number %d next is %d\n", i, next[i]);
}
else{ //不是0的话,j还能往前找 value移位之后,next数组的东西都要看下要不要改
j = next[j];
}
}
}
next[i+1] = -5; //方便输出next数组检查对错用,在数组最后加了个-1
}
void Print_next(int* next)//比字符串多一位的输出,不过暂时是多一位的
{
int i;
for(i=0; next[i]!=-5; i++ )
{
printf("%d\t", next[i]);
}
}
void KMP(char* a, char *b, int* next) //主串是a,子串是b
{
int i, j; //i是主串位置,j是子串位置
i = 0, j = 0;
for( ; i <= strlen(a) && j <= strlen(b) ; ) //i或者j结束了就出来了
{
if( a[i] == b[j] ){ //每个i比的时候相等时,会继续走不回头,所以直接i++了
i++;
j++;
}
else if( j != 0){
j = next[j] + 1;
}
else if( j == 0){
i++;
}
}
if( j > strlen(b) ){ //这个时候的i和j都是多加了1的,然后j满了就溢出了.j是子串的长度,i是匹配完的位置(第i个),
printf("有子串,数组下标是%d\n", i - j + 1); // (i-1)-(j-1) = i-j表示i在匹配j的第一个之前有i-j个字符
} // 开始匹配的位置(子串的第一个位置)就是i-j+1
else{
printf("无子串\n");
}
}