ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

字符串算法吃透指南:从数组思维到双指针与KMP实战

字符串算法吃透指南:从数组思维到双指针与KMP实战 字符串这一块在算法面试里属于典型的“看着简单、一写就错”。代码随想录第八天把 LeetCode 上最经典的字符串题集中拉出来突破从反转字符串一路打到翻转单词、左旋转再铺垫 KMP。我刷完这一天的最大感受是字符串题考的根本不是字符串本身而是你对数组操作、双指针、边界条件的掌控力。这篇文章不打算复述题解而是把这一天的题目串起来讲讲背后的思维框架、每一道题的破题点以及那些题解里不会明说的坑。想把这个专题真正吃透的人这篇文章可以帮你少走不少弯路。1. 字符串题的核心认知它就是一个披着羊皮的数组1.1 为什么字符串题总让人措手不及很多人刷题初期都会有这种体验数组题做得很顺一到字符串就各种奇奇怪怪的问题。归根结底是因为字符串在底层就是字符数组它支持索引访问、支持遍历、支持切片但同时又叠加了语言层面的各种限制导致同样一套思路在不同语言里写出来的代码天差地别。比如 C 语言里字符串是char[]结尾带一个\0Java 里String是不可变的每次修改都要新建对象Python 的str同样不可变但切片操作s[::-1]写起来特别爽JavaScript 的字符串也是不可变类型想原地操作必须先split()转数组。初学者最容易栽的跟头就在这里思路想明白了但落笔时被语言特性卡住然后开始怀疑自己是不是不懂算法。实际上字符串题和数组题共享同一套底层思维双指针、滑动窗口、原地修改、前缀和、哈希表计数。代码随想录第八天安排的全是这些底层思维在字符串场景下的变种。你如果能把字符串当成“带了一点语言限制的数组”来看待题目的难度直接降一半。1.2 库函数用还是不用这是个问题刷字符串题时最多人纠结的就是库函数。reverse()一行就能反转字符串split()直接切分单词replace()一行替换空格那我还写什么算法这里我特别认同代码随想录里给出的判断标准当库函数本身就是这道题想考察的核心知识点时不要用当库函数只是辅助工具时可以用。反转字符串这道题核心考点就是“你知不知道如何原地交换两个字符”你用reverse()等于把答案抄了一遍这题就白刷了。而像“字符串转数字”这种题parseInt()或者int()只是辅助核心考点其实是边界判断和非法字符处理那库函数该用就用。判断方法就一句话这道题主要考什么相关的那一步就别用库函数。用这个方法去审视每一道题你就不会陷入“学了 C 的std::reverse就以为自己会反转字符串”的错觉里。2. 反转类题型一网打尽才是真本事2.1 344. 反转字符串双指针的入门课这道题放在第八天开头不是没有道理的。它要求原地反转一个字符数组空间复杂度 O(1)。最直接的思路就是双指针left指向开头right指向结尾交换两个位置的字符然后leftright--直到两个指针相遇。代码写起来非常简单Python 里甚至可以一行搞定def reverse_string(s): left, right 0, len(s) - 1 while left right: s[left], s[right] s[right], s[left] left 1 right - 1注意这里从左到右的交换是“先取值再赋值”没有任何中间变量的问题。有些同学会纠结要不要用异或操作来交换比如s[left] ^ s[right]这种我建议不要在新手阶段追求这种所谓的“炫技”。异或交换在面试中容易出错、可读性差而且现代编译器对临时变量的优化已经足够好老老实实用一个temp或者 Python 的多重赋值就完了。这道题的真正考点是在面试官追问时你能不能准确说出“为什么不能直接s s[::-1]”——因为那种写法会创建一个新数组空间复杂度是 O(n)不符合题目要求。这就回到了上一节说的库函数原则s[::-1]做的是“返回一个新字符串”而不是“原地修改”。2.2 541. 反转字符串 II模拟里的边界艺术这道题可以说是反转字符串的升级版也是很多人第一次被边界条件毒打的地方。题目要求每隔 2k 个字符反转前 k 个字符如果剩余字符少于 k 个则反转全部剩余字符如果剩余字符在 k 到 2k 之间仍然只反转前 k 个。第一次做这道题的人十个里有八个会把循环步长写成i k然后发现反转的区间完全乱套。正确的做法是循环步长必须是i 2 * k因为题目说的“每隔 2k 个字符”描述的是区间分布而不是单步推进。你把i想象成一个矩形扫描窗口窗口长度是2k每次窗口整体后移2k这才是题目本来的意思。反转区间的边界判断也有套路可循。代码随想录给的模板是def reverse_str(s: str, k: int) - str: s list(s) n len(s) for i in range(0, n, 2 * k): left i right min(i k - 1, n - 1) # 核心取较小值 while left right: s[left], s[right] s[right], s[left] left 1 right - 1 return .join(s)这里min(i k - 1, n - 1)一行的作用就是统一处理所有边界情况如果剩余字符不足 k 个右边界会超出数组直接取到末尾如果剩余字符足够 k 个就老老实实只反转前 k 个。用min之后分支判断直接消失代码变得非常干净。这种“用数学取最小/最大值代替分支判断”的思路在字符串题里出现频率极高值得单独记下来。我第一次做这道题时用了一堆if/else写边界结果错在两处一是把“剩余字符大于等于 k”和“小于 k”的讨论搞混二是忘了“剩余字符恰好等于 k”时应该反转全部。后来用min法重写一次过。这道题适合用来训练自己写“无分支边界处理”的能力。2.3 剑指 Offer 05. 替换空格从后往前填充的智慧这道题在 LeetCode 上已经把原数组改成了字符串用replace()一行搞定但代码随想录里仍然保留了经典的“从后往前填充”解法因为这是面试官最爱问的考点之一如果要求空间复杂度 O(1)在原数组上做替换你会怎么做思路是这样的先遍历一遍原数组统计空格数量把数组扩容到“替换后”的长度。然后设置两个指针p指向原数组末尾q指向扩容后数组末尾。从后往前遍历遇到普通字符就直接复制遇到空格就把%20依次填入q指向的位置。因为是从后往前填前面的字符移动不会覆盖还没处理的字符每个字符最多被移动一次时间复杂度 O(n)。举例说明假设原数组是[a, , b]长度 3。统计出 1 个空格扩容 2 格新长度 5。p指向索引 2字符bq指向索引 4。从后往前p指向b赋值给q然后p和q都往前移一位p指向空格依次填入0、2、%注意顺序是从后往前填所以先填0再填2再填%实际效果是%20p指向a赋值给q。最终数组变成[a, %, 2, 0, b]。这个“从后往前双指针”的技巧本质上是利用“后面的空间还没被占用”这一事实避免从前往后插入时发生的整体搬移。类似的题目还有合并两个有序数组也是要求原地、从后往前填代码随想录在数组章节就提到过。如果你能把这个技巧从数组迁移到字符串就说明你真正理解了它而不是背了一个模板。2.4 151. 翻转字符串里的单词三步走还是三步走这道题是反转类题型的集大成者。题目要求给定一个字符串逐个翻转单词且要把多余的空格去掉。比如the sky is blue变成blue is sky the。解法被我总结为“三步走”移除多余空格首尾空格 单词之间的连续空格只保留单词间一个空格对整个字符串进行反转对每个单词按空格切开的子串单独反转。之所以是三步而不是直接拆分拼接核心原因是空间复杂度的要求。如果你用split()切分再逆序拼接空间复杂度是 O(n)虽然思路简单但不符合很多面试官对“原地处理”的期待。三步走法可以在原字符数组上完成所有操作空间复杂度 O(1)。这里最难的是第一步——移除多余空格。它和数组章节里的“移除元素”是同一种思想用双指针一个slow记录新字符串的写入位置一个fast遍历原字符串。遇到非空格字符就把它复制到slow位置同时slow遇到空格时如果slow不在字符串开头且前一个字符不是空格就补一个空格否则跳过。遍历完一轮后还要处理末尾可能多出来的那个空格。def reverse_words(s: str) - str: # 第一步移除多余空格 chars list(s) slow 0 n len(chars) i 0 while i n: if chars[i] ! : if slow ! 0: chars[slow] slow 1 while i n and chars[i] ! : chars[slow] chars[i] slow 1 i 1 i 1 # 此时 chars[0:slow] 就是去掉多余空格后的字符串 # 第二步整体反转 chars chars[:slow] chars.reverse() # 第三步逐个单词反转 start 0 for end in range(len(chars) 1): if end len(chars) or chars[end] : chars[start:end] reversed(chars[start:end]) start end 1 return .join(chars)这里每一步单独拿出来都不难但合在一起就非常容易出错。我的调试经验是每一步都打印一次中间结果确认“移除空格”→“整体反转”→“单词反转”这三个结果都符合预期再往下走。很多时候你发现最终答案不对其实不是第三步的问题而是第一步就没处理干净导致单词反转时按错了切分点。2.5 剑指 Offer 58-II. 左旋转字符串反转三次的艺术这道题是“整体反转 局部反转”的另一个应用。题目要求把一个字符串的前 n 个字符移到末尾比如abcdefgn2结果应该是cdefgab。代码随想录给出的解法是先反转前 n 个字符再反转后面剩余部分最后整体反转。三步全部是反转操作def reverse_left_words(s: str, n: int) - str: chars list(s) # 第一步反转前 n 个 reverse(chars, 0, n - 1) # 第二步反转第 n 到末尾 reverse(chars, n, len(chars) - 1) # 第三步整体反转 reverse(chars, 0, len(chars) - 1) return .join(chars)这个算法为什么成立我们手动模拟一次原串abcdefgn2。反转前 2 个ba cdefg反转后面 5 个ba gfedc整体反转cdefg ab。结果是cdefgab和预期一致。核心原因是把一个串分成 AB 两段左旋就是把 A 和 B 对调位置。先局部反转 A 和 B得到 A B再整体反转就成了 (B) (A) B A。这个数学论证简单又优雅所以反转三次法一定正确。类似的右旋转字符串原理也一样只需要调整分段位置而已。把 2.1 到 2.5 放一起看你会发现反转字符串是基础反转字符串 II 是控制区间替换空格是反向填充翻转单词是三步组合左旋转字符串是反转组合的换皮。它们共用一套底层能力对“反转区间”的精确控制以及“整体与局部”的辩证关系。代码随想录把这些题放在同一天目的就是让你在连做五道反转题的过程中形成肌肉记忆。3. 字符串处理的卡点与细节语言、边界、复杂度3.1 边界条件字符串题最容易死的地方字符串题比数组题更容易出边界问题因为多了很多“空字符串”“只有一个字符”“刚好到末尾”的极端场景。我每次写字符串题会先在草稿纸上列几个用例空串、长度为 1、长度等于 k、长度等于 2k-1、长度等于 2k、长度等于 2k1。这一组用例覆盖了所有边界分支跑通它们基本就不会因为边界翻车。另一个很实用的技巧是统一区间的写法。写反转函数时我强烈建议统一使用左闭右开区间[left, right)这样区间长度就是right - left循环条件直接写while left right。很多同学一会儿用[left, right]一会儿用闭合区间的长度计算把自己绕晕了。统一用左闭右开配合python: s[left:right]的习惯出错率会显著降低。还有一个坑是索引的“差一”问题尤其在反转字符串 II 的right min(i k - 1, n - 1)里如果不小心写成i k就会多反转一个字符。遇到这种地方别硬背直接拿一个长度为 6 的字符串k2手动画一遍索引一次就能确认写没写对。3.2 不同语言下的实现差异会一门语言不代表会所有语言代码随想录专栏整体以 C 为主但它对每种语言都有一个“语言版本说明”。字符串题特别吃语言特性所以我建议至少要能看懂两种语言的写法面试时用最熟的那门复盘时用别的语言对照。C 语言里没有真正的字符串类型只有以\0结尾的字符数组。定义方式有char str[] hello;和char *str hello;两种前者可修改后者指向字符串常量、修改会崩溃。笔试时如果题目明确说“原地修改”记得用数组形式或者自己 malloc 一块新空间。Java 的String不可变所以“替换空格”这类题如果要求 O(1) 空间其实没法在String上直接做必须转成char[]或者StringBuilder再操作。很多 Java 写的题解里第一步都是char[] arr s.toCharArray();原因就在这里。Python 的str也不可变所以模拟反转时常用list(s)转数组循环体里通过索引修改最后.join(list)再转回来。这套“字符串→列表→操作→字符串”的模式在 Python 刷题中出现频率极高。JavaScript 则要先s.split()转数组操作完再join()。理解了底层为什么需要这些转换你再看任何语言的题解都不会觉得陌生。3.3 字符串匹配与 KMP第八天之后一定要补上的一课代码随想录的第八天通常以 KMP 算法作为收尾或者把 KMP 单独安排到后面几天。我个人建议第一遍刷的时候不要死磕 KMP先把 next 数组的构建流程跑通理解“最长相等前后缀”这个概念的物理意义再去做 28. 找出字符串中第一个匹配项的下标 和 459. 重复的子字符串。KMP 的核心价值在于它在匹配过程中不回溯主串的指针i而是通过 next 数组把模式串的指针j向前回退到合适的位置。这个“合适位置”就是当前已匹配部分的最长相等前后缀的长度。理解 next 数组的构建过程不要死记模板。我见过很多人能把代码默写出来但被问“为什么这里要j next[j - 1]”就答不上来这就是典型的“只背不悟”。字符串匹配的暴力解法时间复杂度是 O(n*m)KMP 是 O(nm)看起来提升明显但在实际工程里语言自带的正则引擎和 indexOf/hash 查找已经足够快。刷 KMP 的目的更多是训练一种“动态规划 回退”的思维以及应对面试中可能的追问。我常常把 KMP 比作迷宫里的绳索从某个岔路进去发现走不通不需要退回入口重走而是把绳索收回到最近一个仍然可能的分叉点继续探索。想明白这个比喻KMP 的大部分疑惑就消失了。4. 常见问题与排查实录刷字符串题踩过的坑4.1 反转顺序不对先检查区间区间再检查方向很多同学在做左旋转字符串这类题目时最终结果完全不对第一反应是“我的算法思路有问题”。但实际上80% 的情况是某个反转区间的起点或终点写错了。比如把“反转前 n 个”写成了“反转前 n-1 个”或者把左右边界传反了。我自己的排查步骤是拿一个长度最短的例子比如ab手动模拟每一步输出每步的反转结果对比中间结果和题解中的中间结果定位到具体是第几步出问题如果是区间边界问题打印left和right的值检查是否落在预期区间内。第四次做这种题时我甚至直接在代码里写了print(freverse chars[{left}:{right}])来辅助调试。这个方法看似笨但非常有效。4.2 修改不可变字符串报错先明确语言特性在 Python 里写s[0] a会直接报TypeError: str object does not support item assignment在 Java 里写str.charAt(0) a会编译错误。第一次遇到这些报错很多人会以为自己算法写错了折腾半天才发现是语言根本不允许原地改字符串。解决方案很简单在动手写代码之前先确认当前语言里字符串是否可变。如果不可变就先转换成字符数组或StringBuilder最后再转回字符串。这套转换也是最终代码的一部分别把它当“额外步骤”忽略掉。4.3 从工程视角看字符串搜索、排序与格式转换刷题之外字符串处理在平时的工程代码里也到处都是坑。比如在编辑器里按字符串搜索时如果目标串里面有换行符、Tab、制表符或正则保留字符直接搜文本模式和搜转义序列结果完全不一样。有些编辑器默认处理转义有些默认不处理不看清设置就会导致“明明有这个字符串搜索却找不到”。再比如字符串排序很多人想当然用默认的字典序排序结果发现数字10排在了2前面因为字典序是比较字符的 ASCII 码而不是数值大小。做版本号排序、IP 排序这类任务都需要先把字符串拆成数字字段再比较。这类工程经验在刷题中不会遇到但面试的“项目深挖”环节却经常被问到所以我把它们写在这里提醒大家别只盯着题解。4.4 字符串常见问题速查表故障表现常见原因排查与解法反转后顺序完全不对反转区间写错或区间开闭不统一统一用左闭右开[left, right)打印区间边界修改字符串报错语言中 String / str 不可变转到字符数组或 StringBuilder最后再转回反转字符串 II 的结果多反转或漏反转循环步长写成 k 而非 2k或右边界没取 min步长固定为 2k右边界用min(i k - 1, n - 1)翻转单词后首尾有多余空格移除多余空格步骤没处理干净单独调试“去空格”步骤检查末尾是否多补了空格字符串数字排序结果异常用了字典序而非数值比较先转数值再比较或手动按字段拆分搜索字符串找不到搜索模式未处理转义符/正则保留字符确认编辑器处于文本搜索模式还是正则模式5. 后续还能怎么挖字符串专题的进阶方向字符串是一棵大树第八天刷完反转家族只是把最粗的那根树干摸了一遍。后面更值得投入精力的方向还有几个。第一个是回文串。回文串几乎可以说是字符串题的“亲儿子”从最朴素的中心扩展法到进阶的马拉车算法再到动态规划判断回文子串每一年面试都会以各种变体出现。特别是“最长回文子串”这道题虽然暴力解法能过但面试官一定会追问“你能不能再优化”。代码随想录往后几天就会进入动态规划回文串正好是动态规划在字符串上的典型应用。第二个是子串与子序列问题。子串要求连续子序列不要求连续这两者在解法上的差异非常值得梳理。判断两个字符串是否互为字符重排、求最长公共子序列、编辑距离这些都是“字符串 动态规划”的高频题目也是后端工程师面试中的常客。第三个是字符串与数据结构的结合。比如用哈希表统计字符频率解决“找第一个只出现一次的字符”用前缀树解决大量字符串的字典查找用栈解析带嵌套层级的字符串表达式。这些题目的共同特点是字符串只是数据的载体真正考的还是数据结构的设计与选择。我个人的建议是第八天的题目刷完后先用 3 到 5 天把“移除元素、反转、替换、切分、拼接”这些基本功做成肌肉记忆然后再进入回文串和动态规划的专题。每一步都稳稳踩住后面进度会越来越快。最后分享一个我个人的小习惯每次做完一道字符串题不管对错都会用“三步回答法”给自己做一次复盘——第一步这道题考的核心知识点是什么第二步我哪一步的边界处理做得不好第三步如果换一种语言这道题的代码结构会发生什么变化。这个方法坚持下来字符串题对你来说就不会再是“玄学”而是像数组题一样有清晰的套路可循。
返回列表