利用双指针解决链表问题

先说快慢指针再谈滑动窗口

快慢指针

快慢指针顾名思义就是在题目中定义两个指针,根据两个指针移动的速度和条件不同。大体上可以分为两类,一类快慢指针 ,主要用于解决链表问题,如判断链表是否有环。一类是左右指针,主要用于解决数组和字符串问题,比如在二分查找中的应用。

141. 环形链表

这个题目就可以定义零个指针,一个是慢指针slow指向head,一个是快指针fast指向head或者是head.next。区别就是slow一次走一步,fast一次走两步。如果存在环的话,那么快指针一定可以追得上慢指针,就像操场跑步,跑得快的人一定可以追上跑得慢的人。即使一开始跑步快的人落后于跑步慢的人,并且可以实现相遇时多跑几圈的情况。

代码如下:

public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head==null || head.next==null) return false;
        ListNode fast=head.next;
        ListNode slow=head;
        while(fast!=slow){    //终止条件就是fast==slow,此时正好追得上。
            if(fast.next==null || fast.next.next==null) return false;//如果没有环,快指针会提前结束
            slow=slow.next;
            fast=fast.next.next;
        }
        return true;
    }
}

142. 环形链表 II

这个题目和上一个题目的区别是不止是判断是否有环,而且还要返回环的入口。比如示例1,就要返回2,因为2是环的入口。

img

这个题目要分为两步,第一步和上题相同,判断是否有环,第二步是返回环的入口。

判断是否有环可以使用和上一题相同的代码。

下面讲解一下判断环的入口

 

如图所示是一个有环的链表,其中a表示不在环上的链表长度,b表示环的链表部分,整个链表长度为a+b。

在判断环相遇的时候,假设此时快指针和慢指针在环内某一点相遇。因为每次慢指针都走一步,快指针走两步。所以f快指针走的长度是慢指针的两倍,即fast=2slow。而且fast=slow+nb,因为fast一直在环内绕圈最后和慢指针相遇,那么一定是走了慢指针的长度加上环长度的整数倍(至于是几倍看具体看链表)。所以可以得到 slow=nb,fast=2nb。

我们在看如果从head头部开始走,走多少步就可以在环的入口,是a+nb,其实n是任意非负整数。联想刚刚的slow指针已经走了nb,只需要再走a步,就可以到入口处,这个和从头部开始走的指针恰好可以在入口处相遇。

那么我们就可以在判断是否有环之后,将快指针重新指向head,然后和慢指针同时每次移动一步,最后相遇几位入口。

代码如下:

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast=head;
        ListNode slow=head;
        while (true) {
            if (fast == null || fast.next == null) return null;
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) break;
        }
        fast = head;
         while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
}

209. 长度最小的子数组

这个题目可以使用暴力法来做,把所有的子数组都求出来,满足sum>=s的时候更新最小长度,最后返回。代码如下:

class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int len=nums.length;
        int min=Integer.MAX_VALUE;
        for(int i=0;i<len;i++){
            int sum=0;
            for(int j=i;j<len;j++){
                sum+=nums[j];
                if(sum>=s){
                    min=Math.min(min,j-i+1);
                    break;
                }
            }
        }
        return min!=Integer.MAX_VALUE?min:0;
    }
}

上面的时间复杂度为n的平方。我们可以优化一下,使用滑动窗口,定义左右指针。这样值需要遍历一次。思路如下:

1,首先定义左右指针同时指向0;

2, 右指针向右加一,sum加num[right],再判断sum是否小于s,如果小于,右指针继续加一,循环判断sum是否大于s,大于或者等于的话,跳出循环;

3,此时判断最小长度,再向右滑动左指针,sum减去nums[l],循环判断是否大于等于s,如果大于,左指针继续加一,如果小于,跳出循环;

4,若右指针没有到重点,则继续判断。

代码如下

class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int len=nums.length;
        if(len<1) return 0;
        int l=0,r=0;
        int min=Integer.MAX_VALUE;
        int sum=0;
        while(r<len){
            while(sum<s && r<len) {
                sum += nums[r];
                r++;
            }
            while(l>=0 && sum>=s){
                min=Math.min(min,r-l);
                sum=sum-nums[l];
                l++;
            }
        }
        return min == Integer.MAX_VALUE ? 0 : min;

    }
}

