# [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;
    }
};
```
