【0x15字符串】浅谈KMP算法

本文介绍KMP算法,它能在线性时间内判定字符串A是否为字符串B的子串并求出出现位置。先阐述KMP思想与模板,包括next数组的概念、作用及实现过程,还给出代码。接着探讨KMP周期,即最小循环长度,通过例题说明思路和论证过程,并给出对应代码。

KMP算法,又称模式匹配算法,能够在线性时间内判定字符串 A[1~N] 是否为字符创 B[1~M] 的子串,并求出字符串 A 在字符串 B 中各次出现的位置。——《算法竞赛进阶指南》  

食用须知:

      *本文的字符串下标以 0 开始。字符串范围:字符串 A[0~N-1],字符串 B[0~M-1]。

      *以下概念来自个人理解,并不十分准确。慎! 

<KMP思想与模板>

      首先,O(NM)的朴素做法是:尝试枚举字符串 B 的每一个位置 i 。每确定一个位置 i ,枚举判断 A[0] 与 B[i] 、A[1] 与 B[i+1] ...... A[N-1] 与 B[i+N-1]是否一一相等(即称为"匹配"),如果相等,答案累加,i 指针往后移动;如果不相等,则 i 指针直接往后移动一位,直到不能再往后移动(i+N-1>=M-1)时,停止。

      KMP算法能够更高效、更准确地处理这个问题,并且,我们可以通过KMP算法获得其他一些信息。

      这里,给出几个 概念

  • 前缀:字符串 A 的前缀即为字符串 A 中区间 0~0、0~1 ...... 0~N-1 所构成的子串。
  • 后缀:与前缀相仿,字符串 A 的后缀即为字符串 A 中区间 N-1~N-1、N-2~N-1 ...... 0~N-1 所构成的子串。

   举个栗子:

A          "abcab"

前缀

"a","ab","abc","abca","abcab"
后缀"b","ab","cab","bcab","abcab"
  • next 数组:next[i] 表示 " A 中以 i 结尾的非前缀 " 与 " A 的前缀 " 能够匹配的最长长度。(以 i 结尾的非前缀:即字符串1~i的后缀。在以上的例子中,当 A="abcab",i=N-1 时,以 i 结尾的非前缀即为:"b","ab","cab","bcab");换一种说法,对于字符串 A 的前 i 个字符构成的子串,next[i] 存储的是既是它的后缀又是它的前缀的字符串中的最大长度(本身除外)。
  • 关于 next 数组的作用:实现 j 从大到小依次遍历 A 串中可能能够与当前 i 位置的字符匹配的 A[j] (由于 next 数组的定义,保证从 i 开始的前 j个字符已经与 1~j 个字符已匹配好了,只需比较的 A[i] 与更改后的 A[j] 即可,这样大大减少了逐一判断所需要的时间)。 

   举个栗子:

A"abcab"
i = 0next[0]=0
i = 1next[1]=0

i = 2 

