题目描述(中等难度)

287*. Find the Duplicate Number - 图1

1n 范围内的某些数,放到大小为 n + 1 的数组中,数组要放满,所以一定会有一个重复的数字,找出这个重复的数字。比如 [2,2,2,1]

假设重复的数字只有一个。

解法一和解法二先不考虑题目中 Note 的要求。

解法一 排序

最简单的,先排序,然后两两判断即可。

  1. public int findDuplicate(int[] nums) {
  2. Arrays.sort(nums);
  3. for (int i = 0; i < nums.length - 1; i++) {
  4. if (nums[i] == nums[i + 1]) {
  5. return nums[i];
  6. }
  7. }
  8. return -1;
  9. }

解法二 HashSet

判断重复数字,可以用 HashSet,这个方法经常用了。

  1. public int findDuplicate(int[] nums) {
  2. HashSet<Integer> set = new HashSet<>();
  3. for (int i = 0; i < nums.length; i++) {
  4. if (set.contains(nums[i])) {
  5. return nums[i];
  6. }
  7. set.add(nums[i]);
  8. }
  9. return -1;
  10. }

解法三 二分查找

参考 这里%3A-O(nlog(n)) 。

我们知道二分查找要求有序,但是给定的数组不是有序的,那么怎么用二分查找呢?

原数组不是有序,但是我们知道重复的那个数字肯定是 1n 中的某一个,而 1,2...,n 就是一个有序序列。因此我们可以对 1,2...,n 进行二分查找。

mid = (1 + n) / 2,接下来判断最终答案是在 [1, mid] 中还是在 [mid + 1, n] 中。

我们只需要统计原数组中小于等于 mid 的个数,记为 count

如果 count > mid ,鸽巢原理,在 [1,mid] 范围内的数字个数超过了 mid ,所以一定有一个重复数字。

否则的话,既然不在 [1,mid] ,那么最终答案一定在 [mid + 1, n] 中。

  1. public int findDuplicate(int[] nums) {
  2. int n = nums.length - 1;
  3. int low = 1;
  4. int high = n;
  5. while (low < high) {
  6. int mid = (low + high) >>> 1;
  7. int count = 0;
  8. for (int i = 0; i < nums.length; i++) {
  9. if (nums[i] <= mid) {
  10. count++;
  11. }
  12. }
  13. if (count > mid) {
  14. high = mid;
  15. } else {
  16. low = mid + 1;
  17. }
  18. }
  19. return low;
  20. }

解法四 二进制

参考 这里-solution-using-bit-manipulation-in-10-lines)。137 题 以及 169 题 其实已经用过这个思想,但还是不容易往这方面想。

主要就是我们要把数字放眼到二进制。

然后依次统计数组中每一位 1 的个数,记为 a[i]。再依次统计 1n 中每一位 1 的个数,记为 b[i]i 代表的是哪一位,因为是 int,所以范围是 032

记重复的数字是 res

如果 a[i] > b[i] 也就意味着 res 当前位是 1

否则的话,res 当前位就是 0

举个例子吧,1 3 4 2 2

  1. 1 3 4 2 2 写成 2 进制
  2. 1 [0 0 1]
  3. 3 [0 1 1]
  4. 4 [1 0 0]
  5. 2 [0 1 0]
  6. 2 [0 1 0]
  7. 1 n,也就是 1 2 3 4 也写成 2 进制
  8. 1 [0 0 1]
  9. 2 [0 1 0]
  10. 3 [0 1 1]
  11. 4 [1 0 0]
  12. 依次统计每一列 1 的个数, res = XXX
  13. 原数组最后一列 1 的个数是 2
  14. 1 4 最后一列 1 的个数是 2
  15. 2 不大于 2,所以当前位是 0, res = XX0
  16. 原数组倒数第二列 1 的个数是 3
  17. 1 4 倒数第二列 1 的个数是 2
  18. 3 大于 2,所以当前位是 1, res = X10
  19. 原数组倒数第三列 1 的个数是 1
  20. 1 4 倒数第三列 1 的个数是 1
  21. 1 不大于 1,所以当前位是 0, res = 010
  22. 所以 res = 010, 也就是 2

上边是重复数字的重复次数是 2 的情况,如果重复次数大于 2 的话上边的结论依旧成立。

简单的想一下,1 3 4 2 2 ,因为 2 的倒数第二位的二进制位是 1,所以原数组在倒数第二列中 1 的个数会比14 这个序列倒数第二列中 1 的个数多 1 个。如果原数组其他的数变成了 2 呢?也就2 的重复次数大于 2

如果是 1 变成了 2,数组变成 2 3 4 2 2 , 那么倒数第二列中 1 的个数又会增加 1

如果是 3 变成了 2,数组变成 1 2 4 2 2 , 那么倒数第二列中 1 的个数不会变化。

所以不管怎么样,如果重复数字的某一列是 1,那么当前列 1 的个数一定会比 1n 序列中 1 的个数多。

  1. public int findDuplicate(int[] nums) {
  2. int res = 0;
  3. int n = nums.length;
  4. //统计每一列 1 的个数
  5. for (int i = 0; i < 32; i++) {
  6. int a = 0;
  7. int b = 0;
  8. int mask = (1 << i);
  9. for (int j = 0; j < n; j++) {
  10. //统计原数组当前列 1 的个数
  11. if ((nums[j] & mask) > 0) {
  12. a++;
  13. }
  14. //统计 1 到 n 序列中当前列 1 的个数
  15. if ((j & mask) > 0) {
  16. b++;
  17. }
  18. }
  19. if (a > b) {
  20. res = res | mask;
  21. }
  22. }
  23. return res;
  24. }

解法五

参考 这里-time-and-O(1)-space-without-modifying-the-array.-With-clear-explanation.) ,一个神奇的解法了。

把数组的值看成 next 指针,数组的下标看成节点的索引。因为数组中至少有两个值一样,也说明有两个节点指向同一个位置,所以一定会出现环。

举个例子,3 1 3 4 2 可以看成下图的样子。

287*. Find the Duplicate Number - 图2

  1. nums[0] = 3
  2. nums[3] = 4
  3. nums[4] = 2
  4. nums[2] = 3

所以我们要做的就是找到上图中有环链表的入口点 3,也就是 142 题

具体证明不说了,只介绍方法,感兴趣的话可以到 142 题 看一下。

我们需要快慢指针,同时从起点出发,慢指针一次走一步,快指针一次走两步,然后记录快慢指针相遇的点。

之后再用两个指针,一个指针从起点出发,一个指针从相遇点出发,当他们再次相遇的时候就是入口点了。

  1. public int findDuplicate(int[] nums) {
  2. int slow = nums[0];
  3. int fast = nums[nums[0]];
  4. //寻找相遇点
  5. while (slow != fast) {
  6. slow = nums[slow];
  7. fast = nums[nums[fast]];
  8. }
  9. //slow 从起点出发, fast 从相遇点出发, 一次走一步
  10. slow = 0;
  11. while (slow != fast) {
  12. slow = nums[slow];
  13. fast = nums[fast];
  14. }
  15. return slow;
  16. }

看起来比较简单的一道题,思想用了不少。经典的二分,从二进制思考问题,以及最后将问题转换的思想,都很经典。

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情