双指针的运用

一、双指针

双指针
常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针。

1.1 对撞指针:⼀般⽤于顺序结构中,也称左右指针。

对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
left == right (两个指针指向同⼀个位置)
left > right (两个指针错开)

1.2 快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。

这种⽅法对于处理环形链表或数组⾮常有⽤。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。
快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。
链表中判断是否有环
求链表中间结点的位置

二、习题

2.1 移动零

283. 移动零

        使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。

注意到以下性质:

        左指针左边均为非零数;

        右指针左边直到左指针处均为零。

因此每次交换,都是将左指针的零与右指针的非零数交换,且非零数的相对顺序并未改变

//双指针
class Solution {

    public void moveZeroes(int[] nums) {
        int zero = -1; //处理 零
        int cur = 0; //遍历数组 处理非零
        int size = nums.length;
        for(cur = 0;cur < size;cur++){
            if(nums[cur] != 0){
                zero++;
                int tmp = nums[cur];
                nums[cur] = nums[zero];
                nums[zero] = tmp;
            }
        }
    }
}

 2.2 复写零

1089. 复写零

 

1.先确定最后一个被复写的数(cur)

通过不能超过数组长度写入元素,dest到数组末尾或者越界可以判断

2.其次处理,dest指针越界的情况

dest越界,说明dest所指位置原本应该被复写为0,越界之后这里不用复写,dest-- 回到数组末尾(size-1的位置),然后复写0。两个指针再往前移动dest--,cur--

3.从后往前,复写0

class Solution {
    public void duplicateZeros(int[] arr) {
        //1.找到最后一个“复写”的数
        int dest = -1;
        int cur = 0;
        int size = arr.length;
        while(dest < size){
            if(arr[cur] == 0){
                dest += 2;
            }else{
                dest ++;
            }
            if(dest >=size-1){
                break;
            }
            cur ++;
        }
        //2.处理边界情况
        if(dest >= size){
            // arr[size-1]=0; 这句和下一句一个意思
            arr[dest-1] = 0;
            dest -=2;
            cur--;
        }
        //3.指针从后往前复写0
        while(cur >= 0){
            if(arr[cur] ==0){
                arr[dest] = 0;
        
                dest--;
                arr[dest] = 0;
            }else{
                arr[dest] = arr[cur];
                
            }
            dest--;
            cur--;
        }
    }
}

2.3 快乐数(medium)

202. 快乐数 

题目中,关键信息:

  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。

也就是说,如果最后这个数变为1,1的平方和也是1,1会循环

(1的平方和永远也是1 :循环1-> 1->1 ->1)

                也有可能最后始终变不到 1,也会无限循环

