字符串和KMP

保姆级字符串操作+KMP算法详解(含完整代码)

前言:字符串是编程中最常用的数据结构之一,而模式匹配(找子串)是字符串的核心操作,比如文本搜索、关键词匹配等场景都离不开它。本文从字符串基础操作入手,逐步讲解朴素模式匹配算法,再深入拆解KMP算法的核心逻辑、next数组的求法,全程带可直接运行的C语言代码,新手也能轻松看懂、上手练习。

本文结构清晰,循序渐进,建议收藏备用,重点掌握KMP算法的核心(next数组)和朴素算法与KMP的效率差异,避免面试/笔试踩坑。

一、字符串基础操作(必备铺垫)

在讲解模式匹配之前,先实现3个最基础的字符串操作,后续所有匹配算法都会用到这些工具函数,分别是:字符串比较(compareString)、求子串(getSubstring)、字符串定位(indexOf,后续模式匹配的核心目标)。

说明:本文以C语言为例(最贴近底层,易理解),字符串采用char数组存储,约定:字符串以'\0'结尾,长度不包含'\0'。

1.1 字符串比较(compareString)

功能:比较两个字符串s1和s2,返回值规则:

  • s1 == s2 → 返回0

  • s1 > s2 → 返回正数(差值)

  • s1 < s2 → 返回负数(差值)

核心逻辑:逐字符对比,直到遇到不同字符或字符串结尾,结尾判断长度差异。

cpp 复制代码
int CompareSrtring(String a, String b)
{
	int i = 1;
	int j = 1;
	while (i < a.length && j < b.length)
	{
		if (a.str[i] - b.str[j] > 0)
			return 1;
		if (a.str[i] - b.str[j] < 0)
			return -1;
	}
	return a.length - b.length;
}

1.2 求子串(getSubstring)

功能:从字符串s的第pos个位置(下标从0开始),截取长度为len的子串,存入sub中。

注意事项:判断pos和len的合法性(pos不能超出s的长度,pos+len不能超出s的长度),避免数组越界。

cpp 复制代码
bool SubString(String& sub,String S, int pos, int len)
{
	if (pos > S.length || pos < 1)
	{
		return false;
	}
	for (int i = 1; i <= len; ++i)
	{
		sub.str[i] = S.str[pos - i + 1];
	}
	return true;
}

1.3 字符串定位(indexOf)

功能:在主串s中,查找子串t第一次出现的起始下标,找到返回下标,找不到返回-1。

说明:字符串定位本质就是"模式匹配",这里先给出基础实现(后续朴素算法、KMP算法都是对这个功能的优化),核心依赖上面的compareString函数。

cpp 复制代码
int Index(String s, String t)//采用基本操作实现朴素模式匹配
{
	for (int i = 1; i <=s.length-t.length+1; ++i)
	{
		String tempt;
		InitString(tempt);
		SubString(tempt, s, i, t.length);
		if (CompareSrtring(tempt, t) == 0)
		{
			return i;
		}
		else
		{
			continue;
		}
	}
	return 0;
}

二、朴素模式匹配算法(暴力匹配)

上面的indexOf函数,本质就是朴素模式匹配的思路,只是用了子串截取和比较的工具函数,我们这里拆解其核心逻辑,写出更简洁、高效(减少函数调用开销)的朴素匹配算法。

2.1 算法核心思想

朴素匹配的核心是"暴力对比",步骤如下:

  1. 设主串s长度为n,子串t长度为m(n ≥ m);

  2. 主串指针i从0开始,子串指针j从0开始;

  3. 逐字符对比s[i]和t[j]:

    1. 若s[i] == t[j]:i++,j++,继续对比下一个字符;

    2. 若s[i] != t[j]:i回溯到i = i - j + 1(回到本次匹配的下一个起始位置),j重置为0,重新开始对比;

  4. 若j == m(子串全部匹配完成),返回i - j(子串在主串中的起始下标);

  5. 若i遍历完主串(i > n - m),j仍未达到m,返回-1(未找到)。

2.2 朴素模式匹配完整代码

cpp 复制代码
	String s,t;
	InitString(s);
	InitString(t);
	int i = 1;
	int j = 1;
	while (i <=s.length && j <= t.length)
	{
		if (s.str[i] == t.str[i])
		{
			i++;
			j++;
		}
		else
		{
			j = 1;
			i = i - j + 2;
		}
	}
	if (j > t.length)int pos = i - t.length;
	else int pos = 0;

2.3 朴素算法的优缺点

优点:逻辑简单、容易实现,适合子串和主串较短的场景,面试时快速写出朴素算法,能保证基础分。

缺点:效率低下,存在大量不必要的回溯。最坏情况下(比如主串为"aaaaa...a",子串为"aaab"),时间复杂度为O(n*m)(n为主串长度,m为子串长度),当n和m较大时,性能会急剧下降。

正是因为朴素算法的低效,才有了KMP算法------通过预处理子串,消除主串指针的回溯,将时间复杂度优化到O(n+m)。

三、KMP模式匹配算法(核心重点)

KMP算法的核心创新点:主串指针i从不回溯,只回溯子串指针j。而实现这一点的关键,就是预处理子串t,得到一个"部分匹配表"------也就是我们常说的next数组。

先记住:KMP算法的步骤 = 预处理子串得到next数组 + 利用next数组进行模式匹配。

3.1 先理解:为什么主串指针不用回溯?

朴素算法中,当s[i] != t[j]时,i会回溯到i-j+1,j重置为0,这是低效的根源。而KMP算法发现:

在匹配过程中,我们已经知道了主串s中i-j到i-1的字符(和子串t中0到j-1的字符完全匹配),可以利用这些"已知信息",直接确定子串j应该回溯到哪个位置,而不需要让主串i回溯。

