“快慢指针”思想在物理或者逻辑循环中的应用: 为什么用快慢指针而不使用set集合判定循环?

1 基础概念

1.1 什么是物理循环和逻辑循环?

物理循环是指物理索引访问顺序上相邻,逻辑上也相邻,比如循环链表,逻辑循环则指物理的索引上不一定相邻

1.2 快慢指针本质上可以解决逻辑循环问题,而物理循环也属于逻辑循环问题。

2 用快慢指针找出物理循环

2.1 LC141. 环形链表

public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head==null||head.next==null)return false;
        ListNode f=head;
        ListNode s=head;

        while(f!=null&&f.next!=null){
            f=f.next.next;
            s=s.next;
            if(f==s){
                return true;
            }
        }
        return false;
    }
}

3 用快慢指针找出逻辑循环

3.1 LC457. 环形数组是否存在循环

在这里插入图片描述

3.1.1 解析

务必参考题解:快慢指针解法

// 大神的写法,整洁但是难想
class Solution {
    public boolean circularArrayLoop(int[] nums) {
        int n=nums.length;
        for(int i=0;i<n;i++){
            if(nums[i]==0)continue;
            int s=i;
            int f=next(nums,i);
            while(nums[s]*nums[f]>0&&nums[s]*nums[next(nums,f)]>0){
                if(s==f){
                    if(s!=next(nums,s)){
                        return true;
                    }else{
                        break;
                    }
                }
                s=next(nums,s);
                f=next(nums,next(nums,f));
            }

            int add=i;

            while(nums[add]*nums[next(nums,add)]>0){
                nums[add]=0;
                add=next(nums,add);
            }
        }
        return false;
    }
    int next(int nums[],int i){
        int n=nums.length;
        return ((i+nums[i])%n+n)%n;
    }
}
// 我个人的写法
class Solution {
    int next(int[]nums,int i,int sign){
        if(i==10001){
            return i;
        }
        int n=nums.length;
        int ni=((i+nums[i])%n+n)%n;
        if(nums[ni]*sign<0||ni==i){
            return 10001;
        }
        return ni;
    }
    public boolean circularArrayLoop(int[] nums) {
        
        int n=nums.length;

        for(int i=0;i<n;i++){
            int sign=nums[i]<0?-1:1;

            int indexS=i,indexF=i;
            while(true){
                int ns=next(nums,indexS,sign);
                int nf=next(nums,next(nums,indexF,sign),sign);
                if(ns==10001||nf==10001)break;
                if(ns==nf){
                    return true;
                }
                indexS=ns;
                indexF=nf;
                
            }
        }

        return false;
    }
}

3.1.2 Q1:针对这道题,为什么可以用快慢指针呢?快慢指针本身不是针对链表嘛?而且每次只能走一步啊

您的疑问是非常合理的。快慢指针方法通常用于链表环路检测,但其实它也可以适用于这种环形数组问题。这里的关键在于理解快慢指针方法的本质:它是一种用于检测循环或周期性的算法。

在环形数组问题中,即使每次移动的步数不是一步,我们仍然可以使用快慢指针。在这个问题中,"快指针"每次移动两个nums[i]的距离,而"慢指针"每次移动一个nums[i]的距离。这种设置保证了如果存在一个循环,快指针最终会追上慢指针。这就是为什么可以使用快慢指针方法来解决这个问题。

具体到这个问题:

  • 每个元素nums[i]指示从当前位置应该移动的步数,正数表示向前移动,负数表示向后移动。
  • 环形数组意味着数组是循环的,即从数组末尾的下一个元素是数组开头的元素。
  • 快慢指针在这里是用来检测循环的存在。如果存在一个循环,那么快指针最终会在某个点上追上慢指针。
  • 在追踪快慢指针的过程中,还需要考虑移动的方向。如果在移动过程中方向改变(即从正转负或从负转正),则当前路径不构成有效循环。

因此,尽管这个方法最初是为链表设计的,但其检测循环的基本原理在环形数组问题中同样适用。

3.1.3 Q2:这里的步数是指逻辑上位置的步数对吧?

是的,您理解得正确。在这个环形数组问题中,"步数"指的是逻辑上位置的步数。具体来说:

  • 数组中的每个元素 nums[i] 表示从位置 i 出发应该移动的步数。
  • 如果 nums[i] 是正数,则表示向前(即下标递增的方向)移动 |nums[i]| 步。
  • 如果 nums[i] 是负数,则表示向后(即下标递减的方向)移动 |nums[i]| 步。
  • 由于数组是环形的,所以到达数组的末端后会再次从头开始,反之亦然。

