KMP算法

简介:

背景

KMP 算法由计算机科学家 Donald Knuth、Vaughan Pratt 和 James H. Morris 在 1977 年共同提出,目的是提高字符串匹配的效率。在字符串匹配问题中,给定一个主字符串和一个模式字符串,目标是找出模式字符串在主字符串中出现的位置。经典的朴素字符串匹配算法在遇到不匹配时,往往需要回退到主字符串的下一个字符位置,这导致了效率低下。

原理

KMP 算法的关键在于部分匹配表(也称为 LPS数组),通过利用模式字符串中已经匹配的部分信息,避免了不必要的重复比较。当模式串与主串不匹配时,可以直接跳过一些字符的比较,而不是重新从头开始。

部分匹配表(LPS 数组)

LPS 数组存储的是模式串中每个位置的最长可匹配前缀的长度。具体来说,LPS 代表的是模式串中从开头到当前位置的子串中,最左边的前缀和最右边的后缀最长的匹配长度。

如模式串 "ABABCA" 的 LPS 数组计算过程如下:

  • LPS[0] = 0,因为没有前缀和后缀。
  • LPS[1] = 0,因为 "A" 没有重复。
  • LPS[2] = 1,因为前缀 "A" 和后缀 "A" 相同。
  • LPS[3] = 2,因为前缀 "AB" 和后缀 "AB" 相同。
  • LPS[4] = 0,因为 "ABC" 无法匹配。
  • LPS[5] = 1,因为前缀 "A" 和后缀 "A" 相同。

最终,LPS数组为 [0, 0, 1, 2, 0, 1]。

算法步骤

  1. 构建 LPS 数组

    • 初始化一个长度为模式串长度的 LPS 数组,并设置第一个元素为 0。
    • 遍历模式串,根据相等和不等的情况填充 LPS 数组。
  2. 进行匹配

    • 初始化主字符串和模式字符串的索引。
    • 逐个进行字符比较:
      • 如果当前字符相同,继续比较下一个字符。
      • 如果全部匹配,则记录下匹配位置,并利用 LPS 数组更新模式字符串的索引。
      • 如果不相等且已有匹配,则根据 LPS 数组更新模式字符串的索引,跳过已经匹配的部分;如果没有匹配,则移动主字符串的索引。

示例

假设主字符串为 "ABABDABACDABABCABAB" ,模式字符串为 "ABABCABAB"。

  1. 构建 LPS 数组

    • 对于模式串 "ABABCABAB",计算出 LPS 数组为 [0, 0, 1, 2, 0, 1, 2, 3, 4]。
  2. 匹配过程

    • 从主字符串的第一个字符开始与模式字符串的第一个字符进行比较。
    • 依次比较,直到发现模式字符串在主字符串中有完整的匹配。
    • 在匹配失败时,根据 LPS 数组的值调整模式字符串的起始匹配位置,而不是回到主字符串的下一个字符。

时间复杂度

KMP 算法的时间复杂度为 O(n + m),其中 n 是主字符串的长度,m 是模式字符串的长度。这是因为 KMP 通过 LPS 数组避免了在主字符串中重复检查已经比较过的部分。

优缺点

优点

  • 效率高:在处理大规模文本或需要多次查询匹配时,KMP 能显著减少时间消耗。
  • 适用范围广泛:几乎可以应用于所有需要字符串匹配的场景,如搜索引擎、文本编辑器、数据分析等。

缺点

  • 实现相对复杂:LPS 数组的构建和匹配过程相比于简单的暴力匹配方法稍显复杂,不适合初学者。
  • 对小样本的处理效率并不明显:对于短文本,KMP 相比于其他方法,如简单的暴力匹配,其优势在执行时间上并不明显。

应用场景

KMP 算法广泛应用于以下领域:

  • 文本编辑器:查找特定的字符串或关键字。
  • 搜索引擎:快速匹配并定位网页中的关键词。
  • DNA 序列分析:寻找 DNA 片段中的特定序列。
  • 数据挖掘:处理大量文本数据,进行模式匹配和分析。

总结

KMP算法是解决字符串匹配问题的一种高效算法,通过利用部分匹配表避免了重复比较,显著提高了匹配效率。它在信息检索、文本处理等领域有着广泛的应用,是算法与数据结构中的经典内容之一。

代码示例:

Python:

