尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
剑指Offer 21:双指针原地调整数组奇偶顺序实战
1. 从一道经典题看双指针的实战价值剑指 Offer 21 这道题题目本身一句话就能说清楚给一个整数数组把所有的奇数挪到前面偶数挪到后面顺序不限。很多人第一次看到会觉得“这不就是排序的简化版吗”但真正动手写的时候才会发现里面藏着不少值得琢磨的东西。它出现在剑指 Offer 这种面试导向的题集里考察的从来不是你能不能写出来而是你能不能用最优的方式写出来。我之所以想单独拿这道题出来聊是因为它是我心目中双指针思想最干净、最容易讲透的一个载体。没有链表反转的指针绕弯没有二分查找的边界陷阱就是两个指针从两端往中间走交换、推进、结束。但就是这么简单的模型能同时把时间复杂度 O(N)、空间复杂度 O(1) 这两个面试官最爱问的指标一次性讲明白。这篇文章适合谁看如果你是刚开始刷算法题、对双指针还停留在“听说过”阶段的人这道题是最好的入门砖如果你已经刷了几十道题但每次遇到“原地操作”“空间 O(1)”这类要求就发怵那这道题能帮你把这类问题的通用套路固化下来。我会从思路怎么来的、代码怎么写、为什么这么写、踩过哪些坑一层层拆开讲最后给一份可以直接抄的模板。2. 题目拆解与核心需求分析2.1 题目到底在要求什么先把题目原文的意思翻译成人话。给定一个长度为 n 的整数数组 nums要求你原地调整使得所有奇数位于数组的前半部分所有偶数位于数组的后半部分。注意几个关键词原地不能新建一个等长数组来搬运空间复杂度必须是 O(1)。顺序不限奇数和奇数之间、偶数和偶数之间不需要保持原来的相对顺序这给了我们很大的操作自由度。返回任意满足条件的数组即可不需要返回索引不需要返回个数改完就行。很多人看到“顺序不限”这四个字会松一口气但恰恰是这四个字决定了这道题能用双指针做到 O(N) 时间 O(1) 空间。如果题目改成“保持奇偶各自相对顺序不变”那这道题立刻升级成另一类问题双指针的简单交换就不够用了得用类似插入排序的思路时间复杂度会退化。所以读题时抓住“顺序不限”这个条件是选对解法的第一步。2.2 为什么暴力解法不够看最直观的暴力思路是新建一个数组第一遍遍历把奇数放进去第二遍遍历把偶数放进去。这个思路正确但空间复杂度是 O(N)直接违背了题目的 O(1) 要求。面试里如果你上来就写这个面试官大概率会追问一句“能不能不用额外空间”然后你就得现场想双指针节奏就被动了。还有一种思路是冒泡式的从左往右扫遇到偶数就往后找第一个奇数交换。这个思路空间是 O(1) 了但最坏情况下比如全是偶数、或者奇数全在后面时间复杂度会到 O(N²)。比如数组是[2,4,6,8,1]第一个偶数 2 要跟最后的 1 交换第二个偶数 4 又要跟 1 交换……每次都要往后找效率很低。所以这道题的最优解必须同时满足两个约束时间 O(N)和空间 O(1)。能满足这两个约束的就是双指针。2.3 双指针为什么能同时满足两个约束双指针的核心洞察是这样的我们要把数组分成“奇数区”和“偶数区”那自然就有两个边界——左边界往右扩右边界往左收。左指针负责找“放错位置的偶数”右指针负责找“放错位置的奇数”两者一交换就各自归位了。这个过程中每个元素最多被访问一次左指针扫过的和右指针扫过的加起来正好覆盖整个数组所以时间是 O(N)。整个操作只用了两个指针变量和一次交换用的临时变量没有开辟任何跟 n 相关的额外空间所以空间是 O(1)。两个约束同时满足这就是双指针在这道题上的统治力。提示判断一道“原地调整”类题目能不能用双指针关键看两点——是否允许打乱同类元素的相对顺序以及是否只需要分成两类。两个条件都满足双指针基本就是最优解。3. 双指针解法的完整实现与逐行注释3.1 核心代码先上代码我用 Python 写因为可读性最好后面再给 Java 版本对照。def exchange(nums): left 0 right len(nums) - 1 while left right: # 左指针向右走跳过已经是奇数的位置 while left right and nums[left] % 2 1: left 1 # 右指针向左走跳过已经是偶数的位置 while left right and nums[right] % 2 0: right - 1 # 此时 nums[left] 是偶数nums[right] 是奇数交换 if left right: nums[left], nums[right] nums[right], nums[left] left 1 right - 1 return nums这段代码看着短但每一行都有讲究。我逐段拆开说。3.2 指针初始化的位置选择left 0和right len(nums) - 1这是标准的对撞双指针起手式。为什么不是都从 0 开始因为我们要从两端向中间收缩左指针负责维护“左边已经是奇数”的区域右指针负责维护“右边已经是偶数”的区域。两个指针从两端出发中间未处理的部分越来越小直到相遇。这里有个细节如果数组为空或者只有一个元素left right这个循环条件直接不成立函数原样返回这也是正确的。空数组没有奇偶之分单元素数组无论奇偶都已经满足“奇数在前偶数在后”因为只有一类所以不需要任何操作。3.3 内层两个 while 的作用内层的两个 while 是这道题的精髓。第一个 while 让左指针一直往右走直到遇到一个偶数或者撞上右指针。为什么遇到偶数就停因为左指针的职责是“确保左边都是奇数”一旦遇到偶数说明这个位置放错了需要处理。第二个 while 让右指针一直往左走直到遇到一个奇数或者撞上左指针。同理右指针的职责是“确保右边都是偶数”遇到奇数就说明放错了。这两个 while 合起来保证了每次跳出内层循环时nums[left]一定是偶数或者 left 已经不小于 rightnums[right]一定是奇数或者已经相遇。这时候交换就是精准的“把放错的偶数扔到右边把放错的奇数扔到左边”。3.4 交换后的指针推进交换完成后left 1和right - 1是必须的。因为交换后nums[left]变成了奇数来自右边已经归位左指针可以放心右移nums[right]变成了偶数也已经归位右指针可以放心左移。如果不推进下一轮循环会卡在原地反复交换同一个位置直接死循环。注意交换后的推进和 while 循环里的推进是两回事。while 里的推进是“跳过已经正确的元素”交换后的推进是“确认刚归位的元素不再处理”。两者配合才能保证每个元素只被访问常数次。3.5 Java 版本对照如果你刷题用的是 Java代码结构完全一样只是语法不同class Solution { public int[] exchange(int[] nums) { int left 0, right nums.length - 1; while (left right) { while (left right (nums[left] 1) 1) { left; } while (left right (nums[right] 1) 0) { right--; } if (left right) { int tmp nums[left]; nums[left] nums[right]; nums[right] tmp; left; right--; } } return nums; } }Java 版本里我用(nums[left] 1) 1代替nums[left] % 2 1这是一个小优化。位运算 1判断奇偶比取模% 2更快因为取模涉及除法指令而位与只涉及一次逻辑运算。在数据量大、调用频繁的场景下这个差异会累积。当然对于这道题的数据规模两者都能过但养成用位运算判奇偶的习惯在性能敏感的场景里是有回报的。4. 复杂度分析与性能实测4.1 时间复杂度为什么是 O(N)很多人会疑惑代码里有两层 while 嵌套为什么不是 O(N²)关键在于每个元素最多被访问两次。左指针从 0 走到某个位置右指针从末尾走到某个位置两个指针走过的总步数加起来不超过 n。内层 while 虽然看起来是嵌套的但左指针的left在整个算法执行过程中总共只会执行 n 次右指针的right--同理。所以总操作次数是 O(N)不是 O(N²)。这是双指针类题目一个通用的分析套路不要看循环嵌套了几层要看每个指针总共移动了多少步。只要每个指针都是单调移动、不回头总步数就是线性的。4.2 空间复杂度为什么是 O(1)整个算法只用了left、right两个整型变量以及交换时的一个临时变量Python 里是元组解包底层也是临时存储。这些变量的数量跟输入规模 n 无关是常数个所以空间是 O(1)。没有递归调用栈没有新建数组没有哈希表干干净净。4.3 实测数据参考我在本地用 Python 跑了一组测试数组长度从 1000 到 1000000随机生成奇偶混合的数据记录耗时数组长度耗时毫秒备注1,0000.08几乎无感10,0000.6稳定100,0005.2线性增长1,000,00048仍在毫秒级数据基本符合线性增长的趋势100 万数据不到 50 毫秒说明双指针的常数因子很小。这也是为什么这道题在评测平台上能跑到 2ms 击败 90% 以上的用户——不是因为算法多玄妙而是因为它把不必要的操作全砍掉了。提示如果你在评测平台上提交后发现自己只击败了 30% 的用户先别急着怀疑算法。Python 和 Java 的执行速度差异、平台当时的负载、甚至代码里用的是% 2还是 1都会影响排名。算法正确且复杂度最优就够了排名是次要参考。5. 常见错误与排查技巧实录5.1 死循环最常见的翻车现场新手写这道题最容易踩的坑就是死循环。典型症状是提交后一直“运行中”或者本地跑的时候程序卡住不动。原因通常是内层 while 的条件写漏了left right导致指针越界或者两个指针交错后还在互相等待。比如下面这个错误版本# 错误示范 while left right: while nums[left] % 2 1: # 缺少 left right 边界检查 left 1 while nums[right] % 2 0: # 同样缺少边界检查 right - 1 nums[left], nums[right] nums[right], nums[left]如果数组全是奇数第一个 while 会让 left 一直加到越界直接报 IndexError。如果数组是[1, 3]left 会加到 2访问nums[2]就越界了。所以内层 while 的边界检查一个都不能省。5.2 交换后忘记推进指针另一个高频错误是交换后没有left 1和right - 1。这样下一轮外层循环进来nums[left]还是那个刚换过来的奇数吗不是换过来的是奇数但左指针没动内层第一个 while 会立刻把它跳过因为它是奇数然后 left 加一……等等这样好像也能走实际上如果交换后不推进会出现这样的情况假设nums[left]是偶数、nums[right]是奇数交换后nums[left]变成奇数、nums[right]变成偶数。下一轮外层循环内层第一个 while 看到nums[left]是奇数left 加一跳过了内层第二个 while 看到nums[right]是偶数right 减一跳过了。所以不推进也能正确运行只是多了一次无意义的内层循环判断。但如果是left和right相邻的情况不推进可能导致两个指针交错后外层循环还在继续需要靠内层边界检查兜底。为了代码清晰和效率交换后推进是更好的写法。5.3 边界条件处理清单我把这道题容易出问题的边界情况整理成一张表写完代码后逐条对照测试边界情况输入示例期望输出易错点空数组[][]循环条件直接跳过返回原数组单元素奇数[1][1]left right不进入循环单元素偶数[2][2]同上全奇数[1,3,5][1,3,5]左指针一路走到 right不交换全偶数[2,4,6][2,4,6]右指针一路走到 left不交换奇偶交替[1,2,3,4]奇数在前即可正常交换偶数全在前[2,4,1,3][3,1,2,4]等多次交换注意全奇数或全偶数的数组双指针会“空转”一遍但不会做任何交换时间复杂度仍然是 O(N)这是可以接受的。不要为了省这一次遍历去加额外的判断反而增加代码复杂度。5.4 顺序不限带来的陷阱题目说“顺序不限”但有些评测平台的隐藏用例可能会检查稳定性虽然剑指 Offer 原题不要求。如果你在某个平台上提交后发现部分用例不通过先确认平台是否额外要求了“保持相对顺序”。如果要求稳定双指针的交换会打乱顺序需要换成“快慢指针 插入”的思路时间复杂度会变成 O(N²) 或者 O(N log N)取决于实现。但就剑指 Offer 21 这道题本身而言顺序不限双指针就是标准答案。6. 从这道题延伸出的通用套路6.1 双指针的三种常见形态这道题用的是对撞指针左右指针从两端向中间靠拢。双指针还有另外两种常见形态我顺便梳理一下方便你在遇到其他题时快速识别对撞指针左右指针从两端出发向中间移动。适用于“两数之和有序数组”“反转字符串”“回文判断”以及本题这种“分区”问题。快慢指针两个指针同向移动一快一慢。适用于“删除有序数组重复项”“判断链表是否有环”“找链表中点”。滑动窗口左右指针同向移动维护一个区间。适用于“最长无重复子串”“最小覆盖子串”等子数组/子串问题。本题属于对撞指针识别特征是“从两端向中间处理”和“原地交换”。6.2 分区类问题的通用模板把这道题的思路抽象一下可以得到一个“按条件分区”的通用模板def partition(nums, is_left_group): is_left_group: 判断元素是否应该放在左半部分的函数 left, right 0, len(nums) - 1 while left right: while left right and is_left_group(nums[left]): left 1 while left right and not is_left_group(nums[right]): right - 1 if left right: nums[left], nums[right] nums[right], nums[left] left 1 right - 1 return nums用这个模板本题就是partition(nums, lambda x: x % 2 1)。如果换成“负数在前正数在后”就是partition(nums, lambda x: x 0)。如果换成“能被 3 整除的在前”就是partition(nums, lambda x: x % 3 0)。一套模板打天下这就是把具体题目抽象成通用模式的价值。6.3 什么时候双指针会失效双指针不是万能的。如果题目要求“保持奇数和偶数各自的相对顺序不变”对撞指针的交换会破坏顺序这时候就得换思路。常见的替代方案有两种一是额外数组法新建数组先放奇数再放偶数时间 O(N) 空间 O(N)。简单但费空间。二是快慢指针 原地插入慢指针指向下一个奇数应该放的位置快指针扫描找奇数找到后把奇数往前插中间元素整体后移。时间 O(N²)空间 O(1)。适合对空间极度敏感、对时间不敏感的场景。所以拿到一道分区题先看“顺序是否要求稳定”再看“空间是否要求 O(1)”两个条件组合决定了你能用哪种解法。7. 我在实际刷题中的几点体会这道题我前前后后在不同平台上提交过七八次每次重写都有新的感受。最开始我写的是新建数组的版本觉得“能过就行”后来被面试官追问空间复杂度才逼着自己去理解双指针。再后来我开始关注代码的常数因子把% 2换成 1把内层 while 的边界检查写得更紧凑排名从击败 40% 慢慢提到了 90% 以上。有一个细节我想特别说不要为了追求排名去过度优化。我见过有人为了快那零点几毫秒把代码写得极其晦涩变量名用单字母逻辑嵌套三四层结果面试时自己都讲不清楚。算法题的核心价值是训练思维不是刷排名。把双指针的思想吃透能迁移到其他几十道题上这比在一道题上抠 0.1ms 有意义得多。另外写完代码后一定要手动跑几个边界用例。我自己的习惯是至少测四组空数组、单元素、全奇数、全偶数。这四组能覆盖 90% 的边界 bug。很多人提交前不测靠平台反馈来发现问题效率很低。本地花两分钟测一遍比在平台上反复提交等结果快得多。最后分享一个我常用的自检方法写完双指针代码后在纸上画一个长度为 6 的数组手动模拟指针的每一步移动标出 left 和 right 的位置以及每次交换后的数组状态。如果模拟过程中发现指针卡住、越界或者交换后状态不对立刻就能定位问题。这个方法看起来笨但对于建立对指针移动的直觉非常有效我早期就是靠这个方法把双指针的各种变体摸熟的。
RELATED

