双指针练习

三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:

输入:nums = []
输出:[]

示例 3:
输入:nums = [0]
输出:[]

提示:
0 <= nums.length <= 3000
-105 <= nums[i] <= 105

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/3sum

通过排序,从而将便于利用双指针判断对应的情况,同时可以避免重复数字再次枚举,从而减少程序运行的时间。

这时候我们需要考虑指针移动的原则:

nums[ i ] + nums[ j ] + nums[ k ] > 0

当nums[ i ] + nums[ j ] + nums[ k ] > 0的时候,我们需要将 k 这个指针往左边移动,即执行 k --,尝试将他们的和变小,从而判断是否存在他们的和等于0的情况。而不是将 k 指针往右移动,这样只会使她们的和变得更加大,从而偏离了0,不符合我们题目的要求(找到三个数的和为0)。这时候同样不可以对 j 指针进行右移,这样的操作同样会使和变得更加大,不符合题目要求,如果对 j 指针左移的话,就会导致将nums[ j - 1]这个数重新枚举了,所以对 j 指针进行左移同样不合理的。

nums[ i ] + nums[ j ] + nums[ k ] <= 0

当nums[ i ] + nums[ j ] + nums[ k ] == 0,那么表示当前nums[ i ] \nums[ j ]只能和nums[ k ]组合实现他们的和为0,从而将在当前的nums[ i ]的时候,当前的 j 指针的所有情况枚举了,所以需要更新 j 指针,即 j ++;
如果和小于0,表示当前的和小于0,那么表示当前nums[ i ]\nums[ j ]无论如何都没有办法和 j 右边的数组合实现和为0,所以需要更新 j ,即 j++;
之所以不对k进行左右移动的操作,这是因为当前的和小于0,那么表示当前nums[ i ]\nums[ j ]无论如何都没有办法和 j 右边的数组合实现和为0,如果将k右移,那么就会导致他们的和变大,偏离了0,明显不合理。如果将k左移,只会使他们的和变得更小,更加不合理,所以当三者的和小于等于0的时候,需要将j指针往右移动

对应的代码:

class Solution {
    List<List<Integer>> list = new ArrayList<List<Integer>>();
    List<Integer> subList;
    public List<List<Integer>> threeSum(int[] nums) {
         if(nums == null || nums.length < 3)
           return list;
         Arrays.sort(nums);//将对数组进行排序,从而减少重复判断
         int i,j,k;
         for(i = 0; i < nums.length - 2; i++){
             k = nums.length - 1;
             if(i == 0 || nums[i] != nums[i - 1]) {
             /*
             这样可以减少枚举,因为再i不等于0,并且nums[i - 1] == nums[i]
             的时候,nums[i - 1]已经将所有可能的情况都枚举了,所以不需要
             再看nums[i]了
             */
                 for(j = i + 1; j < nums.length - 1 && k > j; j++){
                     if(j == i + 1 || nums[j] != nums[j - 1]){
                         //这里j的if判断和上面i进行判断是一样的
                         while(k > j && nums[i] + nums[j] + nums[k] > 0)
                             k--;
                         }
                         if(k > j && nums[i] + nums[j] + nums[k] == 0){
                            subList = new ArrayList<Integer>();
                            subList.add(nums[i]);
                            subList.add(nums[j]);
                            subList.add(nums[k]);
                            list.add(subList);
                        }
                     }
                 } 
             }
            

         } 
         return list;
    }
   

}

运行结果:
在这里插入图片描述

最接近的三数之和

给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

示例:
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

提示:

3 <= nums.length <= 10^3
-10^3 <= nums[i] <= 10^3
-10^4 <= target <= 10^4

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/3sum-closest

本题和三数之和道理一样的,只是求解的target变成了0而已。

对应的代码:

