977. 有序数组的平方
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
提示:
1 <= nums.length <= 10^4-10^4 <= nums[i] <= 10^4nums已按 非递减顺序 排序
进阶:
- 请你设计时间复杂度为
O(n)的算法解决本问题
第一阶段:最直观的解法(先平方,后排序)
思考过程
当我们刚看到这个问题时,最直接的想法就是完全按照题目的描述来做:
- 第一步: "返回每个数字的平方组成的新数组" -> 好的,那我们就创建一个新数组,遍历输入的
nums数组,把每个元素的平方计算出来,放进新数组里。 - 第二步: "要求也按非递减顺序排序" -> 好的,我们把刚才得到那个全是平方数的新数组,调用一个排序函数(比如 Java 的
Arrays.sort())给它排序。 - 完成: 返回这个排好序的数组。
这个思路的代码会是这样:
// 思路1:先平方,再排序
public int[] sortedSquares_Naive(int[] nums) {
int n = nums.length;
int[] result = new int[n];
// 步骤1:计算每个数字的平方
for (int i = 0; i < n; i++) {
result[i] = nums[i] * nums[i];
}
// 步骤2:对平方后的数组进行排序
Arrays.sort(result);
return result;
}
评估与反思
- 正确性: 这个方法是完全正确的,能得到正确答案。
- 效率: 计算平方的循环是 O(n) 的,但
Arrays.sort()的时间复杂度是 O(n log n)。所以,整个算法的瓶颈在排序,总时间复杂度是 O(n log n)。 - 问题: 题目“进阶”部分要求我们设计一个 O(n) 的算法。这说明 O(n log n) 不是最优解。我们一定遗漏了什么关键信息。
第二阶段:寻找 O(n) 解法的突破口
思考过程
O(n log n) 的解法之所以慢,是因为它没有利用到一个非常重要的已知条件:输入的 nums 数组本身是已经排好序的!
我们来看看平方后,数组的顺序是怎么被打乱的:
输入: [-4, -1, 0, 3, 10]
平方后: [16, 1, 0, 9, 100]
观察平方后的数组,我们能发现什么规律?
- 负数部分的平方
[-4, -1]->[16, 1]是一个递减的序列。 - 非负数部分的平方
[0, 3, 10]->[0, 9, 100]是一个递增的序列。
核心洞察: 对于一个按非递减顺序排序的数组,平方后最大的值一定产生于数组的两端!也就是绝对值最大的那些数。
- 对于
[-4, -1, 0, 3, 10],绝对值最大的是10和-4。10^2 = 100,(-4)^2 = 16。最大的平方数100来自于最右边的10。 - 对于
[-7, -3, 2, 3, 11],绝对值最大的是11和-7。11^2 = 121,(-7)^2 = 49。最大的平方数121来自于最右边的11。
这就像是两组有序的牌(一组是负数平方后的递减序列,一组是正数平方后的递增序列),我们要将它们合并成一个有序序列。
第三阶段:双指针法(O(n) 解法)
既然最大的平方值总是出现在原数组的两端,我们可以利用这个特性来直接构建一个排好序的结果数组,而无需在最后进行整体排序。
思考过程
- 指针: 我们可以设置两个指针,一个
left指向原数组的开头(绝对值可能很大的负数),另一个right指向原数组的结尾(正数,绝对值也可能很大)。 - 结果数组: 我们创建一个新的结果数组
result。既然我们每次都能从原数组的两端找出当前最大的平方值,那么我们就可以从后往前填充这个result数组。我们用另一个指针p指向result数组的末尾。 - 比较与填充:
- 比较
nums[left]的平方和nums[right]的平方。 - 哪个更大,就把它放到
result[p]的位置。 - 然后移动相应的指针(如果
nums[left]的平方更大,就移动left;否则移动right)。 - 同时,将
p向前移动一位。
- 比较
- 循环: 重复这个过程,直到
left和right指针相遇或交错。
Java 实现
import java.util.Arrays; // 仅用于 main 方法的测试输出
import java.util.Scanner; // 用于 ACM 模式
public class Main { // ACM 模式类名通常为 Main
public static void main(String[] args) {
// ACM 模式的输入处理
Scanner scanner = new Scanner(System.in);
String line = scanner.nextLine();
String[] parts = line.trim().split("\\s+"); // 按空白符分割
int[] nums = new int[parts.length];
if (parts.length == 1 && parts[0].isEmpty()) {
nums = new int[0]; // 处理空行输入
} else {
for (int i = 0; i < parts.length; i++) {
nums[i] = Integer.parseInt(parts[i]);
}
}
scanner.close();
// 调用核心算法
int[] result = sortedSquares(nums);
// ACM 模式的输出处理
StringBuilder sb = new StringBuilder();
for (int i = 0; i < result.length; i++) {
sb.append(result[i]);
if (i < result.length - 1) {
sb.append(" ");
}
}
System.out.println(sb.toString());
}
/**
* LeetCode 977: 有序数组的平方
* 使用双指针法,在 O(n) 时间复杂度和 O(n) 空间复杂度下解决问题。
*
* @param nums 按非递减顺序排序的整数数组
* @return 每个数字的平方组成的新数组,也按非递减顺序排序
*/
public static int[] sortedSquares(int[] nums) {
if (nums == null) {
return null;
}
int n = nums.length;
// 创建一个用于存放结果的新数组
int[] result = new int[n];
// --- 双指针初始化 ---
// left 指向数组开头,right 指向数组末尾
int left = 0;
int right = n - 1;
// p 指向结果数组的末尾,用于从后往前填充
int p = n - 1;
// --- 循环填充结果数组 ---
// 当 left 指针没有超过 right 指针时继续
while (left <= right) {
// 计算左右指针指向元素的平方
int leftSquare = nums[left] * nums[left];
int rightSquare = nums[right] * nums[right];
// 比较两端的平方值
if (leftSquare > rightSquare) {
// 如果左边的平方更大,则将其放入结果数组的末尾
result[p] = leftSquare;
// 左指针向中间移动
left++;
} else {
// 如果右边的平方更大或相等,则将其放入结果数组的末尾
result[p] = rightSquare;
// 右指针向中间移动
right--;
}
// 结果数组的填充位置向前移动
p--;
}
// 循环结束后,result 数组就已经被填充完毕并且是有序的
return result;
}
}
总结:
我们从一个显而易见但不够高效的 O(n log n) 解法开始,通过分析平方后数据的内在结构(两端大,中间小),找到了问题的突破口,最终设计出了一个利用双指针、一次遍历就完成所有工作的 O(n) 解法。这种从“暴力解 -> 分析特性 -> 寻找优化 -> 设计高效算法”的流程是解决很多算法问题的通用模式。