next[2]=0
i = 3 next[3]=1( "a"与"a" )
i = 4 next[4]=2("ab"与“ab")

      KMP算法分为 两步 

  • 对于字符串 A 进行自我"匹配":求出一个数组 next 。   
  • 将字符串 A 与 B 进行与第一步类似的"匹配"操作。

      字符串 A 的自我"匹配"(即 next 数组的实现过程):

      我们将字符串 A 复制成两个相同的字符串 A 和 A' (*分成两个相同的字符串仅为了方便比较)。i 指针指向在字符串 A 中所匹配到的位置,j 指向在字符串 A' 中所匹配到的位置(即时,所要求的是next[i])。如图:

      当 A[i]==A'[j] (相当于能够继续"匹配"),next[i]=++j 。

      当 A[i]!=A'[j] ,j=next[j-1](查找前面连续的能够尽量匹配的数),直到 A[i]==A[j] 或 j==0 时停止。

      再给出一个例子:

      开始时,next[0]=0

      当 i = 1,j =0 时(*因为是" 非前缀 " 与 " 前缀 " 的匹配,所以 "a" 不能与 "a" 匹配),j=next[0]=0,由此,next[1]=0

      当 i = 2,j = 0 时,A[i]!=A'[j] ,  j=next[1]=0,由此,next[2]=0

      当 i = 3 ,j = 0 时,A[i]==A'[j] , next[3]=++j=1

  

      当 i = 4,j = 1 时,A[i]==A'[j],next[4]=++j=2

      当 i = 5,j = 2 时 ,A[i]!=A[j],j=next[4]=2,这时,依旧 A[i]!=A[j],j=next[1]=0

       ......如此类推,代码实现如下(A 与 A' 不再分开计算):


	int j=0; nxt[0]=0;
	for(int i=1;i<A.size();i++){
		for(;A[i]!=A[j]&&j;) j=nxt[j-1];
		if(A[i]==A[j]) j++;
		nxt[i]=j;
	}

      字符串 A 与 字符串 B 匹配:

      思路类似,不再详述。代码实现如下:


	j=0;
	for(int i=0;i<B.size();i++){
		for(;B[i]!=A[j]&&j;) j=nxt[j-1];
		if(B[i]==A[j]) j++;
		if(j>=A.size()) ans++;//ans统计B中含多少个A子串
	}

      线性复杂度玄学证明(by hzk)

      对于外层循环,复杂度为O(N)。

      对于内层循环,只有 j > 0 时才进行,而 j 又只加 N 次 。

      ∴ 很线性

      如果没听懂,就多抄代码。——by hzk

 <KMP周期(最小循环长度)>

      这样的题目很多,例如


      给你一个字符串,它是由某个字符串不断自我连接形成的。

      但是这个字符串是不确定的,现在只想知道它的最短长度是多少。

      样例输入: cabcabca

      样例输出:3

      样例解释:可由 "cab" 不断自我连接得到 "cabcabcab","cabcabca" 是它的子串。


      给出的读入是由循环节不断自我连接得到的字符串的子串,与直接的求最短循环节相等。思路:KMP模板,答案为 N-A[N-1](1 开始则为:N-A[N])。

      论证:如果存在循环节,字符串 0~i 的最短循环长度为 i-next[i]。

      伪证:

      当next[i]=0时,0~i 整个字符串为循环节,结论显然(by hzk)成立.

      当next[i]>0时,必然会出现这样的情况:

       (图片出处:https://blog.youkuaiyun.com/hzk_cpp/article/details/87895335 %% by hzk)

      黑色的表示字符串A,红色为最大匹配(也就是说两条线段长度为 next[i])。最末行的两条蓝色线段相等,由匹配的结果可得,每上下覆盖的两条线段都相等,那么显然(by hzk)在蓝色的线段都必须相等。

      当next[i]%(i-next[i])!=0,即表示红色线段不能被不重叠的蓝色线段完全覆盖,那么就不能构成完整的循环节。否则,循环节就为(i-next[i])。以上给出的例题则不需要判断,因为不需要构成完整的循环节。

      对于例题,代码如下:

    cin>>s;
    int N=s.size();
	int j=0;
	for(int i=1;i<N;i++){
		for(;s[i]!=s[j]&&j;) j=nxt[j-1];
		if(s[i]==s[j]) j++;
		nxt[i]=j;
	}
	printf("%d",N-nxt[N-1]);
	return 0;

 

<think>嗯,用户问的是测试岗位为什么经常考字符串处理类算法。首先,我得回忆一下测试岗位的职责,他们需要处理各种输入验证、数据解析之类的任务,而字符串处理正是这些任务的基础。比如,验证用户输入的邮箱格式是否正确,或者解析日志文件中的特定信息,这些都需要字符串操作。 然后,可能涉及到自动化测试脚本的编写。测试工程师经常需要写脚本来自动化测试流程,比如用Python处理字符串来生成测试用例或者检查输出结果是否符合预期。这时候,字符串分割、替换、正则表达式匹配就显得很重要了。 另外,字符串处理算法在测试中应用广泛,比如分割字符串、查找子串、替换特定字符等。比如在引用5提到的例子中,字符串转列表就需要替换和分割操作,这可能是测试中处理数据的一种常见需求。还有像引用3中的字符串解压算法,测试时可能需要验证解压后的字符串是否正确,这也涉及到字符串处理。 再者,测试岗位需要处理各种边界条件和异常情况,字符串处理算法能很好地覆盖这些情况。例如,空字符串、超长字符串、特殊字符等,这些都需要测试用例来覆盖,而编写这些测试用例本身就需要对字符串处理有深入的理解。 还有,字符串处理能力也反映了编程基本功。像引用4提到的华为机试备考,常考算法类型中可能包含字符串处理,因为这是基础中的基础。能高效处理字符串问题,说明候选人具备良好的逻辑思维和代码实现能力。 另外,在测试中,日志分析和数据验证也是常见的任务。比如分析服务器日志,提取错误信息或统计特定事件发生的次数,这些都需要字符串匹配和解析的能力。正则表达式在这里就特别有用,比如引用2提到的模式匹配可能涉及到KMP算法,虽然用户可能不需要自己实现,但理解其原理有助于编写更高效的测试代码。 还有,测试框架和工具中经常需要处理字符串。比如生成测试报告、格式化输出信息,或者与API交互时处理JSON/XML数据,这些都离不开字符串操作。例如,解析JSON响应中的某个字段是否正确,可能需要用到字符串分割或正则表达式提取。 最后,字符串处理问题在面试中容易设计,可以考察候选人的多方面能力。比如,分割回文串(引用1提到的动态规划和回溯)不仅考察算法,还考察对问题分解的能力,这在测试中设计测试用例时也是需要的。 综合这些点,测试岗位考察字符串处理算法主要是因为其在实际工作中的高频应用,以及它能够有效评估候选人的编程能力和逻辑思维。需要把这些点有条理地组织起来,用中文清晰回答,并加入相关的引用支持。</think>测试岗位常考字符串处理类算法主要有以下原因: ### 一、业务场景高频应用 1. **输入验证与数据清洗** 测试工程师需要处理表单验证(如邮箱格式$[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-z]+$)、日志解析(如提取时间戳$2024-05-01T12:00:00$)等任务,依赖正则表达式匹配和字符串分割技术[^5]。 2. **API测试与数据转换** JSON/XML等结构化数据的解析需要字符串操作,例如通过正则提取字段值或处理转义字符(如$\text{\"key\":\"value\"}$)[^3]。 3. **测试用例生成** 动态生成测试数据时,常需拼接字符串(如$test\_case\_\${id}.txt$)或处理模板替换[^1]。 ### 二、算法能力考察维度 1. **边界条件处理** 空字符串、超长字符串(如长度$10^6$)、特殊字符(如$\unicode{0x2603}$)等边界场景的覆盖能力,体现测试思维的严谨性[^4]。 2. **时间复杂度优化** 例如在KMP算法中通过预处理模式串构建部分匹配表$PMT$,将匹配复杂度从$O(mn)$优化到$O(m+n)$[^2]。 3. **问题拆解能力** 复杂问题如「分割回文串」需结合动态规划(预计算子串状态$dp[i][j]$)与回溯算法,考察逻辑分层能力[^1]。 ### 三、岗位技能映射 ```python # 示例:测试中常用的字符串分割代码(引用自实际场景) def parse_log(log_str): # 处理带分隔符的日志"ERROR;2024-05-01;server1" cleaned = log_str.replace(';', ',').split(',') return {'level': cleaned[0], 'time': cleaned[1], 'source': cleaned[2]} ``` ### 四、扩展能力评估 1. **正则表达式引擎原理** 测试框架中的断言(如`assert_match(regex, output)`)依赖对正则引擎的理解,例如NFA/DFA的实现差异。 2. **压缩与编码算法** 类似华为OD机试中的字符串解压问题(如$3[a]2[bc]$→$aaabcbc$),需要栈操作与字符串拼接[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值