题面
给定一个包含 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 <= 105nums.length == n + 11 <= nums[i] <= nnums中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次
进阶:
- 如何证明
nums中至少存在一个重复的数字? - 你可以设计一个线性级时间复杂度
O(n)的解决方案吗?
二分查找
-
利用鸽巢原理:对于范围
[1, mid],如果数组中小于等于mid的数字个数超过mid,说明重复数一定在[1, mid]区间内;否则重复数在[mid+1, n]区间内。通过不断缩小查找范围,最终找到重复数 -
代码实现
#include <vector> using namespace std; int findDuplicate_binarySearch(vector<int>& nums) { int left = 1, right = nums.size() - 1; // 数字范围是[1, n],n = nums.size()-1 while (left < right) { int mid = left + (right - left) / 2; // 避免溢出 int count = 0; // 统计数组中<=mid的数字个数 for (int num : nums) { if (num <= mid) { count++; } } // 鸽巢原理:个数超过mid,重复数在左区间 if (count > mid) { right = mid; } else { // 否则在右区间 left = mid + 1; } } return left; // 最终left==right,即为重复数 }
位运算
-
逐位分析:对于每一位(如第
i位),统计数组中所有数字在该位上的1的个数cnt_nums,再统计1~n所有数字在该位上的1的个数cnt_1n。如果cnt_nums > cnt_1n,说明重复数的该位是1,否则是0。最终将所有位的结果组合起来就是重复数 -
代码实现
class Solution { public: int findDuplicate(vector<int>& nums) { // n为数组长度,题目中数组长度是n+1(数字范围1~n),所以这里n = (n+1),后续用n-1表示数字的最大范围 int n = nums.size(), ans = 0; // 第一步:确定需要遍历的二进制最高位(避免遍历无意义的高位) int bit_max = 31; // 初始假设最高位是31(int类型的最大有效位) // 循环条件:(n-1)右移bit_max位后为0,说明该位及更高位都是0,无需遍历 while (!((n - 1) >> bit_max)) { bit_max -= 1; // 逐步减小,找到(n-1)的最高有效二进制位 } // 第二步:逐位分析每一个二进制位,确定重复数的每一位是0还是1 for (int bit = 0; bit <= bit_max; ++bit) { // x:统计数组中所有数字的第bit位为1的个数 // y:统计1~(n-1)中所有数字的第bit位为1的个数(因为数字范围是1~n-1) int x = 0, y = 0; // 遍历数组,同时完成x和y的统计(合并循环,提升效率) for (int i = 0; i < n; ++i) { // 统计数组元素nums[i]的第bit位是否为1,若是则x加1 if (nums[i] & (1 << bit)) { x += 1; } // 统计1~(n-1)中数字i的第bit位是否为1(i>=1才有效,因为数字从1开始) if (i >= 1 && (i & (1 << bit))) { y += 1; } } // 核心逻辑:如果数组中该位为1的个数 > 1~n-1中该位为1的个数,说明重复数的该位是1 if (x > y) { // 将第bit位设为1,累加到最终结果中 ans |= 1 << bit; } } // 所有位分析完成后,ans即为重复的数字 return ans; } };
快慢指针
-
将数组视为一个 “链表”:数组下标
i对应 “节点”,nums[i]对应 “节点的下一个指针”。由于存在重复数,链表必然存在环,重复数就是环的入口节点- 慢指针(龟)每次走 1 步:
slow = nums[slow]; - 快指针(兔)每次走 2 步:
fast = nums[nums[fast]]; - 当快慢指针相遇后,将慢指针重置到起点,然后快慢指针都每次走 1 步,再次相遇时的位置就是环的入口(即重复数)
- 慢指针(龟)每次走 1 步:
-
代码实现
#include <iostream> #include <vector> // 1. 检测数组中是否存在重复数字(仅返回是否存在) bool hasDuplicateNoExtraSpace(const std::vector<int>& nums) { // 处理边界情况:空数组或单个元素 if (nums.size() <= 1) { return false; } int slow = nums[0]; int fast = nums[nums[0]]; // 快慢指针寻找相遇点(证明环存在) while (slow != fast) { // 越界检查:数字超出数组长度,不符合[1, n]前提,无法检测 if (fast >= nums.size() || nums[fast] >= nums.size()) { return false; } slow = nums[slow]; // 慢指针走1步 fast = nums[nums[fast]]; // 快指针走2步 } // 相遇则存在环(重复数字) return true; } // 2. 找到数组中具体的重复数字(前提:已确定存在重复,且数字范围[1, n]) int findDuplicateNumber(std::vector<int>& nums) { // 边界保护:确保数组长度符合前提 if (nums.size() <= 1) { throw std::invalid_argument("数组长度必须大于1"); } // 第一步:快慢指针找到相遇点 int slow = nums[0]; int fast = nums[0]; do { slow = nums[slow]; fast = nums[nums[fast]]; } while (slow != fast); // 第二步:重置慢指针到起点,快慢指针同速移动找环的入口(重复数) slow = nums[0]; while (slow != fast) { slow = nums[slow]; fast = nums[fast]; } // 相遇点即为重复数字 return slow; }
这篇文档有帮助吗?