【LeetCode学习计划】《算法-入门-C++》第2天 双指针



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:双指针

​我们可以设置两个指针leftright分别指向数组的开头和末尾。每次循环中,比较nums[left]^2nums[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. 轮转数组

LeetCode

中 等 \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)原地 算法解决这个问题吗?

注意

  1. 题目要求为在原数组上进行修改,并非返回一个新的数组。
  2. 题意为:将数组nums整体向右移动k
  3. 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]
可以发现它就是原数组的全局翻转。也就是说原数组到结果数组共经历了如下过程:

  1. 全局翻转
  2. 左边k项翻转
  3. 右边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()。而翻转的过程可用双指针完成。

我们可以设定两个指针leftright分别指向起始项和终止项,每次调换这两个指针指向的值,然后各自向中间前进一个单位,直到它们指向同一个元素(一个元素没必要和自己调换)或者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--;
        }
    }
};

请注意:

  1. 数组下标从0开始,因此对数组左边k项进行翻转,是从第0项至第k-1项进行翻转。数组右侧同理。
  2. 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,可以发现一轮遍历只能交换-13。因此我们要找到一个能够使这个过程继续下去的方法。


我们先考虑这个问题:从起点0到终点0的过程中,一共遍历了多少元素?

由于最终回到了起点,则我们必然走了整数数量的圈,一圈长度为n,假设走了a圈回到了起点;再设这一轮中总共遍历了b个元素,而每个元素之间的间隔即为k,也就是每走一步(每走k格找到下一个目标)。因此我们可以得出:an=bka圈的总长为an,遍历b个元素所走的路程为bk。因此an必然是nk的公倍数。同时,我们要在第一次回到起点时就结束,因此an要取最小公倍数。

如果a不是最小公倍数:我们改变一个系数:2an=2bk,即a变成了原来的两倍,也就是两圈。相应地,b也变成了两倍。这就相当于走了2圈。而这是没有必要的,我们仅需要一圈就能完成转换。多一圈会将数组转换回去。

annk的最小公倍数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指的是最大公约数。这是基于以下的事实:对于数abab的乘积等于它们的最小公倍数和最大公约数之积。
a ∗ b = l c m ( a , b ) ∗ g c d ( a , b ) a*b=lcm(a,b)*gcd(a,b) ab=lcm(a,b)gcd(a,b)

由上述推导过程可得:交换的轮数roundnk的最大公约数(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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亡心灵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值