【力扣100题】90.寻找重复数

一、题目描述

给定一个包含 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

二、解题思路总览

方法 时间复杂度 空间复杂度 说明
Floyd 判圈(快慢指针) O(n) O(1) 转化为环形链表检测
二分查找 O(n log n) O(1) 基于抽屉原理
位运算 O(n) O(1) 统计每一位出现次数
哈希表 O(n) O(n) 简单但不符合要求

Floyd 判圈是满足 O(1) 空间的最优解


三、方法一:Floyd 判圈算法(推荐)

3.1 核心思想

将数组转化为环形链表:

  • 数组值代表下一个节点的索引
  • 因为有重复数字,所以存在环
  • 找到环的入口点就是重复数字

为什么能转化成环形链表?

复制代码
数组: [1, 3, 4, 2, 2]

索引: 0 -> 1 -> 3 -> 2 -> 4 -> 2 -> 3 -> 2 -> 4 -> ...
值:   1    3    4    2    2

从索引0出发:
  0 -> nums[0] = 1
  1 -> nums[1] = 3
  3 -> nums[3] = 2
  2 -> nums[2] = 4
  4 -> nums[4] = 2  (重复!)
  2 -> nums[2] = 4  (进入环)
  ...

链表结构:
  0 -> 1 -> 3 -> 2 -> 4 ----+
          ↑                |
          +----------------|

环的入口是 2,就是重复数

3.2 算法流程图

复制代码
数组: [1, 3, 4, 2, 2],转换为链表:

        0 -> 1 -> 3 -> 2 -> 4 -+
            ↑                |  |
            +------------------|

环的入口节点是 2(重复数)

+------------------------------------------------------------+
|  Phase 1: 检测环(找相遇点)                                 |
+------------------------------------------------------------+

初始化:
  slow = 0, fast = 0

+------------------------------------------------------------+
|  第一次迭代:                                                |
|  slow = nums[slow] = nums[0] = 1                            |
|  fast = nums[nums[fast]] = nums[nums[0]] = nums[1] = 3      |
|  slow=1, fast=3, 不相等,继续                               |
+------------------------------------------------------------+
         slow              fast
    0 -> 1 -> 3 -> 2 -> 4 -+
            ↑            |
            +------------+

+------------------------------------------------------------+
|  第二次迭代:                                                |
|  slow = nums[slow] = nums[1] = 3                            |
|  fast = nums[nums[fast]] = nums[nums[3]] = nums[2] = 4      |
|  slow=3, fast=4, 不相等,继续                               |
+------------------------------------------------------------+
                   slow    fast
    0 -> 1 -> 3 -> 2 -> 4 -+
            ↑            |
            +------------+

+------------------------------------------------------------+
|  第三次迭代:                                                |
|  slow = nums[slow] = nums[3] = 2                            |
|  fast = nums[nums[fast]] = nums[nums[4]] = nums[2] = 4      |
|  slow=2, fast=4, 不相等,继续                               |
+------------------------------------------------------------+
                       slow    fast
    0 -> 1 -> 3 -> 2 -> 4 -+
            ↑            |
            +------------+

+------------------------------------------------------------+
|  第四次迭代:                                                |
|  slow = nums[slow] = nums[2] = 4                            |
|  fast = nums[nums[fast]] = nums[nums[4]] = nums[2] = 4      |
|  slow=4, fast=4, 相等!找到相遇点                           |
+------------------------------------------------------------+
                           slow/fast
    0 -> 1 -> 3 -> 2 -> 4 -+
            ↑            |
            +------------+

Phase 1 完成: slow 和 fast 在节点 4 相遇

+------------------------------------------------------------+
|  Phase 2: 找环入口(入口就是重复数)                         |
+------------------------------------------------------------+

初始化:
  slow = 0 (从起点开始)
  fast = 4 (从相遇点开始)

+------------------------------------------------------------+
|  第一次迭代:                                                |
|  slow = nums[slow] = nums[0] = 1                            |
|  fast = nums[fast] = nums[4] = 2                            |
|  slow=1, fast=2, 不相等                                    |
+------------------------------------------------------------+
         slow              fast
    0 -> 1 -> 3 -> 2 -> 4 -+
            ↑            |
            +------------+

+------------------------------------------------------------+
|  第二次迭代:                                                |
|  slow = nums[slow] = nums[1] = 3                            |
|  fast = nums[fast] = nums[2] = 4                            |
|  slow=3, fast=4, 不相等                                    |
+------------------------------------------------------------+
                   slow    fast
    0 -> 1 -> 3 -> 2 -> 4 -+
            ↑            |
            +------------+

+------------------------------------------------------------+
|  第三次迭代:                                                |
|  slow = nums[slow] = nums[3] = 2                            |
|  fast = nums[fast] = nums[4] = 2                            |
|  slow=2, fast=2, 相等!找到环入口                           |
+------------------------------------------------------------+
                       slow/fast
    0 -> 1 -> 3 -> 2 -> 4 -+
            ↑            |
            +------------+