在这个问题的上下文中,步数并不是指物理意义上的步行距离,而是指在数组索引上的移动距离。例如,如果 nums[i] = 3,则意味着从索引 i 向前移动3个索引位置。由于是环形数组,当超过数组末端时,计数会从数组开头继续。

3.1.4 Q3:对于快慢指针而言,只要存在环,无论快和慢指针的的初始位置怎么变,最终快慢可以相遇对吗(解释代码中slow和fast初始位置为什么设置的不同)

是的,对于快慢指针法来说,只要存在环,快指针最终都会追上慢指针,从而使两者相遇。这个结论是基于以下几个关键点的:

  1. 不同速度:快指针的移动速度是慢指针的两倍。这意味着如果存在一个环,快指针每轮会逐渐接近慢指针。

  2. 循环性:由于环的存在,快慢指针都会不断地绕环运动。这确保了,只要存在环,快慢指针永远不会“走出”环并停止。

  3. 相遇条件:快慢指针在环中的相对位置每次循环都会变化。由于快指针每次循环比慢指针多走一圈,它们最终必然会在某个点相遇。

这些原则不仅适用于链表中的环检测,也同样适用于类似的环形数组问题。关键是环的存在以及快慢指针的相对速度差。在实际应用中,初始位置一般会从一个固定点开始,例如数组的起始位置,以确保能够覆盖所有可能的环。

3.4.5 (1) 在环中,如果让快指针走三步,慢指针走1步,也最终能相遇吗?(2) 在环中,如果让快指针走三步,慢指针走2步,也最终能相遇吗

在环形结构中,快慢指针是否能相遇取决于它们的相对速度差,而不仅仅是它们各自的速度。我们来分析一下您提出的两种情况:

  1. 快指针每次走三步,慢指针每次走一步:在这种情况下,快指针和慢指针之间的相对速度差是两步。这意味着如果存在一个环,快指针最终会追上慢指针。这是因为快指针每次循环都会逐渐接近慢指针,并最终在某个点与慢指针相遇。

  2. 快指针每次走三步,慢指针每次走两步:在这种情况下,快指针和慢指针之间的相对速度差是一步。这同样意味着如果存在一个环,快指针仍然会最终追上慢指针。虽然这种情况下快慢指针相遇的时间可能会比速度差更大的情况更长,但只要环存在,它们最终还是会相遇。

总结来说,只要快慢指针之间存在速度差,它们就会在环中最终相遇。速度差越大,相遇的可能性就越快出现。在实际应用中,通常选择让快指针的速度是慢指针的两倍,这样可以在保证检测效率的同时简化问题的复杂度。

3.2 LC202. 快乐数

3.2.1 为什么要推荐快慢指针,而不是用set集合

使用 “快慢指针” 思想,找出循环:“快指针” 每次走两步,“慢指针” 每次走一步,当二者相等时,即为一个循环周期。此时,判断是不是因为 1 引起的循环,是的话就是快乐数,否则不是快乐数。
注意:此题不建议用集合记录每次的计算结果来判断是否进入循环,因为这个集合可能大到无法存储;另外,也不建议使用递归,同理,如果递归层次较深,会直接导致调用栈崩溃。不要因为这个题目给出的整数是 int 型而投机取巧。

作者:金字塔下的小蜗牛
链接:https://leetcode.cn/problems/happy-number/solutions/21454/shi-yong-kuai-man-zhi-zhen-si-xiang-zhao-chu-xun-h/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

3.2.1 常规解法:使用set集合记录自己遍历过的节点,后续再次访问时就说明存在环

class Solution {

    public boolean isHappy2(int n) {
        Set<Integer>set=new HashSet<>();
        set.add(n);
        int cur=n;
        while(true){
            int num=cur;
            cur=0;
            while(num!=0){
                int d=num%10;
                cur+=d*d;
                num=(num/10);
            }
            if(cur==1)break;

            if(set.contains(cur))return false;
            set.add(cur);
            
        }
        return true;
    }
}

3.2.2 使用快慢指针

    public boolean isHappy(int n) {

        int s=n,f=n;

        while(true){
            
            int curS=compute(s);
            int curF=compute(compute(f));
            if(curS==1||curF==1)return true;
            if(curS==curF)break;
            s=curS;
            f=curF;
        }
        return false;
    }

    int compute(int num){
        int cur=0;
        while(num!=0){
            int d=num%10;
            cur+=d*d;
            num=(num/10);
        }
        return cur;
    }
  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值