字符串和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算法最难的部分是什么?一起交流学习~

相关推荐
m0_6315298211 小时前
CSS如何利用CSS变量进行渐变色管理_提升渐变配置的灵活性
jvm·数据库·python
2301_8180084411 小时前
数据库模型设计实战:如何正向工程从模型建表_规范化项目开发流程
jvm·数据库·python
Hello--_--World12 小时前
Vue指令:v-if vs v-show、v-if 与 v-for 的优先级冲突、自定义指令
前端·javascript·vue.js
期待のcode12 小时前
Redis的数据清理机制
数据库·redis·缓存
神の愛12 小时前
ReactHooks
前端·javascript·react.js
蝎子莱莱爱打怪12 小时前
用好CC,事半功倍!Claude Code 命令大全,黄金命令推荐、多模型配置、实践指南、Hooks 和踩坑记录大全
前端·人工智能·后端
oradh12 小时前
Oracle数据库服务器端编程介绍
数据库·oracle·oracle基础·oracle数据库基础
本末倒置18312 小时前
VS Code 这次稳了!CSS Anchor Positioning 彻底终结 WebView 定位卡顿
前端
2401_8463395612 小时前
Vue 3 中集成 Three.js 场景的完整实现指南
jvm·数据库·python
MonkeyKing715512 小时前
Flutter状态管理实战:全局、局部、页面状态拆分指南
前端·flutter