当前位置: 首页 > article >正文

【数据结构第四章】- 串的模式匹配算法(BF 算法和 KMP 算法/用 C 语言实现)

目录

一、前言

二、BF 算法

三、KMP 算法

3.2.1 - KMP 算法的原理

3.2.2 - KMP 算法的实现

3.2.3 - KMP 算法的优化


创作不易,可以点点赞,如果能关注一下博主就更好了~ 


一、前言

子串的定位运算通常称为串的模式匹配串匹配。此运算的应用非常广泛,比如在搜索引擎、拼写检查、语言翻译、数据压缩等应用中,都需要进行串匹配。

串的模式匹配设有两个字符串 S 和 T,设 S 为主串,也称正文串;设 T 为子串,也称为模式。在主串 S 中查找与模式 T 相匹配的子串,如果匹配成功,确定相匹配的子串中的第一个字符在主串 S 中出现的位置。

著名的模式匹配算法有 BF 算法KMP 算法。

 

 


二、BF 算法

BF 算法,即暴力(Brute Force)算法,是最简单直观的模式匹配算法。

int IndexBF(const char* S, const char* T)
{
    if (S == NULL || T == NULL)  // 判断是否为空指针
        return -1;
    int i = 0;  // 主串指针
    int j = 0;  // 模式串指针
    int slen = strlen(S);
    int tlen = strlen(T);
    while (i < slen && j < tlen)
    {
        if (S[i] == T[j])
        {
            ++i;
            ++j;
        }
        else
        {
            // 回溯
            i = i - j + 1;  // i = i - (j - 1) = i - j + 1
            j = 0;
        }
    }
    if (j == tlen)  
        return i - j;  // 匹配成功
    else
        return -1;  // 匹配失败
}

 

 


三、KMP 算法

3.2.1 - KMP 算法的原理

KMP 是对 BF 改进后的算法,它由 Knuth、Morris 和 Pratt 同时设计实现,因此简称 KMP 算法。

在 BF 算法中,每当一趟匹配过程中出现字符不相等的情况,主串指针 i 回溯到下标为 i - j + 1 的位置,这是因为在模式匹配中,我们并不知道主串的具体内容,但是在不匹配的字符之前,主串 S 中的一个连续的字符序列与模式串相等,KMP 算法正是利用了已经得到的 "部分匹配" 结果对算法进行了改进

KMP 算法的原理:

  1. 在匹配的过程中,主串的指针 i 不需要回溯,只回溯模式串的指针 j

  2. S[i] != T[j] 时,模式串指针 j 回溯的下标位置由模式串的内容决定

假设主串为 ​gif.latex?%22s_1s_2%20...s_%7Bn%7D%22,模式串为 gif.latex?%22t_1t_2%20...t_%7Bm%7D%22​,在匹配过程中,当 ​ gif.latex?s_i%20%5Cne%20t_j 时,模式串指针 j 回溯到下标为 k (k < j) 的位置,则有 gif.latex?%22t_0t_1%20...t_%7Bk-1%7D%22%20%3D%20%22s_%7Bi-k%7D%20...s_%7Bi-2%7Ds_%7Bi-1%7D%22①​。

而已得到的 "部分匹配" 的结果是 gif.latex?%22t_%7Bj-k%7D%20...t_%7Bj-2%7Dt_%7Bj-1%7D%22%20%3D%20%22s_%7Bi-k%7D%20...s_%7Bi-2%7Ds_%7Bi-1%7D%22②​。

由 ① 和 ② 推得 gif.latex?%22t_0t_1%20...t_%7Bk-1%7D%22%20%3D%20%22t_%7Bj-k%7D%20...t_%7Bj-2%7Dt_%7Bj-1%7D%22③​,它们是模式串匹配失败位置前的内容中的最长公共前后缀

例如

4bcaf88a273c468d8e6e14ab01cd0301.png

 

​jgif.latex?t_0t_1...t_%7Bj-1%7D前缀后缀最长公共前后缀k
0-1
1​a0
2​aba​b​0
3​abaa、​aba、ba​a​1
4​ababa、ab、aba​b、ab、bab​​ab2

next[j] = k,则 next[j] 表示在匹配过程中,当 ​ gif.latex?s_i%20%5Cne%20t_j 时,模式串指针 j 回溯的下标位置

982ceec6a9064da9b3f14184d88e8cac.png

 

3.2.2 - KMP 算法的实现

void GetNext(const char* T, int* next)
{
    int tlen = strlen(T);
    if (tlen == 0) { return; }
    if (tlen == 1) { next[0] = -1; return; }
    if (tlen == 2) { next[0] = -1; next[1] = 0; return; }
    // 计算 next[j]
    next[0] = -1;
    next[1] = 0;
    for (int j = 2; j < tlen; ++j)
    {
        int maxlen = 0;  // 最长公共前后缀的长度
        for (int i = 1; i < j; ++i)
        {
            char* prefix = (char*)calloc(i + 1, sizeof(char));  
            char* suffix = (char*)calloc(i + 1, sizeof(char));  
            assert(prefix && suffix);
            strncpy(prefix, T, i);  // 取前缀
            strncpy(suffix, T + j - i, i);  // 取后缀
            // 判断是否为公共前后缀
            if (strcmp(prefix, suffix) == 0)
            {
                maxlen = i;
            }
        }
        next[j] = maxlen;
    }
}
​
int IndexKMP(const char* S, const char* T)
{
    if (S == NULL || T == NULL)  // 判断是否为空指针
        return -1;
    int i = 0;  // 主串指针 
    int j = 0;  // 模式串指针
    int slen = strlen(S);
    int tlen = strlen(T);
    // 获取 next 数组
    int* next = (int*)calloc(tlen, sizeof(int));  
    assert(next);
    GetNext(T, next);
    while (i < slen && j < tlen)
    {
        if (S[i] == T[j] || j == -1)
        {
            ++i;
            ++j;
        }
        else
        {
            j = next[j];  // 根据 next 数组回溯模式串指针
        }
    }
    if (j == tlen)
        return i - j;  // 匹配成功
    else
        return -1;  // 匹配失败
    return 0;
}

 

