基础算法练习--双指针(已完结)

算法介绍

双指针是一种非常重要的基础算法.

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

对撞指针:⼀般用于顺序结构中,也称左右指针.特点:从两端向中间夹逼,直到相遇.

快慢指针:两个移动速度不同的指针在数组或链表等序列结构上移动.特点:处理循环往复的情况.

习题讲解

1. 移动零 

283. 移动零

 题目解析

难点:

这题的难点在于原地去操作数组,把所有0移动到所有非0元素的后面.

算法应用:

实际上,在操作过程中,数组就可以分为两部分,左边一部分全部是非0元素,右边一部分全部是0,这就可以使用双指针进行处理.cur指针用来遍历数组,dest指针用来标记最后一个非0元素的位置,确保dest是非0元素与0的分界点.

操作细节:

cur指针初始化在0下标位置,但是dest要初始化在-1下标位置,因为一开始不知道0位置是否为非0元素.

代码实现:

public void moveZeroes(int[] nums) {
        for (int cur=0, dest=-1; cur<nums.length; cur++) {
            //找到非0元素,开始交换
            if (nums[cur]!=0) {
                dest++;//最后一个非0元素位置,再向后一位
                //交换cur和dest位置的元素
                int tmp = nums[cur];
                nums[cur] = nums[dest];
                nums[dest] = tmp;
            }
        }
    }

2. 复写零 

1089. 复写零

 题目解析

难点:

这题的难点仍在于原地去操作数组,把所有0复写一遍.

算法应用:

这道题在操作过程中,需要注意必须从后往前进行复写,一位从前往后的顺序会覆盖许多元素.那我们就需要先找到最后一个需要复写的0.这个过程中就可以使用双指针,cur指针用来遍历数组,dest指针用来模拟复写的过程.

当cur等于0时,dest向后移动两位;cur不等于0时,dest向后移动一位,dest越界时,进行判断是否越界到n(只有cur指向0时才可能出现越界情况)(若是,则n-1位置的值改为0,cur向前移动一位,dest向前移动两位,说明这个cur所指的0不能完成复写),cur位置所指的数就是最后一个需要复写的数,随后从后往前进行复写.

操作细节:

cur指针初始化在0下标位置,但是dest要初始化在-1下标位置,对于和cur位置下数有关的dest的移动方式,通常将dest初始化在-1位置.

判断dest位置的操作要在cur移向下一个位置之前.

代码实现:

public void duplicateZeros(int[] arr) {
        int cur,dest,n = arr.length;
        for (cur=0, dest=-1; cur<n; cur++) {
            if (arr[cur]!=0) {
                dest++;
            } else {
                dest+=2;
            }
            //判断操作要在cur移向下一个位置之前
            if (dest>=n-1) break;
        }
        //判断是否越界
        if (dest==n) {
            arr[n-1] = 0;
            cur--;
            dest-=2;
        }
        //复写
        while (cur>=0) {
            if (arr[cur]==0) {
                arr[dest--] = 0;
                arr[dest--] = 0;
                cur--;
            } else {
                arr[dest--] = arr[cur--];
            }
        }
    }

 3. 快乐数 

202. 快乐数

题目解析

难点:

这题的难点仍在于无限循环地去操作一个数字.

算法应用:

要解决这道题,需要有对于无限循环这个操作的理解.题目中告诉我们是否为快乐数的判断标准是:操作到最后是否为1,无论对1进行多少次操作得到的结果恒为1,这就形成了一个环.问题就转化为了:判断一个(隐式)链表是否有环的问题.就可以使用快慢指针算法,慢指针走一步,快指针走两步,判断这两个指针是否最后在1处相遇,就是判断这个数是否为快乐数的标准.

操作细节:

slow指针初始化为要判断的数n,fast指针初始化为操作过一次的数happy(n).出循环条件是fast与slow指向的数相等,初始化若都指向n,就不会进入循环.

代码实现:

public boolean isHappy(int n) {
        int slow = n;
        int fast = happy(n);
        while(fast!=slow) {
            //如果fast能到1,那么slow也必能到1,最后会在1相遇
            if(fast==1) {
                return true;
            }
            fast = happy(happy(fast));
            slow = happy(slow);
        }
        return slow==1;
    }
    public int happy(int n) {
        int ret = 0;
        while(n!=0) {
            ret+=(n%10)*(n%10);
            n/=10;
        }
        return ret;
    }

  4. 盛水最多的容器

11. 盛水最多的容器

题目解析

难点:

这题的难点在于不能使用暴力枚举的方式来解决,而要找到盛水量与容器左右壁的关系.

算法应用:

要解决这道题,要找到盛水量与容器左右壁的关系----盛水量的多少实际上就是由左右壁中较短的一条决定.(容积 = min(左壁,右壁) * 宽度)这里可以使用对撞指针的思想来处理.

我们假设某种状态下容器左壁小于容器右壁,即nums[left]<nums[right].随后,由于采用对撞指针,不论是left++,还是right--,容器宽度都必然减少.

如果让left++,改变nums[left](较短的那个容器壁),就会导致变化后的盛水高度肯定不大于nums[right],这样我们并不知道容积具体的变化情况;然而,我们如果让right--,改变nums[right](较长的那个容器壁)就会导致变化后的盛水高度肯定不大于nums[left],宽度又是在减小的,就保证了变化后的容积必然小于当前容积,由于要找最大的容积,这种情况应该直接舍去.

经过上述分析,可以得到结论: 采用对撞指针,对于每一种情况先判断是否更新最大值,再移动时应该移动指向较短的一条边的指针.

操作细节:

计算容积时的宽度是right-left.

代码实现:

public int maxArea(int[] height) {
        int left = 0,right = height.length-1;
        int min = 0,area = 0;
        while(left<right) {
            min = Math.min(height[left],height[right]);
            area = Math.max(area,min*(right-left));
            if(height[left]<height[right]) left++;
            else right--;
        }
        return area;
    }

  5. 有效三角形的个数

611. 有效三角形的个数

题目解析

难点:

这题的难点在于不能使用暴力枚举的方式来解决,而要找到三角形边长间的单调关系.

算法应用:

要解决这道题,要找到三角形边长间的单调关系.

我们规定三角形三边长分别为a,b,c且a<=b<=c,那么要构成有效的三角形,就只要保证a+b>c.

先对这个数组进行排序,便于寻找单调性.我们就可以先固定最长的一条边c,在它的左边的区间范围中找一个a,一个b.这里我们就可以使用对撞指针的思想,让a=nums[left],b=nums[right].

如果a+b>c,即nums[left]+nums[right]>c,说明a无论取nums[left]~nums[right-1]范围内的值(由于数组有序这些值都大于nums[left]),都能与b组合形成有效的三角形(b视作不动).直接计数,随后让right--.

如果a+b<=c,即nums[left]+nums[right]<=c,说明b无论取nums[left+1]~nums[right]范围内的值(由于数组有序这些值都大于nums[right]),都不能与a组合形成有效的三角形(a视作不动),直接让left++.

操作细节:

固定最长边的时候为了防止重复计算,从后往前遍历固定.

计算有效组数时,有效组数应为right-left.

代码实现:

public int triangleNumber(int[] nums) {
        Arrays.sort(nums);
        int ret = 0,n = nums.length;
        for(int i=n-1; i>=2; i--) {
            int left = 0,right = i-1;
            while(left<right) {
                if(nums[left]+nums[right]>nums[i]) {
                    ret+=right-left;
                    right--;
                }else {
                    left++;
                }
            }
        }
        return ret;
    }

6. 两数之和

LCR 179. 查找总价格为目标值的两个商品

题目解析

难点:

这道题难点在于不能使用暴力算法,计算两个和为s的数字.

算法应用:

我们要观察到这个数组是一个递增数组,就可以利用单调性+双指针(这里是对撞指针)来解决问题.同样定义两个指针left和right.