class Solution {
    public int threeSumClosest(int[] nums, int target) {
        Arrays.sort(nums);
        int i,j,j0,k,k0,sum,res = 10000;//由于题目中nums[i]的范围,所以将res的初始值为10000
        for(i = 0; i < nums.length - 2; i++){
            if(i != 0 && nums[i] == nums[i - 1])
               continue;//如果出现了重复的数字,那么就往跳过
            k = nums.length - 1;
            j = i + 1;
            while(j < k){
                if(j != i + 1 && nums[j] == nums[j - 1])
                   continue;
                sum = nums[i] + nums[j] + nums[k];
                /*
                如果等于target,表示相差为0,此时是最小的,所以直接返回
                sum。虽然没有这一步也可以保证程序运行的结果,但是这样会减
                少运行的时间,如果没有这一步的话,那么就会继续循环,直到将
                所有数字的所有情况的枚举了才可以返回。
                */
                if(sum == target)
                    return sum;
            /*
               (用于检验res初始值为Integer.MAX_VALUE时,Math.abs(res - target)的值)
               System.out.print("Math.abs(res - target) =  " + Math.abs(res - target));
               System.out.println(", sum = " + sum + ", Math.abs(sum - target) = " + Math.abs(sum - target));
            */
                if(Math.abs(sum - target) < Math.abs(res - target)){
                //获取最接近target的三个数的和res
                    res = sum;
                }
                if(sum > target){
                    k0 = k - 1;
                    while(j < k0 && nums[k0] == nums[k])//避免重复数字进行枚举
                       k0--;
                    k = k0;
                }else{
                    j0 = j + 1;
                    while(j0 < k && nums[j0] == nums[j])//避免将重复数字进行枚举
                       j0++;
                    j = j0;
                }
            }
            
        }
        return res;
    }
}

能不能将res初始值为Integer.MAX_VALUE?

如果将res、sum定义的类型为int类型的话,那么定义res的初始值为Integer.MAX_VALUE是不可以的,因为一旦target是一个负数,那么就会导致res - target超出了int的范围,那么这时候Math.abs(res - target)返回的是
Integer.MIN_VALUE.具体原因,如果有大佬知道的话,请指教哈
在这里插入图片描述

运行结果:

在这里插入图片描述

接雨水

在这里插入图片描述
题目来源:https://leetcode-cn.com/problems/trapping-rain-water/

leftMax表示的含义是下标 i 以及其左边最大高度。
rightMax表示的含义是下标 i 以及其右边的最大高度。
所以下标 i 能够接到的雨水的量为两者的最小值减去height[ i ] .

基于此,我们就可以利用双指针进行求解,在程序运行时,需要定义几个变量:
left \ right,leftMax \ rightMax,并且对应的初始值为left = 1,leftMax = height[ 0 ],right = height.length - 2, rightMax = height[height.length - 1].因为两个边界是没有办法接到雨水的,所以只有在[ 1 , height.length - 1]区间中才有可能接到雨水,因此left = 1, right = height.length - 2.所以循环的条件是left <= right才可以保证[ 1, height.length - 1]范围内的下标都已经遍历过了

leftMax = Math.max(leftMax,height[ left ]);
rightMax = Math.max(rightMax,height[ right ] );
通过这个操作,从而得到了left这个指针及其左边的最大值,right这个指针及其右边的最大值
,这时候我们需要如何判断指针移动呢?

当leftMax 小于 rightMax的时候,那么这时候当前left下标右边的最大值大于等于rightMax,所以当前left下标能够接到的雨水量取决于leftMax。因此当前下标left能够接到的雨水量为 leftMax - height[ left ],然后往右移动left
否则,当leftMax大于等于rightMax,那么这时候当前right下标的左边最大值大于等于leftMax,所以当前下标right能够接到的雨水量取决于rightMax,所以下标right能够接到的雨水量为rightMax - height[right],然后往左移动right指针。

在这里插入图片描述

对应的代码:

class Solution {
    public int trap(int[] height) {
         int leftMax = height[0],rightMax = height[height.length - 1],right = height.length - 2,left = 1;
         int total = 0;
         while(left <= right){
             /*
             leftMax表示的时包括当前下标在内的左边最大值,
             所以当leftMax == height[left]的时候,表明当前left前面没有更
             高的高度了,此时它的接到雨水的量为0(满足了leftMax - 
             height[left])
             rightMax表示包括当前right下标在内的在right指针右边的最大值
             所以当rightMax == height[right],表示当前right右边没有更大
             的高度了,此时right指针接到的雨水量就为rightMax - 
             height[right] = 0)
             */
             leftMax = Math.max(leftMax,height[left]);
             rightMax = Math.max(rightMax,height[right]);
             if(leftMax < rightMax){
                 /*
                 如果leftMax小于rightMax,尽管[left + 1,right - 1]中还有
                 一些数字,从而没有办法确定rightMax就是当前下标右边雨水的
                 最大值,但是有一点可以知道的是,当前left指针右边的最大值
                 必然是大于等于rightMax的。
                 并且接水量取决于高度小的那个值,所以当leftMax小于了
                 rightMax,所以表明了当前left下标的接到雨水的量取决于
                 leftMax,对应的量leftMax - height[left]
                 
                 以一旦leftMax小于了rightMax,那么不管rightMax是不是
                 当前下标的右边最大值,都是取leftMax进行计算的
                 */
                total += leftMax - height[left];
                ++left;
             }else{
                 total += rightMax - height[right];
                 --right;
             }
         }
         return total;
    }
}