3.2.3 - KMP 算法的优化

前面定义的 next 数组在某些情况下尚有缺陷。例如模式串 和主串 匹配时,当 S[3] != T[4] 时,由 next[j] 的指示还需要进行 i = 3、j = 2; i = 3、j = 1; i = 3、j = 0 这三次比较。实际上,因为模式串中下标分别为 2、1、0 的三个字符和下标为 3 的字符相等,因此不需要再和主串中下标为 3 的字符相比较,而可以直接进行 i = 4、j = 0 的字符比较。

这就是说,若按上述定义得到 next[j] = k,而模式串中 ,则当 时,不需要再和 进行比较,而直接和 比较,换句话说,此时的 next[j] 应该和 next[k] 相同

void GetNext(const char* T, int* next)
{
    int tlen = strlen(T);
    if (tlen == 0) { return; }
    if (tlen == 1) { next[0] = -1; return; }
    if (tlen == 2) { next[0] = -1; next[1] = 0; return; }
    // 计算 next[j]
    next[0] = -1;
    next[1] = 0;
    for (int j = 2; j < tlen; ++j)
    {
        int maxlen = 0;  // 最长公共前后缀的长度
        for (int i = 1; i < j; ++i)
        {
            char* prefix = (char*)calloc(i + 1, sizeof(char));  
            char* suffix = (char*)calloc(i + 1, sizeof(char));  
            assert(prefix && suffix);
            strncpy(prefix, T, i);  // 取前缀
            strncpy(suffix, T + j - i, i);  // 取后缀
            // 判断是否为公共前后缀
            if (strcmp(prefix, suffix) == 0)
            {
                maxlen = i;
            }
        }
        next[j] = maxlen;
    }
}
​
void GetNextval(const char* T, int* next, int* nextval)
{
    int tlen = strlen(T);
    nextval[0] = -1;
    for (int j = 1; j < tlen; ++j)
    {
        if (T[j] == T[next[j]])
        {
            nextval[j] = nextval[next[j]];
        }
        else
        {
            nextval[j] = next[j];
        }
    }
}
​
int IndexKMP(const char* S, const char* T)
{
    if (S == NULL || T == NULL)  // 判断是否为空指针
        return -1;
    int i = 0;  // 主串指针 
    int j = 0;  // 模式串指针
    int slen = strlen(S);
    int tlen = strlen(T);
    // 获取 next 数组
    int* next = (int*)calloc(tlen, sizeof(int));  
    assert(next);
    GetNext(T, next);
    // 获取 nextval 数组
    int* nextval = (int*)calloc(tlen, sizeof(int));
    assert(nextval);
    GetNextval(T, next, nextval);
    while (i < slen && j < tlen)
    {
        if (S[i] == T[j] || j == -1)
        {
            ++i;
            ++j;
        }
        else
        {
            j = nextval[j];  // 根据 nextval 数组回溯模式串指针
        }
    }
    if (j == tlen)
        return i - j;  // 匹配成功
    else
        return -1;  // 匹配失败
    return 0;
}

 


http://www.kler.cn/a/14447.html

相关文章:

  • Nginx +Tomcat 负载均衡,动静分离集群
  • navicat 远程连接oracle数据库ORA-12170及ORA-28547问题
  • Speech and Language Processing-之文本规范化
  • 工贸企业重大事故隐患判定标准,自2023年5月15日起施行
  • 后端开发常犯的问题(Java版)
  • Python VS C(上篇)
  • IPTV系统架构的分析与研究
  • 部署 Exsi 7.0.3
  • PointPillars点云编码器代码运行过程中的问题及解决
  • 如何借助分布式存储 JuiceFS 加速 AI 模型训练
  • Android App 架构 面试专题,你可能会被问到的 20 个问题
  • 2023年全国职业院校技能大赛软件测试赛题第1套
  • 算法的时间复杂度
  • CHAPTER 5: 《DESIGN CONSISTENT HASHING》 第5章 《设计一致的哈希》
  • ( “树” 之 BST) 530. 二叉搜索树的最小绝对差 ——【Leetcode每日一题】
  • IDA调试
  • 普通家庭,千万不要投入大量时间和金钱,让孩子去苦学和培养AI机器人编程了...
  • 【汽车电子】5分钟了解汽车操作系统(科普篇)
  • JavaScript概述二(Date+正则表达式+Math+函数+面向对象)
  • 深入解读springboot使用注解@value注入static变量