答案: slow = 2,即重复数

3.3 完整代码(slow版本)

cpp 复制代码
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int slow = 0, fast = 0;

        // Phase 1: 找相遇点
        while (1) {
            slow = nums[slow];
            fast = nums[nums[fast]];
            if (slow == fast) break;
        }

        // Phase 2: 找环入口
        int head = 0;
        while (slow != head) {
            slow = nums[slow];
            head = nums[head];
        }

        return slow;
    }
};

3.4 代码逐行解析

Phase 1: 检测环

cpp 复制代码
int slow = 0, fast = 0;
while (1) {
    slow = nums[slow];           // 慢指针走一步
    fast = nums[nums[fast]];    // 快指针走两步
    if (slow == fast) break;     // 相遇说明有环
}
  • 慢指针每次走一步
  • 快指针每次走两步
  • 如果有环,快慢指针一定会相遇

Phase 2: 找环入口

cpp 复制代码
int head = 0;
while (slow != head) {
    slow = nums[slow];
    head = nums[head];
}
  • 环入口的数学性质:起点到入口的距离 = 相遇点到入口的距离
  • 让一个指针从起点开始,一个从相遇点开始,每次走一步
  • 再次相遇时就是环入口

3.5 数学证明(理解要点)

复制代码
设:
  起点到环入口的距离 = F
  环入口到相遇点的距离 = a
  环周长 = C

Phase 1:
  慢指针走的距离 = F + a
  快指针走的距离 = F + a + nC (n >= 1)

  快指针速度是慢指针的2倍:
  2(F + a) = F + a + nC
  F + a = nC

  相遇时: n >= 1,所以 F + a >= C

Phase 2:
  从起点到环入口距离 = F
  从相遇点到环入口距离 = C - a

  相遇点到环入口 = (nC - F) - a = (n-1)C + (C - a - F)

  如果 n = 1: F + a = C, 所以 C - a = F

  所以从相遇点和从起点出发的指针,会在环入口相遇

3.6 复杂度分析

复杂度 说明
时间 O(n) Phase 1 和 Phase 2 各 O(n)
空间 O(1) 只用两个指针变量

四、方法二:二分查找

4.1 核心思想

基于抽屉原理(鸽巢原理):

  • 数组值在 [1, n] 范围
  • 如果小于等于 mid 的数字出现超过 mid 次,则重复数一定在 [1, mid] 区间

4.2 算法流程图

复制代码
数组: [1, 3, 4, 2, 2], n = 5

初始: left = 1, right = 5

+------------------------------------------------------------+
|  mid = 3, 统计 <= 3 的元素个数                              |
|  <= 3 的元素: 1, 3, 2, 2                                    |
|  count = 4                                                 |
|  count(3) = 4 > mid(3), 超过,说明重复数 <= 3              |
|  right = mid = 3                                            |
+------------------------------------------------------------+

+------------------------------------------------------------+
|  mid = 2, 统计 <= 2 的元素个数                              |
|  <= 2 的元素: 1, 2, 2                                       |
|  count = 3                                                 |
|  count(2) = 3 > mid(2), 超过,说明重复数 <= 2              |
|  right = mid = 2                                            |
+------------------------------------------------------------+

+------------------------------------------------------------+
|  mid = 1, 统计 <= 1 的元素个数                              |
|  <= 1 的元素: 1                                             |
|  count = 1                                                 |
|  count(1) = 1 不大于 mid(1), 没超过,说明重复数 > 1        |
|  left = mid + 1 = 2                                        |
+------------------------------------------------------------+

left=2, right=2 -> 答案 = 2

4.3 完整代码

cpp 复制代码
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int left = 1, right = nums.size() - 1;

        while (left < right) {
            int mid = left + (right - left) / 2;
            int count = 0;

            // 统计 <= mid 的元素个数
            for (int num : nums) {
                if (num <= mid) {
                    count++;
                }
            }

            // 如果 count > mid,说明重复数在 [1, mid]
            if (count > mid) {
                right = mid;
            } else {
                // 否则重复数在 [mid+1, right]
                left = mid + 1;
            }
        }

        return left;
    }
};

4.4 复杂度分析

复杂度 说明
时间 O(n log n) 每次统计 O(n),二分 log n 次
空间 O(1) 只用常数个变量

五、方法三:位运算

5.1 核心思想

统计每一位上 1 出现的次数:

  • 如果某一位上 1 的出现次数超过 n/2,说明这一位对重复数有贡献
  • 通过逐位确定,得到重复数

5.2 算法流程图

复制代码
数组: [1, 3, 4, 2, 2], n = 5, 重复数 = 2 (二进制 010)

统计每一位:

+------------------------------------------------------------+
|  第0位:                                                     |
|    数组中第0位为1的数: 1(1), 3(1)                            |
|    1出现次数 = 2                                           |
|    正常应该 <= 5/2 = 2.5,所以2次是正常的                     |
+------------------------------------------------------------+

