【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!

【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!

  • 1. 前言
  • 2. 什么是KMP算法
    • 2.1 为什么主串不回退?
    • 2.2 为什么子串不需要回退到起始位置,而是回退到特定位置呢?
    • 2.3 回退位置的特征
    • 2.4 next数组
      • 2.4 next数组的手动求法
      • 2.4 next数组在程序中的求法
  • 3. C语言代码实现KMP算法
  • 4.KMP算法在next数组上的优化
  • 5. 使用KMP算法实现strstr函数的模拟实现

❤️博客主页: 小镇敲码人
欢迎关注:点赞 留言 收藏
努力和收获,都是自己的,与他人无关。你必须特别努力,才能显得毫不费力。坚信一句话:只有自己足够强大,才不会被别人践踏。
❤️人的能量=思想+行动速度的平方。

【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第1张图片

1. 前言

在【C语言进阶技巧】探秘字符与字符串函数的奇妙世界这篇文章中,我们模拟实现了字符串查找函数strstr,但我们实现的那个办法需要主串不停的回退,当发现有不匹配的字符时,子串也得重新回退到第一个字符的位置,这样是最简单的暴力求解,它还有一个名字叫做BF算法,并且它的时间复杂度已经达到了 O ( m ∗ n ) O(m*n) O(mn),今天我们来介绍一种时间复杂度只有 O ( m + n ) O(m+n) O(m+n)的字符串匹配算法,它就是KMP算法。

2. 什么是KMP算法

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。-- 来自百度百科

  • KMP算法与BF算法的最大区别是主串不回退,子串在匹配失败后也不会回到起始位置,而是回到一个特定的位置。

2.1 为什么主串不回退?

【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第2张图片

2.2 为什么子串不需要回退到起始位置,而是回退到特定位置呢?

【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第3张图片

2.3 回退位置的特征

【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第4张图片
可以发现第一次回退的下标是那两个相同子串的长度,由上述的阐述我们也可以知道,回退的位置与主串并没有关系,因为主串的那一段字符在子串中都是有对应的一段的,所以我们计算回退位置只看子串就行了,子串的每一个字符的位置都对应着一个回退的位置,它都可以用我们刚刚的方法求出,而储存这个回退位置的数组我们称之为next数组。

2.4 next数组

KMP算法的精髓就在于 n e x t next next数组:也就是用 n e x t [ i ] = k next[i] = k next[i]=k来表示。这个 k k k就是当j位置匹配不成功时,它要回退的第一个位置。

2.4 next数组的手动求法

假设j位置匹配不成功,它的第一次回退规则是这样的:

  • 如果在0位置就回退到-1下标处(特殊处理,便于后续的代码实现)
  • 如果在其它位置,就看 0 到 j − 1 0到j-1 0j1位置是否存在最大长度的两个子字符串,他们必须满足这样的要求
  1. 前后缀相同。
  2. 第一个的子串前缀位置在 0 0 0下标处,第二个子串的后缀位置在 j − 1 j-1 j1下标处。
  3. 回退的数组下标就等于这两个子字符串的长度。
  • 对于任意一个长度大于1的子字符串,它的next[0] = -1 next[1] = 0;
    下面我们给出两道题,希望帮助你学会如何手动求j下标对应next数组的值如何求:
    【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第5张图片
  • 注意:那两个最大长度的子串是允许有重叠部分的,但是不能完全重叠。

下面我们给出答案和解析:
练习1:
【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第6张图片
练习2:

【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第7张图片

2.4 next数组在程序中的求法

手算next数组我们会算了,那再程序中又应该如何来处理呢?我们依旧画图来分析:
【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第8张图片

3. C语言代码实现KMP算法

#include 
#include 
#include 
#include 

// KMP算法中的Getnext函数,用于生成子串Sub的next数组
void Getnext(int* next, char* Sub, int lenSub)
{
   // 特殊情况下,当子串长度小于2时,直接设置next[0]为-1
   if (lenSub < 2)
       next[0] = -1;
   else
   {
       // 初始化next数组的前两个元素
       next[0] = -1;
       next[1] = 0;
       int k = 0; // k表示最大相同前后缀的长度
       int i = 2; // i表示当前需要计算next值的下标
       // 使用循环计算next数组的值
       while (i < lenSub)
       {
           // 若k等于-1,或者Sub[k]等于Sub[i-1],则next[i]的值为k+1,然后i和k都自增1
           if (k == -1 || Sub[k] == Sub[i - 1])
           {
               next[i] = k + 1;
               i++;
               k++;
           }
           // 若Sub[k]不等于Sub[i-1],则将k的值更新为next[k],继续检查
           else
           {
               k = next[k];
           }
       }
   }
}

