有序数组的平方:双指针比排序快 10 倍的秘密
从一个反直觉的现象说起
你有没有想过:把一个有序数组每个元素平方后,结果还是有序的吗?
直觉告诉你——当然不是。-4 平方是 16,-1 平方是 1,16 > 1,顺序不就乱了吗?
但如果你仔细观察这个数组:-4, -1, 0, 3, 10
平方后:16, 1, 0, 9, 100
排序后:0, 1, 9, 16, 100
答案取决于你的数组长什么样——如果原数组包含负数,平方后负数会变成大正数,整体顺序确实会乱套。
LeetCode 第 977 题正是围绕这个场景展开:给你一个非递减顺序排序的整数数组,返回每个元素平方后组成的新数组,同样按非递减顺序排序。
最容易想到的解法:先平方再排序
let nums = [-4, -1, 0, 3, 10];
let new_nums = nums.map(n => n * n); // 先平方
new_nums.sort((a, b) => a - b); // 再排序
console.log(new_nums); // [0, 1, 9, 16, 100]
复杂度:平方 O(n) + 排序 O(n log n),总体 O(n log n)。
这没问题,简单直接,能通过大部分测试。但这道题真正想考你的,是能否做到 O(n)。
真正优雅的解法:双指针从两端向中间
核心洞察
观察平方后的规律:
| 原数组 | -4 | -1 | 0 | 3 | 10 |
|---|---|---|---|---|---|
| 平方后 | 16 | 1 | 0 | 9 | 100 |
两端是最大的两个数! 因为绝对值越大,平方越大。
所以我们可以从数组两端开始,把较大的那个平方后放到结果数组的末尾,然后收缩指针,继续比较。
图解过程
原数组: [-4, -1, 0, 3, 10]
↑ ↑
left right
Step 1: 10²=100 > 4²=16 → 放末尾 → [?, ?, ?, ?, 100]
Step 2: 3²=9 > 4²=16 → 放倒数第二 → [?, ?, ?, 9, 100]
Step 3: 4²=16 > 1²=1 → 放倒数第三 → [?, ?, 16, 9, 100]
Step 4: 1²=1 > 0²=0 → 放倒数第四 → [?, 1, 16, 9, 100]
Step 5: 0²=0 → 放第一位 → [0, 1, 16, 9, 100]
最终代码
// LeetCode 标准函数签名
var sortedSquares = function(nums) {
let new_nums = [];
let k = nums.length - 1; // 结果数组的写入位置(从后往前)
let left = 0; // 左指针(负数端)
let right = nums.length - 1; // 右指针(非负数端)
while (k >= 0) {
let leftSquare = nums[left] * nums[left];
let rightSquare = nums[right] * nums[right];
if (leftSquare > rightSquare) {
new_nums[k] = leftSquare;
left++;
} else {
new_nums[k] = rightSquare;
right--;
}
k--;
}
return new_nums; // [0, 1, 9, 16, 100]
};
关键点:
- 结果数组从右往左填入,每次
k-- left指向最小值(左端),right指向最大值(右端)- 谁平方大,谁就放到
new_nums[k]的位置
为什么它快?
| 解法 | 时间复杂度 | 空间复杂度 | 思路 |
|---|---|---|---|
| 平方+排序 | O(n log n) | O(1) 或 O(n) | 暴力枚举 |
| 双指针 | O(n) | O(n) | 贪心策略 |
当 n = 1,000,000 时,双指针比排序快约 20 倍。这在面试中是绝对的加分项。
一句话总结
有序数组平方后,最小的在中间,最大的在两端。双指针从两端向中间走,把大的平方从后往前填,就是天然的排序。
这道题是双指针思想的经典入门题,核心在于利用数组本身的有序性——不需要额外的排序算法,只要动动指针,就能 O(n) 搞定。
学会这道题,你会发现后续很多类似题目(合并有序数组、盛水容器等)都是这个思路的变体。
相关题目推荐:
- 88. 合并两个有序数组
-
- 两数之和 II - 输入有序数组
-
- 盛最多水的容器