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



283. 移动零

LeetCode

简 单 \color{#00AF9B}{简单}

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

说明:

  1. 必须在原数组上操作,不能拷贝额外的数组。
  2. 尽量减少操作次数。

注意

  1. 这道题并未给出数组的最大长度,为了保险起见我们还是将指针类型设为size_t,也就是vector数组的size()方法的返回值,以免数组长度超出int能表示的范围。
    (实际上,在该题的实际提交测试案例中,没有长度超出int范围的数组,因此设为int也能通过。但我们还是或多或少要注重这个思想,以免在某些重要场合漏了马脚。)
  2. 当数组为空数组,或者只有一个元素时,是不存在交换操作的,即nums.size()<=1。因此可以直接return终止函数。

方法1:快慢指针

这道题的双指针解法与往常的双指针不太一样。之前我们设置的是左右指针leftright,分别指向数组的起始项和末尾项,遍历时两个指针往中间进发。而这道题由于要保持元素的相对位置,不能使用该方法。

这题要用到的双指针法是快慢指针。两个指针从同一位置出发,快指针fast移动的较快,或者说始终处于慢指针的前方,找到下一个快指针要找的值;慢指针slow移动的较慢,保留慢指针目标值的位置,等到快指针确定后,两者进行交换等操作。

本题中,我们可以定义慢指针slow先找到0的位置,然后快指针fastslow+1的位置往后找到第一个值不为0的位置,最后执行交换操作。

过程演示
请添加图片描述

#include <vector>
using namespace std;
class Solution
{
public:
    void moveZeroes(vector<int> &nums)
    {
        const size_t n = nums.size();
        if (n <= 1)
            return;

        size_t fast = 0, slow = 0;
        while (true)
        {
            while (slow < n - 1 && nums[slow] != 0)
            {
                slow++;
            }
            if (slow == n - 1)
                return;
            fast = slow + 1;
            while (fast < n && nums[fast] == 0)
            {
                fast++;
            }
            if (fast == n)
                return;
            swap(nums[slow], nums[fast]);
        }
    }
};

注:

(此项可以不考虑)慢指针slow指向的是0所在的位置,当其为n-1时,代表找到了一个存在于数组末尾的0,而此时这个0不需要和任何元素交换,因此slow在指向n-1位置时就可以跳出循环了。

复杂度分析

时间复杂度:O(n),每个元素至多被遍历2次,O(2n)=O(n)。

空间复杂度:O(1)。我们只需要常量空间存放若干变量。

参考结果

Accepted
74/74 cases passed (136 ms)
Your runtime beats 5.69 % of cpp submissions
Your memory usage beats 49.59 % of cpp submissions (18.6 MB)

方法1(变化):快慢指针

方法1中的代码显得稍微有些长,我们可以使用下文中的方法减少代码量。不同于方法1中让fastslow分别出发然后各自找到自己的目标,这个方法让fastslow同时出发,若是遇到了0,则slow不自增,而fast自己继续前进找到第一个不为0的值。这样既符合快慢指针的概念,也是实现快慢指针的另一种思路。

流程如下:

  1. fastslow一起走。如果fast指到了非零元素,则:
    • fast!=slow,则进行交换。slow自增。
    • fast==slow,则代表两者还在同步前进,两个都执行同一个元素,不需要交换。slow自增。
  2. fast指到的是0,则不用管slow如何,自己自增。

流程演示
请添加图片描述

#include <vector>
using namespace std;
class Solution
{
public:
    void moveZeroes(vector<int> &nums)
    {
        size_t n = nums.size();
        size_t fast = 0, slow = 0;
        while (fast < n)
        {
            if (nums[fast] != 0)
            {
                if (fast != slow)
                    swap(nums[slow], nums[fast]);
                slow++;
            }
            fast++;
        }
    }
};

复杂度分析

时间复杂度:O(n),每个元素至多被遍历2次,O(2n)=O(n)。

空间复杂度:O(1)。我们只需要常量空间存放若干变量。

参考结果

Accepted
74/74 cases passed (20 ms)
Your runtime beats 46.48 % of cpp submissions
Your memory usage beats 38.19 % of cpp submissions (18.7 MB)


167. 两数之和 II - 输入有序数组

LeetCode

简 单 \color{#00AF9B}{简单}

给定一个已按照 非递减顺序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。

示例 1:

输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:27 之和等于目标数 9 。因此 index1 = 1, index2 = 2

示例 2:

输入:numbers = [2,3,4], target = 6
输出:[1,3]

示例 3:

输入:numbers = [-1,0], target = -1
输出:[1,2]

提示:

  • 2 <= numbers.length <= 3 * 104
  • -1000 <= numbers[i] <= 1000
  • numbers 按 非递减顺序 排列
  • -1000 <= target <= 1000
  • 仅存在一个有效答案

注意

题目要求答案应从下标1开始计数。我们在过程中还是保持从0开始计数,仅在返回结果时+1,这样可以省去很多麻烦。


方法1:二分查找

最粗暴的方式就是O(n2)的两次完全遍历查找:先确定一个数,然后一轮n次的遍历查找第二个数,这样的查找最多做n轮。

我们可以将内层的循环改为二分查找,即确定一个数,然后进行一轮O(logn)的二分查找。这样的查找最多做n轮,因此时间复杂度为O(nlogn)

#include <vector>
using namespace std;
class Solution
{
public:
    vector<int> twoSum(vector<int> &numbers, int target)
    {
        const int n = numbers.size();
        for (int i = 0; i < n - 1; i++)
        {
            int left = i + 1, right = n - 1;
            while (left <= right)
            {
                int mid = ((right - left) >> 1) + left;
                if (numbers[i] + numbers[mid] == target)
                {
                    return {i + 1, mid + 1};
                }
                else if (numbers[i] + numbers[mid] > target)
                {
                    right = mid - 1;
                }
                else
                {
                    left = mid + 1;
                }
            }
        }

        // 虽然题目保证必有解,即上个循环中必定会返回值
        // 但由于函数在各分支必须有返回,因此需要象征性地返回
        return {};
    }
};

复杂度分析

时间复杂度:O(nlogn)。对于每一个数,我们都需要执行一次O(logn)的二分查找;而我们最外层最多遍历n次(细节一点的话是n-1次,因为外层确定的是最后一个数的话,它后面没有数可以找了),因此为nlogn

空间复杂度:O(1)。我们只需要常量空间存放若干变量。

参考结果

Accepted
19/19 cases passed (4 ms)
Your runtime beats 86.14 % of cpp submissions
Your memory usage beats 97.18 % of cpp submissions (9.2 MB)

方法2:双指针

设置指针leftright分别指向数组的开头和末尾。按以下思路即可完成:

  1. [left]+[right]==target,则返回{left, right}
  2. [left]+[right]<target,由于大的值[right]不能再大了,因此[left]要变大,left要往右走(自增);
  3. [left]+[right]>target,由于小的值[left]不能再小了,因此[right]要变小,right要往左走(自减)。
#include <vector>
using namespace std;
class Solution
{
public:
    vector<int> twoSum(vector<int> &numbers, int target)
    {
        int left = 0, right = numbers.size() - 1;

        while (left < right)
        {
            int sum = numbers[left] + numbers[right];
            if (sum == target)
            {
                return {left + 1, right + 1};
            }
            else if (sum < target)
            {
                left++;
            }
            else
            { // sum > target
                right--;
            }
        }

        // 虽然题目保证必有解,即上个循环中必定会返回值
        // 但由于函数在各分支必须有返回,因此需要象征性地返回
        return {};
    }
};

注:

由于题目保证有解,因此在循环内必能找到答案并return。但编译器要确保每条支路都要有返回,因此在main函数最后要象征性地写一句return

复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)。我们只需要常量空间存放若干变量。

参考结果

Accepted
19/19 cases passed (4 ms)
Your runtime beats 86.14 % of cpp submissions
Your memory usage beats 93.72 % of cpp submissions (9.2 MB)

Animation powered by ManimCommunity/manim

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

亡心灵

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

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

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

打赏作者

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

抵扣说明:

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

余额充值