ARTICLE DETAIL

资讯详情

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

字符串双指针技巧精讲:从反转字符串到替换数字的经典题型

字符串双指针技巧精讲:从反转字符串到替换数字的经典题型 这三道题放在一起刷其实是个很妙的安排。344是双指针的入门模板541立刻在这个基础上加了“每隔2k个处理一段”的循环控制而卡码网54.替换数字则把双指针从“两边往中间走”换成了“从后往前走”顺带解决了字符串长度变化时的原地操作问题。Day7结束之后你会发现字符串类题目里最核心的两个手法—双指针和区间切分—已经全部过了一遍。这篇文章我把三道题的解题思路、代码实现和踩坑点完整梳理一遍全程按我实际刷题时的思考顺序来写而不是照搬题解。为了方便不同语言基础的同学参考正文以Python实现为主关键地方我会补充C版本的区别。毕竟LeetCode上Python写起来最顺但卡码网那题涉及输入输出两种语言的处理方式差异还挺值得展开说说的。1. 先搞清楚三道题到底在考什么1.1 三个题目的内在关联先看344.反转字符串。题目要求原地修改输入字符数组不能额外分配空间。这几乎就是双指针的“hello world”left指向开头right指向末尾交换然后同时往中间移动直到相遇。541.反转字符串 II则多了一个“每隔2k个字符”的规则。表面上是个新题其实本质没变只要确定了需要反转的区间起点和终点区间内部还是那一套双指针交换逻辑。真正麻烦的不是交换而是怎么清晰地界定“每2k个字符中的前k个”和“剩余字符少于k个时全部反转”这两个分支。卡码网54.替换数字则完全是另一种考法。给定一个字符串把里面的数字字符替换成“number”这个单词。最容易想到的做法是新开一个字符串遇到数字就追加“number”。但题目如果要求“原地操作”或者输入是以字符数组形式给出那就得动点脑筋了。核心技巧是先数清楚有几个数字算出扩充后的最终长度然后从后往前填。为什么从后往前因为这样不会覆盖还没处理的字符。把这三题串起来看主线其实只有一条字符串题里的双指针不是用来“遍历”的而是用来“定位和处理区间”的。344是标准的双向夹逼541是“按块划分后的区间反转”54是“从尾部开始的双指针协作”。三种形态覆盖了双指针在字符串题目里的绝大多数用法。1.2 为什么把这三题放在Day7如果你是按代码随想录的刷题路线在走Day7的主题就是“字符串第一部分”。Day1到Day6基本都在处理数组和链表到了字符串这里很多数组里的思想可以直接复用。比如数组里的快慢指针、双指针在字符串里全部重新出现一遍。我自己的体会是字符串题目最大的坑不是算法本身而是语言特性。Python里字符串是不可变对象即使你写s[0] a 也会直接报TypeError所以LeetCode的344才特别说明“输入是字符数组”。而卡码网54题的输入是普通字符串Python里想做“原地扩容”就得先把字符串转成list。这些细节如果不提前搞清楚写题的时候很容易在语言层面卡住。另外提一嘴这三题在后来的面试里出场率都不低。344基本是白板题的暖场题541考验的是边界条件控制能力54则经常被拿来考察“是否了解字符串扩容和从后往前写”的思路。想进大厂的同学这几题值得好好吃透不是刷过就完了。2. 344.反转字符串双指针最经典的入门模板2.1 题目要求与核心思路题目链接是LeetCode 344官方难度“简单”。输入是一个字符数组s要求原地翻转也就是不能用额外的数组。空间复杂度要求O(1)。最简单的写法其实是Python的s.reverse()一行搞定。但这么做面试就完蛋了—你没法解释reverse的底层原理也没法应对面试官的追问“如果不让你用库函数呢”手动实现的核心思路定义两个指针一个从下标0开始left一个从数组末尾开始right交换两个指针指向的元素然后left加1right减1直到left right。这个思路正确性的依据是数学归纳法每一次交换都让正确位置的元素就位指针移动后问题规模缩小2直到中间相遇。代码就是经典的swap三连。2.2 代码实现与细节分析def reverseString(self, s: List[str]) - None: left, right 0, len(s) - 1 while left right: s[left], s[right] s[right], s[left] left 1 right - 1这个代码很简单但有几个细节值得说while left right而不是while left right。因为当left等于right时只有一个元素不需要交换循环多跑一次也没意义。Python的s[left], s[right] s[right], s[left]是语法级别的多变量赋值底层会先把右边的元组算出来再赋值不会出现互相覆盖的问题。这在C里对应swap(s[left], s[right])。不管字符串长度是奇数还是偶数这个循环都能正确处理奇数时最后left和right会相遇在中间元素不交换偶数时left会越过right退出循环。有的同学会写for i in range(len(s) // 2): s[i], s[len(s) - 1 - i] s[len(s) - 1 - i], s[i]这也对而且for循环版本更好理解。双指针版本其实就是把for循环的索引计算拆开了本质完全一样。我个人的习惯是while版本因为思维模型更贴近“指针在移动”的感觉后面遇到更复杂的双指针题比如排序数组去重、滑动窗口时会更顺手。时间复杂度O(n)空间复杂度O(1)完美满足题目要求。2.3 我刷这题时踩过的坑这题太简单了反而容易让新手在“语言细节”上翻车。第一个坑是试图对字符串直接操作。如果你写s[0], s[-1] s[-1], s[0]Python会直接报错TypeError: str object does not support item assignment。所以LeetCode才会把参数类型定义成List[str]而不是str。自己在本地测试时如果传入的是普通字符串记得先list(s)转换。第二个坑是不熟悉s[left], s[right] s[right], s[left]这个语法。有些同学会写成s[left] s[right] s[right] s[left]这就大错特错了。第二行执行时s[left]已经被覆盖成s[right]的值了你再赋回去等于两个位置都变成了同一个值数组直接废掉。正确的交换必须借助中间变量或者使用Python的多变量赋值。第三个坑不是代码层面的而是心理层面的很多同学觉得这题太简单看一眼就跳过。但实际上344是后续所有双指针字符串题的基石541就是在它外面套了一层循环控制。基础不牢后面做541就得多花时间。3. 541.反转字符串 II最容易在边界条件上翻车的一题3.1 题目解读与区间划分先读题给定一个字符串s和一个整数k从字符串开头算起每计数至2k个字符就反转这2k个字符中的前k个字符。如果剩余字符少于k个则将剩余字符全部反转如果剩余字符大于等于k个但小于2k个则反转前k个字符剩余字符保持原样。我第一次读题的时候其实被“每计数至2k个字符”这个表述搞糊涂了。后来才明白它的意思就是从头开始按2k个字符为一组划分每组我只处理前k个字符反转它们后k个字符保持不变。但如果最后一组不足k个那就把这一组全部反转如果最后一组在k到2k之间还是只反转前k个。举个例子s abcdefgk 2第一组是abcd反转前2个得到bacd剩下efg第二组是efg长度3大于等于k2且小于2k4所以只反转前2个得到feg最终结果是bacdfeg这个例子里“剩余3个”是很多人会搞错的点不是“剩余不足k个就全部反转”吗剩余3个明明大于k的2个啊怎么不全部反转因为题目说的是剩余字符小于k个才全部反转。3个并不小于2所以按正常规则只反转前2个。一句话总结只有剩余数量严格小于k时才全部反转否则一律只反转每组的开头k个。3.2 代码实现与复杂度分析有了这个理解代码就好写了。核心是循环的步长设为2k然后在每个区间内判断要反转的右边界def reverseStr(self, s: str, k: int) - str: s list(s) # 字符串不可变转成list才能改 n len(s) for i in range(0, n, 2 * k): # 左边界是i右边界判断一下 # 如果剩余长度不足k右边界直接到字符串末尾 # 否则右边界是 i k - 1 left i right min(i k - 1, n - 1) # 双指针反转区间 [left, right] while left right: s[left], s[right] s[right], s[left] left 1 right - 1 return .join(s)这个写法把344的双指针直接搬过来外面套一个for i in range(0, n, 2 * k)再用一个min(i k - 1, n - 1)来算右边界。min这一行是整套代码的精髓当剩余元素不足k个时i k - 1会超过数组末尾min会把右边界截断到n - 1于是“剩余全部反转”这个分支就被自动处理了不需要显式写if判断。我数一下复杂度每个字符最多被访问两次一次在反转区间内被交换一次被指针扫过时间复杂度O(n)。空间复杂度这里用了额外的list是O(n)严格说不符合“原地”要求。但LeetCode的s是普通字符串不可变所以官方题解里的Python版本也都是先转成list。如果你所在的面试场景要求空间O(1)那就需要选择支持可变字符串的语言比如C的string或者和面试官确认“是否可以额外使用O(n)空间”。3.3 常见错误与排查我见过不少同学在这题上翻车主要集中在两个地方。第一个错误是把右边界写成min(i k, n)然后用切片s[left:right]反转。切片操作本身不复杂但要注意切片右边界是开区间。如果写成左闭右开那要反转k个元素右边界索引就应该是left k。然后切片反转又会产生新的list再赋值回去代码反而绕了一圈没必要。第二个错误是直接套用“每2k个一组的循环”但内部没处理“剩余不足k个”的情况。有的同学会写for i in range(0, n, 2 * k): if i k n: # 反转 i 到 i k - 1 else: # 反转 i 到 n - 1这个if-else版本当然也对但它需要你先判断再执行两个分支。用min的写法只用一行就统一了两种情况代码更简洁也不容易漏分支。第三个坑是忽略了“剩余大于等于k小于2k时只反转前k个”这个规则。比如s长度为5k为2最后剩余1个字符这1个字符小于k所以你要全部反转其实就是原样但如果有同学不小心在循环里写if i 2 * k n and i k n来判断全部反转很容易把“剩余3个大于k”的情况也归进去导致多反转。建议调试时用几个典型用例跑一下s abc, k 2剩余1个小于k反转全部、s abcd, k 2剩余2个等于k反转全部、s abcde, k 2剩余3个大于k小于2k只反转前2个、s abcdef, k 2正好一组2k。把这四个用例跑对基本就稳了。4. 卡码网54.替换数字字符串扩容与从后往前双指针4.1 题目背景与两种实现思路卡码网54题的全称是“替换数字”归在代码随想录的字符串章节里。题目描述是这样的给定一个字符串s里面可能包含小写字母和数字字符请把字符串中的所有数字字符替换成“number”。例如输入“a1b2c3”输出“anumberbnumbercnumber”。这题在卡码网的输入模式是ACM风格需要自己处理input()读取和print()输出。LeetCode默认帮你封装好了函数但卡码网需要你写出完整的main逻辑。第一次刷卡码网的同学常常会在输入输出上卡住这里先提醒一下。思路有两套第一套是“新建字符串”法遍历原字符串遇到数字就追加“number”遇到字母就追加原字符。Python里写法最自然s input() result for ch in s: if ch.isdigit(): result number else: result ch print(result)这个方法的时间复杂度O(n)空间复杂度O(n)。对于大多数场景完全够用而且代码简洁、不容易出错。但如果你要应对面试追问“如果要求原地操作不申请额外空间怎么办”那就得掌握第二套思路先统计数字个数扩充原字符串然后从后往前双指针填写。这就是代码随想录里重点讲的经典解法。为什么从后往前打个比方你在一个装满东西的数组里要把某些格子换成更大的内容。如果从前往后填新内容会覆盖掉还没处理的旧数据但如果从后往前填新内容永远写在使用过的区域“后方”不会干扰前方还没处理的字符。这就是“尾部开始挪移”的思想。4.2 从后往前双指针的完整实现def replace_digits(s: str) - str: # 统计数字个数 count sum(ch.isdigit() for ch in s) # 计算新字符串长度每个数字替换成number6个字符 # 比原来的1个字符多出5个所以总长度 原长度 count * 5 old_len len(s) new_len old_len count * 5 # Python字符串不可变转成list模拟原地扩容 new_s [] * new_len # 双指针从后往前填充 i old_len - 1 # 指向旧字符串末尾 j new_len - 1 # 指向新字符串末尾 while i 0: if s[i].isdigit(): # 从后往前写number for ch in reversed(number): new_s[j] ch j - 1 else: new_s[j] s[i] j - 1 i - 1 return .join(new_s)核心步骤拆解先遍历一次数出数字的个数count。为什么必须提前数因为“number”比单个数字字符多了5个字符你不知道最终的数组要多长就无法确定j的初始位置。new_len old_len count * 5这里要解释一下数字字符是1个字符替换成“number”是6个字符所以每个数字会让总长度增加5。这一点很多同学会算错把增加量算成6。i从旧字符串末尾开始j从新字符串末尾开始从后往前同步移动。遇到数字字符时把长度为6的“number”从j开始逆序写入遇到字母时直接复制到j位置。全部填完之后i等于-1循环结束。这里为什么要用reversed(number)因为你是从后往前填第一个填进去的应该是“number”的最后一个字符‘r’接着往左填‘e’、‘b’、‘m’、‘u’、‘n’这样最终形成的就是正序的“number”。手写的话可以这样# 等价于从后往前写 new_s[j] r; j - 1 new_s[j] e; j - 1 new_s[j] b; j - 1 new_s[j] m; j - 1 new_s[j] u; j - 1 new_s[j] n; j - 1如果一时忘了顺序可以临时在草稿纸上写一下“number”然后倒着读或者直接用for ch in reversed(number)不会出错。我实际刷题时用的是更接近卡码网输入输出的完整版本s input().strip() count 0 for ch in s: if ch.isdigit(): count 1 old_len len(s) new_len old_len count * 5 result [] * new_len i, j old_len - 1, new_len - 1 number number while i 0: if s[i].isdigit(): for k in range(5, -1, -1): result[j] number[k] j - 1 else: result[j] s[i] j - 1 i - 1 print(.join(result))这段代码用result[j] number[k]其中k从5递减到0正好把“number”从右往左填进新数组逻辑上比reversed更直白推荐第一次写的同学用这个版本。4.3 关于“原地”与否不同语言的处理差异卡码网这题在C里的标准做法是s.resize(new_len)直接在原字符串上扩容然后双指针从后往前填。因为C的std::string是可变的扩容后原内容仍然保留在前面后面的新空间是空位从后往前填不会覆盖原数据。Python里没有直接“字符串原地扩容”的语法所以只能创建一个新的list来模拟。这算不算“不满足原地”要求严格来说不算因为空间复杂度是O(n)。但思路的价值在于它教会你“预估最终长度 尾部写指针”这套处理字符串扩容的通用方法论。将来做数组压缩、移除元素、合并两个有序数组从后往前合并时都会用到。这里我插一句个人经验如果面试官问这题你最好先用最直观的“新建字符串”方法答一遍紧接着主动说“如果要求原地操作可以预处理出最终长度然后用从后往前的双指针填”。直接展示两种方法会让面试官觉得你有分层思考的能力而不是只会背题。5. 常见问题与排查技巧实录5.1 我刷这三题时实际遇到的问题第一题344我遇到的是递归条件写错。我一开始写的是while left right结果也没报错就是多交换了一次中间元素白做了无用功。后来仔细一想中间元素和自己交换没意义改成left right更干净。这个影响不大但代码风格上能看出基本功。第二题541我踩过一个大坑把range(0, n, 2 * k)写成了range(0, n, k)。这样一来每k个字符就处理一次反转而不是每2k个字符才处理一次。测试用例“abcdefg”k2错误代码会输出完全不一样的结果。排查了半天才发现是步长问题。所以提醒大家循环的步长必须是2*k这是理解题意的关键。第三题54我第一次写的时候没统计数字个数直接按原长度创建新数组结果j还没填完就落到-1数组越界报IndexError。后来才意识到必须预先知道新数组长度才能确定j的起始位置。这个教训让我记住了“先数数、再扩容”的套路。5.2 三题联考时的时间复杂度分析把三道题放一起来看时间复杂度全部是O(n)空间复杂度除54的Python版本是O(n)外344和541的最优解都是O(1)。在面试中这三件套很适合拿来考察候选人是否真正理解双指针344考察“双向夹逼”541考察“区间切分 循环不变量”54考察“尾部双指针 容量预留”我建议在刷完三道题后自己画一张表记录每道题的核心变量题目循环控制双指针用途关键难点344.反转字符串while left right双向交换原地修改无额外空间541.反转字符串 IIfor i in range(0, n, 2k)区间内双向交换边界条件判断是否小于k54.替换数字while i 0旧指针新指针尾部写先统计数字个数从后往前防覆盖这张表是我给自己做的复习卡片每次刷字符串专题之前过一遍思路会清晰很多。5.3 我的调试技巧与建议调试字符串类题目最推荐的方式是print大法和desmos式的用例构造。print大法就不用多说了在循环体的关键位置打印每个指针的下标和当前数组状态肉眼观察每一步是否符合预期。比如344打印每次交换后的list541打印每次进入循环时的i和right54打印i和j的变化。很多边界问题一看打印结果就明白了。构造用例方面建议对每题准备至少4个测试用例最小规模空字符串、长度为1恰好边界长度等于k、等于2k、等于2k1全数字/全字母验证分支逻辑混合场景数字在开头、中间、结尾用这些用例跑一遍基本能覆盖所有边界分支。比如541的“剩余字符刚好等于k”的情况很多人会漏掉单独测一下就能发现自己的if条件写没写对。我还有一个习惯刷完一道题之后隔天不看任何参考自己把代码默写一遍。默写不出来或者卡住的地方就是还没真正掌握的知识点。Day7的这三题我默写541时在“剩余大于等于k但小于2k”这个分支上卡过一次后来专门把这道题重做了两遍才算真正记牢。这种“间隔默写”的方法比连续刷十道题都有效推荐你也试试。6. Day7之后的扩展思考如果说Day7之前的题目主要训练的是“单指针遍历”那么Day7之后双指针思路可以无缝衔接到以下几个方面字符串的翻转类问题反转单词、反转每对括号内的子串数组的移除与压缩移除元素、删除有序数组中的重复项链表中的双指针环形链表、寻找倒数第k个节点滑动窗口本质上也是两个指针维持一个窗口以“为什么双指针快”这个问题为例可以这样理解暴力法往往需要两层循环用两个指针分别遍历所有可能性而双指针通过让一个指针“记住”另一个指针的位置把两层循环压成一层从而把时间复杂度从O(n²)降到O(n)。这正好就是344里“从两头往中间走”的基本原理也是541里“固定步长跳着处理”的基础。后续你遇到LeetCode上的热门前100题时会发现字符串类高频题和这三道题有很强的血缘关系。比如“反转字符串中的单词”151题就是把整个字符串先整体反转再对单词局部反转“替换空格”和“替换数字”几乎一模一样。Day7这几题吃透后面的路会好走很多。最后再分享一个小技巧刷题的时候不要只满足于AC通过所有用例试着追问自己三个问题——如果输入是空字符串怎么办如果k等于1怎么办如果不让你用库函数你能手写swap吗把这三个问题想清楚这三道题才算真正拿下了。我个人每次复盘时都会用这套“灵魂三问”来检验自己效果显著。
返回列表