287. **Find the Duplicate Number
https://leetcode.com/problems/find-the-duplicate-number/description/
题目描述
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.
Example 1:
Input: [1,3,4,2,2]
Output: 2
Example 2:
Input: [3,1,3,4,2]
Output: 3
Note:
You must not modify the array (assume the array is read only).
You must use only constant,
O
(
1
)
O(1)
O(1) extra space.
Your runtime complexity should be less than
O
(
n
2
)
O(n^2)
O(n2).
There is only one duplicate number in the array, but it could be repeated more than once.
解题思路
表面上这道题标定的是 medium, 实际上要想出来太难…
先给出关于这道题比较清晰的参考:
287. Find the Duplicate Number leetcode 的官方解答
leetcode笔记:Find the Duplicate Number
Find the Duplicate Number 找到重复数字
Find the Duplicate Number 牛人博客, LeetCode 有六百多题的解答.
题目的大意是,给定一个包含 n + 1
个整数的数组,其中每一个整数的大小均在[1, n]之间,证明其中至少有一个重复元素存在。同时假设数组中只有一个数字出现重复,找出这个重复的数字。
注意事项:
不可以修改数组(假设数组是只读的) (否则直接用 442. **Find All Duplicates in an Array 的方法可解决)
只能使用常数空间
运行时间复杂度应该小于O(n^2)
数组中只存在一个重复数,但是该数可能重复多次
思路: 首先, 使用鸽笼(抽屉)原理可以证明, 这 n + 1
个数字中必然有重复数字. 可以将 n + 1
个位置想象成 n + 1
双袜子, 1 ~ n
这 n
个数相当于抽屉, 要让 n + 1
双袜子放到 n
个抽屉中, 必然有一个抽屉要放置两双袜子, 翻译过来就是数组中至少有两个位置要取相同的数.
解决本题需要的主要技巧就是要注意到:由于数组的 n + 1
个元素范围从 1
到 n
,我们可以将数组考虑成一个从集合 {1, 2, ..., n}
到其本身(即集合本身)的函数 f
。这个函数的定义为 f(i) = A[i]
。基于这个设定,重复元素对应于一对下标 i != j
满足 f(i) = f(j)
。我们的任务就变成了寻找一对 (i, j)
。一旦我们找到这个值对,只需通过 f(i) = A[i]
即可获得重复元素。这变成了计算机科学界一个广为人知的“环检测”问题。
由于数组元素范围1
到n
,不存在值为 0
的元素, 因此环的形状必然是 P
型的:
x_0 -> x_1 -> ... x_k -> x_{k+1} ... -> x_{k+j}
^ |
| |
+-----------------------+
由Robert Floyd提出的一个著名算法,给定一个ρ型序列,在线性时间,只使用常数空间寻找环的起点。这个算法经常被称为“龟兔”算法.
当我看完 142. Linked List Cycle II 的解法后(这道检测链表中环的问题的解答可以参考 Gitbooks : Linked List Cycle), 再来解决这道题就轻松很多.
为了用检测链表环的思路来解决这道题, 我们可以发现, 比如对于序列 {3, 1, 2, 3, 4}
, 可以得到如下链表:
## 因为 nums[0] = 3, nums[nums[0]] = 3
0 -> 3 ->
|___|
也就是说, 存在 (i, j)
为 (0, 3)
, 使得 f[0] == f[3] == 3
. 所以环是索引 3
, 而值是 f[3] = 3
. (举出这个例子是想说明, 当存在环时, 后面的 2, 4
之类的值也许不会访问到).
那检测链表中的环, 思路是设置快慢指针 (fast
与 slow
), fast
每次移动两步, 而 slow
每次移动一步, 当 fast
和 slow
重合时(除了起始的时候), 说明链表中存在环, 而要找到环的位置, 详情见:
Gitbooks : Linked List Cycle 摘录下来:
譬如下面这个,环的起点就是 n2
。
n6-----------n5
| |
n1--- n2---n3--- n4|
我们仍然可以使用两个指针fast
和slow
,fast
走两步,slow
走一步,判断是否有环,当有环重合之后,譬如上面在n5
重合了,那么如何得到n2
呢?
首先我们知道,fast
每次比 slow
多走一步,所以重合的时候,fast
移动的距离是 slow
的两倍,我们假设 n1
到 n2
距离为 a
,n2
到 n5
距离为 b
,n5
到 n2
距离为 c
,fast
走动距离为 a + b + c + b
,而 slow
为 a + b
,有方程 a + b + c + b = 2 x (a + b)
,可以知道 a = c
,所以我们只需要在重合之后,一个指针从 n1
,而另一个指针从 n5
,都每次走一步,那么就可以在 n2
重合了。
C++ 实现 1
综上, 代码为: 注意初始的情况
class Solution {
public:
int findDuplicate(vector<int>& nums) {
// 初始的时候, slow 和 fast 都应该是 nums[0] 的, 但由于这里使用的是
// while 循环而不是 do...while, 所以 slow 和 fast 写成了下面的形式,
// do...while 的写法参见 leetcode 的官方解答.
// 下面先将 fast 移动两格, 而 slow 只移动一格
int slow = nums[nums[0]], fast = nums[nums[nums[0]]];
while (fast != slow) {
fast = nums[nums[fast]];
slow = nums[slow];
}
// 当 fast 和 slow 相遇后, 要开始查找环的入口. 参见上面思路中 n1, n2, 的
// 图示, 这里将 fast 指向 n1, 而 slow 目前指向的是 n5, 然后每次让它们移动
// 一格, 最终相遇在 n2.
fast = nums[0];
while (fast != slow) {
fast = nums[fast];
slow = nums[slow];
}
return fast;
}
};