数据结构-KMP算法 带你悟透KMP(超详细)学习笔记

前言:

由于本人的失误,将next 记成了nest ,回过头来早已完成,所以请见谅,望包涵,希望大家也不要犯我这个错误。

本文章是继 上一篇我的文章 数据结构-字符串暴力匹配(超详细)学习笔记在此基础上续写的KMP算法,我将详细介绍下标从1开始和下标从0开始两种情况下的nest值和匹配时的不同

我学习kmp算法时,视频看的是两位哔哩哔哩up主的 数据结构-字符串暴力匹配  和 KMP算法之求next数组代码讲解 。之后总结他们两者的所讲的内容然后加上自己的分析,就完成了此篇文章。 我的文章有点长,希望你能够耐心看完,一定一定会有所收获的!

一、初始化字符串结构体

通过顺序存储方式进行初始化,结构体里定义了char型指针来代表数组的首地址,定义了length来确定串的长度。

//初始化字符串
typedef struct String
{
    char* data;
    int length;
}String;

二、创建空串

定义一个字符串结构体指针,为其开辟空间,初始化数据。

//创建空串
String* initString()
{
    String* s = (String*)malloc(sizeof(String));
    s->data = NULL;
    s->length = 0;
    return s;
}

三、给串分值

注意:函数中传进来的是字符串,而c语言中,没有字符串,仍是char 型数组 只不过末尾自带了\0,仅此而已!

void stringAssign(String* s, char* arr) //传进字符串
{
    if (s->data) {
        free(s->data); //释放其地址
    }
        int len = 0; //创建len 计算 数组的长度
        char* temp = arr;
        while (*temp) { //计算加入字符串的长度 注意: *temp 解引用 指向字符元素的ascll表值 指向\0则结束循环
            temp++;
            len++;
        }
        if (len == 0) {
            s->data = NULL; //传来的字符串为空
            s->length = 0;
        }
        else {
            temp = arr;//将temp重新指向data的初地址
            s->length = len;
            s->data = (char*)malloc(sizeof(char) * (len + 1));//"\0"也占内存   注意: 是char * 型 并用s.data 首地址来接收
            for (int i = 0; i <= len; i++, temp++) {
                s->data[i] = *temp;//指针数组
            }
          
        }
}

若仍是有点迷糊,此代码详解在 数据结构-字符串暴力匹配(超详细)学习笔记 里,前三步和字符串暴力匹配是完全一样的。

四、求 nest数组下标从0开始与下标从1开始的两种方法

1、下标从1开始

// 公共前后缀 求 nest数组
int * getNest(String* s)
{
    int* nest = (int*)malloc(sizeof(int) * s->length);
    int i = 1;
    int j = 0;
    nest[i] = j;//nest[1] = 0; 先将公共前后缀不存在的首元素表示出来
    while(i <= s->length)                        
   {                                          //求哪一个 就看 前一个    
      if( j == 0 || s->data[i] == s->data[j]) //如果前一个 和 它当前的nest数组为下标的值相等
        {
          i++;
          j++;
          nest[i] = j;          //   当前nest的值 = 前一个next值 + 1  
        }
     else                      //   不相等,i 不变  变 j  继续比较 直到 相等 或者  j  ==  -1
        {
         j = nest[j];                    
        } 
   }
   return nest;
}

原理:

代码解释:

1.由规律可知:nest数组上的值每次最多在前一个值的基础上+1,那么是如何判断+1的呢?

通过比较 前一个data数组的值(设为data[X]) 与它的nest值为下标的data数组(设为data[Y]) 是否相等,(即 data[X] == data[Y])  相等 则 +1。

解释:

如:求 nset[4]的值,此时i = 3,j = 1,让data[3] 与 以nest[3] 为下标的 data[1] 进行比较 data[3] == data[1] 即 a == a,  相等 所以 i++,j++,nest[i] = j  即nest[4] = 2;

2.若前一个data数组的值(设为data[X]) 与 它的nest值为下标的data数组(设为data[Y])不相等?

