Skip to content

Commit 50d3a59

Browse files
committed
287
1 parent 20bca28 commit 50d3a59

File tree

2 files changed

+215
-0
lines changed

2 files changed

+215
-0
lines changed

SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,4 +236,5 @@
236236
* [282. Expression Add Operators](leetcode-282-Expression-Add-Operators.md)
237237
* [283. Move Zeroes](leetcode-283-Move-Zeroes.md)
238238
* [284. Peeking Iterator](leetcode-284-Peeking-Iterator.md)
239+
* [287*. Find the Duplicate Number](leetcode-287-Find-the-Duplicate-Number.md)
239240
* [更多](more.md)
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# 题目描述(中等难度)
2+
3+
![](https://windliang.oss-cn-beijing.aliyuncs.com/287.jpg)
4+
5+
`1``n` 范围内的某些数,放到大小为 `n + 1` 的数组中,数组要放满,所以一定会有一个重复的数字,找出这个重复的数字。比如 `[2,2,2,1]`
6+
7+
假设重复的数字只有一个。
8+
9+
解法一和解法二先不考虑题目中 `Note` 的要求。
10+
11+
# 解法一 排序
12+
13+
最简单的,先排序,然后两两判断即可。
14+
15+
```java
16+
public int findDuplicate(int[] nums) {
17+
Arrays.sort(nums);
18+
for (int i = 0; i < nums.length - 1; i++) {
19+
if (nums[i] == nums[i + 1]) {
20+
return nums[i];
21+
}
22+
}
23+
return -1;
24+
}
25+
```
26+
27+
# 解法二 HashSet
28+
29+
判断重复数字,可以用 `HashSet`,这个方法经常用了。
30+
31+
```java
32+
public int findDuplicate(int[] nums) {
33+
HashSet<Integer> set = new HashSet<>();
34+
for (int i = 0; i < nums.length; i++) {
35+
if (set.contains(nums[i])) {
36+
return nums[i];
37+
}
38+
set.add(nums[i]);
39+
}
40+
return -1;
41+
}
42+
```
43+
44+
# 解法三 二分查找
45+
46+
参考 [这里](https://leetcode.com/problems/find-the-duplicate-number/discuss/72844/Two-Solutions-(with-explanation)%3A-O(nlog(n)) 。
47+
48+
我们知道二分查找要求有序,但是给定的数组不是有序的,那么怎么用二分查找呢?
49+
50+
原数组不是有序,但是我们知道重复的那个数字肯定是 `1``n` 中的某一个,而 `1,2...,n` 就是一个有序序列。因此我们可以对 `1,2...,n` 进行二分查找。
51+
52+
`mid = (1 + n) / 2`,接下来判断最终答案是在 `[1, mid]` 中还是在 `[mid + 1, n]` 中。
53+
54+
我们只需要统计原数组中小于等于 `mid` 的个数,记为 `count`
55+
56+
如果 `count > mid` ,鸽巢原理,在 `[1,mid]` 范围内的数字个数超过了 `mid` ,所以一定有一个重复数字。
57+
58+
否则的话,既然不在 `[1,mid]` ,那么最终答案一定在 `[mid + 1, n]` 中。
59+
60+
```java
61+
public int findDuplicate(int[] nums) {
62+
int n = nums.length - 1;
63+
int low = 1;
64+
int high = n;
65+
while (low < high) {
66+
int mid = (low + high) >>> 1;
67+
int count = 0;
68+
for (int i = 0; i < nums.length; i++) {
69+
if (nums[i] <= mid) {
70+
count++;
71+
}
72+
}
73+
if (count > mid) {
74+
high = mid;
75+
} else {
76+
low = mid + 1;
77+
}
78+
}
79+
return low;
80+
}
81+
82+
```
83+
84+
# 解法四 二进制
85+
86+
参考 [这里](https://leetcode.com/problems/find-the-duplicate-number/discuss/72872/O(32*N)-solution-using-bit-manipulation-in-10-lines)[137 题](https://leetcode.wang/leetcode-137-Single-NumberII.html#解法三-位操作) 以及 [169 题](https://leetcode.wang/leetcode-169-Majority-Element.html#解法二-位运算) 其实已经用过这个思想,但还是不容易往这方面想。
87+
88+
主要就是我们要把数字放眼到二进制。
89+
90+
然后依次统计数组中每一位 `1` 的个数,记为 `a[i]`。再依次统计 `1``n` 中每一位 `1` 的个数,记为 `b[i]``i` 代表的是哪一位,因为是 `int`,所以范围是 `0``32`
91+
92+
记重复的数字是 `res`
93+
94+
如果 `a[i] > b[i]` 也就意味着 `res` 当前位是 `1`
95+
96+
否则的话,`res` 当前位就是 `0`
97+
98+
举个例子吧,`1 3 4 2 2`
99+
100+
```java
101+
1 3 4 2 2 写成 2 进制
102+
1 [0 0 1]
103+
3 [0 1 1]
104+
4 [1 0 0]
105+
2 [0 1 0]
106+
2 [0 1 0]
107+
108+
1 到 n,也就是 1 2 3 4 也写成 2 进制
109+
1 [0 0 1]
110+
2 [0 1 0]
111+
3 [0 1 1]
112+
4 [1 0 0]
113+
114+
依次统计每一列 1 的个数, res = XXX
115+
116+
原数组最后一列 1 的个数是 2
117+
14 最后一列 1 的个数是 2
118+
2 不大于 2,所以当前位是 0, res = XX0
119+
120+
原数组倒数第二列 1 的个数是 3
121+
14 倒数第二列 1 的个数是 2
122+
3 大于 2,所以当前位是 1, res = X10
123+
124+
原数组倒数第三列 1 的个数是 1
125+
14 倒数第三列 1 的个数是 1
126+
1 不大于 1,所以当前位是 0, res = 010
127+
128+
所以 res = 010, 也就是 2
129+
```
130+
131+
上边是重复数字的重复次数是 `2` 的情况,如果重复次数大于 `2` 的话上边的结论依旧成立。
132+
133+
简单的想一下,`1 3 4 2 2` ,因为 `2` 的倒数第二位的二进制位是 `1`,所以原数组在倒数第二列中 `1 ` 的个数会比`1``4` 这个序列倒数第二列中 `1 ` 的个数多 `1` 个。如果原数组其他的数变成了 `2` 呢?也就`2` 的重复次数大于 `2`
134+
135+
如果是 `1` 变成了 `2`,数组变成 `2 3 4 2 2` , 那么倒数第二列中 `1 ` 的个数又会增加 `1`
136+
137+
如果是 `3` 变成了 `2`,数组变成 `1 2 4 2 2` , 那么倒数第二列中 `1 ` 的个数不会变化。
138+
139+
所以不管怎么样,如果重复数字的某一列是 `1`,那么当前列 `1` 的个数一定会比 `1``n` 序列中 `1` 的个数多。
140+
141+
```java
142+
public int findDuplicate(int[] nums) {
143+
int res = 0;
144+
int n = nums.length;
145+
//统计每一列 1 的个数
146+
for (int i = 0; i < 32; i++) {
147+
int a = 0;
148+
int b = 0;
149+
int mask = (1 << i);
150+
for (int j = 0; j < n; j++) {
151+
//统计原数组当前列 1 的个数
152+
if ((nums[j] & mask) > 0) {
153+
a++;
154+
}
155+
//统计 1 到 n 序列中当前列 1 的个数
156+
if ((j & mask) > 0) {
157+
b++;
158+
}
159+
}
160+
if (a > b) {
161+
res = res | mask;
162+
}
163+
}
164+
return res;
165+
}
166+
```
167+
168+
# 解法五
169+
170+
参考 [这里](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.) ,一个神奇的解法了。
171+
172+
把数组的值看成 `next` 指针,数组的下标看成节点的索引。因为数组中至少有两个值一样,也说明有两个节点指向同一个位置,所以一定会出现环。
173+
174+
举个例子,`3 1 3 4 2` 可以看成下图的样子。
175+
176+
![](https://windliang.oss-cn-beijing.aliyuncs.com/287_2.jpg)
177+
178+
```java
179+
nums[0] = 3
180+
nums[3] = 4
181+
nums[4] = 2
182+
nums[2] = 3
183+
```
184+
185+
所以我们要做的就是找到上图中有环链表的入口点 `3`,也就是 [142 题](https://leetcode.wang/leetcode-142-Linked-List-CycleII.html)
186+
187+
具体证明不说了,只介绍方法,感兴趣的话可以到 [142 题](https://leetcode.wang/leetcode-142-Linked-List-CycleII.html) 看一下。
188+
189+
我们需要快慢指针,同时从起点出发,慢指针一次走一步,快指针一次走两步,然后记录快慢指针相遇的点。
190+
191+
之后再用两个指针,一个指针从起点出发,一个指针从相遇点出发,当他们再次相遇的时候就是入口点了。
192+
193+
```java
194+
public int findDuplicate(int[] nums) {
195+
int slow = nums[0];
196+
int fast = nums[nums[0]];
197+
//寻找相遇点
198+
while (slow != fast) {
199+
slow = nums[slow];
200+
fast = nums[nums[fast]];
201+
}
202+
//slow 从起点出发, fast 从相遇点出发, 一次走一步
203+
slow = 0;
204+
while (slow != fast) {
205+
slow = nums[slow];
206+
fast = nums[fast];
207+
}
208+
return slow;
209+
}
210+
```
211+
212+
#
213+
214+
看起来比较简单的一道题,思想用了不少。经典的二分,从二进制思考问题,以及最后将问题转换的思想,都很经典。

0 commit comments

Comments
 (0)