977. 有序数组的平方
[LeetCode]
简 单 \color{#00AF9B}{简单} 简单
给你一个按 非递减顺序 排序的整数数组
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)
的算法解决本问题
方法1:直接排序
这个方法的思路很简单,将数组中的每一项平方后,对整个数组进行排序。
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
vector<int> sortedSquares(vector<int> &nums)
{
vector<int> ans;
ans.reserve(nums.size());
for (int num : nums)
{
ans.emplace_back(num * num);
}
sort(ans.begin(), ans.end());
return ans;
}
};
复杂度分析
时间复杂度:O(n logn)
空间复杂度:O(logn)。存放答案的数组不算在内,我们需要O(logn)的栈空间进行排序。
参考结果
Accepted
137/137 cases passed (32 ms)
Your runtime beats 46.63 % of cpp submissions
Your memory usage beats 56.4 % of cpp submissions (25.3 MB)
方法2:双指针
我们可以设置两个指针left
和right
分别指向数组的开头和末尾。每次循环中,比较nums[left]^2
和nums[right]^2
的大小,将最大的那个值逆序放入答案数组的末尾,随后改变相应的指针。
过程演示:
#include <vector>
using namespace std;
class Solution
{
public:
vector<int> sortedSquares(vector<int> &nums)
{
int n = nums.size();
vector<int> ans(n);
for (int left = 0, right = n - 1, k = n - 1; left <= right; k--)
{
int a = nums[left] * nums[left], b = nums[right] * nums[right];
if (a > b)
{
ans[k] = a;
left++;
}
else
{
ans[k] = b;
right--;
}
}
return ans;
}
};
复杂度分析
时间复杂度:O(n)
空间复杂度:O(1)。存放答案的数组不算在内,我们只需要常量空间存放若干变量。
参考结果
Accepted
137/137 cases passed (24 ms)
Your runtime beats 85.42 % of cpp submissions
Your memory usage beats 78.88 % of cpp submissions (25.2 MB)
189. 轮转数组
中 等 \color{#FFB800}{中等} 中等
给你一个数组,将数组中的元素向右轮转
k
个位置,其中k
是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
提示:
- 1 <= nums.length <= 105
- -231 <= nums[i] <= 231 - 1
- 0 <= k <= 105
进阶:
- 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
- 你可以使用空间复杂度为
O(1)
的 原地 算法解决这个问题吗?
注意
- 题目要求为在原数组上进行修改,并非返回一个新的数组。
- 题意为:将数组
nums
整体向右移动k
位 k
的值可能超出数组长度。即:将数组整体移动了k/n
次,返回了最初的位置,接着移动k%n
次,得到新数组。所以在计算下标时一定不要忘记最后的结果要对n
取余。
方法1:使用额外的数组
这可能是本题最简单的思路。我们可以定义一个和原数组nums
等长的数组dummy
,将结果按序放入新数组dummy
中。最后将nums
中的所有项替换为dummy
中的数据。
按照题目意思,长度为n
的原数组中,第i项
往右移动k
位后存入新数组,则第i项
在新数组中的下标可由以下映射关系得到:
i
→
(
i
+
k
)
%
n
i \rightarrow (i+k)\%n
i→(i+k)%n
即:
d
u
m
m
y
[
(
i
+
k
)
%
n
]
=
n
u
m
s
[
i
]
dummy[(i + k) \% n] = nums[i]
dummy[(i+k)%n]=nums[i]
#include <vector>
using namespace std;
typedef unsigned int ui;
class Solution
{
public:
void rotate(vector<int> &nums, int k)
{
int n = nums.size();
vector<int> dummy(n);
for (int i = 0; i < n; i++)
{
dummy[(i + k) % n] = nums[i];
}
nums.assign(dummy.begin(), dummy.end());
}
};
复杂度分析
时间复杂度:O(n)
空间复杂度:O(n)
参考结果
Accepted
38/38 cases passed (28 ms)
Your runtime beats 49.29 % of cpp submissions
Your memory usage beats 27.54 % of cpp submissions (24.9 MB)
方法2:三次翻转
让我们以数组[1,2,3,4,5,6,7]以及k=3为例,观察一下原数组和结果:
[
1
,
2
,
3
,
4
,
5
,
6
,
7
]
[
5
,
6
,
7
,
1
,
2
,
3
,
4
]
[1,2,3,4,\color{red}{5} \color{#000000}{},\color{red}{6} \color{#000000}{},\color{red}{7} \color{#000000}{}] \\ [\color{red}{5} \color{#000000}{},\color{red}{6} \color{#000000}{},\color{red}{7} \color{#000000}{},1,2,3,4]
[1,2,3,4,5,6,7][5,6,7,1,2,3,4]
我们将结果中的左边k
项与右边的n-k
项分别翻转一下:
[
7
,
6
,
5
,
4
,
3
,
2
,
1
]
[\color{red}{7} \color{#000000}{},\color{red}{6} \color{#000000}{},\color{red}{5} \color{#000000}{},4,3,2,1]
[7,6,5,4,3,2,1]
可以发现它就是原数组的全局翻转。也就是说原数组到结果数组共经历了如下过程:
- 全局翻转
- 左边
k
项翻转 - 右边
n-k
项翻转
即:
[
1
,
2
,
3
,
4
,
5
,
6
,
7
]
↓
1
[
7
,
6
,
5
,
4
,
3
,
2
,
1
]
↓
2
[
5
,
6
,
7
,
4
,
3
,
2
,
1
]
↓
3
[
5
,
6
,
7
,
1
,
2
,
3
,
4
]
[1,2,3,4,\color{red}{5} \color{#000000}{},\color{red}{6} \color{#000000}{},\color{red}{7} \color{#000000}{}] \\ {\downarrow}_{1} \\ [\color{red}{7} \color{#000000}{},\color{red}{6} \color{#000000}{},\color{red}{5} \color{#000000}{},4,3,2,1] \\ {\downarrow}_{2} \\ [\color{red}{5} \color{#000000}{},\color{red}{6} \color{#000000}{},\color{red}{7} \color{#000000}{},4,3,2,1] \\ {\downarrow}_{3} \\ [\color{red}{5} \color{#000000}{},\color{red}{6} \color{#000000}{},\color{red}{7} \color{#000000}{},1,2,3,4]
[1,2,3,4,5,6,7]↓1[7,6,5,4,3,2,1]↓2[5,6,7,4,3,2,1]↓3[5,6,7,1,2,3,4]
这3步都是同一种过程,即从某个起始项到终止项之间进行数组的翻转。因此这3步可以看做3次函数调用。我们可以定义该函数为reverse()
。而翻转的过程可用双指针完成。
我们可以设定两个指针left
和right
分别指向起始项和终止项,每次调换这两个指针指向的值,然后各自向中间前进一个单位,直到它们指向同一个元素(一个元素没必要和自己调换)或者left
跑到了right
的右边,循环终止。由此循环的有效条件可以确定为left<right
。
#include <vector>
using namespace std;
class Solution
{
public:
void rotate(vector<int> &nums, int k)
{
int n = nums.size();
k %= n;
reverse(nums, 0, n - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, n - 1);
}
void reverse(vector<int> &nums, int left, int right)
{
while (left < right)
{
swap(nums[left], nums[right]);
left++;
right--;
}
}
};
请注意:
- 数组下标从0开始,因此对数组左边
k
项进行翻转,是从第0
项至第k-1
项进行翻转。数组右侧同理。 k
值可能大于数组长度n
,即将数组整体移动了k/n
次,然后移动k%n
次得到答案。因此此处我们要将k
置为k%n
。
复杂度分析
时间复杂度:O(n)。算上全局翻转和后面的2次各自翻转,数组中的每个元素被翻转了2次,因此时间复杂度为O(2n)=O(n)。
空间复杂度:O(1),我们只需要常量空间存放若干变量。
参考结果
Accepted
38/38 cases passed (28 ms)
Your runtime beats 49.29 % of cpp submissions
Your memory usage beats 91.94 % of cpp submissions (24.2 MB)
方法3:原地解法
方法1使用了额外的数组,使空间复杂度达到了O(n);方法2对数组中的每个元素都进行了2次遍历,时间复杂度为O(2n)=O(n)。那有没有办法使空间复杂度降为常量空间,且只需要对数组元素进行1次遍历呢?答案是有的。
我们可以从方法1进行优化。方法1中,我们对每个元素的操作都是“将元素直接放到它的最终位置上”。这个思路本身没有问题,而问题出在我们遍历元素的方法,而这也是我们用到了额外数组的根本原因。
方法1中,当第i
个元素处理完成,也就是放到(i+k)%n
位置上后,下一个待处理元素的选取策略是顺序选取,也就是第i+1
个元素。这样的策略之下,原本放在(i+k)%n
位置上的元素不仅被覆盖,而且被直接丢弃了。所以我们才需要原数组来记录每个位置上原来是什么,然后将结果写到一个新数组中。因此,若要对方法1进行优化,我们要改变的是下一个待处理元素的选取策略。
方法3开始明了起来了,我们把第i
个元素放到(i+k)%n
位置上后,覆盖了该位置上的元素。因此我们要将(i+k)%n
位置作为待处理元素,将它保存起来,放到(i+2*k)%n
的位置上,以此类推。
以nums=[1,2,3,4,5,6,7]
,k=3
为例,数组的变换过程如下:
可以发现一轮遍历即可完成数组的翻转。循环的结束条件也很简单,就是访问到了本次循环的起始位置,即数组首项。
但是让我们再关注一个例子:nums=[-1,-100,3,99]
,k=2
,可以发现一轮遍历只能交换-1
和3
。因此我们要找到一个能够使这个过程继续下去的方法。
我们先考虑这个问题:从起点0
到终点0
的过程中,一共遍历了多少元素?
由于最终回到了起点,则我们必然走了整数数量的圈,一圈长度为n
,假设走了a
圈回到了起点;再设这一轮中总共遍历了b
个元素,而每个元素之间的间隔即为k
,也就是每走一步(每走k
格找到下一个目标)。因此我们可以得出:an=bk
,a
圈的总长为an
,遍历b
个元素所走的路程为bk
。因此an
必然是n
和k
的公倍数。同时,我们要在第一次回到起点时就结束,因此an
要取最小公倍数。
如果a
不是最小公倍数:我们改变一个系数:2an=2bk
,即a
变成了原来的两倍,也就是两圈。相应地,b
也变成了两倍。这就相当于走了2圈。而这是没有必要的,我们仅需要一圈就能完成转换。多一圈会将数组转换回去。
an
是n
和k
的最小公倍数lcm(n,k),因此
b
=
l
c
m
(
n
,
k
)
k
b=\frac{lcm(n,k)}{k}
b=klcm(n,k),说明单次遍历会访问到
l
c
m
(
n
,
k
)
k
\frac{lcm(n,k)}{k}
klcm(n,k)个元素。为了访问到所有的元素,我们需要进行遍历的次数为:
n l c m ( n , k ) k = n k l c m ( n , k ) = g c d ( n , k ) \frac{n}{\frac{lcm(n,k)}{k}}=\frac{nk}{lcm(n,k)}=gcd(n,k) klcm(n,k)n=lcm(n,k)nk=gcd(n,k)
其中gcd指的是最大公约数。这是基于以下的事实:对于数a
和b
,a
和b
的乘积等于它们的最小公倍数和最大公约数之积。
a
∗
b
=
l
c
m
(
a
,
b
)
∗
g
c
d
(
a
,
b
)
a*b=lcm(a,b)*gcd(a,b)
a∗b=lcm(a,b)∗gcd(a,b)
由上述推导过程可得:交换的轮数round
为n
和k
的最大公约数(gcd),而每轮我们要交换的元素个数为:
c o u n t = l c m ( n , k ) k count=\frac{lcm(n,k)}{k} count=klcm(n,k)
我们也可以不用lcm
函数。对于最大公约数为1的情况(cound==1
),我们每轮需要交换n+1
次,也就是一轮交换完成;对于最大公约数不为1
的情况(cound!=1
),我们一轮只能交换n/round
次。
即:
c
o
u
n
t
=
{
n
+
1
r
o
u
n
d
=
1
n
/
r
o
u
n
d
r
o
u
n
d
≠
1
count= \left\{\begin{matrix} n+1 & round=1 \\ n/round & round \neq 1 \end{matrix}\right.
count={n+1n/roundround=1round=1
这里有一些其它例子的过程演示:
nums=[-1,-100,3,99]
,k=2
:
nums=[1,2,3,4,5,6]
,k=2
:
nums=[1,2,3,4,5,6]
,k=3
:
#include <vector>
#include <numeric>
using namespace std;
class Solution
{
public:
void rotate(vector<int> &nums, int k)
{
int n = nums.size();
k %= n;
if (k == 0)
return;
int round = gcd(n, k);
// int count = round == 1 ? n + 1 : n / round;
int count = lcm(n, k) / k;
for (int i = 0; i < round; i++)
{
int last = nums[i];
for (int j = 1; j <= count; j++)
{
swap(last, nums[(i + j * k) % n]);
}
}
}
};
复杂度分析
时间复杂度:O(n)。n为数组的长度,每个元素只会被遍历一次。
空间复杂度:O(1),我们只需要常量空间存放若干变量。
参考结果
Accepted
38/38 cases passed (20 ms)
Your runtime beats 92.01 % of cpp submissions
Your memory usage beats 98.33 % of cpp submissions (24.2 MB)
Animation powered by ManimCommunity/manim