举个例子:主串s=ababcabcacbab,子串t=abcac。

当匹配到s[6](b)和t[3](a)不相等时,朴素算法会让i回溯到3,j重置为0;而KMP算法会通过next数组,让j回溯到1,i继续保持6,直接对比s[6]和t[1],省去了大量无效的回溯操作。

3.2 核心:next数组(部分匹配表)的求法(重中之重)

next数组是针对子串t的预处理结果,next数组的长度等于子串t的长度,next[j]表示:子串t中,下标为j的字符之前(即t[0..j-1])的"最长相等前后缀"的长度。

先搞懂两个关键概念(必须吃透):

  • 前缀:子串中从开头开始的子串(不包含最后一个字符)。比如t=abcab,t[0..3](abca)的前缀有a、ab、abc;

  • 后缀:子串中从结尾开始的子串(不包含第一个字符)。比如t[0..3](abca)的后缀有a、ca、bca;

  • 最长相等前后缀:前缀和后缀中,长度最长且完全相等的子串。比如t[0..3]的最长相等前后缀是a,长度为1。

3.2.1 next数组的定义(明确规则)
  • next[0] = -1:子串下标为0的字符,前面没有任何字符,约定为-1;

  • next[1] = 0:子串下标为1的字符,前面只有1个字符(t[0]),没有前缀和后缀,最长相等前后缀长度为0;

  • 对于j ≥ 2,next[j] = 子串t[0..j-1]的最长相等前后缀长度。

3.2.2 手动求next数组(举例,快速理解)

举例:子串t=abcac(长度为5),手动求next[0]~next[4]。

  1. j=0:next[0] = -1(约定);

  2. j=1:t[0..0] = "a",无前后缀,next[1] = 0;

  3. j=2:t[0..1] = "ab",前缀[a],后缀[b],无相等前后缀,next[2] = 0;

  4. j=3:t[0..2] = "abc",前缀[a,ab],后缀[c,bc],无相等前后缀,next[3] = 0;

  5. j=4:t[0..3] = "abca",前缀[a,ab,abc],后缀[a,ca,bca],最长相等前后缀是"a",长度1,next[4] = 1;

最终next数组:[-1, 0, 0, 0, 1]

3.2.3 next数组的代码实现(迭代法,必背)

手动求next数组简单,但代码实现需要掌握迭代逻辑,核心思路:利用已有的next值,迭代求解下一个next[j],避免重复计算。

代码解释:迭代的核心是"回溯k"------当t[j] != t[k]时,k回溯到next[k],相当于"寻找更短的相等前后缀",避免重复对比,效率很高。

3.3 KMP模式匹配的完整代码(结合next数组)

有了next数组,KMP匹配的逻辑就很简单了,核心是:主串i不回溯,子串j根据next[j]回溯。

3.4 KMP算法的优化(可选,面试加分)

上面的next数组还存在一点优化空间:当t[j] == t[next[j]]时,j回溯到next[j]后,对比的还是相同的字符,会出现无效对比。因此可以对next数组进行优化,得到nextval数组(优化版的部分匹配表)。

优化思路:如果t[j] == t[next[j]],则nextval[j] = nextval[next[j]];否则,nextval[j] = next[j]

说明:nextval数组能进一步减少无效对比,效率比原始next数组更高,但核心逻辑和KMP算法一致,日常开发/笔试中,原始next数组已足够使用,优化版可作为拓展。

四、总结与对比(面试高频)

4.1 字符串基础操作总结

  • compareString:逐字符对比,判断字符串相等/大小,核心是处理"字符串结尾"和"字符差值";

  • getSubstring:截取子串,核心是判断参数合法性,避免数组越界,手动加'\0';

  • indexOf:字符串定位,本质是模式匹配的基础,依赖子串截取和比较。

4.2 朴素算法与KMP算法对比

算法 时间复杂度 空间复杂度 核心特点 适用场景
朴素模式匹配 O(n*m)(最坏) O(1)(无额外空间) 逻辑简单,主串指针回溯,低效 主串、子串较短,快速实现
KMP算法 O(n+m)(预处理O(m),匹配O(n)) O(m)(存储next数组) 主串指针不回溯,预处理next数组,高效 主串、子串较长,高性能场景

4.3 关键注意点(避坑)

  1. 字符串操作必须注意'\0'结尾,否则会出现乱码或数组越界;

  2. next数组是针对"子串"的预处理,与主串无关;

  3. KMP算法的核心是"主串指针不回溯",next数组的作用是确定子串指针的回溯位置;

  4. 面试时,先写出朴素算法,再讲KMP算法的优化点,最后写出next数组和KMP匹配的代码,会更加分。

结尾

本文从字符串基础操作入手,逐步拆解朴素模式匹配、KMP算法的核心逻辑,重点讲解了next数组的求法(手动+代码),所有代码均可直接复制运行,适合新手入门练习。

KMP算法是面试中的高频考点,建议多手动求几个next数组(比如t=abab、t=aaaaa),再反复调试代码,理解"主串不回溯"和"next数组回溯"的逻辑,就能彻底掌握。

如果觉得本文对你有帮助,欢迎点赞、收藏、转发,关注我,后续更新更多数据结构与算法干货!

评论区可以留言:你觉得KMP算法最难的部分是什么?一起交流学习~

相关推荐
mCell4 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell5 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
小高不会迪斯科5 小时前
CMU 15445学习心得(二) 内存管理及数据移动--数据库系统如何玩转内存
数据库·oracle
恋猫de小郭5 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清5 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
萧曵 丶5 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
银烛木5 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076605 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声5 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易5 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari