LeetCode 977. 有序数组的平方

100 阅读6分钟

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^4
  • nums 已按 非递减顺序 排序

进阶:

  • 请你设计时间复杂度为 O(n) 的算法解决本问题

第一阶段:最直观的解法(先平方,后排序)

思考过程

当我们刚看到这个问题时,最直接的想法就是完全按照题目的描述来做:

  1. 第一步: "返回每个数字的平方组成的新数组" -> 好的,那我们就创建一个新数组,遍历输入的 nums 数组,把每个元素的平方计算出来,放进新数组里。
  2. 第二步: "要求也按非递减顺序排序" -> 好的,我们把刚才得到那个全是平方数的新数组,调用一个排序函数(比如 Java 的 Arrays.sort())给它排序。
  3. 完成: 返回这个排好序的数组。

这个思路的代码会是这样:

// 思路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-410^2 = 100, (-4)^2 = 16。最大的平方数 100 来自于最右边的 10
  • 对于 [-7, -3, 2, 3, 11],绝对值最大的是 11-711^2 = 121, (-7)^2 = 49。最大的平方数 121 来自于最右边的 11

这就像是两组有序的牌(一组是负数平方后的递减序列,一组是正数平方后的递增序列),我们要将它们合并成一个有序序列。


第三阶段:双指针法(O(n) 解法)

既然最大的平方值总是出现在原数组的两端,我们可以利用这个特性来直接构建一个排好序的结果数组,而无需在最后进行整体排序。

思考过程
  1. 指针: 我们可以设置两个指针,一个 left 指向原数组的开头(绝对值可能很大的负数),另一个 right 指向原数组的结尾(正数,绝对值也可能很大)。
  2. 结果数组: 我们创建一个新的结果数组 result。既然我们每次都能从原数组的两端找出当前最大的平方值,那么我们就可以从后往前填充这个 result 数组。我们用另一个指针 p 指向 result 数组的末尾。
  3. 比较与填充:
    • 比较 nums[left] 的平方和 nums[right] 的平方。
    • 哪个更大,就把它放到 result[p] 的位置。
    • 然后移动相应的指针(如果 nums[left] 的平方更大,就移动 left;否则移动 right)。
    • 同时,将 p 向前移动一位。
  4. 循环: 重复这个过程,直到 leftright 指针相遇或交错。
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) 解法。这种从“暴力解 -> 分析特性 -> 寻找优化 -> 设计高效算法”的流程是解决很多算法问题的通用模式。