ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

字符串处理全攻略:从双指针到KMP的刷题与工程实践

2026/10/2 16:45:05 拓冰建站 浏览量
字符串处理全攻略:从双指针到KMP的刷题与工程实践 “代码随想录”的字符串章节我前前后后刷了三遍。第一遍觉得简单第二遍发现真正难的其实是“指针移动的时机”和“边界条件”第三遍才摸到整套题的骨架大部分字符串题表面上是字符串实际上要么是双指针问题要么是匹配问题要么纯粹是语言细节问题。尤其现在刷题网站和面试题里字符串逆序、判断子串、回文串、字符串分割几乎成了标配。这篇就把我刷字符串题的经验、踩过的坑以及平时在项目里处理字符串的那些真实需求一起整理出来给你一条可以直接照着走的路线。这篇文章适合三类人准备面试、正在按《代码随想录》刷题的刚学完基础语法、想搞清楚字符串到底有多少种考法的以及工作中经常被字符串处理卡住、想系统补一课的。我会把原理、代码、易错点揉在一起讲尽量说人话。1. 字符串题为什么值得专门刷一遍1.1 字符串是数据结构题的“包装纸”很多人一开始不重视字符串题觉得不就是一堆字符摆在那里吗实际刷下来你会发现字符串题几乎能把所有基础算法都包装一遍双指针、哈希表、滑动窗口、动态规划、回溯、KMP全都能在字符串上做文章。字符串只是载体真正考的是你“能不能把一个连续字符序列当成一个可操作的线性结构”。比如说“反转字符串”本质就是在数组上做首尾交换“判断回文串”本质是双指针往中间走“无重复字符的最长子串”本质是滑动窗口“分割回文串”本质是回溯。如果你只把字符串当成“一串字符”而不是“一个数组”很多题的解法根本想不出来。这也是《代码随想录》把字符串单独列一章的原因。它不是在教你怎么背 API而是教你“当看到字符串题时第一反应应该是往什么数据结构上靠”。我刷完这章的体会是字符串题是训练“识别算法模型”能力最好的素材因为它的包装层很薄核心逻辑露出来一半另一半藏在边界条件里。1.2 《代码随想录》字符串章节的刷题顺序建议我自己的刷题顺序是这样安排的也和这套题的目的比较匹配先做“反转字符串”系列理解双指针在字符串上的基本操作再做“替换空格”练习从后往前填充的思路然后做“翻转字符串里的单词”综合了去除空格、整体反转、局部反转三步操作接着做“左旋转字符串”体会局部反转加整体反转的通用技巧最后做 KMP 相关的匹配题剩下的时间全部用来反复练 next 数组。这个顺序是层层递进的。前面几题让你熟悉“改字符”“移动指针”后面综合题考的是“多种操作组合不乱”。KMP 放到最后是因为它需要前面建立的“字符比较”直觉直接上手容易劝退。有一个我自己的经验字符串题不要只追求把答案写出来要追求“能不能用双指针原地操作”。很多题如果用库函数一行就搞定了比如s reversed(s)那你就完全没练到点上。刷题阶段库函数是给项目用的不是给你偷懒用的这一点后面细说。2. 反转类题目双指针和“三步翻转法”2.1 反转字符串从左右交换说起反转字符串是最基础的一题但很多人写的时候还是会翻车。题目要求原地修改不能开额外数组。思路就一个左指针从 0 开始右指针从len-1开始两边字符交换然后向中间靠拢直到左右指针相遇。C 的写法长这样void reverseString(vectorchar s) { int left 0, right s.size() - 1; while (left right) { swap(s[left], s[right]); left; right--; } }Python 因为字符串不可变一般用list(s)转成数组再操作最后.join()转回来def reverse_string(s): s list(s) left, right 0, len(s) - 1 while left right: s[left], s[right] s[right], s[left] left 1 right - 1 return .join(s)这题刷完你要建立的直觉是只要是对称交换类操作优先想双指针。后面很多看起来花里胡哨的题比如判断回文串、倒置字符串都是这个基本操作的变形。热词里那个“字符串逆序输出c”就对应着 C 语言里更容易出错的一种版本字符数组操作。C 的字符串本质上是一个以\0结尾的char数组strlen返回的长度不包含结束符交换字符时千万不要动最后一个\0。如果直接拿char*指针从头扫到尾很容易把结束符也交换到头部去输出直接乱掉。正确做法是先把右指针定位到strlen(s) - 1再开始交换。2.2 左旋转字符串局部反转加整体反转这题是《代码随想录》里的老朋友了。给定一个字符串s和一个整数k把字符串前k个字符移到末尾。比如s abcdefgk 2结果是cdefgab。最直接的做法是拼接s.substr(k) s.substr(0, k)但这又是在用库函数取巧。真正想练算法要用“三步翻转法”反转前k个字符反转k到末尾的字符反转整个字符串。为什么这样能成立可以拿数学上的“逆序”性质来理解对一个序列先部分反转、再整体反转等价于把两段顺序调换。这个技巧在很多字符串旋转题里都能复用包括“翻转字符串里的单词”。C 代码string reverseLeftWords(string s, int k) { int n s.size(); k % n; // 防止 k 大于 n reverse(s.begin(), s.begin() k); reverse(s.begin() k, s.end()); reverse(s.begin(), s.end()); return s; }这里的k % n就是典型的边界处理。很多测试用例会故意给k n的情况不做取模直接反转就会越界或者结果错误。这种细节就是刷题和做项目的本质区别。2.3 按单词反转先整体后局部“翻转字符串里的单词”是这一节里综合度最高的题。题目要求把字符串里的单词顺序反转同时把多余空格去掉。比如 hello world! 变成world! hello。我一开始做这题时直接用了split大法Python 里就是 .join(s.split()[::-1])一行完事。但《代码随想录》这类题的训练目标是双指针所以我后来重新用双指针写了一遍流程分三步去除多余空格包括首尾空格、单词间的多个空格整体反转字符串对每个单词再做一次反转。第三步是精髓整体反转后单词顺序反了但单词内部的字母也反了。于是再对每个单词做一次局部反转单词内部就恢复正常而单词顺序保持反序恰好得到正确答案。这题的易错点几乎都集中在第一步。比如去除空格时要保证“单词之间只留一个空格”并且开头和结尾不能有空格。很多人的写法在最后一个单词后面多插一个空格结果整体反转后开头多了一个空格测试用例直接红。3. 匹配与子串KMP 手动实现一次就够3.1 什么时候该手写 KMP字符串匹配在生活中太常见了java 判断字符串中是否包含某个字符串、oracle判断字符串是否包含某个字符串、js判断字符串是否包含这些热词说明大家天天都在做包含判断。但刷题场景不一样面试官问 KMP不是为了让你在生产环境里手写一遍而是想确认你理解“如何避免重复比较”。朴素匹配的写法是双重循环主串从每个位置开始逐个字符和模式串比较。最坏情况下复杂度是 O(n*m)比如主串是aaaaaab模式串是aaab每次都要比到最后一个字符才失败然后再从头开始非常浪费。KMP 的核心思想是匹配失败时主串指针不回溯只让模式串指针向前跳到某个位置继续比。这个“跳”的依据来自 next 数组也叫前缀表它记录了模式串每个位置之前的最长相等前后缀长度。我的建议是刷题阶段至少手写一次 KMP但不一定每道匹配题都用它。很多题用哈希表或者库函数就够KMP 的价值更多体现在“让你理解匹配的本质”。3.2 next 数组的构建和常见翻车点next 数组是 KMP 最容易崩的地方。《代码随想录》推荐的构建方式是使用前缀表同时用“回退”的思路来求。以 C 为例vectorint buildNext(const string needle) { int n needle.size(); vectorint next(n, 0); int j 0; for (int i 1; i n; i) { while (j 0 needle[i] ! needle[j]) { j next[j - 1]; } if (needle[i] needle[j]) { j; } next[i] j; } return next; }这里容易犯两个错误。第一j回退时写成了j--。正确的回退是j next[j - 1]因为当前的j位置匹配不上要跳到前面“最长的相等前后缀”位置再比。写成j--只是在退一步完全没有利用前缀表的信息而且可能陷入死循环。第二新建 next 数组时初始长度不为 0。习惯上先把第一个元素设为 0因为第一个字符没有真正意义上的“前后缀”。如果让next[0] -1也能写出一版 KMP但逻辑会更绕我不推荐新手一开始就搞两种流派认准一种写熟就好。3.3 判断包含与查找子串的工程实现至于工程上的“包含判断”就没有必要手写 KMP 了。各语言的实现都很成熟关键是选对方法。这里我把常见的几个场景整理成一张表语言/场景常用写法备注Java 判断包含str.contains(sub)/str.indexOf(sub) ! -1contains 内部就是 indexOfPython 判断包含sub in str/str.find(sub) ! -1推荐用in可读性好JavaScriptstr.includes(sub)/str.indexOf(sub) ! -1includes 返回布尔值C 语言strstr(s, sub) ! NULL返回指针位置Oracle SQLINSTR(str, sub) 0/LIKE %sub%INSTR 性能更可控SQL ServerCHARINDEX(sub, str) 0与 Oracle 的 INSTR 类似C#str.Contains(sub)/str.IndexOf(sub) 0注意大小写可加 StringComparison这张表是我平时写代码时经常翻的清单。尤其要注意 SQL 和编程语言在“大小写是否敏感”上的差异Oracle 默认不区分大小写但 MySQL 的默认排序规则又可能区分这就是典型的“看起来一样的操作换个环境结果完全不同”。4. 多语言字符串细节对照刷题之外的真实需求4.1 C/C字符数组、指针和结束符C 语言字符串的坑从热词里就能看出来“cstring字符串结束符”“codeblock 字符串 宽字符l 表示 出错”“指针数组存放字符串”“c语言字符串函数”。说实话大学里 C 语言的字符串题一半的 Bug 都出在结束符和指针上。字符串字面量在 C 里是只读的存在只读数据段如果你试图通过指针修改它行为是未定义的。常见做法是用char s[] hello这样的字符数组这样内容存在栈上可以修改。char* p hello和这个问题完全不是一回事。strlen不计结尾的\0而sizeof计。很多人用strlen当数组长度去复制结果拷贝字符串时少复制了一个结束符后面的输出就出现乱码。写字符数组初始化时最安全的习惯是char s[32] {0};这样即使你操作失误字符串也能安全结束不会一路读到未知内存。另外“宽字符”或者中文环境下听说有人用wchar_t或者L...表示宽字符串。这里的问题在于一个宽字符可能占 2 个或 4 个字节但strlen是按字节数的不能直接用来处理宽字符。如果你做的是跨平台界面开发比如 Qt 里经常要在QString和std::string之间转换就得先搞清楚编码是 UTF-8、UTF-16 还是本地编码否则中文一传就是乱码。4.2 Java、Python不可变字符串的注意事项Java 和 Python 的字符串都是不可变对象。这意味着你不能直接修改字符串内部的某个字符。热词“python字符串直接赋值更改”说的就是这个问题。Python 里这样写会直接报TypeErrors hello s[0] H正确做法是重新生成字符串s hello s H s[1:]如果你要反复修改就老老实实转成list再操作最后.join()回来。Java 也有类似情况String不可变所以拼接大量字符串要优先用StringBuilder。还有一个常见的困惑是字符串比较Java 里比较的是引用地址不是内容内容比较一定要用equals()。而 Python、C#、C 的string用比较内容。很多人从 Java 转 Python 后会怀疑“怎么 Python 的 能直接比较字符串了”这就是语言细节的差异。常用操作对照// Java String[] arr a,b,c.split(,); String joined String.join(,, arr); boolean isDigit Character.isDigit(1); boolean isLetterOrDigit Character.isLetterOrDigit(a);# Python arr a,b,c.split(,) joined ,.join(arr) is_digit 1.isdigit() is_alnum a1.isalnum()4.3 数据库、脚本和嵌入式场景中的字符串处理刷题之外字符串问题在工作中非常高频热词里能看到大量这种真实需求。SQL Server 里把字符串转数字常见写法是CAST(123 AS INT)或者CONVERT(INT, 123)。但如果字符串里混有非数字字符比如123abc直接转换会报错这时候要用TRY_CAST或者TRY_CONVERT它转换失败时返回 NULL而不是中断运行。Oracle 判断字符串是否包含某个字符串可以用INSTR也可以用LIKE。区别在于LIKE通常配合通配符适合模糊匹配INSTR适合判断“是否出现”以及获取出现位置。数据量大的时候建议先确认字段是否走索引否则LIKE %xxx%这种写法可能会全表扫描。C# 里字符串转 ASCII 码要用Encoding.ASCII.GetBytes(str)拿到的byte[]里每个元素就是对应字符的 ASCII 值。而“把字符串基于指定字符成数组”对应的方法是string[] parts a,b,c.Split(,);注意Split的默认行为在不同重载下有细微差别如果字符串里连续出现两个分隔符默认会生成一个空字符串元素需要的时候可以加StringSplitOptions.RemoveEmptyEntries。Qt 里double转字符串用QString::number(value, f, 2)可以指定保留两位小数。直接转的话默认精度可能输出一长串有效数字反而不符合需求。Vim 里查找字符串就是/关键词回车按n向下继续查找按N向上回退。这个太基础了但很多人确实第一次用 Vim 时会卡住。安卓开发里要判断字符串包含某个字和普通 Java 一样用contains()或者indexOf()但要注意contains是区分大小写的。如果要做不区分大小写的包含判断可以用toLowerCase()统一转小写后再比。工业触摸屏脚本比如昆仑通态的脚本里要让字符串换行不同型号的脚本引擎处理方式不一样有的支持\n有的需要\r\n。最稳妥的做法是先查对应脚本手册里的“字符串换行”章节或者在脚本里拼一个换行符常量赋值给显示控件。这类环境没有标准答案只能靠查手册和试错。“枚举类型转换为字符串”这个需求也很常见。C# 里直接enumValue.ToString()就能得到枚举名也可以用Enum.GetName(typeof(EnumType), value)。Java 里枚举有内置的name()方法。不要把枚举的toString()和展示文案混在一起业务上显示“已完成”这种建议单独写映射表而不是靠枚举名硬拼。“ODBC连接字符串”也是字符串处理的重灾区。连接字符串本质是一个带分号分隔的配置文本比如Driver{SQL Server};ServermyServer;DatabasemyDb;UidmyUser;PwdmyPass;这里最容易出问题的是密码或数据库名本身包含分号、大括号、空格导致解析错误。常规做法是把敏感信息放到配置中心或者环境变量里不要在代码里硬编码同时连接字符串的拼装要使用专门的构建类而不是手写字符串拼接。5. 刷字符串题最容易踩的 5 个坑5.1 库函数能救你也能毁了你很多题用库函数确实几行搞定比如反转、分割、去空格。但面试的时候如果你一上来就split、reverse面试官大概率会追问如果不让你用这些 API你怎么实现这不是为了刁难你而是想看你对底层逻辑的理解。我的做法是刷题时先想“如果用最底层的字符操作怎么做”写完后再对比库函数写法总结两者差异。这样既练了逻辑也熟悉了 API。工作中我绝对支持你多用库函数那是效率问题刷题时要少用那是训练问题。5.2 边界条件和结束符字符串题跑挂十有八九是边界问题。空字符串、单字符字符串、全是空格、目标字符在开头或结尾、k大于字符串长度这些用例一定要自己先在心里跑一遍。C 系语言则要格外注意结束符。strlen返回长度不包含\0但在构造字符串时要自己补sprintf、strcpy这类函数会自动补结束符而一些自定义的字符操作不会。我见过太多人用memcpy拷贝字符串后不补\0最后输出乱炖字符。5.3 字母数字判断需求要问清楚热词“java 判断字符串中是否不是字母和数字”看起来很简单但仔细想这里的需求可能是“判断字符串中是否存在非字母数字字符”也可能是“判断字符串是否全部由字母数字组成”还可能是“判断是否一个字母/数字都没有”。三句话三个含义。Java 里常用Character.isLetterOrDigit(char)配合遍历。C 语言里就是isalnum()。Python 里是s.isalnum()但它检查的是“整个字符串是否全部由字母数字组成”不是“是否存在至少一个”。所以遇到这类需求第一步不是写代码而是确认判断标准是针对单个字符还是整个字符串是否允许空格下划线算不算大小写是否敏感这些细节没问清楚写出来的代码大概率要返工。5.4 中文、编码与字符串长度“字符串长度”“汉字算一个字符英文字母和数字两个算一个字符”这些说法其实是业务里非常典型的统计需求。但注意这里的“一个字符”在不同编码下含义完全不同。Java 里的String.length()返回的是 UTF-16 编码下的代码单元数量。一个汉字在 UTF-16 里通常占 1 个代码单元但生僻字或者 emoji 可能占 2 个如果你用length()去限制用户输入很可能会误判。按字节数统计要用getBytes(UTF-8).length中文在 UTF-8 下通常占 3 个字节。业务上“汉字算一个字母数字算半个或者两个”这种需求本质是一种显示宽度的估算不是真正的字符长度。实现时一般要自己写统计函数根据字符的 Unicode 码位范围加权计算。这里最容易犯的错是直接拿String.length()当显示宽度用结果输入法输入一串英文时布局和中文对不齐。做题时如果题目没说字符集默认按 ASCII 或者纯小写字母处理即可一旦涉及中文字符先确认编码再动手。5.5 不可变字符串的赋值陷阱Java 和 Python 里字符串是不可变的直接给某个位置赋值会报错或无效。很多人刚开始刷题时用 Python 写反转习惯性写s[i] s[j]结果提示TypeError这就是没转过弯来。处理方式前面也提过要么转成列表要么用切片重新创建字符串。还有一点Java 里大量使用拼接字符串底层会不断创建新对象在循环里尤其明显。刷题时无所谓但项目里如果循环几千次拼接一定要换成StringBuilder。6. 一道综合题完整复盘去除空格并反转单词6.1 题目设定与解题思路前面讲了那么多套路这里我用一道综合题完整走一遍流程题目就是前面提到的“翻转字符串里的单词”。限定条件输入字符串可能包含首尾空格、单词之间多个空格输出要求单词之间只保留一个空格要求原地操作不开额外数组C 的 string 可以原地修改。这道题把双指针、整体反转、局部反转全考了一遍非常适合作为字符串章节的收尾复盘。6.2 代码实现与关键步骤说明完整代码我整理成了这样string reverseWords(string s) { // 第一步去除多余空格 int slow 0; for (int i 0; i s.size(); i) { if (s[i] ! ) { if (slow ! 0) { s[slow] ; } while (i s.size() s[i] ! ) { s[slow] s[i]; } } } s.resize(slow); // 第二步整体反转 reverse(s.begin(), s.end()); // 第三步逐个单词局部反转 int start 0; for (int i 0; i s.size(); i) { if (i s.size() || s[i] ) { reverse(s.begin() start, s.begin() i); start i 1; } } return s; }第一步里的slow指针是核心。它记录了“有效字符应该放到的位置”i指针负责向前扫描。遇到非空格字符时如果slow不是 0说明这不是第一个单词要在前面补一个空格然后一口气把当前单词的所有字符搬过来。这样连续多个空格会自动被压缩成单词之间的一个空格首尾空格也不会出现。第二步整体反转后字符串变成“单词逆序每个单词字母逆序”。第三步遍历整个字符串遇到空格就说明一个单词结束了把这个单词区间反转回来。注意循环条件是i s.size()因为最后一个单词后面没有空格需要靠i s.size()来触发反转。很多人就是这里写成了导致最后一个单词永远没有反转。6.3 测试用例与易错点回顾这道题我实际跑过的测试用例有这些输入预期输出注意点the sky is blueblue is sky the标准情况 hello world world hello去除首尾空格a good exampleexample good a连续多个空格aa单字符空字符串 全空格每个用例我都建议实际敲一遍不要只靠 IDE 跑。特别是“全空格”输入如果第一步里没有处理好resize之后可能留下一个或多个空格输出就不是空串。还有一个经验写完这题之后把“去除多余空格”这一步单独抽出来写成一个小函数。后面做“按单词长度反转”“整理 SQL 字段”这类需求时你都能直接复用。字符串题的很多能力不是靠背题而是靠这种“把高频操作沉淀成函数”的迁移意识。这段时间刷下来我个人体会最深的一点是字符串题切忌“一看就会一写就废”。它不像图论题那样需要很复杂的算法设计但非常考验代码的边界处理能力和细心程度。把《代码随想录》字符串章节的题按“反转、替换、匹配、综合”四类各练几遍每种题型沉淀出一套自己的标准写法之后再遇到五花八门的字符串题你会发现自己已经不太需要看题解了。