// KMP算法实现,用于在主串str中查找子串Sub,返回第一次出现的位置
char* KMP(char* str, char* Sub, int pos)
{
   assert(str && Sub); // 断言str和Sub非空
   int lenstr = strlen(str);
   int lenSub = strlen(Sub);
   // 特殊情况下,当主串或子串为空串时,直接返回NULL
   if (str[0] == '\0' || Sub[0] == '\0')
       return NULL;
   // 特殊情况下,当查找位置小于0或超出主串长度减1时,返回NULL
   if (pos < 0 || pos > lenstr - 1)
       return NULL;
   int* next = (int*)malloc(sizeof(int) * lenSub); // 分配next数组的内存空间
   assert(next); // 确保内存分配成功
   Getnext(next, Sub, lenSub); // 调用Getnext函数生成子串Sub的next数组
   int i = pos; // i表示在主串中的当前查找位置
   int j = 0; // j表示在子串中的当前查找位置
   // 使用循环进行匹配
   while (i < lenstr && j < lenSub)
   {
       // 若j等于-1,或者主串当前字符和子串当前字符相等,则i和j分别自增1,继续匹配
       if (j == -1 || str[i] == Sub[j])
       {
           i++;
           j++;
       }
       // 若主串当前字符和子串当前字符不相等,则将j的值更新为next[j],继续匹配
       else
       {
           j = next[j];
       }
   }
   free(next); // 释放next数组的内存空间
   next = NULL; // 将next指针置为NULL,防止产生野指针
   // 若j等于子串长度,说明匹配成功,返回匹配到的位置的指针
   if (j >= lenSub)
   {
       return str + (i - j);
   }
   // 若匹配失败,返回NULL
   return NULL;
}

int main()
{
   char str1[] = "abcdefabcdef";
   char Sub[] = "a";
   char* ret = KMP(str1, Sub, 0); // 在主串str1中查找子串Sub,从位置0开始查找
   if (ret != NULL)
   {
       printf("%s\n", ret);
   }
   else
   {
       printf("子串未在主串中找到\n");
   }
   return 0;
}

注意:这里我们的i是从i = 2开始计算next数组的,因为前两个值已经已知了,后续求next数组,就相当于已知next[i-1]求next[i],此时判断等于的条件应该变成Sub[k] == Sub[i-1],这是和我们分析时有所区别的。
另外匹配成功时,返回的地址是主串中匹配成功那段子串的起始地址
我们画图来分析为什么是str+i-j

【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第9张图片

运行结果:

【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第10张图片

找到了就和strstr函数一样从找到位置开始打印主串。

4.KMP算法在next数组上的优化

这里我们依旧通过画图来分析:

【数据结构与算法】之「KMP秘术」:揭开字符串匹配的神秘面纱!_第11张图片
代码实现:

#include 
#include 
#include 
#include 

// 改进后的KMP算法中的Getnext函数,用于生成子串Sub的next数组
void Getnext(int* next, char* Sub, int lenSub)
{
    // 特殊情况下,当子串长度小于2时,直接设置next[0]为-1
    if (lenSub < 2)
        next[0] = -1;
    else
    {
        // 初始化next数组的前两个元素
        next[0] = -1;
        next[1] = 0;
        int k = 0; // k表示最大相同前后缀的长度
        int i = 2; // i表示当前需要计算next值的下标
        // 使用循环计算next数组的值
        while (i < lenSub)
        {
            // 若k等于-1,或者Sub[k]等于Sub[i-1],则next[i]的值为k+1,然后i和k都自增1
            if (k == -1 || Sub[k] == Sub[i - 1])
            {
                next[i] = k + 1;
                i++;
                k++;
            }
            // 若Sub[k]不等于Sub[i-1],则将k的值更新为next[k],继续检查
            else
            {
                k = next[k];
            }
        }
        i = 1;
        // 进行第二次优化,处理next数组中连续相等的情况
        while (i < lenSub)
        {
            if (Sub[i] == Sub[next[i]])
            {
                next[i] = next[next[i]];
            }
            i++;
        }
    }
}

// 改进后的KMP算法实现,用于在主串str中查找子串Sub,返回第一次出现的位置
char* KMP(char* str, char* Sub, int pos)
{
    assert(str && Sub); // 断言str和Sub非空
    int lenstr = strlen(str);
    int lenSub = strlen(Sub);
    // 特殊情况下,当主串或子串为空串时,直接返回NULL
    if (str[0] == '\0' || Sub[0] == '\0')
        return NULL;
    // 特殊情况下,当查找位置小于0或超出主串长度减1时,返回NULL
    if (pos < 0 || pos > lenstr - 1)
        return NULL;
    int* next = (int*)malloc(sizeof(int) * lenSub); // 分配next数组的内存空间
    assert(next); // 确保内存分配成功
    Getnext(next, Sub, lenSub); // 调用Getnext函数生成子串Sub的next数组
    int i = pos; // i表示在主串中的当前查找位置
    int j = 0; // j表示在子串中的当前查找位置
    // 使用循环进行匹配
    while (i < lenstr && j < lenSub)
    {
        // 若j等于-1,或者主串当前字符和子串当前字符相等,则i和j分别自增1,继续匹配
        if (j == -1 || str[i] == Sub[j])
        {
            i++;
            j++;
        }
        // 若主串当前字符和子串当前字符不相等,则将j的值更新为next[j],继续匹配
        else
        {
            j = next[j];
        }
    }
    free(next); // 释放next数组的内存空间
    next = NULL; // 将next指针置为NULL,防止产生野指针
    // 若j等于子串长度,说明匹配成功,返回匹配到的位置的指针
    if (j >= lenSub)
    {
        return str + (i - j);
    }
    // 若匹配失败,返回NULL
    return NULL;
}