+------------------------------------------------------------+
|  第1位:                                                    |
|    数组中第1位为1的数: 2(1), 3(1), 2(1)                     |
|    1出现次数 = 3                                           |
|    正常应该 <= 5/2 = 2.5,3 > 2.5,说明重复数的第1位是1       |
+------------------------------------------------------------+

+------------------------------------------------------------+
|  第2位:                                                    |
|    数组中第2位为1的数: 4(1)                                  |
|    1出现次数 = 1                                            |
|    正常应该 <= 2.5,1 <= 2.5,说明重复数的第2位是0             |
+------------------------------------------------------------+

得到: 重复数 = 010 = 2

5.3 完整代码

cpp 复制代码
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int n = nums.size();
        int ans = 0;

        // 统计每一位
        for (int bit = 0; bit < 31; bit++) {
            int count = 0;
            int mask = 1 << bit;

            for (int num : nums) {
                if (num & mask) {
                    count++;
                }
            }

            // 如果 count > n/2,说明这一位是 1
            if (count > n / 2) {
                ans |= mask;
            }
        }

        return ans;
    }
};

5.4 复杂度分析

复杂度 说明
时间 O(n * 31) ≈ O(n) 统计 31 位
空间 O(1) 只用常数个变量

六、方法对比总结

方法 时间复杂度 空间复杂度 推荐指数
Floyd 判圈 O(n) O(1) ★★★★★
二分查找 O(n log n) O(1) ★★★★☆
位运算 O(n) O(1) ★★★☆☆
哈希表 O(n) O(n) ★☆☆☆☆
复制代码
Floyd 判圈的优势:

1. O(1) 空间,不修改数组
2. 时间复杂度 O(n)
3. 代码简洁
4. 体现数学思维(环形链表检测)

为什么快慢指针能检测环?
  - 快指针走两步,慢指针走一步
  - 如果有环,快慢指针一定相遇
  - 数学保证

七、面试追问 FAQ

问题 回答
Q: 为什么快慢指针一定会相遇? 快慢指针速度差为1,在环中相当于相对速度为1,一定会追上
Q: 为什么从相遇点和从起点出发的指针会在环入口相遇? 数学性质:起点到入口的距离 = 相遇点到入口的距离(相位差)
Q: 为什么数组值可以作为下一个索引? 因为数组值在 1,n 范围,且数组长度为 n+1,所以值总是一个有效索引
Q: 如果有多个重复数怎么办? Floyd 算法仍然能找到环入口,但可能不是题目要求的那个
Q: 二分查找的抽屉原理怎么理解? 如果每个数最多出现一次,则 <= mid 的数最多有 mid 个,超过就说明有重复
Q: 位运算方法的时间复杂度? O(32n) = O(n),常数因子大

八、相关题目

题目 难度 关键点
287. 寻找重复数 中等 Floyd 判圈 / 二分
142. 环形链表 II 中等 Floyd 判圈求环入口
141. 环形链表 简单 Floyd 判圈检测环
136. 只出现一次的数字 简单 异或运算
268. 丢失的数字 简单 异或 / 数学公式

九、总结

要点 说明
核心思想 将数组转化为环形链表,找环入口
Phase 1 快慢指针检测环,找相遇点
Phase 2 从起点和相遇点同时出发,在环入口相遇
二分法 基于抽屉原理,统计 <= mid 的元素个数
时间复杂度 O(n)
空间复杂度 O(1)
关键性质 数组值作为索引 -> 隐式链表结构

相关推荐
鱼子星_1 小时前
【数据结构】排序的拓展——快速排序的生态多样性与归并排序沾染文件操作
c语言·数据结构·算法
alphaTao1 小时前
LeetCode 每日一题 2026/6/8-2026/6/14
算法·leetcode
KaMeidebaby1 小时前
卡梅德生物技术快报|噬菌体展示文库构建全流程解析 | 大豆球蛋白纳米抗体筛选实践
人工智能·python·tcp/ip·算法·机器学习
CC数学建模2 小时前
2026年第十六届APMCM 亚太地区大学生数学建模竞赛(中文赛项)赛题B题:高性能芯片热管理系统的优化问题完整思路、代码、模型、文章,全网首发高质量分享!
python·算法·数学建模
爱睡懒觉的焦糖玛奇朵2 小时前
【视觉检测之人员奔跑检测算法开发思路】
人工智能·python·深度学习·算法·yolo·视觉检测
05候补工程师2 小时前
【408考研复习】数据结构核心笔记:字符串模式匹配与内部排序算法全解析
数据结构·经验分享·笔记·考研·算法·排序算法
阿文的代码库2 小时前
浅谈:无向图的欧拉回路
算法
-Thinker2 小时前
AI 算法核心原理与实现
人工智能·算法·机器学习
Eloudy2 小时前
最小权重完美匹配(MWPM)与表面码纠错
算法·量子计算