给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间,包括 1 和 n ,可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:
输入: [1,3,4,2,2]
输出: 2
示例 2:
输入: [3,1,3,4,2] 输出: 3
说明:
- 不能更改原数组(假设数组是只读的)。
- 只能使用额外的 O(1) 的空间。
- 时间复杂度小于 O(n2) 。
- 数组中只有一个重复的数字,但它可能不止重复出现一次。
二分法:
很多博客直接上来就是代码,让人云里雾里,解释的不也到位,需要阅读者花费很长时间来理解和吸收。这里直接以举例的方式来讲解。比如原数组a为(重复数字为2)
[1,2,2,3,4,5]
一共n+1个数字(n=5),数字范围为1-n及1-5,根据抽屉原理,在数字1-5中有6个数字,则至少有一个数字出现了两次以上。我们初始化low=1(数字的范围下限),high=5(数字的范围上限),mid=(high+low)/2=3,则mid把原数组分为1-3(包含3)和大于3的部分,如果原数组的数字在1-3的出现的次数cnt大于mid(简单理解为数字1-3最多只能包含3个数字,如果包含的数字个数大于3个,那么一定会出现重复的数字,及重复的数字一定在1-3的范围内),所以每次二分,在每个二分循环内都遍历一次数组,时间复杂度为O(nlogn),空间复杂度为O(1)。
代码如下:
int findDuplicate(vector<int>& nums) { //数字下限 int low = 1; //数字上限 int high = nums.size() - 1; while (low < high) { int cnt = 0; int mid = (low + high) / 2; for (int i = 0; i < nums.size(); i++) { //统计小于等于mid的个数 if (nums[i] <= mid) { cnt++; } } //如果大于mid则在数字在小的那部分,及low-mid if (cnt > mid) { high = mid; } //否则,在大的那部分mid+1-high else { low = mid + 1; } } return low; }
循环链表法:
这种方法非常巧妙,中心思想是如果数组中存在重复数字,那对数组a而言:
i->a[i]->a[a[i]]->a[a[a[i]]]
的访问方式最后一定会陷入循环,且循环的数就是重复的数,以下举例说明:
对于不重复的数组:
[1,2,6,3,4,5,8]
我们以下方式生成链表:下标(0)->下标对应的值(1)->下标对应的值[下标对应的值](2)。。。的方式来遍历数组,可以得到如下链表:
0->1->2->6->8->nullptr
不会出现循环,如果对于重复数组:
[1,2,2,3,4,5]
构建的链表为:
0->1->2->2->2->2->......(为什么不是第一个2,因为循环是从第二个2才开始循环,第一个2只是刚好下标为1的数为2)
节点2开始循环,重复的数字也是2,所以思路转化为寻找带环链表的第一个环节点,通过快慢指针来做,之前leetcode已经分析过,点击这里,这里不再赘述。
int findDuplicate(vector<int>& nums) { int n = nums.size(); if (n > 0) { //访问下标0的值 int slow = nums[0]; //“下标0的值”作为下标访问 int fast = nums[nums[0]]; while (slow != fast) { slow = nums[slow]; fast = nums[nums[fast]]; } fast = 0; while (slow != fast) { slow = nums[slow]; fast = nums[fast]; } return slow; } return -1; }