42. 接雨水

我们先明确一点,什么时候可以积水,肯定是出现凹形才会积水。当然这个是废话,那么如何体现呢,如果在下标为i时,如何判断是否能积水呢,最暴力的方法就是分别向两边遍历,遍历到左右两边各自的最大值,然后比较,相对较小的数值减去i坐标上的数值,就是i坐标上可以积攒的水。

public int trap(int[] height) {
    int res=0;
    int len=height.length;
    for(int i=1;i<len-1;i++){
        int max_left=0,max_right=0;
        for(int j=i;j>=0;j--){
            max_left=Math.max(max_left,height[j]);
        }
        for(int j=i;j<len;j++){
            max_right=Math.max(max_right,height[j]);
        }
        res+=Math.min(max_left,max_right)-height[i];
    }
    return res;
}

遍历数组,对每一个元素,都进行左右遍历,时间复杂度o(n2)

我们可以进行优化,假设我们知道i的数值最高,那么从0-i这段过程中,假设存在一个j,它的最左边存在一个最大值,那么我们就可以确定j上方可以积攒多少水了,因为右边已经存在了最大值了,那么它上面水的高度一定是左边最大值。

class Solution {
    public int trap(int[] height) {
        int len=height.length;
        if(len<2) return 0;
        int index=0;
        int max=height[0];
        
        for(int i=1;i<len;i++){  //找到最大值
            if(height[i]>max){
                index=i;
                max=height[i];
            }
        }

        int res=0;
        int left=height[0];
        for(int i=0;i<index;i++){
            if(height[i]>left){
                left=height[i];
            }else{
                res+=left-height[i]; //如果发现此时的值比左边最大值小,那么它上方积攒的水的高度即为left-height[i]
            }
        }
        
        int right=height[len-1];
        for(int i=len-1;i>index;i--){  //同上面左边的判定
            if(height[i]>right){
                right=height[i];
            }else{
                res+=right-height[i];
            }
        }
        return res;
    }
}

也可以使用双指针,如果left_max<right_max成立,那么它就知道自己能存多少水了。无论右边将来会不会出现更大的right_max,都不影响这个结果。

public int trap(int[] height) {
    int left=0;
    int right=height.length-1;
    int left_max=0,right_max=0;
    int res=0;
    while(left<=right){
        if(left_max<right_max){
            res+=Math,max(0,left_max-height[left]);
            left_max=Maht.max(left_max,height[left]);
            left++;
        }else{
            res+=Math.max(0,right_max-height[right]);
            right_max=Math.max(right_max,height[right]);
            right++;
        }
    }
    return res;
}

876. 链表的中间结点

这个是最典型的双指针用法题目,正常思路是定义快慢指针,快指针走两步,慢指针走一步,快指针停下后,慢指针指向的就是中间节点。但是这题有一个细节,就是数据结构中一般head只有next,不保存数,力扣的head是保存数据的。

class Solution {
    public ListNode middleNode(ListNode head) {
        if(head==null) return null;
        ListNode slow=head;
        ListNode fast=head;
        while(fast!=null&&fast.next!=null  ){
            slow=slow.next;
            fast=fast.next.next;
        }
        return slow;
    }
}

328. 奇偶链表

思路就是定义两个指针,一个指向第一个奇数序列,一个指向第一个偶数序列。分别指向下一个偶数或者奇数序列,之后再让奇数指针指向偶数指针的开头。

代码如下:

class Solution {
    public ListNode oddEvenList(ListNode head) {
        if(head==null || head.next==null) return head;
        ListNode odd=head;   //奇数指针
        ListNode even=head.next;  //偶数指针
        ListNode evenhead=even;  //保存偶数的头指针,以后用于奇数指针指向
        while(even!=null && even.next!=null){
            odd.next=odd.next.next;     //奇数指针指向下一个奇数序列
            even.next=even.next.next;   //偶数指针指向下一个偶数序列
            odd=odd.next;				//奇数指针之下下一个
            even=even.next;
        }
        odd.next=evenhead;   //连接
        return head;
        
    }
}