如果nums[left]+nums[right]<target,说明nums[left]与nums[left+1]~nums[right]范围内的值相加必定小于target,不能得到target.

如果nums[left]+nums[right]>target,说明nums[right]与nums[left]~nums[right-1]范围内的值都相加必定大于target,不能得到target.


如果相等就直接记录下对应的nums[left]和nums[right],直接构造数组返回.

操作细节:

最后多返回一个空数组,来照顾编译器.

代码实现:

public int[] twoSum(int[] nums, int target) {
        int left = 0,right = nums.length-1;
        while(left<right) {
            if(nums[left]+nums[right]>target) {
                right--;
            }else if(nums[left]+nums[right]<target) {
                left++;
            }else {
                return new int[]{nums[left],nums[right]};
            }
        }
        return new int[]{0};
    }

7. 三数之和

15. 三数之和

题目解析

难点:

这题相较于上一题,多加了一个元素并且要找出所有不重复的三元组,即这三个元素的和为0.

算法应用:

处理过程中,我们仍然可以使用上一题的思想.但是在此之前,需要先对数组进行排序(这题的条件中没有数组递增).

随后,从前向后,先去固定一个数nums[i],再在剩下的区间中,找到两个数的和为target=-nums[i],使用上一题的策略即可,这里不过多赘述.找到一组对应三元组后,应该继续缩小区间寻找,防止遗漏符合条件的三元组.

操作细节:

由于答案中不能包含重复的三元组,在对nums[i]的固定和left,right指针移动时,都需要考虑去重.因此,i++的自增条件不能直接放入for循环中.

代码实现:

public List<List<Integer>> threeSum(int[] nums) {
    List<List<Integer>> ret = new ArrayList<>();
    Arrays.sort(nums);
    int n = nums.length;
    for(int i = 0; i < n; ) 
    {
        if(nums[i] > 0) break; 
        int left = i + 1, right = n - 1, target = -nums[i];
        while(left < right)
        {
            int sum = nums[left] + nums[right];
            if(sum > target) right--;
            else if(sum < target) left++;
            else { 
                ret.add(
                  new ArrayList<Integer>(Arrays.asList(nums[i], nums[left], nums[right])));
                left++; right--;
                while(left < right && nums[left] == nums[left - 1]) left++;
                while(left < right && nums[right] == nums[right + 1]) right--;
            }
        }
        i++;
        while(i < n && nums[i] == nums[i - 1]) i++;
    }
    return ret;
}

8. 四数之和

18. 四数之和

题目解析

难点:

这道题相较于上一题的条件和要求,仅仅是增加了一个元素,从三元组变成了四元组,并且这四个元素的和又变成了target.

算法应用:

如果你已经掌握了三数之和这道题目,本题只需要在这个双指针操作之外再包一层循环,先固定nums[i],再固定nums[j],随后使用双指针即可.

操作细节:

本题对nums[i],nums[j],nums[left],nums[right]都需要进行去重.

在对计算所要求的nums[left],nums[right]的和aim时,需要将其定义为long类型,因而需要对target强转为long类型.

代码实现:

public List<List<Integer>> fourSum(int[] nums, int target) {
        Arrays.sort(nums);
        List<List<Integer>> ret = new ArrayList<>();
        int n  = nums.length;
        for(int i=0; i<n;) {
            for(int j=i+1; j<n;) {
                int left = j+1,right = n-1;
                long aim = (long)target-nums[i]-nums[j]; 
                while(left<right) {
                    if(nums[left]+nums[right]>aim) {
                        right--;
                    }else if(nums[left]+nums[right]<aim) {
                        left++;
                    }else {
                        ret.add(Arrays.asList(nums[i],nums[j],nums[left],nums[right]));
                        left++;right--;
                        while(left<right&&nums[left] == nums[left-1]) left++;
                        while(left<right&&nums[right] == nums[right+1]) right--;
                    }
                }
                j++;
                while(j<n&&nums[j]==nums[j-1]) j++;
            }
            i++;
            while(i<n&&nums[i]==nums[i-1]) i++;
        }
        return ret;
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值