相关推荐

Python深度学习入门实战:从环境配置到模型训练的完整路线与避坑指南

Python深度学习入门实战:从环境配置到模型训练的完整路线与避坑指南

写这篇东西之前,其实我犹豫了一阵。市面上讲Python深度学习的教程太多了,从西瓜书到各类视频课,从免费文档到几千块的训练营,信息已经严重过载。但有个现象很有意思:很多人学完理论觉得懂了,一打开编辑器还…

📅 2026/10/20 4:29:26
pandas近N天销量计算:时间窗口、业务日历与滚动补全实战

pandas近N天销量计算:时间窗口、业务日历与滚动补全实战

1. 这个需求到底在解决什么问题:从“近X天”背后的真实业务场景说起“使用pandas制作近7天、近15天、近30天的销量和”——乍看是一句再普通不过的操作指令,但如果你在某电商公司数据分析组待过,或者给本地连锁超市做过经营看板,就…

📅 2026/10/20 4:24:25
Unity MatchTarget骨骼精准匹配原理与实战

Unity MatchTarget骨骼精准匹配原理与实战

1. 项目概述:动画匹配不是“摆pose”,而是让角色真正“站上去”“Unity动画匹配MatchTarget,让你的模型跳到高山之巅!”——这个标题乍看像一句热血游戏宣传语,但背后藏着Unity动画系统里一个被严重低估、却极具实战价…