运行结果:
在这里插入图片描述

旋转链表

在这里插入图片描述
在这里插入图片描述
题目来源:https://leetcode-cn.com/problems/rotate-list/

方法一:环路链表的方式
利用环路链表实现的时候,需要进行两次遍历。
第一次遍历:需要获取链表的长度,同时需要将链表最后一个节点的next指针指向头结点。
第二次遍历:需要遍历原来链表下标为**(len - 1) - k % len的节点,因为这个节点右移k步之后,是环路链表的最后一个节点**。为什么会有这样的关系呢?因为当前这个节点(原来链表中的)的下标是i,右移k个位置之后,新的下标为(i + k) % n,假设这个新下标是环路链表的最后一个下标,所以进行逆推可以知道,当前(i + k) % n = n - 1,可以知道i的下标为(n - 1) - k % n。此时当前这个节点的下一个节点(即第(len - 1) - k % len + 1个节点)就是右移k步之后的链表的头结点
在这里插入图片描述
对应的代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode rotateRight(ListNode head, int k) {
         if(head == null || head.next == null)//链表为空,或者只有一个节点,直接返回这个节点
            return head;
         //获取链表的长度n
         int len = 1,i = 0;
         ListNode cur = head;
         while(cur != null && cur.next != null){
             len++;
             cur = cur.next;
         }
         cur.next = head;//形成环路链表
         cur = head;

         while(i != (len - 1) - k % len){
             cur = cur.next;
             i++;
         }
         ListNode newHead = cur.next;
         cur.next = null;
         return newHead;

    }
}

运行结果:
在这里插入图片描述

方法二:快慢双指针
基本步骤:
1、定义slow、fast两个指针,并且一开始都在head这个位置。
2、获取链表的长度len
2、fast指针先移动k步。但是考虑到k的值有可能大于链表的长度,所以在移动之前需要对k进行取模的操作,即k %= len

为什么需要进行k %= len这一步?

之所以进行这一步,是因为k的值有可能大于等于链表的长度,并且移动k步和移动k % len步是一样的,是因为如果k等于len,那么此时右移k之后的链表和原来的链表是一样的,所以移动k步和移动k % len步的效果是一样的
3、slow、fast同时移动,直到fast的next指针指向为空,此时slow指针指向的节点就是右移k步后的最后一个节点,slow的下一个节点是右移k步之后的第一个节点

为什么fast的next指针为空的时候,slow指针指向的节点是右移k步之后的链表最后一个节点?

相当于求解右移k步之后的头结点在原来链表中的下标,所以(index + k) % n = 0,所以index = n - k,即右移k步后链表的头结点在原来链表中的下标为n - k。也就是说右移k步后的头结点是原来链表第n - k + 1个节点,因此slow指针需要移动n - k步就可以到达这个节点
此时fast已经移动了k步,还有n - k步fast就会变成null,所以在fast.next变成null之前,两个指针都需要移动,当fast.next等于null时,slow移动了n - k - 1步,此时slow指针是右移k步之后的链表的最后一个节点,slow的下一个节点才是右移k步后的链表的头结点,所以进行newHead = slow.next,slow.next = null,fast.next = head操作,这时候newHead就是右移k步止之后的头结点。

对应的代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
     public ListNode rotateRight(ListNode head, int k) {
         if(k == 0 || head == null || head.next == null)//链表为空,或者只有一个节点,直接返回这个节点
            return head;
         //获取链表的长度n
         int len = 1;
         ListNode cur = head;
         while(cur != null && cur.next != null){
             len++;
             cur = cur.next;
         }
         k %= len;//移动k步和移动k % len步的效果是一样的
         if(k == 0)
            return head;
        ListNode slow = head,fast = head;
        //fast移动k步,此时还有n - k步,fast就会变成null,还有n - k - 1步fast.next为null
        while(k > 0){
            fast = fast.next;
            --k;
        }
        //快慢指针同时移动n - k - 1步,完毕之后,slow指向的时右移k步后的链表的最后一个节点,下一个节点才是它的头结点
        while(fast.next != null){
            slow = slow.next;
            fast = fast.next;
        }
        ListNode newHead = slow.next;
        slow.next = null;
        fast.next = head;
        return newHead;

    }
}

运行结果:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值