笔试算法 - 双指针篇(一):移动零、复写零、快乐数与盛水容器

目录

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》《笔试算法

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、移动零

283. 移动零


解法(快排的思想:数组划分区间 - 数组分两块)

算法思路:在本题中,我们可以用一个 cur 指针来扫描整个数组,另一个 dest 指针用来记录非零序列的最后一个位置。根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。在 cur 遍历期间,使 [0, dest] 的元素全部都是非零元素,[dest + 1, cur - 1] 的元素全是零。

算法流程

a. 初始化 cur = 0(用来遍历数组),dest = -1(指向非零元素序列的最后一个位置。因为刚开始我们不知道最后一个非零元素在什么位置,因此初始化为 -1)

b. cur 依次往后遍历每个元素,遍历到的元素会有下面两种情况:

  • i. 遇到的元素是 0,cur 直接 ++。因为我们的目标是让 [dest + 1, cur - 1] 内的元素全都是零,因此当 cur 遇到 0 的时候,直接 ++,就可以让 0 在 cur - 1 的位置上,从而在 [dest + 1, cur - 1] 内;
  • ii. 遇到的元素不是 0,dest++,并且交换 cur 位置和 dest 位置的元素,之后让 cur++,扫描下一个元素。
    • 因为 dest 指向的位置是非零元素区间的最后一个位置,如果扫描到一个新的非零元素,那么它的位置应该在 dest + 1 的位置上,因此 dest 先自增 1;
    • dest++ 之后,指向的元素就是 0 元素(因为非零元素区间末尾的后一个元素就是 0),因此可以交换到 cur 所处的位置上,实现 [0, dest] 的元素全部都是非零元素,[dest + 1, cur - 1] 的元素全是零。
cpp 复制代码
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        for(int cur = 0, dest = -1; cur < nums.size(); cur++)
        {
            if(nums[cur])
                swap(nums[++dest], nums[cur]);
        }
    }
};

二、复写零

1089. 复写零


解法(原地复写 - 双指针)
算法思路 :如果「从前向后」进行原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆盖掉」。因此我们选择「从后往前」的复写策略。

但是「从后向前」复写的时候,我们需要找到「最后一个复写的数」,因此我们的大体流程分两步:

i. 先找到最后一个复写的数;ii. 然后从后向前进行复写操作。

算法流程

a. 初始化两个指针 cur = 0,dest = -1;

b. 找到最后一个复写的数:

  • i. 当 cur < n 的时候,一直执行下面循环:
    • 判断 cur 位置的元素:
      • 如果是 0 的话,dest 往后移动两位;
      • 否则,dest 往后移动一位。
    • 判断 dest 是否已经到结束位置(数组中最后一个数的位置),如果结束就终止循环(最后终止的时候cur指的位置就是需要复写的最后一个数,dest所指的位置正好是要填的位置);
    • 如果没有结束,cur++,继续判断。

c. 判断 dest 是否越界到 n 的位置:

  • i. 如果越界,执行下面三步:
    1. n - 1 位置的值修改成 0;
    1. cur 向前移动一步;
    1. dest 向前移动两步。

d. 从 cur 位置开始往前遍历原数组,依次还原出复写后的结果数组:

  • i. 判断 cur 位置的值:

    1. 如果是 0:dest 以及 dest - 1 位置修改成 0,dest -= 2;
    1. 如果非零:dest 位置修改成 cur 位置的值,dest -= 1;
  • ii. cur减减,复写下一个位置。

cpp 复制代码
class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        //找到最后要复写的元素
        int n = arr.size();
        int cur = 0, dest = -1;
        while(cur < n)
        {
            if(arr[cur]) ++dest;
            else dest += 2;
            if(dest >= n - 1) break;
            cur++;
        }
        //处理越界情况
        if(dest == n)
        {
            arr[n - 1] = 0;
            cur--; dest -= 2;
        }
        //从后向前进行复写
        while(cur >= 0)
        {
            if(arr[cur]) arr[dest--] = arr[cur--];
            else{
                arr[dest--] = 0;
                arr[dest--] = 0;
                cur--;
            }
        }
    }
};

三、快乐数

202. 快乐数

题目分析 :

为了方便叙述,将「对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和」这一个操作记为 x 操作;

题目告诉我们,当我们不断重复 x 操作 的时候,计算一定会「死循环」,死的方式有两种:

  • 情况一:一直在 1 中死循环,即 1→1→1→1......
  • 情况二:在历史的数据中死循环,但始终变不到 1

由于上述两种情况只会出现一种,因此,只要我们能确定循环是在「情况一」中进行,还是在「情况二」中进行,就能得到结果。

解法(快慢指针)
算法思路

根据上述的题目分析,我们可以知道,当重复执行 x 的时候,数据会陷入到一个「循环」之中。而「快慢指针」有一个特性,就是在一个圆圈中,快指针总是会追上慢指针的,也就是说他们总会相遇在一个位置上。如果相遇位置的值是 1,那么这个数一定是快乐数;如果相遇位置不是 1 的话,那么就不是快乐数。