data[X]  保持不变 ,求出  data[Y] 的 nest值为下标的data数组(设为data[Z]) , 继续让data[X]与data[Z]判断是否相等,相等即+1,若不相等,重复此操作,直到 j == 0,意味着:公共前后缀不存在,只能为通过条件判断  令当前nest值 = 1 了。

解释:

如:求 nset[7]的值,此时i = 6,j = 4,让data[6] 与 以nest[6] 为下标的 data[4] 进行比较,c != b, i不变,让 j = nest[4] = 2,让data[6] 与 data[2]进行比较,c != b, i 不变,再让 j = nest[2] = 1,让data[6] 与 data[1]进行比较,c != a, i 不变,再让 j = nest[1] = 0,此时 j == 0,通过条件判断,此时i++,j++,data[7] = 1。

不过在现实中,我们是需要将最后一位元素赋值的,这里只是证明了最后一位元素的nest值不受它本身的data值的影响才故意不赋值。

3.为什么while(i <= s->length)条件里面还要 “ = ”  呢? 一共有7个元素即长度为7,不应该只需要i = 6即可推出吗?

我的结论是:“ = ” 可加可不加。加了更加严谨,且仍然不影响后面的操作。

解释:

现在我们假设一共6位元素了,第7位元素就真的不存在了。那么给nest[6]赋完值时,i = 5,j =4。

注意:我们传进来的结构体字符串指针 s 的data域是 char型数组,并且 是字符串的形式。字符串在c语言中本质是char型数组,不过是末尾带了'\0'。

在c语言中字符串的长度不算上'\0'(即s->length == 6),但实际上char型数组的元素又包含了‘\0’在内。所以,最后继续i++,i = 6,j = 4,是可以继续给第7位 元素 '\0'  求出 nest[7] 的值的 即 nest[7] = 1 。

多求出来的第7位的nest值,是不会影响后续的KMP匹配的,因为当主串与子串进行匹配,匹配到最后时,他们都有‘\0’,仍然是匹配成功的状态!

2、下标从0开始

下标从1开始,nest = 公共前后缀 + 1;当我们要实现模式串数组从下标0开始,就要将nest数组的值全部 - 1 即此时 nest = 公共前后缀。此时 nest[0] = -1,思路是与下标从1开始是完全一样。“ = ” 仍然 可加可不加。

// 公共前后缀 求 nest数组
int * GetNest(String* s)
{
    int* nest = (int*)malloc(sizeof(int) * s->length);
    int i = 0;
    int j = -1;
    nest[i] = j;
    while (i <= s->length - 1)                          
    {                                           //求哪一个 就看 前一个   
       if (j == -1 || s->data[i] == s->data[j]) //如果前一个 和 它当前的nest数组为下标的值相等
        {                                                 
            i++;                                              
            j++;                                             
            nest[i] = j;                     //   当前nest的值 = 前一个nest值 + 1                
        }
        else                                //   不相等,i 不变  变 j  继续比较 直到 相等 或者  j  ==  -1
        {
            j = nest[j];
        }
    }
    return nest;
}

五、kmp匹配

从下标0开始

//kmp匹配
void kmpMatch(String* master, String * sub,int * nest)
{
    int i = 0;
    int j = 0;
    while (i < master->length && j < sub->length)
    {
        if ( j == -1 ||master->data[i] == sub->data[j])
        {
            i++;
            j++;
        }
        else
        {
            j = nest[j];
        }
    }
    if (j == sub->length)
    {
        printf("KMP match success.\n");
    }
    else
    {
        printf("KMP match fail.\n");

    }
}

代码解释:

跟字符串暴力匹配的做法仍是一样的,只不过是 增加了j == -1的条件 和将  暴力匹配中 i = i - j + 1; 换成了 j = nest[j];  此时代码的核心跟求nest的数组的代码的核心是相同的。KMP算法就不用跟字符串暴力匹配一样,让主串进行回溯,而是一直往前。

我们可以通过一个例子就理解了:

在这里我们可以发现一个规律:

若当前发生不匹配状态时的下标的nest 值 为 x,我们就让子串后移 x + 1 个位置。 如果 nest 值 为 -1,此时主串从data[i+1]开始,子串从data[0]开始(即子串又从首元素开始进行匹配了)。

