跳到正文
第 287 题中等

寻找重复数

Find the Duplicate Number

n + 1 个数都在 1..n 之间,只有一个数重复;不修改数组、只用 O(1) 空间找出它。

在力扣中练习

题目描述

给定一个包含 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 <= 105
  • nums.length == n + 1
  • 1 <= nums[i] <= n
  • nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

 

进阶:

  • 如何证明 nums 中至少存在一个重复的数字?
  • 你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?

解题思路

主解法

Floyd 判圈 · 数组当链表

i → nums[i] 就是一条有环链表,环的入口就是重复的数

排序会改变数组,额外集合又超出常数空间。利用值都在 1..n:把下标 i 指向 nums[i],就得到一张每个节点只有一个后继的引用图。从 0 出发最终会进入一个环。

自拟主例 [2,5,4,6,3,1,4] 的路径是 0→2→4→3→6→4,重复数为 4。图中大字是下标,下方小字是数组值,两个概念不能混淆。

分步图解

slow 走一步到 3,fast 走两步到 3,在环内相遇。

从 0 可达的引用 · 追赶相遇值 · next 引用
0nums=2节点 nums=2,值 0,next → 22nums=4节点 nums=4,值 2,next → 44nums=3节点 nums=3,值 4,next → 33nums=6节点 nums=6,值 3,next → 6slowfast6nums=4节点 nums=4,值 6,next → 4
1 / 5
先快慢相遇,再从 0 同速前进找入口,两个阶段不能混为一谈。

第一阶段 slow 走一步、fast 走两步,保证在环内相遇;这个相遇点还不一定是入口。第二阶段把 slow 放回 0,两者各走一步,再次相遇才得到入口。

入口 4 有两个不同来源指向它,对应数组里出现两次的 4。图中另一条环不可从 0 到达,不影响这条路径;代码全程只读取 nums,不修改数组。

时间 O(n)空间 O(1)

易错点

  • 第二阶段忘了把 slow 放回 0:两人已经相等,第二次循环直接跳过,返回的是相遇点而不是入口。
  • 第一阶段里 fast 只走一步,快慢指针同速,永远追不上。
  • 以为相遇点就是答案:相遇点在环里,入口才是重复的数。
对照解法

二分答案 · 数个数

对值域二分:若 ≤ mid 的数超过 mid 个,重复数就在左半边

正确且不修改数组,额外空间 O(1),但比 Floyd 多出一层二分,时间为 O(n log n)。

另一种完全不碰指针的做法:二分答案。

重复数 x 有一个单调性质:对任意 mid,统计数组里 ≤ mid 的个数 cnt。

  • cnt > mid:1..mid 里塞了超过 mid 个数,必然有重复,答案在 [1, mid];
  • 否则答案在 [mid+1, n]。

原因是 1..mid 一共只有 mid 个不同的整数,落下超过 mid 个元素就一定有重复。反过来,cnt ≤ mid 时,大于 mid 的元素至少有 (n+1)−mid 个,而右边只有 n−mid 个位置,重复只能出在右半边。

于是对值域 [1, n] 二分,每轮 O(n) 数一遍,总复杂度 O(n log n),空间 O(1)。

它比 Floyd 慢一个 log,但思路更接近「二分答案」的通用套路,也不依赖「值恰好能当下标」这种巧合。

  1. 1答案区间取 [1, n],n = nums.size() - 1。
  2. 2取 mid,遍历数组统计 ≤ mid 的个数 cnt。
  3. 3cnt > mid 则答案在左半 [lo, mid],否则在右半 [mid+1, hi]。
  4. 4区间缩到单点时,lo 就是重复数。
时间 O(n log n)空间 O(1)

易错点

  • 把 cnt > mid 写成 cnt >= mid,值域收缩方向反了,答案会偏移。
  • hi 取成 nums.size(),值域多了一段永远不可能有重复的区间。

代码与演示

主解法配有图解;对照解法用于比较思路、复杂度或实现方式。

演示输入nums = [2,5,4,6,3,1,4]

把 i → nums[i] 当作 next:slow=2,fast=4。图中大字是下标。

从 0 可达的引用 · 追赶相遇值 · next 引用
0nums=2节点 nums=2,值 0,next → 22nums=4节点 nums=4,值 2,next → 4slow4nums=3节点 nums=3,值 4,next → 3fast3nums=6节点 nums=6,值 3,next → 66nums=4节点 nums=4,值 6,next → 4
1 / 7
查看 C++ 代码

延伸阅读

  • Floyd 判圈是链表找环的通用工具:环形链表(141)判有没有环,环形链表 II(142)找入口,本题只是把 next 指针换成了 nums[i]。
  • 「值域 1..n + 长度 n+1」这个条件非常强,几乎所有 O(1) 空间解法都建立在它上面;少了它就只能排序或哈希。
  • 二分解法可以推广到「找第 k 小的缺失/重复元素」这类题,因为它只依赖计数,不依赖数组有序。