弗洛伊德的乌龟和兔子
在介绍这个概念之前,首先让我们看一道经典的面试题:
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例1:
输入: [1,3,4,2,2]
输出: 2
示例2:
输入: [3,1,3,4,2]
输出: 3
说明:
1.不能更改原数组(假设数组是只读的)。
2.只能使用额外的
O
(
1
)
O(1)
O(1)的空间。
3.时间复杂度小于
O
(
n
2
)
O(n^2)
O(n2)。
4.数组中只有一个重复的数字,但它可能不止重复出现一次。
(这道题(据说)花费了计算机科学界的传奇人物Don Knuth 24小时才解出来。并且我只见过一个人(注:Keith Amling)用更短时间解出此题。)
初次尝试:排序比较法
我们第一眼见到这道题,会首先想到先通过排序使得数组有序,然后再两两比较数组元素,这样就能轻易的得到重复的数字。可是,正准备开码,我们注意到题目说明数组仅为只读。因此我们无法排序,只能放弃这种方法。
再次尝试:哈希字典法
再思考,既然不能更改,那我们可以额外开销 O ( n ) O(n) O(n)个空间来制作hash表,通过一次hash来找到所需元素。但是,我们发现题目已经限制了空间。因此,我们必须再换一种符合题目说明的方法。
最终方案:快慢指针法
绞尽脑汁后,我们终于想到了使用快慢指针,也就是弗洛伊德的乌龟和兔子来寻找这个循环元素。
但是这道题中并没有任何指针,只有一个数组。因此,我们需要使用抽象思维,将这个数组抽象成链表。
如何将数组抽象成链表?
假设我们有一个数组[1,4,3,5,3,2],现在我们将其抽象成一条拥有六个节点的链表,显然,这是一条循环链表:
1 -> 4 -> 3 -> 5 -> 3 -> 2
现在我们只需进行如下操作:
int a = 0;
while(ture)
{
a = nums[a];
}
a = 1;
a = 4;
a = 3;
a = 5;
a = 2;
a = 3;
…
就能够达到遍历整个链表的效果。而要实现快慢指针,我们只需改变对变量a的操作次数即可。
fast = nums[nums[rabbit]];
slow = nums[turtle1];
(前提:给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n))
有了这样的思想,我们终于可以开始设计算法:让一只兔子和一只乌龟同时从起点出发,则兔子与乌龟相遇时,兔子所走的位移就刚好是乌龟的两倍。现在假设进入循环之前的长度为
L
L
L,进入循环之后快慢指针第一相遇时兔子比乌龟多跑了
N
N
N圈, 每一圈的长度为
C
C
C,此时兔子在环内离入环节点的距离为
c
c
c。
此时乌龟走过的距离为:
L
+
c
L + c
L+c
此时兔子走过的距离为:
L
+
c
+
N
∗
C
L + c + N * C
L+c+N∗C
由位移关系:
2
∗
(
L
+
c
)
=
L
+
c
+
N
∗
C
2 * (L + c) = L + c + N * C
2∗(L+c)=L+c+N∗C
整理后得到:
(
N
−
1
)
∗
C
+
(
C
−
c
)
=
L
(N - 1) * C + (C - c) = L
(N−1)∗C+(C−c)=L
由此可知, 若此时有第二只乌龟同时从起点出发,第一只乌龟继续移动,那么两者必然会在入环节点处相遇。
最终的代码实现如下:
class Solution
{
public:
int findDuplicate(vector<int>& nums)
{
int rabbit = 0, turtle1 = 0;
while(true)
{
rabbit = nums[nums[rabbit]];
turtle1 = nums[turtle1];
if(rabbit == turtle1)
{
int turtle2 = 0;
while(nums[turtle1] != nums[turtle2])
{
turtle1 = nums[turtle1];
turtle2 = nums[turtle2];
}
return nums[turtle1];
}
}
}
};
执行用时 | 内存消耗 |
---|---|
8ms | 12.1MB |
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)