int main()
{
    char str1[] = "abcdefabcdef";
    char Sub[] = "aaaaaaaab";
    char* ret = KMP(str1, Sub, 0); // 在主串str1中查找子串Sub,从位置0开始查找
    if (ret != NULL)
    {
        printf("%s\n", ret);
    }
    else
    {
        printf("子串未在主串中找到\n");
    }
    return 0;
}

注意:这种情况可以调试一下,查看next数组是否和我们想的一样进行了优化。

5. 使用KMP算法实现strstr函数的模拟实现

#include 
#include 
#include 
#include 

// 改进后的KMP算法中的Getnext函数,用于生成子串Sub的next数组
void Getnext(int* next, char* Sub, int lenSub)
{
   // 特殊情况下,当子串长度小于2时,直接设置next[0]为-1
   if (lenSub < 2)
       next[0] = -1;
   else
   {
       // 初始化next数组的前两个元素
       next[0] = -1;
       next[1] = 0;
       int k = 0; // k表示最大相同前后缀的长度
       int i = 2; // i表示当前需要计算next值的下标
       // 使用循环计算next数组的值
       while (i < lenSub)
       {
           // 若k等于-1,或者Sub[k]等于Sub[i-1],则next[i]的值为k+1,然后i和k都自增1
           if (k == -1 || Sub[k] == Sub[i - 1])
           {
               next[i] = k + 1;
               i++;
               k++;
           }
           // 若Sub[k]不等于Sub[i-1],则将k的值更新为next[k],继续检查
           else
           {
               k = next[k];
           }
       }
       i = 1;
       // 进行第二次优化,处理next数组中连续相等的情况
       while (i < lenSub)
       {
           if (Sub[i] == Sub[next[i]])
           {
               next[i] = next[next[i]];
           }
           i++;
       }
   }
}

// 模拟实现strstr函数,用于在主串str1中查找子串str2,返回第一次出现的位置的指针
char* my_strstr(char* str1, char* str2)
{
   assert(str1 && str2); // 断言str1和str2非空
   int lenstr = strlen(str1);
   int lenSub = strlen(str2);
   // 特殊情况下,当子串为空串时,直接返回主串的起始位置
   if (str2[0] == '\0')
       return str1;
   // 特殊情况下,当主串为空串时,返回NULL
   if (str1[0] == '\0')
       return NULL;
   int* next = (int*)malloc(sizeof(int) * lenSub); // 分配next数组的内存空间
   assert(next); // 确保内存分配成功
   Getnext(next, str2, lenSub); // 调用Getnext函数生成子串str2的next数组
   int i = 0; // i表示在主串中的当前查找位置
   int j = 0; // j表示在子串中的当前查找位置
   // 使用循环进行匹配
   while (i < lenstr && j < lenSub)
   {
       // 若j等于-1,或者主串当前字符和子串当前字符相等,则i和j分别自增1,继续匹配
       if (j == -1 || str1[i] == str2[j])
       {
           i++;
           j++;
       }
       // 若主串当前字符和子串当前字符不相等,则将j的值更新为next[j],继续匹配
       else
       {
           j = next[j];
       }
   }
   free(next); // 释放next数组的内存空间
   next = NULL; // 将next指针置为NULL,防止产生野指针
   // 若j等于子串长度,说明匹配成功,返回匹配到的位置的指针
   if (j >= lenSub)
   {
       return str1 + (i - j);
   }
   // 若匹配失败,返回NULL
   return NULL;
}

int main()
{
   char str1[] = "abcdefabcdef";
   char str2[] = "\0";
   char* ret = my_strstr(str1, str2); // 在主串str1中查找子串str2
   if (ret != NULL)
   {
       printf("子串在主串中的位置:%s\n", ret);
   }
   else
   {
       printf("子串未在主串中找到\n");
   }
   return 0;
}
  • 我们之前实现的KMP与算法与strstr函数的区别就在于pos参数和当子串是\0时,strstr函数会直接返回主串的起始地址,我们略做修改,就是模拟实现strstr函数KMP算法版本,并且它的时间复杂度只有 O ( m + n ) O(m+n) O(m+n)

你可能感兴趣的:(开发语言,数据结构,KMP算法,算法,青少年编程,c语言,经验分享)