[LeetCode] 977. 有序数组的平方

img

1. 问题描述

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 <= 104
  • 104 <= nums[i] <= 104
  • nums 已按 非递减顺序 排序

进阶:

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

2. 实现思路

2.1 自己的思路

无论实现与否,都是群星的一丝痕迹~~(唔,所以一定要写出来啊喂)~

非递减顺序特别加粗了,那么感觉一定有什么特别含义(暂时想不出来

按照示例1的解释,其实已经提供了一种思路,先将数组原地平方,然后排序即可,刚好顺便复习一下快排和归并排序

快排:先确立基准点,然后左右两段排序,最后递归

复杂度类型 平均情况 最坏情况
时间复杂度 O(n log n) O(n²)
空间复杂度 O(log n) O(n)
public class Solution
{
    public int[] SortedSquares(int[] nums)
    {
        for (int i = 0; i < nums.Length; i++)
            nums[i] *= nums[i];
        QuickSort(nums, 0, nums.Length - 1);
        return nums;
    }

    private void QuickSort(int[] nums, int l, int r)
    {
        if (l >= r) return;
        int i = l - 1;
        int j = r + 1;
        // 基准点
        int pivot = nums[(l + r) >> 1];

        while (i < j)
        {
            do { i++; } while (nums[i] < pivot);
            do { j--; } while (nums[j] > pivot);
            if (i < j)
            {
                (nums[i], nums[j]) = (nums[j], nums[i]);
            }
        }

        QuickSort(nums, l, j);
        QuickSort(nums, j + 1, r);
    }
}

归并:先确定mid索引,然后递归左右两侧排序,最后合并

复杂度类型 值
时间复杂度 O(n log n)
空间复杂度 O(n)
public class Solution
{
    public int[] SortedSquares(int[] nums)
    {
        for (int i = 0; i < nums.Length; i++)
            nums[i] *= nums[i];
        MergeSort(nums, new int[nums.Length], 0, nums.Length - 1);
        return nums;
    }

    private void MergeSort(int[] nums, int[] temp, int l, int r)
    {
        if (l >= r) return;
        int mid = l + (r - l) / 2;

        MergeSort(nums, temp, l, mid);
        MergeSort(nums, temp, mid + 1, r);

        // 合并
        int k = 0;
        int i = l;
        int j = mid + 1;
        while (i <= mid && j <= r)
        {
            if (nums[i] <= nums[j])
                temp[k++] = nums[i++];
            else
                temp[k++] = nums[j++];
        }

        while (i <= mid)
            temp[k++] = nums[i++];
        while (j <= r)
            temp[k++] = nums[j++];

        for (i = l, k = 0; i <= r; i++, k++)
            nums[i] = temp[k];
    }
}

2.2 题解思路

977. 有序数组的平方 - 力扣(LeetCode)

  • 方法一:和我上面的大致思路一样,先平方后排序
class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> ans;
        for (int num: nums) {
            ans.push_back(num * num);
        }
        sort(ans.begin(), ans.end());
        return ans;
    }
};

复杂度分析

时间复杂度:O(nlogn),其中 n 是数组 nums 的长度。

空间复杂度:O(logn)。除了存储答案的数组以外,我们需要 O(logn) 的栈空间进行排序。

1. 时间复杂度:O(n log n)

时间复杂度遵循“加法法则”,即总时间等于各个步骤耗时之和,我们取增长最快的部分。

  • 第一步(平方遍历):for (int num: nums) 循环会遍历原数组中的 n 个元素。循环体内执行的是乘法 num * num 和 push_back(向末尾添加元素,这一步均摊时间复杂度为 O(1))。

    这一步的总耗时是 O(n)。

  • 第二步(排序):代码调用了 sort(ans.begin(), ans.end())。在 C++ 标准库中,std::sort 的实现通常是内省排序(Introspective Sort),它是快速排序、堆排序和插入排序的混合体。无论数据分布如何,它的最坏时间复杂度都被严格保证为 O(n log n)。

  • 总时间:总耗时 = O(n) + O(n log n)。

    根据复杂度的取舍原则,当 n 足够大时,n log n 的增长速度远快于 n,因此低阶项 O(n) 被忽略,最终时间复杂度为 O(n log n)。


2. 空间复杂度:O(log n) —— 这里特指 额外空间

你可能会疑惑:“我明明新建了一个 vector<int> ans 存了 n 个数,为什么空间不是 O(n) 而是 O(log n)?”

这里的关键在于题目通常只计算“除了存储答案(或输入)以外的额外辅助空间”。

  • 答案数组 ans:它的确占用了 O(n) 的空间。但在 LeetCode 或绝大多数算法面试中,用于存储返回结果的数组是不计入空间复杂度分析的(因为这是必须返回给调用者的,属于“产出”而非“消耗”)。所以这里把它排除在外。

  • 排序过程中的额外消耗:std::sort 采用的是递归实现(内省排序)。每一层递归调用都会在栈上压入一些指针或整型参数(如左右边界)。递归的深度决定了栈空间的大小。

    由于排序算法每次都将数组对半划分,递归树的深度为 log₂ n。

    因此,除了 ans 本身,排序过程额外占用的栈空间仅为 O(log n)。

  • 动态扩容的细微影响:在 push_back 过程中,ans 的容量可能会翻倍扩容,在扩容瞬间会存在一份短暂的内存拷贝。但这份空间属于 ans 本身的容量开辟,最终容量依然是 O(n),且不属于“额外辅助空间”范畴,通常分析时忽略不计。

  • 方法二:双指针

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。
  • 空间复杂度:O(1)。除了存储答案的数组以外,我们只需要维护常量空间。

