【问题描述】[中等]
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。输入: [1,3,4,2,2]
输出: 2说明:不能更改原数组(假设数组是只读的)。
只能使用额外的 O(1) 的空间。
时间复杂度小于 O(n2) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-the-duplicate-number
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
【解答思路】
1. 排序法(不符合题意)
时间复杂度:O(N) 空间复杂度:O(1)
public int findDuplicate(int[] nums) {Arrays.sort(nums);for (int i = 0; i < nums.length - 1; i++) {if (nums[i] == nums[i + 1]) {return nums[i];}}return -1;
}
2. HashSet (不符合题意)
时间复杂度:O(N) 空间复杂度:O(N)
public int findDuplicate(int[] nums) {HashSet<Integer> set = new HashSet<>();for (int i = 0; i <= nums.length - 1; i++) {if (set.contains(nums[i])) {return nums[i];}else{set.add(nums[i]);}}return -1;
}
3.二分查找(符合题意)
时间复杂度:O(NlogN) 空间复杂度:O(1)
public class Solution {public int findDuplicate(int[] nums) {int len = nums.length;int left = 1;int right = len - 1;while (left < right) {// 在 Java 里可以这么用,当 left + right 溢出的时候,无符号右移保证结果依然正确int mid = (left + right) >>> 1;int cnt = 0;for (int num : nums) {if (num <= mid) {cnt += 1;}}// 根据抽屉原理,小于等于 4 的个数如果严格大于 4 个// 此时重复元素一定出现在 [1, 4] 区间里if (cnt > mid) {// 重复元素位于区间 [left, mid]right = mid;} else {// if 分析正确了以后,else 搜索的区间就是 if 的反面// [mid + 1, right]left = mid + 1;}}return left;}
}
4. 双指针(技巧型 符合体题意)
时间复杂度:O(N) 空间复杂度:O(1)
public int findDuplicate(int[] nums) {int slow = nums[0];int fast = nums[nums[0]];//寻找相遇点while (slow != fast) {slow = nums[slow];fast = nums[nums[fast]];}//slow 从起点出发, fast 从相遇点出发, 一次走一步slow = 0;while (slow != fast) {slow = nums[slow];fast = nums[fast];}return slow;
}
5.二进制(不一定符合题意)
比较1到n和nums各位二进制“1”的数量,nums多出来的1的组合就是重复数组
时间复杂度:O(N) 空间复杂度:O(1)
public int findDuplicate(int[] nums) {int res = 0;int n = nums.length; //统计每一列 1 的个数for (int i = 0; i < 32; i++) {int a = 0;int b = 0;int mask = (1 << i);for (int j = 0; j < n; j++) {//统计原数组当前列 1 的个数if ((nums[j] & mask) > 0) {a++;}//统计 1 到 n 序列中当前列 1 的个数if ((j & mask) > 0) {b++;}}if (a > b) {res = res | mask;}}return res;
}
【总结】
1.二分法查找有序数组 还可以用于确定一个有范围的整数
2.二分查找经验(先搞懂思路,再研究细节,多做问题去应用)
- 把定义区间成为左闭右闭区间,左右边界是无差别的,弄成左闭右开,反而增加了思考的复杂程度;
- 明确 int = left + ( right - left ) / 2 这里除以 2 是下取整;
- 明确 while(left <= right) 和 while(left < right) 这两种写法其实在思路上有本质差别, while(left <= right) 在循环体内部直接查找元素,而 while(left < right) 在循环体内部一直在排除元素,第 2 种思路在解决复杂问题的时候,可以使得问题变得简单;
- 始终在思考下一轮搜索区间是什么,把它作为注释写到代码里面,就能帮助我们搞清楚边界是不是能取到,等于、+1 、-1 之类的细节;
- 思考清楚每一行代码背后的语义是什么,保证语义上清晰,也是写对代码,减少 bug 的一个非常有效的策略。
3. 快慢指针 链表 数组(特定条件)
转载链接:https://leetcode-cn.com/problems/find-the-duplicate-number/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by–52/
参考链接:https://leetcode-cn.com/problems/find-the-duplicate-number/solution/er-fen-fa-si-lu-ji-dai-ma-python-by-liweiwei1419/
快慢指针动画参考链接:https://leetcode-cn.com/problems/find-the-duplicate-number/solution/xun-zhao-zhong-fu-shu-by-leetcode-solution/