常见的几个“双指针”试题

目录

🚀双指针🚀

🚀试题🚀:

1.✈️移动零✈️

2.✈️复写零✈️

3.✈️快乐数✈️

4.✈️盛水最多的容器✈️


🚀双指针🚀

        常见的双指针有两种形式,一种是对撞指针,一种是快慢指针

对撞指针:一般用于顺序结构中,也称为左右指针

  • 对撞指针从两端向中间移动。一个指针从最左端开始,一个从最右端开始,逐渐向中间逼近。
  • 对撞指针的终止条件一般是两个指针相遇或者错开,也就是:

        left == right(两个指针相遇,指向同一位置)

        left <right(两个指针错开)

快慢指针:快慢指针又称为龟兔赛跑算法,其基本思想就是使用两个移动移动速度不同的指针在数组或者链表等序列结构上移动。(对链表不清楚的可以参考该篇博文:【数据结构】双链表---超详解-CSDN博客

这种方法对于处理环形链表或数组非常有用。其实不单单是环形链表或者是数组,只要我们研究的问题出现循环往复的情况,都可以考虑使用快慢指针的思想。

快慢指针的实现方式有很多种,其中最常见的一种就是:

在一次循环中,让慢指针往后移动一位,快指针往后移动两位,从而实现一快一慢。

注意:上述所说的指针不一定就真的一定要是指针才可以,最重要的是这种思想,解题时不是一定要定义两个指针才行,例如数组,我们用下标即可表示这两个所谓的“指针”。

🚀试题🚀

1.✈️移动零✈️

题目链接移动零

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

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

这是一种常见的数组分块题型,主要就是根据一种划分方式,将数组内容划分为左右两个部分。这种类型的题,一般都是采用“双指针”来做。

解题思想:

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

初始化时,我们可以将cur初始化为0,因为cur需要从0开始遍历整个数组,dest指向的是非零元素的最后一个位置,我们将其初始化为-1,因为一开始我们并不知道最后一个非零元素的位置。

cur往后遍历时,当cur遇到零元素时,只需将cur++即可,直到cur遇到非零元素,此时将dest++,然后交换cur和dest在数组中对应的值,这个时候就能将非零元素往左移,并且还能够保持非零元素的相对位置不变,而且dest指向的也是非零元素的最后一个位置(已处理的区间内)。

代码实现(C++):

class Solution
{
public:
    void moveZeroes(vector<int>& nums)
    {
        for(int dest = -1,cur = 0; cur < nums.size(); cur++)
        {
            if(nums[cur])
            {
                swap(nums[++dest],nums[cur]);
            }
        }
    }
};

2.✈️复写零✈️

题目链接复写零

题目描述

给你一个长度固定的整数数组 arr ,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。

注意:请不要在超过该数组长度的位置写入元素。请对输入的数组 就地 进行上述修改,不要从函数返回任何东西。

示例 1:

输入:arr = [1,0,2,3,0,4,5,0]
输出:[1,0,0,2,3,0,0,4]
解释:调用函数后,输入的数组将被修改为:[1,0,0,2,3,0,0,4]

示例 2:

输入:arr = [1,2,3]
输出:[1,2,3]
解释:调用函数后,输入的数组将被修改为:[1,2,3]

解题思路:

    题目要求在原地进行修改,那么我们先来想想在“异地”该如何操作,我们重新定义一个数组,用于存放题目要求的复写结果,定义一个cur用于遍历原数组,cur初始化为0,定义一个dest用于对新数组进行写入,dest从-1开始,当cur指向的是一个非零元素时,我们让dest++,然后再对当前指向位置进行赋值;当cur指向一个零元素时,dest重复上述操作写两个0,如下:

此时我们再将两个数组合在一起在“原地”进行操作看一下,cur同样从0开始,dest从-1开始,不同之处就在于现在cur和dest在同一个数组上操作,如下:

写到此处时,就不用再往下继续了,当cur指向0时,dest复写两个0,直接将原来的第三个元素覆盖了。显然,从前往后进行复写不对。既然从前往后复写不行,那么我们就来看看从后往前进行复写,那么从后往前需要注意些什么呢?从后往前怎样才能复写正确呢?其实,我们在前面“异地”进行复写的时候就已经找到了正确复写时,cur和dest的所处位置,那么现在我们让cur指向数组中元素4的位置,dest指向数组中末尾元素0的位置,当cur指向非零元素时,dest给当前指向赋值这个非零元素,然后dest--,cur--;当cur指向零元素时,dest给当前指向赋0值,然后dest--,重复两次完成复写,再让cur--。当cur或者dest越界后,循环结束。如下:

原地操作的重点就在于需要先找到做后一次复写的位置(也就是上例中cur指向4的位置),而从后往前写dest自然是在最后一个位置,我们让cur从0开始遍历数组,dest从-1开始,cur指向非零元素时,我们让dest加1,当cur指向零元素时,我们让dest加2,这样,当dest指向末尾元素时,我们就找到了最后一个复写元素的位置。但是!还存在一种特殊情况,那就是当数组为[1,0,2,3,0,4]时,此时我们来找最后一个元素,如下:

显然,此时我们从后往前进行复写时,dest会越界访问,所以这种特殊情况,我们单独处理,我们可以直接将数据末尾值赋0值,然后将cur减1,dest减2即可解决,如下:

代码实现(C++):

class Solution {
public:
    void duplicateZeros(vector<int>& arr) 
    {
        //找到最后一个复写的数
        int cur = 0;
        int dest = -1;
        while(cur < arr.size())
        {
            if(arr[cur])
            {
                dest++;
            }
            else
            {
                dest += 2;

            }
            if(dest >= arr.size() - 1)
            {
                break;
            }
            cur++;
        }
        if(dest == arr.size())
        {
            arr[arr.size() - 1] = 0;
            cur--;
            dest -= 2;
        }
        while(dest >= 0)
        {
            if(arr[cur])
            {
                arr[dest] = arr[cur];
                dest--;
            }
            else
            {
                arr[dest--] = 0;
                arr[dest--] = 0;
           }
           cur--;
        }

    }
};

3.✈️快乐数✈️

题目链接:快乐数

题目描述:

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

示例 1:

输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

示例 2:

输入:n = 2
输出:false

解题思路:

我们先将上面的两个例子根据题目意思展开来看一下,如下:

为了⽅便叙述,将「对于⼀个正整数,每⼀次将该数替换为它每个位置上的数字的平⽅和」这⼀个 操作记为 x 操作; 题⽬告诉我们,当我们不断重复 x 操作的时候,计算⼀定会「死循环」,死的⽅式有两种:

▪ 情况⼀:⼀直在 1 中死循环,即 1 -> 1 -> 1 -> 1...... ▪

情况⼆:在历史的数据中死循环,但始终变不到 1

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

简单证明一下为什么只会出现上述的两种情况:

我们知道正整数最大值为2147483647,我们选一个更大的值9999999999,9999999999各位数的平方和为810,也就是说只要是1到9999999999之间的任意数的各位数的平方和都在[1,810]这个区间内,那么,根据鸽巢原理,一个数变化811次后,一定会再次回到[1,810]这个区间内,从而形成一个循环。变化的过程最终会走到一个圈里面,因此我们可以用快慢指针的思想来处理此题。

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

代码实现(C++):

class Solution {
public:
/*
    void move(int& n)
    {
        int count = 0;
        int tmp = n;
        while(tmp)
        {
            count++;
            tmp /= 10;
        } 
        int sum = 0;
        while(n)
        {
            sum += (int)pow(n / (int)pow(10,count - 1),2);
            n %= (int)pow(10,count - 1);
            count--;
        }
        n = sum;
    }
    bool isHappy(int n)
    {
        int fast = n,slow = n;
        move(fast);
        while(fast != slow)
        {
            move(slow);
            move(fast);
            move(fast);
        }
        return fast == 1;
    }
*/

    int move(int n)
    {
        int sum = 0;
        while(n)
        {
            int t = n % 10;
            sum += t * t;
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n)
    {
        int slow = n,fast = move(n);
        while(slow != fast)
        {
            slow = move(slow);
            fast = move(move(fast));
        }
        return fast == 1;
    }
};

4.✈️盛水最多的容器✈️

题目链接:盛水最多的容器

题目描述:

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

示例 1:

输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例 2:

输入:height = [1,1]
输出:1

解题思路:

        根据题目的意思,我们简单的这样理解,就是在数组中找出两个数,然后用两个数中的较小值(木桶效应,装水嘛,肯定只能装到最矮的那块)来乘以两数之间的间隔(也就是宽)得到容积,然后找出众多组合中,容积最大的一个。

        我们先来看一下第一个试例的数组,我们定义一个left指针指向数组的最左边边界,定义一个right指针指向数组的最右边边界,S = 宽*高,此时找出两指针所指向的元素中的较小值是1,宽度正好为right-left(left,right皆为下标,前面我们已是讲过,这里讲的指针不一定非得是指针),现在算出容积为8,如下:

仔细看你会发现一个规律,S = 宽*高,此时当1再依次和剩余的数进行组合时,得到的结果都小于8,因为此时1已经是最小的了,当它和其他数进行匹配的时候,高不变,为1,而宽是在不断减小的,因此s自然也在减小,此时最开始得到的8也就是1和其他数进行组合时的最大值,并且此时它和其他数进行组合时得到的s都在减小,那么这些情况我们都可以不用再进行枚举,因为我们只要最大的一组。

上面这种情况是左边边界为1,已经是最小了,相当于固定了高的情况。那么假如左边边界不是最小的,右边还存在比它小的高呢,是否还遵循这个规律呢?

        看这个数组,我们把试例1中的元素1换成3就满足上面这种情况了,[3,8,6,2,5,4,8,3,7]此时left指向左边边界的3,right指向右边边界的7,s = 3 * 8 = 24,现在确实右边还存在比3小的值,那么有没有一种可能当left指向2然后与其中一个数组合时会比24大?其实不会存在!当left往左走找到一个比3小的数时,意味着它找到了一组高减小,并且宽也在减小的组合(left与right越近,宽越小),此时s肯定是比最开始算的那个值小的,所以这种情况下,同样满足规律:left与right所指向的元素中的较小值乘以此时right与left的间隔就是此时这个最小值与所有元素组合中容积的最大值。

        这个最小值可能在左边也可能在右边,当找完这个最小值的所有情况后,我们更新这个最小值的指针,如果最小值在左边,完成上述操作后,将left++,反之,如果最小值在右边,完成上述操作后,将right--。此题就是一个典型的对撞指针问题。

代码实现(C++):

class Solution {
public:
    int maxArea(vector<int>& height)
    {
        int left = 0,right = height.size() - 1;
        int max = 0;
        while(left < right)
        {
            int altitude = height[left] < height[right] ? height[left] : height[right];
            int v = altitude * (right - left);
            if(height[left] <= height[right]) left++;
            else right--;

            max = max > v ? max : v;
        }

        return max;
    }
};

本篇以双指针问题为主题的试题讲解博客到此就要结束了,希望本篇博客可以帮助到各位小伙伴,博主码字不易,如果本篇博客对你有所帮助,还望各位小伙伴点赞👍,收藏⭐+关注,感谢各位的支持!如果有什么不明白的地方或是另有其他的高见,也可以在评论区留言,博主也会及时回复或进行更改,欢迎指正,谢谢!

刹国(结束)!!!!

  • 30
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值