def kmp_search(text, pattern):
    # 生成部分匹配表
    def compute_lps(pattern):
        lps = [0] * len(pattern)
        length = 0  # 长度为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

    lps = compute_lps(pattern)
    i = j = 0  # i为text的索引,j为pattern的索引

    while i < len(text):
        if pattern[j] == text[i]:
            i += 1
            j += 1

        if j == len(pattern):
            print(f"模式串在主串中出现,起始位置: {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

# 示例使用
text = "ababcabcabababd"
pattern = "ababd"
kmp_search(text, pattern)

代码说明:

  • compute_lps函数用于构建部分匹配表。
  • kmp_search函数用于在主字符串中搜索模式字符串,并输出所有出现的位置。

c++:

#include <iostream>
#include <vector>
#include <string>

void computeLPSArray(const std::string &pattern, std::vector<int> &lps) {
    int length = 0; // 最长前缀后缀的长度
    lps[0] = 0; // lps[0] 总是 0
    int i = 1;

    // 计算 lps 数组
    while (i < pattern.length()) {
        if (pattern[i] == pattern[length]) {
            length++;
            lps[i] = length;
            i++;
        } else {
            if (length != 0) {
                length = lps[length - 1];
            } else {
                lps[i] = 0;
                i++;
            }
        }
    }
}

void KMPSearch(const std::string &text, const std::string &pattern) {
    int M = pattern.length();
    int N = text.length();

    // 创建 lps 数组
    std::vector<int> lps(M);
    computeLPSArray(pattern, lps);

    int i = 0; // 主字符串的索引
    int j = 0; // 模式字符串的索引

    while (i < N) {
        if (pattern[j] == text[i]) {
            i++;
            j++;
        }

        if (j == M) {
            std::cout << "模式串在主串中出现,起始位置: " << i - j << std::endl;
            j = lps[j - 1]; // 重置模式字符串索引
        } else if (i < N && pattern[j] != text[i]) {
            if (j != 0) {
                j = lps[j - 1]; // 继续查找
            } else {
                i++;
            }
        }
    }
}

int main() {
    std::string text = "ababcabcabababd";
    std::string pattern = "ababd";
    KMPSearch(text, pattern);
    return 0;
}

代码说明:

  1. computeLPSArray:该函数用于计算模式字符串的最长前缀后缀数组(LPS数组)。
  2. KMPSearch:该函数用于在主字符串中搜索模式字符串,并输出所有出现的位置。
  3. main:在主函数中定义主字符串和模式字符串,并调用 KMPSearch 函数进行搜索。

Pascal:

program KMPAlgorithm;

uses SysUtils;

procedure ComputeLPSArray(pattern: string; var lps: array of Integer);
var
  length, i: Integer;
begin
  length := 0;  // 最长前缀后缀的长度
  lps[0] := 0;  // lps[0] 总是 0
  i := 1;

  // 计算 lps 数组
  while i < Length(pattern) do
  begin
    if pattern[i + 1] = pattern[length + 1] then
    begin
      Inc(length);
      lps[i] := length;
      Inc(i);
    end
    else
    begin
      if length <> 0 then
        length := lps[length - 1]
      else
      begin
        lps[i] := 0;
        Inc(i);
      end;
    end;
  end;
end;

procedure KMPSearch(text, pattern: string);
var
  lps: array of Integer;
  i, j: Integer;
begin
  SetLength(lps, Length(pattern));
  ComputeLPSArray(pattern, lps);

  i := 1; // 主字符串索引
  j := 1; // 模式字符串索引
  while i <= Length(text) do
  begin
    if pattern[j] = text[i] then
    begin
      Inc(i);
      Inc(j);
    end;

    if j > Length(pattern) then
    begin
      WriteLn('模式串在主串中出现,起始位置: ', i - j);
      j := lps[j - 2]; // 重置模式字符串索引
    end
    else if (i <= Length(text)) and (pattern[j] <> text[i]) then
    begin
      if j <> 1 then
        j := lps[j - 2] // 继续查找
      else
        Inc(i);
    end;
  end;
end;

var
  text, pattern: string;
begin
  text := 'ababcabcabababd';
  pattern := 'ababd';
  KMPSearch(text, pattern);
end.

代码说明:

  1. ComputeLPSArray:该过程用于计算模式字符串的最长前缀后缀数组(LPS数组)。
  2. KMPSearch:该过程用于在主字符串中搜索模式字符串,并输出所有出现的位置。
  3. 主程序:定义了主字符串和模式字符串,并调用 KMPSearch 进行搜索。
希望这些代码能帮助您理解并解决这个问题,如果有问题,请随时提问。
  蒟蒻题解,神犇勿喷,点个赞再走吧!QAQ

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值