[LeetCode]287. Find the Duplicate Number 图解Floyd判圈(龟兔赛跑)算法

题目描述

LeetCode原题链接:287. Find the Duplicate Number

Given an array of integers nums containing n + 1 integers where each integer is in the range [1, n] inclusive.

There is only one repeated number in nums, return this repeated number.

You must solve the problem without modifying the array nums and uses only constant extra space.

Example 1:

Input: nums = [1,3,4,2,2]
Output: 2

Example 2:

Input: nums = [3,1,3,4,2]
Output: 3

Example 3:

Input: nums = [1,1]
Output: 1

Example 4:

Input: nums = [1,1,2]
Output: 1

Constraints:

  • 1 <= n <= 105
  • nums.length == n + 1
  • 1 <= nums[i] <= n
  • All the integers in nums appear only once except for precisely one integer which appears two or more times.

解法:快慢指针(c++版)

 1 class Solution {
 2 public:
 3     int findDuplicate(vector<int>& nums) {
 4         int fast = 0, slow = 0;
 5         while(true) {
 6             slow = nums[slow];
 7             fast = nums[nums[fast]];
 8             if(slow == fast) break;
 9         }
10         fast = 0;
11         while(true) {
12             slow = nums[slow];
13             fast = nums[fast];
14             if(slow == fast) break;
15         }
16         return slow;
17     }
18 };

先上代码,这个精简的解法相信大家在各种解析评论里已经见烂了。其核心是使用Floyd判圈算法(Floyd Cycle Detection Algorithm),又称龟兔赛跑算法(Tortoise and Hare Algorithm)来找到环的入口,即那个重复的数。网友 在线疯狂 的 参考解法 里的解析已经比较详细了,但是理解起来还是稍微有点难度的(起码对于我是这样TAT...看了好几遍才看懂)。下面给出一个带图的详细解析作为补充,希望能让大家更好理解Floyd判圈算法背后的逻辑原理以及这道题目是如何和该算法挂钩的。不得不感慨一下这道题的设计,真是无巧不成题呀!

题干分析

题目告诉我们,数组中的元素范围是 [1, n] ,在该范围内如果没有重复,那应当有 n 个元素;但是给定的数组却有 n+1 个元素,这很明显说明在这些数中有且仅有一个数是重复的,即数组中的元素是由1到n范围内的所有正整数和重复的那个数构成的。对于一个有 n+1 个元素的数组,它的下标取值范围是 [0, n],等等!这里是不是觉得有点意思:给定数组下标范围是[0, n],数组中元素取值范围是[1, n] 。什么意思呢?我们可以巧妙地运用下标和元素的值来访问数组。具体来说,我们可以把数组的每一项看作是一个“任意门”,把当前位置的元素值当作是下一个要传送到的位置的下标值,比如 nums[3] = 4 就表示我们将要被传送到下标为4的位置,然后再根据 nums[4] 中存储的值决定下一次被传送的位置;进一步思考,倘若数组中有重复的元素,假设为k,那么将会有两个“任意门”可以到达 nums[k] ;把这两扇“任意门”合并成一个,是不是就能形成一个环路了呢?nums[k] 就是这个环路的入口。而对于 nums[0] 这个位置,因为数组中的元素都是 >= 1的数,因此没有任何一个“任意门”可以到达 nums[0] 位置,也就是说 nums[0] 到 nums[k] 之间是一个直线型链条,nums[0] 位置就是这个链条的起点。

把数组变成环

话不多说直接举个栗子🌰,假设给定的数组是 [1, 3, 2, 4, 7, 3, 5, 6] ,按照上面的解释,我们可以简单的画出从下标为0的位置访问这个数组时的路线:

 

 

需要补充说明的是,对于 nums[2] 这个位置的元素,没有其他任何元素可以到达它而它也只能到达自己所在的位置,所以该元素其实是不参与整个链路的构成的,可以忽略。进一步简化这个链路图,我们就能很清晰地看到,整个过程其实是一个 ρ 型环:一条链条型元素 + 一个环(形似 ρ 这个字母的样子,因此得名)。

 

 

