news 2026/10/8 2:46:58

LeetCode 977 双指针解法:有序数组平方排序的 O(n) 技巧

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 977 双指针解法:有序数组平方排序的 O(n) 技巧

1. 写在前面:这道“入门题”为什么能卡住很多人

LeetCode 977题“有序数组的平方”,在题库里被标为“简单”,很多人刷题没几天就会碰到它。但我敢说,这道题是典型的“看着简单,写起来翻车”的题目——我见过不少刷了上百题的人,在这道题上第一次意识到什么叫“双指针的坑”。就连我自己,第一次做的时候也交过一版 O(n log n) 的暴力解,然后被面试官追问“能不能不要排序”才恍然大悟。

题目本身一句话就能说清:给你一个按非递减顺序排序的整数数组,要求返回每个数字平方后、同样按非递减顺序排序的新数组。比如输入[-4,-1,0,3,10],输出就是[0,1,9,16,100]。

难在哪?难点在于负数和正数混在一起,平方之后原来的顺序大概率会被打破。负数平方变大,正数平方也变大,但绝对值小的数平方反而小。直接平方再排序当然能做,但如果不允许用排序(或者面试官要求 O(n)),你就必须动脑子。

这篇文章不打算只贴一份能通过的代码。我会把这道题从头到尾拆开讲:暴力做法为什么不够好、双指针的思路是怎么自然推导出来的、代码实现里有哪些细节容易踩坑、还有哪些变形题和面试追问可以直接套用这套思路。特别是你如果准备校招、社招笔试,这道题的“唯一正解”双指针写法,几乎是必须刻在脑子里的模板之一。

2. 先把题读懂:有序数组平方后的排序特征

2.1 从“绝对值的视角”理解输入数组

首先我们得建立一个直觉。原数组是按非递减顺序排列的,也就是说从小到大。但里面可以有负数。比如[-9,-3,0,1,2]。

如果只看每个元素的绝对值,会发现在原数组中,绝对值最大的元素一定在两端:要么最左边(负得最狠),要么最右边(正得最大)。绝对值最小的元素,则一定在“正负交界处”附近。这其实用一个生活类比就能懂:想象数轴上站了一排人,位置按从小到大排列,负号的人站在左边,正号的人站在右边。要问谁离原点最近,你肯定从中间开始找,而不是从两边。

我们平方之后,负号消失,所有数被“折叠”到正半轴。原来左边最小的负数,平方之后可能比右边小正数还大。但有一条是可以确定的:平方后的最大值只会出现在原数组的两端之一,绝不会出现在中间。这条观察是整个双指针解法成立的根基。

2.2 为什么不能直接用排序糊弄过去

最直观的解法就是遍历一遍,对每个元素平方,最后调用标准库排序。在 Python 里甚至能写成一行的return sorted(x*x for x in nums)。简单是简单了,但复杂度是 O(n log n)。

放到面试场景里,这个答案不会给你零分,但大概率会引来追问:“你能做到 O(n) 吗?”这时候如果你没准备,就有点尴尬了。更实际的说,LeetCode 的题目设计成“简单”不代表没有考察意图——它就是希望你发现:原数组有序这个条件,平方后虽然整体无序,但它的“部分有序性”可以被利用。直接用排序等于无视了题目的核心约束,后面一堆进阶题(比如合并两个有序数组)也容易踩类似思路不转换的坑。

所以这道题本质不是考察你会不会写map或sort,而是考察你有没有意识到“数据结构自带的有序性质应该被使用”。这才是真正的算法基本功。

3. 双指针解法:从两个方向逼近,逆序填充

3.1 思路推导:最大值一定出现在两端的“竞争者”里

我们先不急着写代码,而是想清楚“平方后最大的数是什么”。假设我有很多个数的平方,要找出最大的那个,肯定是比较左端和右端这两个极值。因为输入数组有序,所以左端是负数里绝对值最大的,右端是正数里绝对值最大的。这两个数平方后就是整个数组平方结果里最大的候选。比较一下两者谁大,谁就是当前剩余范围里平方最大的元素。

