题目:
Given an array of integers
nums
containingn + 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 arraynums
and uses only constant extra space.
之前其实大概说过这道题用快慢指针做,不过时隔一年,重新写的时候发现只是记得一个 快慢指针,但是不记得具体的实现方法了,所以想着重新整理一下。
这道题本质上是个链表题
首先捋一下题目的重点:
n + 1
个数字[1, n]
首先考虑一下,如果数组中不包含重复数字,并且每个数字的限定范围依旧是 [1, n]
的场景,那也就是说,排序后一定会出现 [ 1 , 2 , . . . , n ] [1, 2, ..., n] [1,2,...,n],将当前下标中的值视作下个数字的下标,视觉上的效果就是这样的:
这个时候就算随意将其打乱,最终形成的结果也是:
也就是说不管怎么打乱,它都会存在一个 1-to-1 的关系,最终结束循环。但是如果数组中出现了一个重复数字,运用快慢指针的技巧,在某个情况下 (依旧是 O ( n ) O(n) O(n) 的时间复杂度),一定会两个指针一定会通过重复数字,而抵达同一个结点的情况下。
以我手残不小心打错的 LC 提供的案例来说,视觉效果如下:
可以非常清楚的看到,出现了两个结点(这里将 array 中的每个 index 视作一个结点)指向另外一个结点的情况。
简单的过一下这个案例,效果如下:
黄色代表慢指针,蓝色代表快指针,绿色代表两点相交
iteration | graph |
---|---|
1 | |
2 | |
3 | |
4 | |
5 |
最终快慢指针是否会落在重复数字上与数组有关,LC 上的一个案例 [2,5,9,6,9,3,8,9,7,1]
最终的落点在 7
上。这个数组比较长,我就不跑了。
现在已经找到这个圈了,接着将其中一个指针指向数组的开始,继续走到两点相交的情况,就能够找到重复的点了:
我个人觉得,这个指向重复的点还是挺清楚的,开始和结束的指针都一样,也就代表着圈的开始,所以这就是重复的数字。
代码如下:
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
slow, fast = 0, 0
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
slow = 0
while True:
slow = nums[slow]
fast = nums[fast]
if slow == fast:
return slow
这个算法的时间复杂度为 O ( n ) O(n) O(n),是属于 Follow up 的解法
这个二分搜索也属于变种方法,其思路是:
找到一个数字 k ∈ [ 1 , n − 1 ] k \in [1, n - 1] k∈[1,n−1]
这里取 [ 1 , n − 1 ] [1, n - 1] [1,n−1] 这个范围的原因是,因为重复数字的关系,数组里面最大的数字只可能是 n − 1 n - 1 n−1
计算小于等于 k k k 的数字,使得 c o u n t ( k ) = ∣ X ∣ x ∈ n u m s a n d x ≤ k ∣ count(k) = |{X | x \in nums \, and \, x \le k}| count(k)=∣X∣x∈numsandx≤k∣
以 1, 3, 4, 2, 2
为例,这里的 k k k 取 3,那么计算小于等于 3 的数字就有 4 个,那么自然就包含一个重复数字,反之亦然。
代码如下:
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
l, h = 1, len(nums) - 1
while l < h:
m = (l + h) // 2
c = sum(1 for num in nums if num <= m)
if c <= m:
l = m + 1
else:
h = m
return l
因为这里是一个二分搜索整个数组的长度+遍历整个数组,所以时间复杂度为 O ( n l o g ( n ) ) O(n log(n)) O(nlog(n))