LeetCode/solutions/287. Find the Duplicate Number.md
2020-01-30 17:36:46 +08:00

165 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# [287. Find the Duplicate Number](https://leetcode.com/problems/find-the-duplicate-number/)
# 思路
给定一个长度为n+1的只读数组里面所有的元素都位于[1, n]所以很明显有重复元素题目假设只有一个重复数字但可以重复多次求这个重复数字。要求空间复杂度为常数且时间复杂度小于O(n^2)。
## 思路一、二分法
题目说的时间复杂度要小于O(n^2)那么我们可以想到比这个复杂度小一个量级的常见复杂度O(nlogn),由此可想到二分法。
由于重复值肯定是在[1, n]之间所以我们可以首先得到这个范围的中点mid然后遍历整个数组统计所有小于等于mid的数的个数count
* 如果`count <= mid`则说明重复值一定是大于mid可以自己举例子理解应进入右半部分继续二分
* 否则重复值不大于mid。
此时时间复杂度就是O(nlogn)
## 思路二、位操作
由于要求常数空间复杂度所以就不能用hash什么的然后要求时间复杂度小于O(n^2),暴力计数也不行。既然直接计数不行,那么我们可以考虑位操作:
计算32位中每一位当中1出现的次数。具体来讲对于第i位0 <= i <= 31我们用count1代表整个nums数组中所有元素第i位是1的个数
用count2代表0~n这些数中第i位是1的个数。若 count1 > count2说明最终所求的重复元素在第i位是1否则是0。这样我们就可以通过分别计算32位1出现的次数来得到结果。
时间复杂度为O(n)
**注意此思路work的前提是数组中重复元素只有一个**
## 思路三
此题还有一个十分巧妙的方法[可参考[此处](https://leetcode.com/problems/find-the-duplicate-number/discuss/72846/My-easy-understood-solution-with-O(n)-time-and-O(1)-space-without-modifying-the-array.-With-clear-explanation.)]。此时要把nums数组看成一个链表`nums[i] = j`表示链表中结点i的next结点为j例如数组
```
index : 0 1 2 3 4
nums[i]: 3 1 3 4 2
```
表示的链表为
```
0 -> 3 -> 4 -> 2
^ |
|_________|
```
可以看到这个链表是有环的为什么有环呢就是因为数组nums中存在重复元素3。仔细分析我们可以发现链表中环的开始结点即为重复元素
即题目转变为求一个带环链表中环的开始结点,这其实就是[142. Linked List Cycle II](https://leetcode.com/problems/linked-list-cycle-ii/),可参见这题我的[题解](https://github.com/ShusenTang/LeetCode/blob/master/solutions/142.%20Linked%20List%20Cycle%20II.md),这里直接给出代码。
**注意此思路work的前提是数组中元素不可能为0即0一定不在环内**
## 若可改变数组
本题要求数组nums是只读的即不能改变nums如果可改变nums那么此题还有其他时间复杂度O(n)空间复杂度O(1)的解法剑指offer第二版面试题3
### 思路四
由于所有元素都在区间[1, n]之内所以如果没有重复的数字那当数组排序后数字m会排在第m位即下标是m-1
所以我们可以重排这个数组。用while循环从头到尾扫描这个数组当扫描到nums[i]时,
1. 如果`i == nums[i] - 1`则这个数字已经在它该在的位置i加1然后进入下一循环即可
2. 否则,如果`nums[i] == nums[nums[i]-1]`,即`nums[i]`出现了两次,找到了重复值,返回即可。
3. 否则,即`nums[i] != nums[nums[i]-1]`,应该交换`nums[i]`和`nums[nums[i]-1]`进入下一循环i不加1
因为调用swap的次数是线性的所以时间复杂度还是O(n)而没有引入额外的数组所以空间复杂度是O(1)。
### 思路五
由于不能有额外的数组或者数据结构如hashmap来标记某个数字之前是否出现过但是注意数组里数字的范围保证在[1, n]之间所以可以利用现有数组设置标志当一个数字被访问过后可以将它索引到的数减n也可加n只是溢出风险大一些之后如果发现当前元素索引到的数小于等于0就说明当前元素之前出现过一次了。此方法相对于思路四的优点是算法结束前可以将数组恢复原样。
## 需要注意此题的几个变种
1. 重复数字可能有多个如剑指offer面试题3此时思路二不再适用
2. 元素范围为[0, n-1]如剑指offer面试题3_1此时思路三不再适用。
# C++
## 思路一
``` C++
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size() - 1;
int low = 1, high = n;
while(low < high){
int mid = low + (high - low) / 2;
int count = 0;
for(auto &num: nums)
if(num <= mid) count++;
if(count <= mid) low = mid + 1;
else high = mid;
}
return low;
}
};
```
## 思路二
``` C++
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int res = 0, mask = 1, n = nums.size() - 1;
for(int i = 0; i < 32; i++){
int count1 = 0, count2 = 0;
for(int j = 0; j <= n; j++){
if(mask & nums[j]) count1++;
if(mask & j) count2++;
}
if(count1 > count2) res += mask;
if(i < 31) mask <<= 1;
}
return res;
}
};
```
## 思路三
``` C++
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = 0, fast = 0;
while(true){
slow = nums[slow];
fast = nums[nums[fast]];
if(slow == fast) break;
}
int p = 0;
while(true){
if(p == slow) break;
slow = nums[slow];
p = nums[p];
}
return slow;
}
};
```
## 若可改变数组
### 思路四
``` C++
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size() - 1;
int i = 0;
while(i <= n){
if(i == nums[i] - 1) i++;
else if(nums[i] != nums[nums[i]-1]) swap(nums[i], nums[nums[i]-1]);
else return nums[i];
}
return 0;
}
};
```
### 思路五
``` C++
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size() - 1;
for(int i = 0; i <= n; i++){
int tmp = nums[i];
if(tmp <= 0) tmp += n; // 当前元素实际值
// tmp索引到的数小于等于0说明tmp之前出现过
if(nums[tmp] <= 0) return tmp;
nums[tmp] -= n;
}
// 算法结束前可以再遍历一遍数组将其恢复原样
return 0;
}
};
```