好了,现在我们已经把这个题目转换成找 ρ 型环中环的入口了。是时候让「Floyd判圈算法」登场了!

Floyd判圈算法是个啥

Floyd判圈算法是一个可以在有限状态机、迭代函数或者链表上判断是否存在环,以及判断环的入口与长度的算法。这个算法的妙处是它可以在线性时间内仅使用常数空间就能找到我们想要的环的起点。

一、环存在性判断

我们可以回忆一下小学时候常做的跑步应用题:两个小朋友一个跑的快,一个跑的慢,他们在400m的圆形操场上从同一起点出发赛跑,求相遇的时间。 我们注意到,这个应用题的背景或前提是在圆形赛道上,因为根据常识我们很容易知道,两者从同一位置出发速度不同,如果是在直线方向移动的话,永远不可能相遇。仿照跑步的例子,我们可以用快慢指针来检测是否有环:让两个指针从同一位置出发,以不同的速度移动,如果两者在链表头以外的某一点相遇(即相等)了,那么说明链表中存在环路;否则,如果(快指针)到达了链表的结尾则说明没环。

二、求环的长度

设相遇的点是 A 点,此时让一个指针固定在这个位置不动,另一个指针以一个单位步长向前移动,再次相遇时走过的总步数即为环的长度。

三、求环的入口

设相遇的点是 A 点,将一个指针会退到链表起点,另一个指针在相遇的点,然后同时出发,每次移动一个单位的步长,再次相遇时的位置即为环的入口。

四、数学证明

怎么证明上面的结论呢?我们来推导一下。设 ρ 型环的链条部分的长度为m,环形部分的长度为n,第一次相遇时慢指针走过的路程为X,相遇的点距离环形入口的距离为d,则有:

X = m + n * a + d (a为慢指针在环形区域转过的圈数)

如果快指针的速度是满指针的二倍,则相遇时(两者运动了相同的时间)快指针走过的路程为:

2 * X = m + n * b + d (b为快指针在环形区域转过的圈数)

两式相减,可以得到:

X = n * (b - a) 

也就是说慢指针走过的路程为圈长的整数倍。

此时如果将快指针移到起点,慢指针在相遇点,两者同时向前以一个单位步长移动,当从起点出发的指针到达环入口时,其移动距离为m,而从相遇点出发的指针和起点相距 X + m;因为 X 是圈长的整数倍,所以实际该指针距离起点也为m,即位于环的入口。也就是说,两个指针再次相遇时候的位置就是环的入口。

上述推导过程就是结论三可以这么做的理论依据,至于结论二,很好理解:第一次相遇的位置一定位于环的某处,此时一个指针固定不动,另一个指针以单位步长移动,再次相遇时走过的路程必然就是环长了。

最后再来撸一遍代码

再来回头看一遍最开始给出的代码,不得不再次感慨这道题实在是太巧妙了!非常有趣~

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int fast = 0, slow = 0; // 初始化时将两个指针都放到起点 nums[0]
        while(true) {
            // 慢指针每次移动一个单位
            slow = nums[slow]; 
            // 快指针每次移动两个单位
            // 相当于把下面两个语句合并成了一个:
            // fast = nums[fast];
            // fast = nums[fast];
            fast = nums[nums[fast]]; 
            // 第一次相遇
            if(slow == fast) break;
        }
        // 将其中一个指针放回起点
        fast = 0;
        while(true) {
            // 两个指针每次都移动一个单位
            slow = nums[slow];
            fast = nums[fast];
            // 第二次相遇
            if(slow == fast) break;
        }
        return slow;
    }
};
posted @ 2021-07-10 16:20  夭夭夭夭夭桃子  阅读(125)  评论(0编辑  收藏  举报