首页 > 编程语言 >KMP算法——理解 next 数组

KMP算法——理解 next 数组

时间:2024-08-15 19:28:40浏览次数:13  
标签:匹配 space next 算法 数组 KMP ptr PM

!注意!本文与《王道》,《严书》有所不同,字符串均从第 0 位开始,next 数组没有添加常数 1。博客为梳理思路所用,难免纰漏,希望不吝赐教。

在字符串匹配中,设 m 为待匹配的主串 S 长度,n 为找寻的模式串 T 长度。

如:在主串 S = 'ababc' 中寻找模式串 T = 'abc' 则字符串匹配算法返回 S 中第一个匹配字串开始的位置下标 2,即 S 中的第二个 'a' 的位置.(原则上可以找到所有匹配字符串,方便起见只找寻第一个)

简单匹配算法(暴力求解)


介绍暴力求解算法是为了凸显 KMP 算法的强大。

不论何种算法,计算机均是一个字符一个字符地进行匹配。记 S 串中正在考察的字符的下标为 i,T 串为 j.(i,j 均从 0 开始)

简单匹配算法原理为,从模式串的第 0 个字符,到第 n - 1 个字符与主串匹配,遇到不匹配的情况则将模式串整体向右移动一位,可以看到,最坏的时间复杂度为 O(m*n),如 T = '00001',S = '000000000000000000000000001'

img1

如上图所示,简单匹配算法中第三趟已成功匹配 4 个字符,可以从已经匹配的 abca 得知(因为这时主串和模式串的前 4 个字符完全相等),将模式串向后移动 1 位的结果将是 ab 比较,显然他们是不等的。故简单匹配算法中的一次移动一个字符的匹配方法,没有用上已经匹配过的信息。

KMP 算法

KMP 算法具有 O(m+n) 的时间复杂度。

引入


KMP 的核心思想为充分利用模式串本身的信息。模式串 T 的信息用 next[n] 的数组进行保存。

为了便于理解,我们引入以下概念:

  • 前缀:除了字符串中最后一个字符 \(\alpha\) 外,其前面的所有字符所组成字符串的子串(子串需要包括第一个字符)之并。例如 ab 其前缀为 { a },对于 abc,其前缀为 { a, ab }
  • 后缀,除了字符串中第一个字符 \(\beta\) 外,其后面所有字符所组成字符串的子串之并。(子串需要包括最后一个字符)例如 ab 其后缀为 { b },对于 abc,其后缀为 { c, bc}
  • 最大部分匹配:对于一个字符串,其某个前缀与某个后缀相同,且没有更大的前缀或后缀满足此要求,该前缀(或后缀)为这个字符串的最大部分匹配。如 ab,的最大部分匹配字符串为 \(\empty\),abcabab
  • PM 数组(partial match),用于辅助构造 next[n] 数组,PM[i] 表示字符串 \(t_0t_1\space...\space t_i\) 的最大部分匹配字符串长度。

例 1

T:  a b c a c
PM: 0 0 0 1 0

根据 PM 构造 next 数组


PM 数组是我觉得王道方法中最聪明的一点,搭建了到 next 数组的桥梁,同时也便于用观察法看出答案

假设最后一个成功匹配字符的下标为 j - 1,注意到,模式串 T 向右移动的位数应当为 已匹配位数 j - 最大部分匹配长度 (PM[ j - 1 ]) 即:

\[rightShift = j-PM[j - 1] \]

img

模式串 T 为 abcac ,如上图所示,最后一个匹配的字符为 T[1] = b,由上方 例 1 可知 PM[1] = 0,即子串 ab 的最大部分匹配长度为 0,则模式串 T 向后移动 2 - 0 = 2 位,如下图所示。(红色框为当前正在比较的字符串)
img