从下标1开始

因为我们普遍都是从下标0开始,下标1的演示就不给出了,仍是一样的思路。

//KMP匹配
void kmpMatch(String* master, String * sub,int * nest)
{
    int i = 1;
    int j = 1;
    while (i < master->length && j < sub->length)
    {
        if ( j == 0 ||master->data[i] == sub->data[j])
        {
            i++;
            j++;
        }
        else
        {
            j = nest[j];
        }
    }
    if (j == sub->length)
    {
        printf("KMP match success.\n");
    }
    else
    {
        printf("KMP match fail.\n");

    }
}

六、遍历nest数组

下标从1开始:

void printNext(int* nest, int len)
{
    for (int i = 1; i <= len; i++)
    {
        printf(i == 1 ? "%d" : "->%d", nest[i]);
    }
    printf("\n");
}

下标从0开始:

void printNext(int* nest,int len)
{
    for (int i = 0; i < len; i++)
    {
        printf(i == 0 ? "%d" : "->%d",nest[i]);      
    }
    printf("\n");
}

七、遍历字符串

void printString(String* s)
{
    for (int i = 0; i <s->length; i++)
    {
        printf(i == 0 ? "%c" : "->%c", s->data[i]);
    }
    printf("\n");
}

八、完整代码

下标从0开始:

#include<stdio.h>
#include<stdlib.h>
typedef struct String
{
    char* data;
    int length;
}String;
//空表
String* initString()
{
    String* s = (String*)malloc(sizeof(String));
    s->data = NULL;
    s->length = 0;
    return s;
}
//给串分值
void assignString(String * s,char * arr)
{
    if (s->data)
    {
        free(s);
    }
    else
    {
        int len = 0;
        char* temp = arr;
        while (*temp)
        {
            len++;
            temp++;
        }
        if(len == 0)
        {
            s->data = 0;
            s->length = 0;
        }
        else
        {
            temp = arr;
            s->length = len;
            s->data = (char*)malloc(sizeof(char) * (len + 1));// \0
            for (int i = 0; i <= len; i++, temp++)
            {
                s->data[i] = *temp;
            }
            
        }
    }
}
// 公共前后缀 求 nest数组
int * GetNest(String* s)
{
    int* nest = (int*)malloc(sizeof(int) * s->length);
    int i = 0;
    int j = -1;
    nest[i] = j;
    while (i <= s->length - 1)                          
    {                                                  //求哪一个 就看 前一个   
        if (j == -1 || s->data[i] == s->data[j])      //如果前一个 和 它当前的nest数组为下标的值相等
        {                                                 
            i++;                                              
            j++;                                             
            nest[i] = j;                     //   当前nest的值 = 前一个nest值 + 1                
        }
        else                                //   不相等,i 不变  变 j  继续比较 直到 相等 或者  j  ==  -1
        {
            j = nest[j];
        }
    }
    return nest;
}
//kmp匹配
void kmpMatch(String* master, String * sub,int * nest)
{
    int i = 0;
    int j = 0;
    while (i < master->length && j < sub->length)
    {
        if ( j == -1 ||master->data[i] == sub->data[j])
        {
            i++;
            j++;
        }
        else
        {
            j = nest[j];
        }
    }
    if (j == sub->length)
    {
        printf("KMP match success.\n");
    }
    else
    {
        printf("KMP match fail.\n");

    }
}
//遍历nest数组
void printNext(int* nest,int len)
{
    for (int i = 0; i < len; i++)
    {
        printf(i == 0 ? "%d" : "->%d",nest[i]);       
    }
    printf("\n");
}
//遍历字符串
void printString(String* s)
{
    for (int i = 0; i <s->length; i++)
    {
        printf(i == 0 ? "%c" : "->%c", s->data[i]);
    }
    printf("\n");
}
int main()
{   
    String* s1 = initString();
    String* s2 = initString();
    assignString(s1, "ABACCABABD");
    assignString(s2, "ABAB");
    printString(s1);
    printString(s2);
    GetNest(s2);
    int* nest = GetNest(s2);
    printNest(next, 4);
    kmpMatch(s1,s2,nest);
    return 0;
}