核心部分也可以这样写

​
            odd.next=even.next;//偶数的下一个节点就是奇数节点
            odd=odd.next;
            even.next=odd.next;
            even=even.next;
​

86. 分隔链表

这个题目和上一题,可以定义两个指针,分别指向比val大的元素和小的元素。最后再让小的元素指针下一次指向大元素指针的开头。

这个就需要在定义两个指针存放上面指针的开头。

代码如下:

class Solution {
    public ListNode partition(ListNode head, int x) {
        ListNode min=new ListNode(-1);   //设置两个虚拟节点
        ListNode max=new ListNode(-1);
        ListNode temp1=min;     //用于指向比val小的节点
        ListNode temp2=max;		//用于指向比val大的节点
        ListNode node=head;		//用于存放比val大的节点的头
        while(node!=null){
            if(node.val<x){
                min.next=node;
                min=min.next;
            }else{
                max.next=node;
                max=max.next;
            }
            node=node.next;
        }
        min.next=temp2.next;   //让min指向max的头节点
        max.next=null;         //这一步一定要加,我跳的坑
        return temp1.next;

    }
}

这里一定要说明一下最后一定要让max.next=null.比如【1,3,5,6,2】这个链表,假设 x=3,则可得到两个链表分别是【1,2】和【3,5,6】,但此时右链表的尾部,即6的尾部仍指向2,如果不把右链表的尾部置为None,最后就会得到 1->2->3->5->6->2 这个链表,在节点2处成环。因此必须将右链表尾部手动置为None,实现断链,最后得到 1->2->3->5->6->None。

下面再来看一下两个有联系的题目

206. 反转链表

示例

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

这个题目有很多的解法,但是我只会用双指针············。首先定义一个空指针pre,cur指向head,然后循环遍历,让cur.next指向pre,然后同时向后移动一位。

代码如下:

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre=null;
        ListNode cur=head;
        while(cur!=null)
        {
            ListNode temp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=temp;
        }
        return pre;
    }
}

25. K 个一组翻转链表

示例:

给你这个链表:1->2->3->4->5

当 k = 2 时,应当返回: 2->1->4->3->5

当 k = 3 时,应当返回: 3->2->1->4->5

有示例可知,这个题目是上一题的加强版。上一题是把所有节点都反转,这个题目是每k个反转一下。首先可以定义一个反转的函数,输入某段链表开头和结尾。就可以将其反转。难点就是要记住开始反转的头节点,让上一个返回的尾节点指向它,一直循环,直至剩下的节点数小于K。返回整个节点的头节点。

代码如下:

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        if(head==null) return head;
        ListNode dummy=new ListNode(-1);  //虚设一个节点
        dummy.next=head;
        ListNode prev=dummy;  //记录每次反转的开头节点
        ListNode end=dummy;   //记录每次反转的尾部节点
        while(end.next!=null){
            for(int i=0;i<k && end!=null;i++){    //让尾部节点指向K个之后的节点。
                end=end.next;
            }
            if(end==null) break;    //这一步一定要加,让剩下数量小于k的节点不进行下面的反转,直接跳出。
            ListNode start=prev.next;   //继承prev
            ListNode end_next=end.next;  //记录此时的尾节点的下一个节点
            prev.next=reverse(start,end.next);  //进行反转,注意这里的尾节点传的是end.next,因为反转链表最后一个没有反转,为了可以反转,需要输入end.next。prev.next指向返回的首节点
            start.next=end_next;//让start也就是刚刚反转后成了尾部的节点,让它指向end_next
            prev=start;   //重新记录
            end=prev;        
        }
        return dummy.next;   
    }
    
    //反转链表
    //!!!!这里输入1->2->3  反转成null<-1<-2  3->
    public ListNode reverse(ListNode start,ListNode end){  
        ListNode pre=null;
        ListNode cur=start;
        while(cur!=end){
            ListNode temp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=temp;
        }
        return pre;
    }
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值