Algorithm - KMP 字符串匹配算法

本文介绍了两种字符串匹配算法:BF算法和KMP算法。BF算法通过简单地比较目标串和模式串来查找匹配项,但效率较低。KMP算法通过创建包含局部匹配信息的数组来减少不必要的比较,从而提高匹配速度。

前言

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

以上是百度百科对 KMP 的描述,在开始介绍 KMP 算法时我们先想一下如何对一字符串找到和它是否匹配的字符串。

BF 算法

暴风(Brute Force)算法是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。时间复杂度为 O (m * n)。

代码实现:

int bfMarch(String text, String pattern) {
    int index = -1;
    int i = 0;
    int j = 0;
    int k = 0;
    while (i < text.length() && j < pattern.length()) {
        if (text.charAt(i) == pattern.charAt(j)) {
            i++;
            j++;
        } else {
            j = 0;
            i = ++k;
        }
    }
    if (j == pattern.length()) {
        index = i - pattern.length();
    }
    return index;
}

以上代码等同于

int bfMarch(String text, String pattern) {
    int index = -1;
    for (int i = 0; i < text.length(); i++) {
        for (int j = 0; j < pattern.length(); j++) {
            if (text.charAt(i) != pattern.charAt(j)) {
                break;
            }
            if (j == pattern.length() {
                return i - pattern.length();
            }
        }
    }
    return index;
}

KMP 算法

BF 算法比较简单,因为符合我们的逻辑思维,时间复杂度接受不了,在某种意义上为 O (m^2)。接下来我们看看另一种算法,它的时间复杂度为 O (n + m),空间复杂度为 O(m)。刚开始学习这个算法时,在网上找的资料我看的是蒙*的,说的太高深了,直到我在 B 站看了一个大神的视频,资源里有链接。我非常推荐大家先去看视频,比我描述的好多了。

pattern 字符串是和 text 字符串匹配的字符串,pattern.length() <= text.length()。

KMP 的主要思想是创建一容量和 pattern 字符串一样的整型数组,里面存储与前缀相同字符的位置,如:

abcdabaa  // pattern
01234567  // 数组下标
00001211  // 数组存储的值

值如何求?使用两个指针 i、j,arr[0] = 0,i 指向 arr[0],j 指向 arr[1],当判断 pattern.charAt(0) == pattern.charAt(1) 时,arr[1] = i + 1,即 0 + 1,如上列的第 3 和第 4 位,之后 i++ 和 j++。当不匹配时,则向前移一位,i 被赋值为 arr[i - 1],即 i = arr[i - 1],直到 i 变为零,如果还不匹配,则 j++。最后,j > pattern.length() 时结束。
数组的值表示,当 pattern 和 text 对比不匹配时,向前移动一位,pattern 移动的到该值表示的位置。

代码:

/**
 * 当字符不匹配时,可根据下标返回对应位置。
 * 目的:减少匹配次数
 *
 * @param patternStr
 * @return
 */
private static int[] computeTemporaryArray(String patternStr) {
    char[] pattern = patternStr.toCharArray();
    int[] lps = new int[pattern.length];
    int index = 0;
    for (int i = 1; i < pattern.length; ) {
        if (pattern[i] == pattern[index]) {
            lps[i] = index + 1;
            index++;
            i++;
        } else {
            if (index != 0) {
                index = lps[index - 1];
            } else {
                lps[i] = 0;
                i++;
            }
        }
    }
    return lps;
}

然后就开始匹配两字符串了,代码如下:

/**
 * 找到 pattern 在 text 第一次出现的位置
 *
 * @param textStr
 * @param patternStr
 * @return pattern 第一个字符的位置
 * -1 表示不匹配
 */
private static int indexOfStr(String textStr, String patternStr) {
    int index = -1;
    char[] text = textStr.toCharArray();
    char[] pattern = patternStr.toCharArray();
    // KMP
    int lps[] = computeTemporaryArray(patternStr);
    int i = 0;
    int j = 0;
    while (i < text.length && j < pattern.length) {
        if (text[i] == pattern[j]) {
            i++;
            j++;
        } else {
            if (j != 0) {
                j = lps[j - 1];
            } else {
                i++;
            }
        }
    }
    if (j == pattern.length) {
        index = i - pattern.length;
    }
    return index;
}

完整代码

这里还有另一种写法

public class KMP {
    public static int KMPSearch(String txt, String pat, int[] next) {
        int M = txt.length();
        int N = pat.length();
        int i = 0;
        int j = 0;
        while (i < M && j < N) {
            if (j == -1 || txt.charAt(i) == pat.charAt(j)) {
                i++;
                j++;
            } else {
                j = next[j];
            }
        }
        if (j == N)
            return i - j;
        else
            return -1;
    }
    public static void getNext(String pat, int[] next) {
        int N = pat.length();
        next[0] = -1;
        int k = -1;
        int j = 0;
        while (j < N - 1) {
            if (k == -1 || pat.charAt(j) == pat.charAt(k)) {
                ++k;
                ++j;
                next[j] = k;
            } else
                k = next[k];
        }
    }
    public static void main(String[] args) {
        String txt = "BBC ABCDAB CDABABCDABCDABDE";
        String pat = "ABCDABD";
        int[] next = new int[pat.length()];
        getNext(pat, next);
        System.out.println(KMPSearch(txt, pat, next));
    }
}

资源

【soso字幕】汪都能听懂的KMP字符串匹配算法【双语字幕】

### PTA 7-2 字符串匹配算法性能对比及实现分析 #### 一、字符串匹配算法概述 字符串匹配问题是计算机科学中的经典问题之一,其目标是在一个较长的文本串中找到某个较短的模式串的位置。常见的字符串匹配算法有朴素匹配算法(Naive Algorithm)、Rabin-Karp 算法以及更高效的 Knuth-Morris-Pratt (KMP) 算法等。 其中,KMP 算法因其高效性和广泛的应用场景而备受关注。该算法通过预先处理模式串来构建部分匹配表(Partial Match Table),从而避免了在不必要的情况下回溯文本指针,显著提高了匹配速度[^1]。 --- #### 二、PTA 7-2 的核心需求 题目要求对不同字符串匹配算法进行性能对比并提供具体实现分析。这通常涉及以下几个方面: 1. **时间复杂度**:评估每种算法的时间消耗。 2. **空间复杂度**:考察额外存储的需求。 3. **实际运行表现**:针对特定输入数据集测试各算法的实际执行效率。 对于本题而言,重点在于比较朴素匹配算法KMP 算法之间的差异,并给出具体的 C 或 Python 实现代码。 --- #### 三、朴素匹配算法 vs KMP 算法 ##### (1)朴素匹配算法 朴素匹配算法是最简单的字符串匹配方法,其实现逻辑如下: - 将模式串逐一与文本串中的子串进行比较; - 如果发现某一位字符不匹配,则将模式串向右滑动一位重新开始比较。 尽管简单易懂,但此算法存在明显的缺点——当遇到大量重复失配时会浪费大量的计算资源。因此,其最坏情况下的时间复杂度为 O(n * m),其中 n 是文本串长度,m 是模式串长度[^3]。 ##### (2)KMP 算法 相比朴素匹配算法KMP 算法则更加智能化。它通过对模式串预处理得到的部分匹配表(即 LPS 数组),能够有效减少不必要的字符比较操作。具体来说: - 部分匹配表记录了模式串每个位置处的最大相同前缀和后缀长度; - 当发生失配时,可以根据当前状态快速调整模式串起始位置,无需重置整个过程。 这种优化使得 KMP 算法能够在绝大多数情况下达到线性时间复杂度 O(n + m)[^2]。 --- #### 四、C 和 Python 实现示例 ##### (1)C语言实现 KMP 算法 以下是基于 C 语言的 KMP 算法实现代码片段: ```c #include <stdio.h> #include <string.h> void computeLPSArray(char* pattern, int M, int* lps) { int length = 0; lps[0] = 0; // lps[0] is always 0 int i = 1; while (i < M) { if (pattern[i] == pattern[length]) { length++; lps[i] = length; i++; } else { if (length != 0) { length = lps[length - 1]; } else { lps[i] = 0; i++; } } } } int KMPSearch(char* text, char* pattern) { int N = strlen(text); int M = strlen(pattern); int lps[M]; // Create LPS array computeLPSArray(pattern, M, lps); int i = 0; // index for text[] int j = 0; // index for pattern[] while ((N - i) >= (M - j)) { if (pattern[j] == text[i]) { j++; i++; } if (j == M) { printf("Found pattern at index %d \n", i - j); j = lps[j - 1]; } else if (i < N && pattern[j] != text[i]) { if (j != 0) j = lps[j - 1]; else i++; } } return -1; } ``` ##### (2)Python 实现 KMP 算法 下面是使用 Python 编写的 KMP 算法版本: ```python def compute_lps_array(pattern): lps = [0] * len(pattern) length = 0 i = 1 while i < len(pattern): if pattern[i] == pattern[length]: length += 1 lps[i] = length i += 1 else: if length != 0: length = lps[length - 1] else: lps[i] = 0 i += 1 return lps def kmp_search(text, pattern): lps = compute_lps_array(pattern) i = 0 # Index for text j = 0 # Index for pattern while i < len(text): if pattern[j] == text[i]: i += 1 j += 1 if j == len(pattern): print(f"Pattern found at index {i-j}") j = lps[j-1] elif i < len(text) and pattern[j] != text[i]: if j != 0: j = lps[j-1] else: i += 1 ``` --- #### 五、总结 综上所述,虽然朴素匹配算法易于理解和实现,但在面对大规模数据时显得力不从心;相比之下,KMP 算法凭借其精妙的设计思路大幅提升了字符串匹配的速度,尤其适合于需要频繁查找的情况。然而需要注意的是,任何一种算法的选择都应视具体情况而定,例如待解决问题规模大小、硬件条件限制等因素均可能影响最终决策。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值