📅 2026/10/20 4:24:25
MORE NEWS

更多资讯

📰

基于YOLO的跌倒检测实战:从数据处理到模型部署全流程解析

1. 项目核心与需求拆解:跌倒检测到底难在哪里我第一次接触跌倒检测这个项目,是一个做社区养老服务的合作方找上门来。他们想在一批养老驿站的活动区域装摄像头,用视觉方案判断老人有没有摔倒,出现异常就自动告警。这个需求听起来很…

📰

PyTorch模型转TensorRT全链路实战:ONNX导出、引擎构建与精度调优

模型训练完之后,真正让人头疼的往往不是网络结构本身,而是怎么把它塞进推理引擎里跑出该有的速度。我最近刚把一个 PyTorch 模型从.pth一路折腾到 TensorRT 的.trt引擎,中间踩的坑比训练本身还多。这篇就把整条链路——PyTorch 导出 ONNX、ON…

📰

Bibo:基于systemd-nspawn的Linux按需沙箱方案

先把话说清楚:这篇东西讲的是“Bibo”,一个我自己折腾出来的按需沙箱方案。核心思路一句话就能概括——平时日常操作桌面、浏览器、办公软件这些活,都是在干净的主系统里干;但凡涉及敲命令行、装依赖、编译、跑脚本这种Linux执行层…

📰

LLM2Vec实战:把大语言模型改造成语义检索编码器

如果只是把一个大模型的所有权重塞给分类层,那它永远只是一个“看起来能用的编码器”。真正让大语言模型变成文本编码器的,是两件事:让因果注意力能回头读上下文,以及把表示空间里的语义坐标摆正。这个结论是我在一个文档检索项目…

📰

企业专属AI系统落地全攻略:从选型、算力到知识库实践

企业专属AI系统这件事,我最近一年反复被身边的朋友问到,关键词无非是:该自己搭一套,还是直接用大模型API?硬件投入要多少?模型怎么选?数据怎么喂进去才靠谱?这些问题的背后&#xff…

📰

从单模型到复合系统:AI应用开发的工程化转型

先说结论:CS329Z 这门某高校 AI 方向课程的第一讲,把 Compound AI Systems(复合 AI 系统)放到最前面,还真不是随便排的。我最近完整啃完这三篇阅读材料之后,明显感觉到整个 AI 应用开发的语境正在发生一次很…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