龟兔算法检测是否有环

龟兔算法思路

在做LeetCode 287. Find the Duplicate Number 时,接触到了这个算法,在网上搜的讲解感觉有一些不够详细,虽然讲出了思路,但是有些跳跃,所以自己用比较徐,但是比较详细的话总结了一遍,方便日后回顾

  • 初始化:设定两个指针,乌龟slow,兔子fast
  • 第一步:判断是否有环。

    • slowfast同时从起点出发,slow一次走一步,fast一次走两步。如果存在环,slow肯定会在一点相遇。而且相遇的点在环里。如果不存在环,fast先会到结尾处。比如两个人在同时从教室往操场跑道上跑,到了跑道之后开始绕圈跑,跑的快的肯定会在跑道的某一点和跑得慢的相遇。跑道即是环,跑道入口就是环的起点。

    • 假设数组起点到环起点长度为m,第一次相遇点到环起点长度为k(前面已经说过k肯定在环内),环长度为n。当第一次相遇时,slow走的总距离i=m+an+kfast走的总距离2i=m+bn+kanbn表示在环里绕了a圈和b圈。fast总距离是2i因为fast一次走两步,所以总距离是slow的两倍。

    • 两个指针走的距离只差i=(b-a)*n。也就是说fastslow多走的距离是环长度的整数倍,slow一共走的距离也是环长度的整数倍。
    • 这里写图片描述
  • 第二步:将其中一个指针拿回数组起点(fastslow都行,这里我们拿fast),另一个(在这是slow)留在第一次相遇的点。让两个指针同时以每次一步的速度往前走。当fast走了m+xn之后(即走了m到达环起点又绕了x圈),fast距离数组起点的距离是m+xn。此时slow从第一次相遇点也走了m+xn,再加上最开始走的islow此时一共走了i+m+xn的距离,由于i=(b-a)*n,即in的整数倍,也就是说,现在slow距离数组起点也是m加上一定的圈数。所以此时slowfast第二次相遇,相遇在环起点,这样就知道环起点在哪了。

  • 时间复杂度:O(n) // 解释待补充

例题

例题一

Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.

Note:
1. You must not modify the array (assume the array is read only).
2. You must use only constant, O(1) extra space.
3. Your runtime complexity should be less than O(n2).
4. There is only one duplicate number in the array, but it could be repeated more than once.

分析:

表面上看是找重复元素,可以挨个比较,但是时间复杂度是O(n2),不满足题目要求。也可用二分法查找,时间复杂度O(nlogn)。但是仔细分析会发现如果数组中存在重复元素,其实可以看成数组形成了一个环。

1->2->3->4->5
         4  | 
       /    |
      8     6
       \   /
         7

所以可以用龟兔算法找到环的入口,即是重复的元素。题目说假设只有一个重复元素。

代码实现:

public int findDuplicate(int[] nums) {
        if (nums.length > 1) {
            // 初始化两个指针,由于第一次相遇的判断是fast和slow是否相等,
            // 如果都初始化为nums[0],第一个while循环就直接跳过了
            // 这里的走两步并不是fast=nums[1],可以将nums数组看做映射关系,走两步是指映射两次
            int slow = nums[0];
            int fast = nums[nums[0]];

            // 快的指针走两步,慢的走一步
            // 此时找到的是第一次相遇的点,在环内。而slow和fast的值是相遇的点的数组的下标,相当于知道了m+k
            while (fast != slow) {
                slow = nums[slow];
                fast = nums[nums[fast]];
            }


            // 将fast移到数组起点。slow仍在第一次相遇的位置。现在两个指针以相同的一次一步的速度往前走
            fast = 0; // 此处不能赋值为nums[0]
            while (fast != slow) {
                fast = nums[fast];
                slow = nums[slow];
            }
            return fast;
        }
        return -1;
    }

例题二

Given a linked list, determine if it has a cycle in it.

直接用上述算法即可。

代码实现

// 节点定义
class ListNode {
     int val;
     ListNode next;
     ListNode(int x) {
         val = x;
         next = null;
     }
 }

public ListNode hasCycle(ListNode head) {
        //不用额外内存,可以用两个指针,一个快一个慢,快的每次走两步,慢的每次走一步,
        //当他俩重合时时,则是环,快的到链表结尾时,不是环
        //即使第一回合没重合,后面肯定能重合

        if (head == null || head.next == null) {
            return null;
        }

        ListNode slow = head;
        ListNode fast = head.next; // 此时的走两步则是多往后一个next

        // 判断是否存在环
        while (slow != fast) {
            if (fast == null || fast.next == null)
                return null;
            fast = fast.next.next;
            slow = slow.next;
        }

        // 寻找环起点,将fast拿回list的起点,不能直接放到head,而要放到head的前一个节点
        ListNode pre = new ListNode(0);
        pre.next=head;
        fast=pre;

        while(slow!=fast){
            System.out.println("2");
            fast=fast.next;
            slow=slow.next;
        }
        return fast;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值