下标从1开始:

#include<stdio.h>
#include<stdlib.h>
typedef struct String
{
    char* data;
    int length;
}String;
//空表
String* initString()
{
    String* s = (String*)malloc(sizeof(String));
    s->data = NULL;
    s->length = 0;
    return s;
}
//给串分值
void assignString(String* s, char* arr)
{
    if (s->data)
    {
        free(s);
    }
    else
    {
        int len = 0;
        char* temp = arr;
        while (*temp)
        {
            len++;
            temp++;
        }
        if (len == 0)
        {
            s->data = 0;
            s->length = 0;
        }
        else
        {
            temp = arr;
            s->length = len;
            s->data = (char*)malloc(sizeof(char) * (len + 1));// \0
            for (int i = 0; i <= len; i++, temp++)
            {
                s->data[i] = *temp;
            }
            
        }
    }
}
// 公共前后缀 求 nest数组
int* GetNest(String* s)
{
    int* nest = (int*)malloc(sizeof(int) * s->length);
    int i = 1;
    int j = 0;
    nest[i] = j;//nest[1] = 0; 先将公共前后缀不存在的首元素表示出来
    while (i <= s->length)
    {                                          //求哪一个 就看 前一个    
        if (j == 0 || s->data[i] == s->data[j]) //如果前一个 和 它当前的nest数组为下标的值相等
        {
            i++;
            j++;
            nest[i] = j;          //   当前nest的值 = 前一个nest值 + 1  
        }
        else                      //   不相等,i 不变  变 j  继续比较 直到 相等 或者  j  ==  -1
        {
            j = nest[j];
        }
    }
    return nest;
}
//KMP匹配
void kmpMatch(String* master, String* sub, int* nest)
{
    int i = 1;
    int j = 1;
    while (i < master->length && j < sub->length)
    {
        if (j == 0 || master->data[i] == sub->data[j])
        {
            i++;
            j++;
        }
        else
        {
            j = nest[j];
        }
    }
    if (j == sub->length)
    {
        printf("KMP match success.\n");
    }
    else
    {
        printf("KMP match fail.\n");

    }
}
//遍历nest数组
void printNext(int* nest, int len)
{
    for (int i = 1; i <= len; i++)
    {
        printf(i == 1 ? "%d" : "->%d", nest[i]);
    }
    printf("\n");
}
//遍历字符串
void printString(String* s)
{
    for (int i = 0; i < s->length; i++)
    {
        printf(i == 0 ? "%c" : "->%c", s->data[i]);
    }
    printf("\n");
}
int main()
{
    String* s1 = initString();
    String* s2 = initString();
    assignString(s1, "ABACCABABD");
    assignString(s2, "ABAB");
    printString(s1);
    printString(s2);
    GetNest(s2);
    int* nest = GetNest(s2);
    printNext(nest, 4);
    kmpMatch(s1, s2, nest);
    return 0;
}

九、运行结果

下标从0开始:

下标从1开始:

补充

KMP算法其实还可以继续改进。如:主串s=“aaabaaaab” 子串t=“aaaa” 发生不匹配时,按照我们现在的做法,要发生4次不匹配的情况,才能最终匹配成功,要是2次到位的话,岂不是快多了,所以又对nest数组进行了改进,引出了nestvalue数组。nestvalue数组的求法和代码实现,我将在下一篇文章继续进行详细介绍。

总结

KMP算法是一种高效的模式匹配算法,它牺牲了一定的空间去保存nest数组,提高了匹配效率。KMP算法还能更加智能的移动字符串,让字符串达到匹配的状态。KMP算法的核心是减少主串指针的移动,主串指针没有回溯,并且快速达到了匹配状态。其算法的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度,可以看出其比暴力匹配算法的O(nm)要更加高效。KMP算法在字符串匹配、数据压缩等领域有着广泛的应用。

制作不易,真心想让你懂,还是有不足的地方,望见谅嘞,鼓励是最大的支持,希望大家如果又更好的思路和想法也可以一起分享。

  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小苏先生.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值