这个最大的元素应该放在结果数组的什么位置?结果是按非递减排序的,最大自然放最后一位。于是我们可以倒着填数组,从下标 n-1 填到 0。每次比较左端和右端,谁大就放进当前的空位,然后移动对应端的指针。

这就是双指针做法,时间和空间都能做到 O(n)(如果原地操作甚至空间 O(1))。核心就是四个字:反向填充。

3.2 为什么是“比两端”而不是“比中间”

有些同学可能会想:既然平方后最小数在正负交界附近,为什么不先找中间再往两边扩散?这个思路其实也不错,理论上也能 O(n),但实现更复杂——你需要先线性扫描找到绝对值最小的位置,然后向两边双指针扩散。代码量翻倍,边界判断也多,面试时容易绕晕自己。

从两端往中间聚拢,实际上是一种常见的双指针模式。它和二分查找那种“从中间切一刀”是两种完全不同的思维。两端聚拢最大的好处在于:你不需要提前知道“分界点在哪”,因为每轮比较都天然产生了分界信息。谁被选中了,谁就“出局”,下一步继续比较剩下的两端即可。

可以这样理解:一个数组,左右各站着举重选手,谁平方值大谁把最后一个坑位拿走,然后退场。剩下的人还在原来的相对位置,再来一轮比较。这个过程天然产生全局有序的结果,不需要任何“排序”动作。

3.3 完整代码示例与逐行说明

我用 Go 写一下,因为最近面试考 Go 的比较多,但后面会附 Python 和 C++ 版本:

func sortedSquares(nums []int) []int { n := len(nums) ans := make([]int, n) left, right := 0, n-1 pos := n - 1 for left <= right { lv := nums[left] * nums[left] rv := nums[right] * nums[right] if lv > rv { ans[pos] = lv left++ } else { ans[pos] = rv right-- } pos-- } return ans }

核心逻辑:

  • left和right分别指向当前还没处理区间的最左和最右。
  • pos指向结果数组的当前填充位,从后往前推进。
  • 每轮比较两端平方值,较大的写入ans[pos],然后移动对应的指针。
  • 循环结束条件是left <= right,也就是区间里还剩至少一个元素。最后一次循环会处理掉最后一个数。

这里有同学会问:lv > rv时选左边,否则选右边,那如果lv == rv呢?我们的else分支选了右边,其实选谁都一样,因为两个数平方相等,但值本身可能不同(比如 -3 和 3)。只要结果数组只要求“值”,不要求“稳定排序”,选谁都正确。但如果你写的是if lv >= rv选左边,也无所谓。

3.4 为什么不能正向填充

我见过不少初学双指针的人会试图正向填充:比较两端,把较小的放进结果数组开头。这在元素都是正数时没问题,但一旦有负数就必错。因为原数组有序,正数从左边往右越来越小(按绝对值)?不对,我们梳理一下:

  • 如果正向填充,我们需要找到全局最小值。全局最小平方值在原数组“靠近零”的位置,可能在中间偏左,也可能在中间偏右。如果你只比较两端,是找不出最小值的。
  • 反向填充则天然匹配“最大值”——最大值总是出现在当前区间的某一端。这个性质才是问题的关键。

所以记住这个判断标准:如果要在有序结构里找“最大”,两端比较大概率有效;如果要找“最小”,一般得另想办法(比如从中间向两边扩散或在有序数组上做二分)。这是双指针模式的一个非常经典判断框架。

4. 实战细节:初始化、溢出、类型选择的硬核经验

4.1 平方溢出的“隐形杀手”

如果不限定语言,这道题在小数据范围内很安全,但一旦面试官扩展了场景,比如输入是int[] nums,每个数的平方可能不只 32 位呢?C++ 的int范围大约是 -21亿到21亿,如果数组里有50000,平方立刻溢出成一个奇怪的负数,然后比较逻辑就会出错。

