[算法] 优选算法(一): 双指针算法

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (91平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀Java EE(94平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
今天开始,算法专栏正式开始更新,欢迎订阅~~
在这里插入图片描述

1. 概述

常见的双指针算法有两种:一种是对撞指针,一种是快慢指针.

1.1 对撞指针

对撞指针,也叫左右指针.一般用于顺序结构中.

  • 对撞指针从两端向中间移动,一个指针从最右边开始,一个指针从最左边开始.
  • 对撞指针的最终结果就是:left == right两个指针相遇或者left >= right两个指针刚好错开,也可能在循环遍历的途中找到结果直接break掉.

1.2 快慢指针

又称为龟兔赛跑算法,基本思想就是在一个链表或者序列结构上定义两个移动速度不同的指针,一个走得快,一个走的慢,最常用的就是一个指针移动一位,另一个指针移动两位.
这种算法在解决环形结构的时候非常有用.如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想.

2. 移动零(难度:🟢1度)

OJ链接

  • 题目描述

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]

  • 算法原理
    本题最核心的原理就是:数组区间划分.
    我们可以定义两个指针,一个是cur,一个是dest,dest用来记录最后一个非零数字的位置,cur用来遍历数组.在划分的时候,[0,dest]区间全部是非零元素,[dest+1,cur]区间中全部是0元素,[cur+1,array.length]区间是未处理的元素,这样就使用两个指针把这个数组分段为了三个区间.
  • 解决步骤
    1. 定义cur = 0定义dest = 0,之后cur向后移动.
    2. cur移动到一个元素不为0的时候,交换destcur位置的元素.之后dest++.
    3. cur遇到一个为0的元素的时候,不做任何处理,cur直接++.
    4. 直到cur移动到数组的末尾,结束循环.
  • 代码编写
class Solution {
    public void moveZeroes(int[] nums) {
        int cur = 0;
        int dest = 0;
        while(cur < nums.length){
            if(nums[cur] == 0){
                cur++;
            }else{
                int tmp = nums[dest]; 
                nums[dest] = nums[cur];
                nums[cur] = tmp;
                cur++;
                dest++;
            }
        }
    }
}

3. 复写零(难度:🟡3度)

OJ链接

  • 题目描述

给你一个长度固定的整数数组 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]

  • 算法原理
    如果使用从前向后复写的思路来解决的话,这样就会使得0被复写的时候,后面一个未被复写的元素被覆盖掉.
    所以我们采用从后向前复写的方法,但是如果从后向前复写的话,就必须最后一个被复写的对象.
    找到最后一个被复写的对象,我们就可以采用从前向后模拟指针移动方式来找到.
  • 算法步骤
    1. 初始化cur = 0 dest = 0.
    2. 找到最后一个复写的数字.
    3. cur遍历数组,当cur遇到非0的数字的时候,dest向后移动1位,当cur遇到0的时候,cur向后移动两位,直到dest走到数组的末尾的时候,cur停止遍历,此时cur 的位置就是最后一个要复写的数字.
    4. 判断dest位置的情况,如果dest位置在数组越界一个下标的位置的时候:我们就可以让n-1位置的值修改为0,之后dest-=2,cur-=1.之所以会出现越界的情况,是因为cur最后一个遍历到的元可能是0,这就使得dest元素向后移动了2个位置,导致数组越界.
    5. dest从前向后移动,当cur遇到非0的时候,dest的位置就重写一次cur位置的元素,如果遇到0,就重写两次0,直到dest走到数组的首位.
  • 代码编写
class Solution {
    public void duplicateZeros(int[] arr) {
        int cur = 0;
        int dest = -1;
        //找到最后一个复写的元素
        while(cur < arr.length){
            if(arr[cur] == 0){
                dest+=2;
            }else{
                dest++;
            }
            if(dest >= arr.length-1){
                break;
            }
            cur++;
        }
        //处理边界越界情况
        if(dest == arr.length){
            arr[arr.length-1] = 0;
            dest-=2;
            cur--;
        }
        //向前移动完成复写操作
        while(cur >= 0){
            if(arr[cur] != 0){
                arr[dest]=arr[cur];
                cur--;
                dest--;
                
            }else{
                arr[dest] = 0;
                dest--;
                arr[dest] = 0;
                dest--;
                cur--;
            }
        }
    }
}

4. 快乐数(难度:🔵2度)

OJ链接

  • 题目描述

编写一个算法来判断一个数 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
    由于上述两种情况只会出现⼀种,因此,只要我们能确定循环是在「情况⼀」中进⾏,还是在「情
    况⼆」中进⾏,就能得到结果。
    在这里插入图片描述

