diff --git a/solutions/287. Find the Duplicate Number.md b/solutions/287. Find the Duplicate Number.md index ab029be..5dd3224 100644 --- a/solutions/287. Find the Duplicate Number.md +++ b/solutions/287. Find the Duplicate Number.md @@ -1,18 +1,9 @@ # [287. Find the Duplicate Number](https://leetcode.com/problems/find-the-duplicate-number/) # 思路 -给定一个长度为n+1的数组,里面所有的元素都位于[1, n],所以很明显有重复元素,题目假设只有一个重复数字(但可以重复多次),求这个重复数字。要求空间复杂度为常数 -且时间复杂度小于O(n^2)。 +给定一个长度为n+1的只读数组,里面所有的元素都位于[1, n],所以很明显有重复元素,题目假设只有一个重复数字(但可以重复多次),求这个重复数字。要求空间复杂度为常数且时间复杂度小于O(n^2)。 -## 思路一、位操作 - -由于要求常数空间复杂度,所以就不能用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) - -## 思路二、二分法 +## 思路一、二分法 题目说的时间复杂度要小于O(n^2),那么我们可以想到比这个复杂度小一个量级的常见复杂度O(nlogn),由此可想到二分法。 由于重复值肯定是在[1, n]之间,所以我们可以首先得到这个范围的中点mid,然后遍历整个数组,统计所有小于等于mid的数的个数count: * 如果`count <= mid`,则说明重复值一定是大于mid(可以自己举例子理解),应进入右半部分继续二分; @@ -20,6 +11,14 @@ 此时时间复杂度就是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) + ## 思路三 此题还有一个十分巧妙的方法[可参考[此处](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,例如数组 ``` @@ -35,29 +34,20 @@ nums[i]: 3 1 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),这里直接给出代码。 -# C++ -## 思路一 -``` C++ -class Solution { -public: - int findDuplicate(vector& 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; - } -}; -``` +## 若可改变数组 +本题要求数组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)。 + +# C++ + +## 思路一 ``` C++ class Solution { public: @@ -80,6 +70,27 @@ public: }; ``` +## 思路二 +``` C++ +class Solution { +public: + int findDuplicate(vector& 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 { @@ -102,3 +113,20 @@ public: } }; ``` + +## 若可改变数组 +``` C++ +class Solution { +public: + int findDuplicate(vector& 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; + } +}; +```