LeetCode 原题测试用例一般不会卡这个(原题范围通常是 -10^4 到 10^4,平方10^8,还在int范围内)。但我在实际面试模拟中见过几次面试官临时把这个题目“加码”:如果输入可以到±10^5甚至±10^9,你就要考虑使用 64 位类型。

建议习惯性写成:

  • C++:long long或直接int64_t。
  • Go/Java:int64、long。
  • Python:无所谓,Python 的整数是任意精度的,但如果你涉及到大量计算,仍然要考虑性能。

代码层面,这几行不要写成nums[left] * nums[left]然后赋给int,而是在乘法之前先做类型转换,或者用安全的math函数。细节虽小,但在现场写代码时很能体现经验的差别。

4.2 原地操作:空间复杂度到底能不能做到 O(1)

LeetCode 原题要求返回一个新数组,所以额外空间 O(n) 是允许的。但如果题目改成“原地修改输入数组,把平方后的结果存储回原数组”,你能不能做?

想原地操作,必须特别小心指针覆盖的问题——你是从后往前填充,同时读取的两个指针也指向数组的两端。由于你是用left和right读原值,然后往数组末尾写,整个过程会不会覆盖还没读到的值?

答案是:不会,前提是你从左端读到的值写入了数组末尾,而数组末尾位置在物理上处于right指针的右侧(或者就是right位置的旁边)。具体分析一下:

  • 当right指向n-1,left指向0时,把某个较大的平方值写入ans[n-1]时,如果写入值本身来自right位置的平方,那么覆盖的就恰好是right自己——你还需不需要它?不需要了,因为这次已经读过了。如果写入的是left的平方,覆盖的是right位置,但right位置还没读取。嗯?

等等,这里有个细节必须想清楚。原地操作时,指针移动模式可能改变:从右端读出来的值,写回右端,可能覆盖还没移动的right位置吗?如果直接把nums[right] * nums[right]赋给nums[pos],当pos == right时是安全的;如果pos < right,那写的位置在left和right之间,也还没被读取?不好说。

原地操作其实隐性逻辑有点绕,面试现场容易翻车。我的建议是:如果没有明确要求原地,就直接新开数组,清晰表达思路最重要。面试官问能优化吗,你再给出原地思路,同时强调指针运动方向是两端向内压缩,所以覆盖的位置要么是已经处理过的位置、要么是将来不再需要的位置,整体安全。这个话术能体现你对内存布局的敏感度。

4.3 循环边界:left < right还是left <= right

循环条件写left <= right还是left < right,在最后剩一个元素时有区别。如果数组长度是奇数,最终left和right会指向同一个元素。这个元素必须被处理,否则结果数组少填一个位置。所以必须用<=。

我见过有人这么写:

for left < right { // 比较处理 } // 循环外面手动处理最后一个元素 fmt.Println("left == right:", nums[left])

这样写也能过,但不如直接<=来得自然。你如果选择在循环外补一个尾巴,记得结果数组那个空位是由pos控制的,别漏掉。

我的建议是保持<=,因为这样你少一个分支,也少一处潜在 bug。如果面试官问边界条件,你甚至可以把“只剩一个元素时会同时被左右指针访问”作为记忆点说出来,这算加分细节。

4.4 代码风格的工程化建议

既然这道题是面试题,写出“工程级”风格(而不是“算法竞赛式”风格)可能更招面试官喜欢。比如:

  • 不要用一行的三元表达式把整个逻辑压缩到看都看不懂。
  • 变量命名用left,right,pos或者l,r,i都可以,但别用a,b,c。
  • 提前计算平方值,存到临时变量里,可读性更好,也避免双倍乘法运算(性能不重要,但清晰很重要)。

Go 版本我会这么写:

func sortedSquares(nums []int) []int { n := len(nums) result := make([]int, n) l, r := 0, n-1 for i := n - 1; i >= 0; i-- { leftSquare := nums[l] * nums[l] rightSquare := nums[r] * nums[r] if leftSquare > rightSquare { result[i] = leftSquare l++ } else { result[i] = rightSquare r-- } } return result }

循环里用for i := n-1; i >= 0; i--,配合三个指针的移动,逻辑非常清晰,几乎不会写错。

5. Python 与 C++ 对照:不同语言里的实现注意点

5.1 Python 的简洁与陷阱

Python 写这个题很容易过度简洁,比如:

def sorted_squares(nums): return sorted(x * x for x in nums)

这在笔试时可写,但面试时如果只写这版,基本等于主动放弃亮点。推荐给出双指针版本:

def sortedSquares(nums): n = len(nums) ans = [0] * n left, right = 0, n - 1 pos = n - 1 while left <= right: if abs(nums[left]) > abs(nums[right]): ans[pos] = nums[left] * nums[left] left += 1 else: ans[pos] = nums[right] * nums[right] right -= 1 pos -= 1 return ans

注意:在 Python 里直接比较abs(nums[left])和abs(nums[right])与直接比较平方值等价(因为 abs 是增函数),代码读起来更像在描述“绝对值博弈”。但求平方比 abs 性能更高?其实也差不多。两种写法都行,我习惯用平方直接比较,因为语言无关、面试官在伪代码里更容易看。

另外 Python 切片要小心:别写出ans[-(n-pos)] = value这种魔法代码,不直观而且容易索引错。

5.2 C++ 里的溢出与迭代器习惯

C++ 解法,我推荐使用索引而不是迭代器。因为双指针通常就是两个下标,用迭代器会让代码更绕:

class Solution { public: vector<int> sortedSquares(vector<int>& nums) { int n = nums.size(); vector<int> ans(n); int left = 0, right = n - 1; for (int pos = n - 1; pos >= 0; --pos) { long long lv = (long long)nums[left] * nums[left]; long long rv = (long long)nums[right] * nums[right]; if (lv > rv) { ans[pos] = lv; ++left; } else { ans[pos] = rv; --right; } } return ans; } };

关键在于这两行:

long long lv = (long long)nums[left] * nums[left];

如果写成int lv = nums[left] * nums[left],遇到极端值会产生未定义行为?其实是有符号整数溢出,C++ 标准里是未定义行为,编译器可能给任何奇怪结果。别赌测试数据覆盖不到,工程习惯要养好。

还有一个小细节,vector<int> ans(n)提前指定容量,避免反复 push_back。如果不指定,你用ans.push_back配合插入位置其实更加别扭。要么用ans.resize(n)再按下标赋值,要么直接构造时给长度。

5.3 Rust/Fortran/其他语言,怎么答才不像背题

如果你用 Rust 写,所有权和借用容易绕:

pub fn sorted_squares(nums: Vec<i32>) -> Vec<i32> { let n = nums.len(); let mut ans = vec![0; n]; let (mut left, mut right) = (0, n - 1); let mut pos = n - 1; while left <= right { let lv = nums[left] * nums[left]; let rv = nums[right] * nums[right]; if lv > rv { ans[pos] = lv; left += 1; } else { ans[pos] = rv; right -= 1; } if pos == 0 { break; } pos -= 1; } ans }

Rust 的 while 条件里要小心usize下溢:left <= right当left是 usize 时,如果right变成很大?要注意不让 right 变成负数。用下标i递减时尤其要小心i >= 0在 Rust 里会死循环。我的建议是用for i in (0..n).rev(),配合while双指针递减一个标志,这样更安全。

说实话,面试不指定语言时,用你最有信心的语言写清楚就行。算法思路才是面试官真正关心的。

6. 复杂度分析与正确性证明

6.1 为什么是 O(n) 而不是 O(n log n)

双指针算法中,每次循环left递增或right递减,两者之间至少有一个指针在移动。循环最多执行 n 次,因为每执行一次,未处理的区间长度就减少 1。每次循环内做常数次平方运算和比较、一次写入,所以总时间是 O(n)。

辅助空间方面,新开了长度 n 的结果数组,所以是 O(n)。如果要强调优化空间,可以在原数组上操作,实现 O(1) 额外空间。

这里对比一下:

方法时间复杂度空间复杂度是否利用输入有序性
暴力平方+排序O(n log n)O(1)(原地sort)或 O(n)否,浪费了有序条件
先找分界点再合并O(n)O(n)是,但要先扫描找中点
两端双指针反向填充O(n)O(1)/O(n)是,直接利用两端的极值竞争

从表中能看出来,双指针方案在时间、空间、代码复杂度上全面胜出。

6.2 用“数学归纳法”说服面试官

面试官可能会要你证明算法正确。别慌,这个算法的正确性证明其实很简单。

归纳假设:当我们要填充结果数组的第k个位置(从后往前数第k个)时,当前左右指针指向的区间[left, right]是还未处理元素的集合,该集合里的平方最大值必然在left或right取得。由于原数组有序,平方的最大值只能出现在两端,因此比较两端平方值并选择较大者,就是全局选择正确。将选出的值放入当前填充位,剩余区间仍然满足同一假设。递归/归纳下去,最终所有位置都被正确填充。

这个证明可以压缩成一句话:两端双指针每次都能选出当前区间平方最大元素,放置到结果数组的从大到小空隙中,因此结果正确。这一句话在面试中讲清楚,能拿到的评价绝对高过“我觉得思路是这样,代码跑一下试试”。

6.3 和“合并两个有序数组”的关系

这里专门展开一下,因为 977 其实和 88 题“合并两个有序数组”是一对好兄弟。

88 题的经典做法也是从后往前填充,因为两个数组的尾部有更大空间,可以避免移动元素。而 977 的平方数组,本质上可以理解为:把负数们的平方数组(它其实是递减序列)和正数们的平方数组(递增序列)合并起来。

  • 负数部分:假设有[-5, -3, -1],平方后是[25, 9, 1],这其实是递减序列。
  • 正数部分:[2, 4, 6],平方后是[4, 16, 36],递增序列。

我们要把这两个有序序列合并成一个递增序列。最自然的方法是双指针:一个指针从负数平方数组的尾部(因为尾部最小)开始往前扫,另一个从正数平方数组头部开始往后扫,比谁小放谁。这就是“归并排序”的合并步骤。

但 977 的双指针其实是把这个合并过程“隐式”地做完了。你仔细看:left从负数绝对值大的开始(相当于负数平方数组的头部),right从正数最大值开始(相当于正数平方数组的尾部),两者比较的是当前未处理元素里的平方最大值。这与合并的思路正好反向,一个是找最小,一个是找最大。

理解这层关系后,你会发现双指针变体题之间是能互相印证的。我强烈建议你把 88 题和 977 题放在同一天刷,对比人体会“反向填充”这个技巧在不同场景下的运用,效果远胜过一天刷十道题。

7. 常见错法实录:这些坑我都踩过

7.1 错误一:直接平方后调用 sort

这是默认解法,也是默认扣分点。很多人会说“我用 Python 一行搞定了”,但面试官会问“还有没有更好的”。如果你意识不到 O(n log n) 比 O(n) 差,这才是真正的失分点。

7.2 错误二:先找“离0最近”的分界点,再往两边扩散

这个思路更像归并排序,理论正确,实现起来容易出 bug:

  • 如果所有数都是负数,分界点在右端外面。
  • 如果所有数都是正数,分界点在左端外面。
  • 如果有重复的零,分界点选择会让指针处理逻辑复杂化。

我见过一个同学写这个思路,为了处理全正全反边界,加了四五个 if,最后还是 WA。这种方案的时间复杂度也是 O(n),但代码复杂度高太多。面试时最优解已经足够,没有理由选更绕的。

7.3 错误三:正向双指针(试图找最小)

前面分析过,正向找最小会失败,因为最小值不一定在两端。如果题目数组本身全是正数,正向双指针也能过,但一旦有负数,测试必然失败。这属于思路根本性错误,建议想清楚“两端最大值”和“中间最小值”的对称性。

7.4 错误四:把pos--写成pos++

这种低级 bug 在代码量大的时候很容易出现。运行时表现是结果数组前几个值是 0,后面正确,或者数组越界。建议写完代码后用[-4,-1,0,3,10]这个最经典例子在纸上推演一遍,一两个小样例就能暴露这种错误。

7.5 错误五:忽略重复元素与零

[-1,-1,0,0,1]这种输入,双指针完全没有问题,但如果有人习惯性在比较相等值时把两边指针同时移动,就会漏掉元素。正确做法是每次只移动一侧指针,而且一边指针动、另一边不动,这样才能保证每个元素都被处理。

零的平方是 0,作为最小值必然出现在结果数组开头,但它可能在数组中间位置。双指针反向填充时,零一定会被最后填到 result[0](如果只有一个)或前几个位置(如果有多个)。只要循环条件用<=,一次性处理所有零也没问题。

8. 变体与追问:一口气把这道题吃透

8.1 变体一:平方后按递减顺序输出

要实现平方后递减输出,最简单的改法是把反向填充改成正向填充?不对,我们来理一理。

如果按从大到小输出,那本质上和“找最大值放前面”一致。所以我们只需要把结果数组从前往后填,每次比较两端,谁大放谁,然后移动对应指针。代码几乎一样,但pos从 0 递增。

func sortedSquaresDesc(nums []int) []int { n := len(nums) ans := make([]int, n) left, right := 0, n-1 pos := 0 for left <= right { lv := nums[left] * nums[left] rv := nums[right] * nums[right] if lv > rv { ans[pos] = lv left++ } else { ans[pos] = rv right-- } pos++ } return ans }

这个变体在面试里不一定直接考,但能体现你“掌握了模式而非背模板”。因为很多题问“最大K个元素”,你会条件反射想到两端比较。

8.2 变体二:不是平方,是立方、立方根甚至任意递增变换

如果题目变成“对有序数组的立方做排序”,结论还一样吗?

结果仍然一样。立方函数在整个实数域上是单调递增的,所以[-2,-1,0,1,2]的立方是[-8,-1,0,1,8],仍然有序,根本不用双指针。真正麻烦的是非单调变换,比如平方本身。再比如“有序数组乘以一个正负比例系数后的排序”,本质上取决于变换是否保持“关于原点的对称性”。

如果变换是单调函数,直接遍历就行;如果变换让负数变大、正数也变大(典型就是平方、取绝对值、乘 -3 后再平方等),那么原数组的正负交界两侧会变成两个方向相反的单调序列,双指针合并的思路就适用。

8.3 追问:如果输入不是数组,而是两个有序数组呢

面试官如果把这题改造成“两个有序数组分别平方后合并成一个有序数组”,那你其实是在做标准的归并。这种情况下双指针指向两个数组的头部,比大小然后前移,重点从“两端极值竞争”变成“两数组头部竞争”。

我的建议是,在解完 977 后,主动跟面试官聊聊这个变形:把负数平方后的递减序列和正数平方后的递增序列分别看成两个有序数组,问题立刻变成两个有序数组合并问题。这个角度能让面试官觉得你有系统化思维,而不只是刷过题。

9. 刷题习惯与复盘建议:这道题值得你花一下午吗

9.1 不是背代码,而是背“决策模型”

很多刷题指南会把 977 列在“双指针入门题”清单里,和 167“两数之和 II”、283“移动零”并列。这几道题合在一起,你应该提炼一个决策模型:什么情况下用双指针?两个线索:

  • 目标与数组两端的值有关(最大、最小、和等于 target、差等于 target)。
  • 数组本身有序(有序是前提,但没有序可能可以排序后构造)。

用这个模型回头验证 977:数组有序(前提成立),目标是找平方后的最大/最小值(与两端值有关)。所以双指针直接用。这种归纳能力,才是刷题带给你的长期红利。

9.2 一道题至少用三种语言写一遍

我个人不太赞成只刷数量不刷质量。像 977 这种短小精悍的题,很适合你用三四种语言各写一遍,对比细节差异。我的体会是:

  • 用 Go 写一遍,你开始注意指针和切片的索引边界。
  • 用 Rust 写一遍,你学会处理 usize 下溢问题。
  • 用 Python 写一遍,你用列表推导式和 while 循环做对比。
  • 用 C++ 写一遍,你会强迫自己注意溢出。

这样一轮下来,一道简单题的时间可能比你刷五个中等题还长,但记忆牢固程度和融会贯通的程度是刷题数量的好几倍。很多算法大神的刷题方法就是反复打磨经典题,而非贪多嚼不烂。

9.3 进一步扩展:带上“平方”的延续学习

题目标题是“有序数组的平方”,你可以连续刷下面这些题加深理解:

    1. 长度最小的子数组:同样利用双指针,但属于滑动窗口模式。
    1. 合并两个有序数组:反向填充,双指针。
    1. 两数之和 II - 输入有序数组:两端夹逼,双指针。
    1. 移除元素:快慢指针。

这几道题都离不开“数组有序”或“部分有序”这个大前提。从 977 出发,慢慢把双指针家族串起来,形成你自己的知识图谱,比照着题解抄一百题有用得多。

10. 面试中如何回答这道题?

10.1 最优回答路径

如果面试官直接抛出 977,可以按这样的节奏回答:

  1. 先复述题目,确认边界(数组长度、有无负数、能否修改原数组)。
  2. 给出直觉:因为原数组有序,平方值最大的候选只可能出现在数组两端。
  3. 提出双指针从两端向中间扫描,结果数组从后往前填充,时间复杂度 O(n),空间 O(n)。
  4. 追问空间时,说明可以原地操作,但通常不需要(因为题目允许新开数组)。
  5. 最后补一句:如果需要稳定排序且要求保留原数组位置,那就另当别论——但这里没有这个要求。

按这个节奏,即使你代码还有小 bug,整体表达能力已经拿到分了。面试官不会苛求一次 bug-free,但会很看重思路的清晰度。

10.2 常见的“低级失误”能否靠测试样例救回

这题给两个测试样例就能覆盖大多数逻辑:

  • 标准混合样例:[-4,-1,0,3,10]
  • 全负数样例:[-5,-3,-2](期望[4,9,25])
  • 全正数样例(边界):[1,2,3](期望[1,4,9])
  • 重复零样例:[-1,0,0,1]

我实测过,如果代码写错方向(比如正向填充),全正数样例反而能过,混合样例就会输出错乱。所以刷题时给自己列一个小样例集,覆盖正数、负数、零、重复值几种情况,基本能防住大部分手滑 bug。

10.3 在对抗与追问中稳住心态

面试官有时会故意反问:“为什么你觉得两端平方值里一定有一个是最大的?”这个问题确实是初学双指针的同学最容易卡住的。其实只要拿[-1, 2]这种极端例子反问自己就知道了:总共两个元素,最大平方值只能是其中一个端。更多元素时,任意内部元素 x 满足x <= max(|left|, |right|)(因为原数组有序),所以平方之后内部元素不可能超出两端平方值的较大者。这个论证用通俗话说就是:“离原点最远的点一定在线段两端。”这个直觉用几何解释特别有说服力,我曾在面试现场画了一个数轴,面试官连连点头。

11. 最后的个人实操心得

这道 977,我现在面试别人时也会考,而且考得还挺频繁。有件事很有意思:很多候选人能背出双指针解法,但你问他“为什么不是先找最小值位置”,他会愣住。这说明一些人刷题是“记住答案”,而不是“理解决策树”。所以这篇文章我花了很多篇幅讲“为什么是两端找最大、反向填充”,就是希望你看完后能成为那个“愣住几秒后给出完整分析”的候选人。

我自己在实际写这道题时,有个小习惯:在变量命名时用left/right/pos而不是i/j/k。名字一旦像表达含义,读代码、Debug 都会更顺。我甚至见过有人因为pos命名成p,结果写出p--和left++弄混的低级错误。

另外,遇到这类题,我强烈建议你准备一个“答题笔记本”,每次总结出一种模式,就配上两道代表题。双指针这个模式我记了大概二十道题,其中 977 和 88 是我用来反向填充思想的两个锚点。之后遇到任何需要“从后往前填结果数组”的题目,我都会先回忆这两道题。

最后再分享一个实战技巧:面试时如果时间紧张,写完代码先手动跑一遍[-4,-1,0,3,10],用口头描述每轮两个指针的指向和 pos 的填充值。这个过程大概三十秒,但能让你在面试官喊停之前自己发现大多数逻辑错误。我个人靠这个小动作,在面试里避免过至少三处低级 bug。这种“光写代码、不推演”的习惯,其实才是很多所谓“临场翻车”的真相。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 2:45:49

HarmonyOS 7 zod:远程配置热更新Schema迁移与回滚

一、把开关下发成功&#xff0c;当成配置生效成功 FlagCanaryLab 起初是为了验证首页信息流的灰度开关。服务端下发 version43&#xff0c;客户端日志打印 200&#xff0c;页面也显示“更新成功”。37 秒后&#xff0c;实验组冻屏率从 0.4% 抬到 1.8%&#xff0c;自动回滚逻辑却…

作者头像 李华
网站建设 2026/10/8 2:45:48

LangChain智能体监控必知:LangSmith告警配置与容错实战

在做LangChain智能体开发时&#xff0c;我吃过最大的亏不是模型效果不好&#xff0c;而是“出了严重问题但没人知道”。有一次线上一个客服智能体在半夜突然开始反复调用同一个搜索工具&#xff0c;每次走完十几步工具链又回到原点&#xff0c;生成了一整屏看似正常实则无用的回…

作者头像 李华
网站建设 2026/10/8 2:45:29

基于TCP/IP的拧紧枪通讯控制:架构、协议与上位机实现

简介&#xff1a;面向工业自动化设备控制的C# Winform资源包&#xff0c;聚焦如何通过TCP/IP通信与OpenProtocol协议实现拧紧枪的远程操控。资源以Atlas拧紧控制示例为核心&#xff0c;涵盖Socket建立连接、控制指令构建、CRC校验、异步收发及UI交互等关键环节&#xff0c;适合…

作者头像 李华
网站建设 2026/10/8 2:45:26

认知无线电与随机梯度迭代:动态干扰环境下的智能发射参数优化

1. 这个项目到底想优化什么1.1 认知二字拆开看&#xff1a;从“盲发”到“边看边发”我最近在整理一个无线通信方向的优化项目&#xff0c;标题写的是“认知 随机梯度迭代算法优化智能干扰”&#xff0c;翻译成人话就是&#xff1a;在电磁环境不断变化的场景里&#xff0c;让设…

作者头像 李华
网站建设 2026/10/8 2:45:16

电子齿轮比计算与设置详解:从原理到实战避坑指南

1. 从一次“飞车”事故聊起&#xff1a;这东西到底解决什么问题搞过数控设备调试或者伺服系统维护的朋友&#xff0c;大概率都见过这么一幕&#xff1a;明明指令只给了1毫米的位移&#xff0c;电机却“嗡”地一声带着负载冲出去老远&#xff0c;要么撞上限位&#xff0c;要么直…

作者头像 李华
网站建设 2026/10/8 2:44:59

PCB数据集构建指南:从设计意图到产线落地的工程化实践

1. 这不是一张图&#xff0c;而是一套“PCB视觉理解”的基础设施你手头那张刚导出的Gerber文件截图&#xff0c;或者嘉立创EDA里拖拽完成的板子预览图——它本身不是数据集。真正的PCB数据集&#xff0c;是把成百上千块真实电路板的设计意图、制造约束、物理缺陷、电气特性&…

作者头像 李华