刷题使我快乐,满脸开心.jpg
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/find-the-duplicate-number/
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题目
给定一个包含 n + 1
个整数的数组 nums
,其数字都在 [1, n]
范围内(包括 1
和 n
),可知至少存在一个重复的整数。
假设 nums
只有 一个重复的整数 ,返回 这个重复的数
。
你设计的解决方案必须 不修改
数组 nums
且只用常量级 O(1)
的额外空间。
示例 1:
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
输入:nums = [3,1,3,4,2]
输出:3
示例 3 :
输入:nums = [3,3,3,3,3]
输出:3
提示:
1 <= n <= 10^5
nums.length == n + 1
1 <= nums[i] <= n
nums
中只有一个整数
出现两次或多次
,其余整数均只出现 一次
思路
这道题最关键的一句话就是
包含
n + 1
个整数的数组nums
,其数字都在[1, n]
范围内
这个条件带来两个重要信息
- 根据抽屉原理,假如有
n+1
或多于n+1
个元素放到n
个集合中去,其中必定至少有一个集合里有两个元素,这个跟题目中保证的有重复元素还不太一样,因为这里的意思是,在原有的[1, n]
范围内,会有一个多出来的数字是重复的。可能部分元素是缺失的,但是因为只有一个重复元素,所以缺失元素会被重复元素补位
,这个对于二分法很重要。 - 直接将
坐标
和值
建立了一种关系,至于其他的常量级额外空间
,不修改数组
,则更像是推动你把坐标
和值
联系到一起去思考似的
二分法
任意找一个值 mid
,统计数组中 小于等于mid
的数量,正常应该是 不大于mid
的,但如果这个数量 大于mid
那么说明,重复的值肯定是在 mid
左边。因为 抽屉原理
,是存在重复元素的,而重复数字混入才会使得某个区间内的数字变多。
而又因为第二个信息,我们就可以用 坐标
来选取 mid
,通过每选取一个 坐标
就统计小于等于mid
的数量的方式,就可以无视原本乱序的数组,确认重复元素所在的 坐标
了。
等效链表法
这个解法非常巧妙,可是比较难想到。
就是根据第二个信息,既然坐标
和 值
在同一个范围内,那就把数组的值看做是 要跳转到的下标
,形成一个 肯定存在环
的链表,这时思路就变成 寻找有环链表的环入口
了,参考这个题:
LeetCode-142. 环形链表 II
思路就说到这里,上代码看注释~
代码
二分法
func findDuplicate(nums []int) int {
left, right := 0, len(nums)-1
for left < right {
// 用坐标当作锚点寻找重复数字所在区间
mid := (left + right) / 2
count := 0
// 统计数量方式可以无视数组无序
for _, num := range nums {
if num <= mid {
count++
}
}
// 正常应该是小于等于,如果大于肯定是有重复元素在前半部分
if count > mid {
right = mid
} else {
left = mid + 1
}
}
return left
}
等效链表快慢指针法
func findDuplicate(nums []int) int {
// 每走一步,间距就+1,某个间距等于 环长度倍数 时,就相遇了
slowP, fastP := 0, 0
for {
slowP = nums[slowP]
fastP = nums[nums[fastP]]
if slowP == fastP {
break
}
}
// 重置另一个的值,一起走就可以在环入口相遇
// 证明参考 LeetCode-142. 环形链表 II 的分析
slowP = 0
for {
slowP = nums[slowP]
fastP = nums[fastP]
if slowP == fastP {
break
}
}
return slowP
}