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

1. 问题描述
给你一个按 非递减顺序 排序的整数数组 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 <= 104104 <= nums[i] <= 104nums已按 非递减顺序 排序
进阶:
- 请你设计时间复杂度为
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 题解思路
- 方法一:和我上面的大致思路一样,先平方后排序
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. 补充知识と杂谈,总结
看来应该还是稍微多思考一下,不要想着我去题没见过,我感觉没什么思路就马上放弃。
其实很多算法思路之前可能无意中用过,但是没有具体定下来名字或者总结

浙公网安备 33010602011771号