为什么可以这样计算呢?

  • 由前情假设,主串和模式串已匹配的长度为 \(j\) 可知,\(s_{i-j}s_{i-j+1}\space...\space s_{i-1}\) = \(t_0t_1\space...\space t_{j-1}\),现在将模式串向右移动,等价于用 \(t_0t_1\space...\space t_{j-1}\) 的某前缀字符串与其某后缀字符串匹配,同时也等价于用 \(s_{i-j}s_{i-j+1}\space...\space s_{i-1}\) 的某个前缀字符串匹配其某个后缀字符串。
  • 由 PM 定义可知,假设 PM[ j ] = k,(k>0) 则有 \(t_0t_1\space...\space t_{k-1}\) = \(t_{j-k}t_{j-k+1}\space...\space t_{j-1}\),且不存在 \(k^\prime\gt k\) 使得 \(t_0t_1\space...\space t_{k^\prime-1}\) = \(t_{j-k^\prime}t_{j-k^\prime+1}\space...\space t_{j-1}\)
  • \(t_0t_1\space...\space t_{j-1}\) 的最大部分匹配字符串,意味着模式串需要右移的距离最小。任何右移长度小于 j - k 的操作意味着失配。如:
    • 若主串 S 为 abcabp。模式串 T 为abcabd,其 PM 数组为 {0, 0, 0, 1, 2, 0}。在 d 字符处失配,有 j = 5, PM[ j - 1] = 2,最大部分匹配字符串为 ab,则右移的最小距离为将 abcabd 中前缀的 ab 与后缀的 ab 对齐,即右移 \(j - PM[ j - 1] = 5 - 2 = 3\) 位。
      img

    • 如果向右移动距离过小,会导致失配,如下图:
      img

通过 PM 得到 next
在字符串比较中,我们的改变比较字符的方式是移动指示当前比较字符的指针(即数组下标。主串 S 为 i,模式串 T 为 j)。next 数组就记录了模式串指针的变换,next[j] 表示第 j 位上的字符失配时,比较指针 j 将被赋值为 next[j] 继续与 S[i] 进行比较。next 数组将直观上模式串整体的右移,转换为模式串比较指针 j 的左移。数值计算方法:

\[next[j] = j - (j - PM[j - 1]) = PM[j - 1] \\ next[j] = PM[j - 1] \]

对于 j 为 0 的情况,若第一个失配,则会移动主串 S 的比较指针,\(i = i + 1.\) 规定 \(next[0] = -1.\) (后续会看到置为 -1 的作用)

仍然回到这个例子:

T:    a  b c a c
PM:   0  0 0 1 0
next: -1 0 0 0 1 

构造 next 数组算法


在刚才的方法中,PM 数组我们是通过观察得出的,对于计算机如何得到 next 数组呢?
上述公式的另一种表达方式如下:

\[next[j]=\left\{ \begin{array}{c} \begin{align} &-1, j=0 \\ &max\{k\mid1\lt k \lt j-1,且 t_0t_1\space ...\space t_{k-1} = t_{j-k}t_{j-k+1}\space...\space t_{j-1}\} \\ & 0,其余情况(即 PM[j-1] = 0 的情况) \end{align} \end{array} \right. \]

采用一种递推的方式获取 next[j] 值,假设 next[j - 1] = k - 1 = ptr,即新建一个指针变量 ptr,请注意,k - 1 的值表示了 \(t_0t_1\space ... \space t_{j-2}\) 的最大部分匹配长度(设\(j\ge 2\)),下文中所做的字符对比均为 \(t_{ptr}\) 与 \(t_{j-1}\) 间的对比:

  1. 当 \(\bold{t_{ptr} = t_{j-1}}\) 时,有 \(t_0t_1\space ...\space t_{k-1} = t_{j-k}t_{j-k+1}\space ...\space t_{j-1}\),则 \(t_0\space ...\space t_{j-1}\) 最大部分匹配长度为 k,即 $next[j] = ptr + 1\iff next[j] = next[j-1] + 1 $

举个例子:对模式串 T = abcabd,求其 next[5],即 d 所对应 next 值。由 \(t_{next[4]}=t_1=t_{j-1}=t_4=b\),可知 next[5] = next[4] + 1 = 2
img

  1. 当 \(\bold{t_{ptr} \ne t_{j-1}}\) 时,求取最长的 k 又可以视为一个字符串匹配问题。子模式串 T' = \(t_0t_1\space ...\space t_{k-1}\) 对 T 进行匹配,由于第 k - 1 位失配,故 \(ptr = next[ptr]\),即等价于将子模式串 T' 向右移动。此时再次将 \(t_{ptr}\) 与 \(t_{j-1}\) 比对,若相等,则同第一种情形 \(next[j] = ptr+1\);若不等,则重复进行 \(ptr=next[ptr]\) 的赋值。当 \(ptr\) 被赋值为 -1 时,表示 \(PM[j-1] = 0\),故 \(next[j] = 0\),也满足 \(next[j] = ptr + 1.\)

举个例子:对模式串 T = abcabde,求其 next[6],即 e 所对应 next 值。由 \(t_{ptr} = t_{next[5]}=t_2=c\ne t_{j-1} = t_5 = d\),故失配,用新的子模式串 T' = abc(\(t_0t_1\space ...\space t_{k-1}\))来匹配 T,子模式串指针 ptr 移动到 next[ptr]上,得到 ptr = 0
img
子模式串移动后:
img
仍然失配,但是当前的 next[ptr] = -1,无法再移动,即 \(t_0t_1\space...\space t_5\) 的最长部分匹配长度为 0,故赋值 next[j] = 0,即 next[6] = 0.

next 数组构造 C 语言表示

// 辅助数据结构
typedef struct {
  char *str;      // 字符串
  int length;     // 字符串长度
} String; 

// 根据上方分析我们得到:
// 1. 当 ptr = -1 或 T[ptr] = T[j-1] 时,有 next[j] = ptr + 1.
// 2. 当 ptr != -1 且 T[ptr] != T[j-1] 时,有 ptr = next[ptr]

void calculateNext(String T, int next[]){
  int n = T.length;
  // 注意到对模式串 T 现在作为字符串匹配的主串,其指针不会回退,只会前进。利用此特点从 0 ~ n-1 计算 next 数组
  int j = 1, ptr;
  next[0] = -1;     // 将第一个字符对应的 next 置为 -1
  ptr = next[0];    // 一开始计算 next[1] 时,ptr = next[1-1] = next[0]
  while (j < n) {
    if (ptr == -1 || T.str[ptr] == T.str[j - 1]) {
      // 满足 1.
      next[j] = ptr + 1;
      j++;
      ptr++;
    }
    else {
      // 满足 2.
      ptr = next[ptr];
    }
  }
}

KMP 匹配算法


现在有了 next 数组,再来进行匹配就十分方便了。

// 1. 当 j != -1 且 S[i] = T[j] 时,i++,j++,(成功匹配两串均移动一位)
// 2. 当 j != -1 且 S[i] != T[j] 时,即失配。j = next[j].等价于模式串右移。
// 3. 当 j = -1 时,表示 S[i] 与 T[0] 不匹配,需要移动主串,同时模式串的比较指针也需要移动到第 0 位 i++,j++;(与第一种情形统一起来)

// S:主串
// T:模式串
// next: 模式串的 next 数组
// return:返回主串中第一个匹配到的子串的首字符下标,没有匹配子串则返回 -1
int KMP(String S, String T, int next[]) {
  // 与构造 next 数组同,主串指针只增不减。
  int i = 0,j = 0;
  int matchPtr = -1;        // 返回的数组下标
  while (i < S.length && j < T.length) {
    if (j == -1 || S.str[i] == T.str[j]) {
      // 情形 1. 3.
      j++;
      i++;
    }
    else {
      // 情形 2.
      j = next[j];
    }
  }
  if (j == T.length) {
    // 表明找到匹配串
    matchPtr = i - T.length;    
  }
  return matchPtr;
}

小总结


再次重申,KMP 算法核心思想为:利用主串和模式串中已经匹配了的字符串的信息,这就做到了主串不回溯(主串的匹配指针只能增加)。如何利用已匹配信息呢:在模式串 T 的 T[ j ] 失配,隐含着 T[ 0 ]~T[ j - 1 ] 均匹配,则只用考虑模式串本身的结构即可——计算 next 数组。

计算 next 数组的两种方法:

  1. 通过部分匹配数组(PM)计算,由于观察法可以轻松看出匹配的最长前缀和后缀,除了 next[0] = -1 外其余 next 值只用把 PM 数组的值向右平移一位即可。(方便记忆,加速计算)
  2. 通过 \(next[j] = ptr + 1\) 和 \(ptr = next[ptr]\) 公式递推得到 next 数组,注意 j 的开始值取 1,ptr 值取 -1,(因为 next[0] \(\equiv\) -1next[1] \(\equiv\) 0) 这主要用于算法实现。

KMP 算法本身就是根据 next 数组移动模式串的比较指针的过程,这在用公式法计算 next 数组时已经有所体现。

注:《严书》和《王道》中采用将 next 数组整体加 1 的格式,主要由于其字符串下标从 1 开始。

进一步优化

KMP 算法还有优化的空间,即利用 T[ j ] 失配的信息,见下例:

主串 S 为 aaabaaaab,模式串 T 为 aaaab,计算得到 T 的 next 数组如下图,当 S[3] 与 T[3] 失配时,S[3] 还需要与 T[2],T[1],T[0] 进行三次注定失配的比较,因为 T[0] = T[1] = T[2] = T[3].也就是将模式串右移后 j 指向的字符与右移前相同。这个相等的信息,在 next 数组中可以包含进去。即 若 \(t_{next[j]} = t_{j}\),则 \(next[j] = next[ptr]\) ,注意这里的 \(ptr\) 是指已经 +1 后的,也可以理解为 \(next[j] = next[next[j]]\)。

img

// 改进的 next 数组计算
void calculateNextVal(String T, int nextVal[]){
  int n = T.length;
  // 注意到对模式串 T 现在作为字符串匹配的主串,其指针不会回退,只会前进。利用此特点从 0 ~ n-1 计算 nextVal 数组
  int j = 1, ptr;
  nextVal[0] = -1;     // 将第一个字符对应的 nextVal 置为 -1
  ptr = nextVal[0];    // 一开始计算 nextVal[1] 时,ptr = nextVal[1-1] = nextVal[0]
  while (j < n) {
    if (ptr == -1 || T.str[ptr] == T.str[j - 1]) {
      // 满足 1.
      nextVal[j] = ptr + 1;
      ptr++;
      // 将第 j 位失配的信息传递到 next 数组,也就是需要额外判断更新之后的 next[j](即当前 ptr) 是否与 j(失配位)所指向字符相同
      if (T.str[nextVal[j]] == T.str[j]) {
        // str[next[j]] 与 str[j] 相同,重新设置 next[j] 的值
        nextVal[j] = nextVal[ptr];
      }
      j++;
    }
    else {
      // 满足 2.
      ptr = nextVal[ptr];
    }
  }
}

参考文献


  1. 2022 数据结构与算法《王道》学习笔记(十一)KMP 算法 详细归纳总结 改进的模式匹配算法
  2. 2025《王道数据结构》
  3. 《数据结构(C 语言版)》严蔚敏 吴伟民

标签:匹配,space,next,算法,数组,KMP,ptr,PM
From: https://www.cnblogs.com/zerosister-blogs/p/18359282

相关文章

  • Python实现CNN(卷积神经网络)对象检测算法
    目录1.引言2.对象检测的基本原理2.1对象检测的目标2.2常见对象检测方法2.2.1基于滑动窗口的传统方法2.2.2基于区域提议的现代方法2.2.3单阶段检测器2.3本次实现的检测方法3.代码实现3.1环境准备3.2数据准备与预处理3.3构建CNN模型3......
  • 【机器学习算法】梯度提升决策树
    梯度提升决策树(GradientBoostingDecisionTrees,GBDT)是一种集成学习方法,它通过结合多个弱学习器(通常是决策树)来构建一个强大的预测模型。GBDT是目前最流行和最有效的机器学习算法之一,特别适用于回归和分类任务。它在许多实际应用中表现出色,包括金融风险控制、搜索排名、......
  • 代码随想录算法训练营 | 动态规划 part01
    509.斐波那契数509.斐波那契数状态转移方程:F(0)=0,F(1)=1F(n)=F(n-1)+F(n-2),其中n>1递归,太多重复计算classSolution{public:intfib(intn){if(n==0||n==1){returnn;}returnfib(n-1)......
  • 代码随想录算法训练营第一天 | 704. 二分查找,27. 移除元素
     704.二分查找题目链接:https://leetcode.cn/problems/binary-search/1,左闭右闭publicintsearch(int[]nums,inttarget){intlow=0;inthigh=nums.length-1;while(low<=high){intmid=(high+low)/2;if(num......
  • 【HarmonyOS Next】Unity(团结引擎)对接HarmonyOS
    一、环境UnityTuanjieV1.2.3DevEcoStudioNEXTDeveloperBeta1-BuildVersion:5.0.3.402二、官方资料参考Unity鸿蒙:https://docs.unity.cn/cn/tuanjiemanual/Manual/openharmony.htmlC#调用鸿蒙ArkTS:https://docs.unity.cn/cn/tuanjiemanual/Manual/openharmon......
  • 鸿蒙HarmonyOS NEXT:使用axios方法请求实时天气数据
    通过axios方法请求高德天气查询api,实现获取实时天气数据,接下来是实现步骤:模块导入与数据准备:通过以下语句导入了必要的模块和数据:importaxios,{AxiosResponse,AxiosError}from'@ohos/axios'//导入axiosimport{cities}from'./tools/citys';//调用事先存好的城......
  • 初始化一个Abpvnext项目
    文章目录一、安装ABPCLI安装更新二、使用CLI创建项目命令解析官网连接三、调整项目结构四、配置统一返回结果1.创建响应码枚举2.创建响应实体3.创建响应实体(泛型)五、配置并使用统一返回结果过滤器1.创建NoWrapperAttribute2.创建ResultWrapperFilter3.在HttpApiHost......
  • 深度学习梯度下降算法,链式法则,反向传播算法
    多层神经网络的学习能力比单层网络强得多。想要训练多层网络,需要更强大的学习算法。误差反向传播算法(BackPropagation)是其中最杰出的代表,它是目前最成功的神经网络学习算法。现实任务使用神经网络时,大多是在使用BP算法进行训练,值得指出的是BP算法不仅可用于多层前馈神经网......
  • 代码随想录算法训练营第43天:动态规划part10:子序列问题
    300.最长递增子序列力扣题目链接(opensnewwindow)给你一个整数数组nums,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]是数组[0,3,1,6,2,2,7]的子序列。示例1:输入:nums=[10,9,2......
  • 【MATLAB源码-第138期】基于matlab的D2D蜂窝通信仿真,对比启发式算法,最优化算法和随机
    操作环境:MATLAB2022a1、算法描述D2D蜂窝通信介绍D2D蜂窝通信允许在同一蜂窝网络覆盖区域内的终端设备直接相互通信,而无需数据经过基站或网络核心部分转发。这种通信模式具有几个显著优点:首先,它可以显著降低通信延迟,因为数据传输路径更短;其次,由于减少了基站的中转,可以提高......