【算法拾遗·字符串篇】KMP字符串匹配算法

本文详细介绍了KMP字符串匹配算法,从暴力搜索匹配的O(M*N)时间复杂度,过渡到KMP算法的优化过程。通过图解分析了KMP算法中最长公共前后缀表的构建,以及在匹配不成功时如何利用前缀表进行有效位移,降低时间复杂度到O(N+M)。文章最后总结了KMP算法的本质和匹配策略。
摘要由CSDN通过智能技术生成

问题描述

已知字符串 text 和 字符串 pattern,
问 pattern 是否为 text 的子串

暴力搜索匹配

int main() {
    const char *pattern = "ACTG";
    const char *text = "CGTACGTACCTAGCACTGACTAGCCA"; 
    int cnt = search(pattern, text);
    printf("%d\n", cnt);
    return 0;
}

int search(const char *pattern, const char *text) {
    int M = strlen(pattern);
    int N = strlen(text);
    for (int i = 0; i < N - M; i++) {
        int j;
        for (j = 0; j < M; j++) {
            if (*(pattern+j) != *(text+j+i))
                break;
        }
        // pat 全都匹配了
        if (j == M) return i;
    }
    return -1;
}

时间复杂度:O(M*N)
其中M为子串长度,N为母串长度

KMP匹配

图解:

1、构建最长公共前后缀表

(1)给定两个字符串T, P
在这里插入图片描述
(2)建立P的最长公共前后缀表
在这里插入图片描述
(举例)abab的最长公共前后缀是:ab
在这里插入图片描述
(3)获得结果
在这里插入图片描述(4)为方便日后的边界判定,去掉最后一位(用不到),添加头部指示符
在这里插入图片描述

2、KMP匹配操作

(1)当 P[3] != T[3]时,前缀表prefix[3] = 1,即可以将P[1]移至T[3]处继续向后匹配(此时的P[1]之前部分 与 对应的T[3]之前的部分相同,故不用再做比较)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200929152207647.png?x-oss-process=image,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01hdXNoYXdraW4=,size_16,color_FFFFFF,t_70#pic_cente
在这里插入图片描述
(2)当 P[1] != T[3+1]时,前缀表prefix[1] = 0,即可以将P[0]移至T[3+1]处继续向后匹配。
在这里插入图片描述
(3)当 P[0] != T[4+0]时,前缀表prefix[0] = -1,即此时T[4]不匹配 P 中任意字符,故 P 向后移一位对齐下一位 T。
在这里插入图片描述
在这里插入图片描述内容来自视频:正月点灯笼.

总结:

本质:
对串的前缀进行去处理
得到所有可能出现匹配的位置
从而减少不必要的位移

1、关于 pattern 建立最长公共前后缀表prefix:
如何最快查找其公共前后缀 {参考动态规划}

2、通过 prefix 辅助 pattern 对 text 的匹配
如何借助prefix进行移位匹配操作
{匹配成功/失败(如何更新索引状态)}

代码:

#include <stdio.h>
#include <stdlib.h>
#define LENLIMIT 128

// 输入信息
int get_text(char demo[]);
// 建立pattern字符串的公共前后缀长度表prefix
void prefix_table(char pattern[], int prefix[], int n);
// 为方便匹配,故向右移动prefix
void move_prefix(int prefix[], int n);
void show_prefix(int prefix[], int n);
void kmp_search(char text[], char pattern[], \
int prefix[], int text_len, int pattern_len);

int main(int argc, char const *argv[]) {
    
    char *text = (char *)malloc(sizeof(char)*LENLIMIT);
    char *pattern = (char *)malloc(sizeof(char)*LENLIMIT);
    int *prefix = (int *)malloc(sizeof(int)*LENLIMIT);

    printf("typein text\t: ");
    int text_len = get_text(text);
    printf("typein pattern\t: ");
    int pattern_len = get_text(pattern);

    printf("text\t: %s\n", text);
    printf("pattern\t: %s\n", pattern);

    prefix_table(pattern, prefix, pattern_len);
    show_prefix(prefix, pattern_len);
    kmp_search(text, pattern, prefix, \
    text_len, pattern_len);

    free(pattern);
    free(prefix);
    return 0;
}

// 动态数组的赋值方法:
// 数组指针【char *demo】<=>【char demo[]】<=>【demo[i]】
int get_text(char demo[]) {
    char ch;
    int i = 0;
    while((ch=getchar())!='\n') {
        demo[i++] = ch;
        demo[i] = '\0';
    }
    return i;
}

void prefix_table(char pattern[], int prefix[], int n) {
    /* 操作对象:pattern的子数组
     * i = 1~n-1
     * len为当前最长公共前后缀记录
     * 1、前一刻len的后一位(新晋前缀) 等于 当前i位置(新晋后缀):
     *      len+1并赋给pattern[i], i++
     * 2、否则
     * 2.1、len = 0: pattern[i] = 0, i++
     * 2.2、len > 0: 更新len为len-1时的prefix, 继续寻找len
     */
    prefix[0] = 0;
    int i = 1;
    int len = 0;
    printf("%d\n", n);
    while (i < n) {
        // 通常情况,动态规划
        if (pattern[i] == pattern[len]) {
            prefix[i] = ++len;
            i++;
        }
        else {
            // 非边界异常情况,左斜处取长度
            if (len > 0) {
                len = prefix[len - 1];
            }
            // 边界异常情况,等0后移
            else {
                prefix[i] = len;
                i++;
            }
        }
    }
    // 为方便后续匹配
    move_prefix(prefix, n);
}

void move_prefix(int prefix[], int n) {
    int i;
    for (i=n-1; i>0; i--) {
        prefix[i] = prefix[i-1];
    }
    prefix[0] = -1;
}

void show_prefix(int prefix[], int n) {
    int i;
    for (i=0; i<n; i++) {
        printf("%d\t", prefix[i]);
    }
    printf("\n");
}

void kmp_search(char text[], char pattern[], \
int prefix[], int text_len, int pattern_len) {
    /* 各数组的索引声明
     * text[i]
     * pattern[j]
    */
    int i = 0;
    int j = 0;
    while (i<text_len) {
        if (text[i]==pattern[j]) {
            // pattern 完全匹配 
            if (j==pattern_len-1) {
                printf("At: %d, get!\n", i-j);
                j = prefix[j]+1;  //不停下来,继续往下比 
            }
            // 当前位匹配且继续往下比
            else {
                j++;
            }
            i++;
        }
        else {
            // pattern 首位不匹配时,text后移(i+1)
            if (prefix[j]==-1) {
                i++;
            } 
            // pattern 迁移到公共前缀记录处
            else {
                j = prefix[j];
            }
        }
    } 
    printf("Over...\n");
}

时间复杂度:O(N+M)
其中M为子串长度,N为母串长度

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值