(例如题目中的2 :2 -> 4 -> 16 -> 37 -> 58 -> 89 -> 145 -> 42 -> 20 -> 4 -> 16

无论怎样一定会形成环

为什么?

        因为n的取值为 最大为( 2,147,483,648 -1),最大数的平方和(260) 小于 自己本身,

其他数的平方和也绝对小于本身。260 -> 20 -> 4 ->16 -> 37-> 58-> 89-> 145-> 42 ->20(然后一直重复循环,永远无法为1)

        无论是否为1,他的平方和总在一定范围内变大变小,且永远不会大于自身本身。

一定会形成环

        那么提到环,就很容易想到 快慢指针,判断是否有环,因为快慢指针会在环内相遇。

根据上面描述,环要么一直是1,要么一直不是1。所以只需要判断环内任意一个数是否为1,就能判断出是否为快乐数了,而刚刚好,快慢指针相遇的点就是环内的任意一点。(很巧妙~)

//这些数会形成环 
//链表有环 - 快慢指针

class Solution {
    //求整数的每个位置上的数字的平方和
    public int function(int n){
        int sum = 0;
        while(n != 0){
            sum += Math.pow(n%10,2);
            n /= 10;
        }
        return sum;
    }
    public boolean isHappy(int n) {
        
        //1.定义快慢指针(思想)
        //不一定要使用真的指针,可以让一个数充当指针
        int slow = n;
        int fast = function(n);
        //2.找到相遇点
        while(slow != fast){
            slow = function(slow);
            fast = function(function(fast));
        }
        //3.判断相遇点的值是否为1
        return slow==1?true:false;
    }
}

2.4  盛⽔最多的容器(medium)

 

可以发现水的容量是,两个元素最小的那个值 * 两个元素之间的距离 

        类似于,数组求最大数一样,先设一个最大数max,然后比较,如果比max大就更新max,否则继续找。

        在这里,设left指向数组头,right指向数组尾,他俩先算出一个水容量最为比较的基准maxval

然后移动指针,继续比较:

       

如果此时我们固定⼀个边界,改变另⼀个边界,⽔的容积会有如下变化形式:
容器的宽度⼀定变⼩。随着指针的移动,宽度是一定会变小的。
水的高度,取决于较小的那边( 下限 )。
         如果left < right,移动left,水的高度可能增大或减小 水的容量就有机会变大
         如果移动right,水高的下限不变永远是这么小,再加上宽度减小,水容量必定减少
        由此可⻅,指针所指元素较小的指针需要移动,寻求容量变大的可能
class Solution {
    public int maxArea(int[] height) {
        int left = 0;
        int right = height.length-1;
        int maxV = 0;//保存最大容量
        int i = 0;
        while(left < right){
           int h = Math.min(height[left],height[right]); //水桶的容量取决于最短的木板
           int w = right - left;
           maxV = Math.max(maxV,h*w);//更新最大值
           i++; 
           if(height[left] < height[right]){
                left++;
           }else{
                right--;
           }
        }
        //遍历v,取最大的值
        return maxV;

    }
}

 2.5 有效三⻆形的个数(medium)

611. 有效三角形的个数

根据三角形的定义:任意两条边大于第三边

优化-> 两条最小边大于第三边

1.首先将数组从小到大排序

2.固定最长边,max指向最长边, 取值范围max >=2

3.在剩余的区间[0,max-1] 去找两边之和 大于 这条第三边

//利用单调性
//首先将数组从小到大排序
//再利用三角形的的原理:任意两条边大于第三边 优化为-> 两条最小边大于第三边
//双指针
class Solution {

    public int triangleNumber(int[] nums) {
        Arrays.sort(nums);//排序成:小到大
        // bubblesort(nums);//排序成:小到大

        int size = nums.length;
        int sum = 0; //计数有效组合
        int max = size-1;//最长边的位置
        for(max = size-1;max >= 2;max--){
            int left = 0;//设最小边left的位置
            int right = max-1;//设最小边right的位置
            while(left < right){
                if(nums[left] + nums[right] > nums[max]){
                    sum += right-left; //组合数量
                    right--;
                }else{
                    left++;
                }
            }
        }
        return sum;
        

    }
}

2.6 和为 s 的两个数字(easy)

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

 

给出的数据,是已经排好序的,刚好可以利用这一点

双指针,一头一尾,如果和等于target目标值,返回;

                小于目标值,left++;

                大于目标值,right--;

class Solution {
    public int[] twoSum(int[] price, int target) {
        int size = price.length;
        int left = 0;
        int right = size-1;

        while(left < right){
            if(price[left] + price[right] > target){
                right--;
            }else if(price[left] + price[right] < target){
                left++;
            }else{
                break;
            }
        }
        return new int[]{price[left],price[right]};
    }
}

2.7 三数之和(medium)

可以先将数组排序,利用两数之和的特点

先固定一个数,然后再剩余的区间,求两数之和

        目标是三个数和为 0,固定一个数x之后,剩余两个数之和target就应该是-x

        如果 和大于 target,right--;

        如果 和小于 target,left++;

        等于,则加入到list中

注意点:

        1.去重(可以使用set、或者下面代码里的方法)

        2.找到之后,继续缩小区间寻找

class Solution {

    public List<List<Integer>> threeSum(int[] nums) {

        // 1.排序
        Arrays.sort(nums);

        // 2.固定一个数 nums[i]
        List<List<Integer>> ret = new ArrayList<>();
        int size = nums.length;
        int i = 0;
        // 优化2:排完序,最小和最大值乘积为正数,那比必不可能和为0
        if (nums[0] * nums[size - 1] > 0) {
            return ret;
        }
        while (i < size) {
            // 优化1:当i所指的元素已经为正数,后面的区间永远都是整数,三数之和永远不可能为0
            if (nums[i] > 0) {
                break;
            }
            int left = i + 1;
            int right = size - 1;

            int target = -nums[i];
            while (left < right) {
                // 指定的数为nums[i],区间的和应该是 -nums[i]
                int sum = nums[left] + nums[right];
                // 两者相比较
                if (sum > target) {
                    right--;
                } else if (sum < target) {
                    left++;
                } else {
                    ret.add(new ArrayList<Integer>(Arrays.asList(nums[left], nums[right], nums[i])));
                    // 继续寻找
                    left++;
                    right--;
                    // 去重
                    while (left < right && nums[left] == nums[left - 1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right + 1]) {
                        right--;
                    }

                }

            }
            i++;
            while (i < size && nums[i] == nums[i - 1]) {
                // 去重,要跳过i指向的重复元素
                // 如果i下标指向的值 与 上一次指向的值相同,i直接++
                i++;
            }
        }
        return ret;
    }

}

2.8 四数之和(medium)

与三数之和类似

三数之和固定一个数,再使用双指针

四数之和,固定两个数,再使用双指针

同样需要注意,去重和继续寻找

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        // 1.排序
        Arrays.sort(nums);
        // 固定两个数
        List<List<Integer>> ret = new ArrayList<>();
        int size = nums.length;
        int left = 0;
        int right = size - 1;
        
        //固定数a
        for (int i = 0; i < size;) {
            //固定数b
            for (int j = i + 1; j < size;) {

                long target2 = (long)target - nums[i] - nums[j];
                
                left = j + 1;
                right = size - 1;
                while (left < right) {
                    if (nums[left] + nums[right] > target2) {
                        right--;
                    } else if (nums[left] + nums[right] < target2) {
                        left++;
                    } else {
                        ret.add(new ArrayList<Integer>(Arrays.asList(nums[i], nums[j], nums[left], nums[right])));
                        // 继续找
                        left++;
                        right--;
                        // 去重 left right
                        while (left < right && nums[left] == nums[left - 1]) {
                            left++;
                        }
                        while (left < right && nums[right] == nums[right + 1]) {
                            right--;
                        }
                    }
                }
                j++;
                // 去重b
                while (j < size && nums[j] == nums[j - 1]) {
                    j++;
                }

            }
            i++;
            // 去重a
            while (i < size && nums[i] == nums[i - 1]) {
                i++;
            }
        }
        return ret;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值