在链表的那道判断链表是否有环的题目中(数据结构之单链表和环形链表的应用(二)),是定义了Node*来充当快慢指针,上一道题是将数组的下标充当双指针,由于这道题是是一个个的数字,就拿环中的数字来充当指针,千万不要被双指针的名字限制了自己的思维,认为名字是双指针就要定义指针来操作

这个题目告诉我们的两种情况就是要么变为1,要么无限循环始终变不到1。大概在5,6年前,这个题目是没有直接限制在这两种情况之内的,此时题目就变得很恶心了,此时在我们的视角就有可能出现第三种情况:有不成环的可能,一直变下去,无限衍生。接下来就证明一下,为什么在变化的过程中是一定会存在环的,这涉及一个鸽巢原理,或者说抽屉原理,还是挺有意思的,如果对这种数学证明不感兴趣也可以直接写代码看题解。

简单证明

  • a. 经过一次变化之后的最大值 92 ∗ 10=810(231−1=2147483647。选一个更大的最大值 9999999999),也就是变化的区间在 [1, 810] 之间;
  • b. 根据「鸽巢原理」,一个数变化 811 次之后,必然会形成一个循环;c. 因此,变化的过程最终会走到一个圈里面,因此可以用「快慢指针」来解决。

补充知识 :如何求一个数 n 每个位置上的数字的平方和。

a. 把数 n 每一位的数据提取出来:循环迭代下面步骤:

  • i. int t = n % 10 提取个位;
  • ii. n /= 10 干掉个位;

直到 n 的值变为 0;

b. 提取每一位的时候,用一个变量 tmp 记录这一位的平方与之前提取位数的平方和

  • tmp = tmp + t * t
cpp 复制代码
class Solution {
public:
    int yunzeSum(int n)
    {
        int sum = 0;
        while(n)
        {
            int t = n % 10;
            sum += t * t;
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n) {
        //为了能够进入循环,初始slow和fast不能相等,初始的时候fast指向第二个位置
        int slow = n, fast = yunzeSum(n);
        //当两个指针不相等的时候,一直循环
        while(slow != fast)
        {
            //慢指针走一步,快指针走两步
            slow = yunzeSum(slow);
            fast = yunzeSum(yunzeSum(fast));
        }
        //相遇位置的值不为1,不是快乐数
        return slow == 1;
    }
};

尤其注意这里的循环截至条件while(slow != fast)

循环继续的条件:还没相遇,说明还没找到环

循环终止的条件:已经相遇,说明已经进环了
快慢指针在环里的运动过程

环里是一个圈,快指针速度是慢指针 2 倍:慢指针走 1 格 → 快指针走 2 格。快指针只会不断缩短和慢指针的距离 ,最终从后方追上慢指针,两者重合。它永远不会 "超过、跑到前面",圈里追上 = 相遇相等

四、盛最多水的容器

11. 盛最多水的容器


解法一(暴力求解)
算法思路

枚举出能构成的所有容器,找出其中容积最大的值

  • 设两指针i, j,分别指向水槽板的最左端以及最右端,此时容器的宽度为j - i。由于容器的高度由两板中的短板决定,因此可得容积公式:v = (j - i) * min(height[i], height[j])

解法二(对撞指针,如图所示)

cpp 复制代码
class Solution {
public:
    int maxArea(vector<int>& height) {
        int n = height.size();
        int left = 0, right = n - 1, ret = 0;
        while(left != right)
        {
            int v = (right - left) * min(height[left], height[right]);
            ret = max(ret, v);
            if(height[left] <= height[right])
            {
                left++;
            }else{
                right--;
            }
        }
        return ret;
    }
};

结语

相关推荐
小堃学编程2 小时前
【项目实战】基于protobuf的发布订阅式消息队列(4)—— 服务端
c语言·c++·vscode·消息队列·gtest·protobuf·muduo
不才小强2 小时前
目标跟踪算法DeepSort实战
人工智能·算法·目标跟踪
小白学大数据2 小时前
解决 Python 爬虫被限制:延迟抓取指令深度解析
开发语言·c++·爬虫·python
papership2 小时前
【入门级-数学与其他:1.数及其运算:进制与进制转换:二进制、八进制、十进制、十六进制】
算法
ComputerInBook2 小时前
数字图像处理(4版)——第 4 章——频域滤波(下)(Rafael C.Gonzalez&Richard E. Woods)
人工智能·算法·计算机视觉·频域滤波
会编程的土豆2 小时前
【复习】二分查找
数据结构·c++·算法
yuanpan3 小时前
Python 调用 DLL 动态库入门:Windows 下调用 C++ 与 C# 动态库完整示例
c++·windows·python
Yzzz-F3 小时前
Problem - D - Codeforces
算法
chas_883 小时前
macbook air M5 32G本地跑ddtree-mlx效果
算法