思路与算法

方法一没有利用「数组 nums 已经按照升序排序」这个条件。显然,如果数组 nums 中的所有数都是非负数,那么将每个数平方后,数组仍然保持升序;如果数组 nums 中的所有数都是负数,那么将每个数平方后,数组会保持降序。

这样一来,如果我们能够找到数组 nums 中负数与非负数的分界线,那么就可以用类似「归并排序」的方法了。具体地,我们设 neg 为数组 nums 中负数与非负数的分界线,也就是说,nums[0] 到 nums[neg] 均为负数,而 nums[neg+1] 到 nums[n−1] 均为非负数。当我们将数组 nums 中的数平方后,那么 nums[0] 到 nums[neg] 单调递减,nums[neg+1] 到 nums[n−1] 单调递增。

由于我们得到了两个已经有序的子数组,因此就可以使用归并的方法进行排序了。具体地,使用两个指针分别指向位置 neg 和 neg+1,每次比较两个指针对应的数,选择较小的那个放入答案并移动指针。当某一指针移至边界时,将另一指针还未遍历到的数依次放入答案。

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int n = nums.size();
        int negative = -1;
        // 确定negative位置
        for (int i = 0; i < n; ++i) {
            if (nums[i] < 0) {
                negative = i;
            } else {
                break;
            }
        }

        // 合并
        vector<int> ans;
        int i = negative, j = negative + 1;
        while (i >= 0 || j < n) {
            // 如果左半段(负数的一侧)放完了,直接放右半段
            if (i < 0) {
                ans.push_back(nums[j] * nums[j]);
                ++j;
            }
            // 同上
            else if (j == n) {
                ans.push_back(nums[i] * nums[i]);
                --i;
            }
            // 比较放较小值
            else if (nums[i] * nums[i] < nums[j] * nums[j]) {
                ans.push_back(nums[i] * nums[i]);
                --i;
            }
            else {
                ans.push_back(nums[j] * nums[j]);
                ++j;
            }
        }

        return ans;
    }
};
  • 方法三:双指针

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。
  • 空间复杂度:O(1)。除了存储答案的数组以外,我们只需要维护常量空间。

思路与算法

同样地,我们可以使用两个指针分别指向位置 0 和 n−1,每次比较两个指针对应的数,选择较大的那个逆序放入答案并移动指针。这种方法无需处理某一指针移动至边界的情况。

为什么呢?

因为数组是非递减排列的,那么数组左右两端的平方相比较,所取得的较大值一定是整个数组的最大值

那么我们就可以将左右两侧的指针相向而行,不断获取最大值放在结果数组的末尾即可

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

作者:力扣官方题解
链接:https://leetcode.cn/problems/squares-of-a-sorted-array/solutions/447736/you-xu-shu-zu-de-ping-fang-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出

2.3 优化(或者敲一遍题解?)

理解了思路后,先来方法二试试。

在实现过程中,对比区域代码块内的left和right写反了(是非递减排序啊喂)

然后第二个坑点是全写成if导致的数组越界,应该是else if和else(

public class Solution
{
    public int[] SortedSquares(int[] nums)
    {
        // 确定neg位置
        int neg = -1;
        for (int i = 0; i < nums.Length; i++)
            if (nums[i] < 0) neg = i;
        int[] ans = new int[nums.Length];
        // 双指针
        int j = 0;
        int left = neg;
        int right = neg + 1;
        while (left >= 0 || right < nums.Length)
        {
            // 左半段放完了
            if (left < 0)
            {
                ans[j] = nums[right] * nums[right];
                right++;
            }
            // 右半段放完了
            else if (right == nums.Length)
            {
                ans[j] = nums[left] * nums[left];
                left--;
            }
            // 对比
            else if (nums[left] * nums[left] > nums[right] * nums[right])
            {
                ans[j] = nums[right] * nums[right];
                right++;
            }
            else
            {
                ans[j] = nums[left] * nums[left];
                left--;
            }
            j++;
        }
        return ans;
    }
}

好,接着自己按照回忆思考一下方法三,核心是平方后,比较数组两端就能获取最大值,因此双指针相向而行即可

在实现过程中的坑是while里left应该≤right而非<

public class Solution
{
    public int[] SortedSquares(int[] nums)
    {
        int[] ans = new int[nums.Length];
        int left = 0;
        int right = ans.Length - 1;
        int pos = ans.Length - 1;
        while (left <= right)
        {
            if (nums[left] * nums[left] < nums[right] * nums[right])
            {
                ans[pos] = nums[right] * nums[right];
                right--;
            }
            else
            {
                ans[pos] = nums[left] * nums[left];
                left++;
            }
            pos--;
        }
        return ans;
    }
}

3. 补充知识と杂谈,总结

看来应该还是稍微多思考一下,不要想着我去题没见过,我感觉没什么思路就马上放弃。

其实很多算法思路之前可能无意中用过,但是没有具体定下来名字或者总结

posted @ 2026-08-10 10:34  绘星tsuki  阅读(13)  评论(0)    收藏  举报