如果这道题不会出现死循环,这道题就会变得非常麻烦,难度至少到达4度,所以题目中规定一定会出现死循环其实是帮我们降低了难度.
这里我们使用使用快慢双指针算法,但是这里 的双指针并不是像上面数组的下标,更不是像链表中的两个引用对象,而是数字结果本身.
根据上述的题⽬分析,我们可以知道,当重复执⾏x 的时候,数据会陷⼊到⼀个「循环」之中。⽽「快慢指针」有⼀个特性,就是在⼀个圆圈中,快指针总是会追上慢指针的,也就是说他们总会
相遇在⼀个位置上。如果相遇位置的值是1 ,那么这个数⼀定是快乐数;如果相遇位置不是1
的话,那么就不是快乐数

  • 算法步骤
    1. 定义x函数,计算每次循环的结果.
    2. 定义slow = n.(第一个数字),定义fast = x(n).(第二个数字),之所以不定义0,是为了可以进得去循环.
    3. 每次slow向后移动一个数字,fast向后移动两个数字.
    4. 直到两个指针相遇,即值相等.
    5. 判断相遇的位置是否等于1.
  • 代码编写
class Solution {
    public int x(int n){
        int sum = 0;
        while (n != 0){
            sum += (n%10)*(n%10);
            n/=10;
        }
        return sum;
    }
    public boolean isHappy(int n) {
        int slow = n;
        int fast = x(n);
        while (slow != fast){
            slow = x(slow);
            fast = x(x(fast));
        }
        if (slow == 1){
            return true;
        }else{
            return false;
        }
    }
}

5. 盛水最多的容器(难度:🟡3度)

OJ链接

  • 题目描述

给定一个长度为 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 分别指向容器的左右两个端点,此时容器的容积:
    v = (right - left) * min( height[right], height[left]) 容器的左边界为height[left] ,右边界为height[right] .
    为了⽅便叙述,我们假设「左边边界」⼩于「右边边界」。
    如果此时我们固定⼀个边界,改变另⼀个边界,⽔的容积会有如下变化形式:
    容器的宽度⼀定变⼩
    ◦ 由于左边界较⼩,决定了⽔的⾼度。如果改变左边界,新的水面高度不确定,但是⼀定不会超
    过右边的柱子高度,因此容器的容积可能会增大

    如果改变右边界,⽆论右边界移动到哪⾥,新的⽔⾯的⾼度⼀定不会超过左边界,也就是不会
    超过现在的⽔⾯⾼度,但是由于容器的宽度减⼩,因此容器的容积⼀定会变⼩的

    综上,我们只需要改变高度较小的一边即可.
    由此可⻅,左边界和其余边界的组合情况都可以舍去所以我们可以left++ 跳过这个边界,继续去判断下⼀个左右边界
    当我们不断重复上述过程,每次都可以舍去大量不必要的枚举过程,直到left right 相遇。期间产生的所有的容积里面的最大值,就是最终答案。
  • 代码编写
class Solution {
    public int maxArea(int[] height) {
        int left = 0;
        int right = height.length-1;
        int max = 0;
        int v = 0; 
        while (left <= right){
            v = Math.min(height[left],height[right])*(right-left);
            max = Math.max(v,max);
            if (height[left] < height[right]){
                left += 1;
            }else{
                right -= 1;
            }
        }
        return max;
    }
}

6. 有效三角形的个数(难度:🔵2度)

OJ链接

  • 题目描述

购物车内的商品价格按照升序记录于数组 price。请在购物车中找到两个商品的价格总和刚好是 target。若存在多种情况,返回任一结果即可。
示例 1:
输入:price = [3, 9, 12, 15], target = 18
输出:[3,15] 或者 [15,3]
示例 2:
输入:price = [8, 21, 27, 34, 52, 66], target = 61
输出:[27,34] 或者 [34,27]

  • 算法原理
    先将数组排序
    我们可以固定⼀个==「最⻓边」,然后在⽐这条边⼩的有序数组中找出⼀个⼆元组==,使这个⼆元组之和⼤于这个最⻓边。由于数组是有序的,我们可以利⽤「对撞指针」来优化。
    设最⻓边枚举到i 位置,区间[left, right] 是i 位置左边的区间(也就是⽐它⼩的区间):
    • 如果nums[left] + nums[right] > nums[i] :
      • 说明[left, right - 1] 区间上的所有元素均可以与nums[right] 构成⽐nums[i] ⼤的⼆元组
      • 满⾜条件的有right - left 种
      • 此时right 位置的元素的所有情况相当于全部考虑完毕, right– ,进⼊下⼀轮判断
    • 如果nums[left] + nums[right] <= nums[i] :
      • 说明left 位置的元素是不可能与[left + 1, right] 位置上的元素构成满⾜条件的⼆元组
      • left 位置的元素可以舍去, left++ 进⼊下轮循环
  • 代码编写
class Solution {
    public int triangleNumber(int[] nums) {
        Arrays.sort(nums);
        int ret = 0;
        for (int n = nums.length-1;n >= 2;n--){//n固定最大边
            int left = 0;
            int right = n-1;
            while (left < right){
                if (nums[left] + nums[right] <= nums[n]){//舍去比right小的元素
                    left++;
                }else{//比left大的元素全部符合条件
                    ret+=right-left;
                    right--;
                }
            }
        }
        return ret;
    }
}

7.两数之和(难度:🟢1度)

OJ链接

  • 题目描述

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]

  • 算法原理
    • 初始化left , right 分别指向数组的左右两端(这⾥不是我们理解的指针,⽽是数组的下
      标)
    • 当left < right 的时候,⼀直循环
      • 当nums[left] + nums[right] == target 时,说明找到结果,记录结果,并且返回;
      • 当nums[left] + nums[right] < target 时:
        • 对于nums[left] ⽽⾔,此时nums[right] 相当于是nums[left] 能碰到的最⼤值别忘了,这⾥是升序数组)。如果此时不符合要求,说明在这个数组⾥⾯,没有别的数符合nums[left] 的要求了(最⼤的数都满⾜不了你,你已经没救了)。因此,我们可以⼤胆舍去这个数,让left++ ,去⽐较下⼀组数据;
        • 那对于nums[right] ⽽⾔,由于此时两数之和是⼩于⽬标值的, nums[right] 还可以选择⽐nums[left] ⼤的值继续努⼒达到⽬标值,因此right 指针我们按兵不动;
      • 当nums[left] + nums[right] > target 时,同理我们可以舍去nums[right] (最⼩的数都满⾜不了你,你也没救了)。让right– ,继续⽐较下⼀组数据,⽽left 指针不变(因为他还是可以去匹配⽐nums[right] 更⼩的数的)。
  • 代码编写
class Solution {
    public int[] twoSum(int[] price, int target) {
        int left = 0;
        int right = price.length-1;
        while (left < right){
            if (price[left]+price[right] == target){
                return new int[]{price[left],price[right]};
            }else if (price[left]+price[right] < target){
                left++;
            }else{
                right--;
            }
        }
        return new int[0];
    }
}

8. 三数之和(难度:🟠4度)

OJ链接

  • 题目描述

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

  • 算法原理
    与两数之和稍微不同的是,题⽬中要求找到所有「不重复」的三元组。那我们可以利⽤在两数之和那⾥⽤的双指针思想,来对我们的暴⼒枚举做优化:
    • 排序
    • 然后固定⼀个数a
    • 在这个数后⾯的区间内,使⽤「双指针算法」快速找到两个数之和等于-a 即可
      但是要注意的是,这道题⾥⾯需要有==「去重」操作==~
    • 找到⼀个结果之后, left 和right 指针要「跳过重复」的元素
    • 当使⽤完⼀次双指针算法之后,固定的a 也要「跳过重复」的元素

最后一点要注意的是在去重操作移动指针的时候,不要出现越界情况.
还有一点小优化就是,在i遍历到>0的数据的时候,就可以停止了,这是因为在剩下的数字中再也找不得两个数字加起来等于负数了.

  • 代码编写
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> ret = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0;i < nums.length;){
            if (nums[i] > 0){//遇到大于0的,一定没有两个数使得条件成立
                break;
            }
            int left = i + 1;
            int right = nums.length-1;
            while(left < right){
                if (nums[left] + nums[right] == -nums[i]){
                    List<Integer> a = new ArrayList<>();
                    a.add(nums[i]);
                    a.add(nums[left]);
                    a.add(nums[right]);
                    ret.add(a);
                    left++;
                    right--;
                    while(left < nums.length && nums[left] == nums[left-1]){//left去重,且不可以越界
                        left++;
                    }
                    while(right > 0 && nums[right] == nums[right+1]){//right去重且不可以越界
                        right--;
                    }
                }else if (nums[left] + nums[right] < -nums[i]){//与前面两数之和的原理相同
                    left++;
                }else{
                    right--;
                }
            }
            //i去重
            i++;
            while(i < nums.length && nums[i] == nums[i-1]){//不可以越界
                i++;
            }
        }
        return ret;
    }
}

9.四数之和(难度:🔴5度)

  • 题目描述

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

  • 算法原理
    a. 依次固定⼀个数a
    b. 在这个数a 的后⾯区间上,利⽤
    「三数之和」找到三个数
    ,使这三个数的和等于target -a 即可。
  • 代码编写
class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> ret = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length;){
            for (int j = i+1;j < nums.length;){
                int left = j+1;
                int right = nums.length-1;
                long t = (long)target-nums[j]-nums[i];
                while(left < right){
                    if (nums[left] + nums[right] == t){
                        List<Integer> a = new ArrayList<>();
                        a.add(nums[left]);
                        a.add(nums[right]);
                        a.add(nums[j]);
                        a.add(nums[i]);
                        ret.add(a);
                        left++;
                        right--;
                        while(left < nums.length && nums[left] == nums[left-1]){
                            left++;
                        }
                        while(right > 0 && nums[right] == nums[right+1]){
                            right--;
                        }
                    }else if(nums[left] + nums[right] < t){
                        left++;
                    }else{
                        right--;
                    }
                }
                j++;
                while(j < nums.length && nums[j] == nums[j-1]){
                    j++;
                }
            }
            i++;
            while(i < nums.length && nums[i] == nums[i-1]){
                i++;
            }
        }
        return ret;
    }
}

从今天的代码中,我们可以发现,这几道题目在不停的利用数组的单调性和双指针在优化暴力解法,排除掉暴力解法中的一些无效情况,以后在我们做题的时候,要充分利用单调性和双指针的配合.

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值