题目地址:
https://leetcode.com/problems/find-all-duplicates-in-an-array/
给定一个长 n n n的int数组,其中每个数字都属于 1 ∼ n 1\sim n 1∼n,其中有些数字出现一次,有些两次。返回那些出现过两次的数。
基本思路是,边遍历数组,边把当前的数 k k k给swap到数组下标 k − 1 k-1 k−1的位置上直到不需要继续swap为止,遍历结束后,只需扫描一遍,看哪些数字不在自己“应该”在的位置上,那些数字就是出现过两次的数字。例如,对于 ( 4 , 3 , 2 , 7 , 8 , 2 , 3 , 1 ) (4, 3, 2, 7, 8, 2, 3, 1) (4,3,2,7,8,2,3,1)我们做如下交换: ( 7 , 3 , 2 , 4 , 8 , 2 , 3 , 1 ) (\textbf{7}, 3, 2, \textbf{4}, 8, 2, 3, 1) (7,3,2,4,8,2,3,1) ( 3 , 3 , 2 , 4 , 8 , 2 , 7 , 1 ) (\textbf{3}, 3, 2, 4, 8, 2, \textbf{7}, 1) (3,3,2,4,8,2,7,1) ( 2 , 3 , 3 , 4 , 8 , 2 , 7 , 1 ) (\textbf{2}, 3, \textbf{3}, 4, 8, 2, 7, 1) (2,3,3,4,8,2,7,1) ( 3 , 2 , 3 , 4 , 8 , 2 , 7 , 1 ) (\textbf{3}, \textbf{2}, 3, 4, 8, 2, 7, 1) (3,2,3,4,8,2,7,1) ( 3 , 2 , 3 , 4 , 1 , 2 , 7 , 8 ) (3,2,3,4,\textbf{1},2,7,\textbf{8}) (3,2,3,4,1,2,7,8) ( 1 , 2 , 3 , 4 , 3 , 2 , 7 , 8 ) (\textbf{1}, 2, 3, 4, \textbf{3}, 2, 7, 8) (1,2,3,4,3,2,7,8)遍历结束。我们发现,凡是只出现过一次的数,都和自己应该呆的序号一一对应,而出现过两次的数,必然其中一次出现在自己“不应该呆的位置”上。也就是说,遍历过程中我们做的事是,每遍历到一个数 n u m s [ i ] nums[i] nums[i],我们就看 n u m s [ n u m s [ i ] − 1 ] nums[nums[i]-1] nums[nums[i]−1]是否等于 n u m s [ i ] nums[i] nums[i],如果不等,我们就换过去,直到相等为止。这样遍历到结尾,每个在“不应该在的位置”上的数即为所求。代码如下:
class Solution {
public List<Integer> findDuplicates(int[] nums) {
for (int i = 0; i < nums.length; i++) {
while (nums[nums[i] - 1] != nums[i]) {
swap(nums, nums[i] - 1, i);
}
}
List<Integer> list = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
if (nums[i] != i + 1) {
list.add(nums[i]);
}
}
return list;
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
时间复杂度 O ( n ) O(n) O(n),空间 O ( 1 ) O(1) O(1)。
算法正确性证明:
首先证明,一遍遍历后,对于任意
n
u
m
s
nums
nums中的数字
k
k
k,
n
u
m
s
[
k
−
1
]
=
k
nums[k-1]=k
nums[k−1]=k。由于遍历是从左到右一个一个扫描过去的,所以
n
u
m
s
nums
nums中的每一个数必然会被扫描到,而一旦被扫描到,算法就会保证它应该呆的位置上的数正好等于它。所以结论成立。接下来分情况讨论:
1、如果某个数
k
k
k只出现过一次,那么由于第一个for循环结束后
n
u
m
s
[
k
−
1
]
=
k
nums[k-1]=k
nums[k−1]=k,所以第二个for循环里
n
u
m
s
[
i
]
=
k
nums[i]=k
nums[i]=k当且仅当
i
=
k
−
1
i=k-1
i=k−1,换句话说
k
k
k不会被加进list里去;
2、如果某个数
k
k
k出现过两次,那么由于第一个for循环结束后
n
u
m
s
[
k
−
1
]
=
k
nums[k-1]=k
nums[k−1]=k,所以第二个for循环里满足
n
u
m
s
[
i
]
=
k
nums[i]=k
nums[i]=k的
i
i
i至少有两个,其中一个是
k
−
1
k-1
k−1,另一个不妨设为
i
′
i'
i′,那么第二个for循环遍历到
i
′
i'
i′的时候,
n
u
m
s
[
i
′
]
=
k
≠
i
′
+
1
nums[i']=k\ne i'+1
nums[i′]=k=i′+1,这时候会把
n
u
m
s
[
i
′
]
nums[i']
nums[i′]也就是
k
k
k加进list里去。
所以一个数被加进list当且仅当它出现过两次,所以算法正确。
算法复杂度证明:
空间复杂度显然。对于时间复杂度只需证明swap最多执行
n
n
n次即可。由于每次swap完后,当前的数字
k
k
k就已经挪到了
n
u
m
s
[
k
−
1
]
nums[k-1]
nums[k−1],我们可以想象成swap完后
n
u
m
s
[
k
−
1
]
nums[k-1]
nums[k−1]被染了色,显然如果进行了染色,每个位置只会被染色一次,所以染色次数显然不超过数组长度,所以swap执行次数一定不会超过数组长度。所以时间复杂度是
O
(
n
)
O(n)
O(n)。
我们有另一种观点来看这道题,可以借鉴静态链表的思想。对于
(
4
,
3
,
2
,
7
,
8
,
2
,
3
,
1
)
(4, 3, 2, 7, 8, 2, 3, 1)
(4,3,2,7,8,2,3,1)我们可以分成几条链,第一条是:
4
→
7
→
3
→
2
→
3
4\rightarrow 7\rightarrow 3\rightarrow 2\rightarrow 3
4→7→3→2→3
(
4
,
3
,
2
,
7
,
8
,
2
,
3
,
1
)
(\textbf{4}, \textbf{3}, \textbf{2}, \textbf{7}, 8, 2, \textbf{3}, 1)
(4,3,2,7,8,2,3,1)当第一遍循环后,交换了
4
4
4次(正好是链的路径长度)之后,这条链上的数已经归位了:
(
3
,
2
,
3
,
4
,
8
,
2
,
7
,
1
)
(\textbf{3},\textbf{2}, \textbf{3}, \textbf{4}, 8, 2, \textbf{7}, 1)
(3,2,3,4,8,2,7,1)也就是说,将来只要遍历到链上的位置,就不会再swap了。第二条链是:
8
→
1
→
3
8\rightarrow1\rightarrow3
8→1→3
(
3
,
2
,
3
,
4
,
8
,
2
,
7
,
1
)
(\textbf{3},2, 3, 4, \textbf{8}, 2, 7, \textbf{1})
(3,2,3,4,8,2,7,1)交换了两次:
(
1
,
2
,
3
,
4
,
3
,
2
,
7
,
8
)
(\textbf{1},2, 3, 4, \textbf{3}, 2, 7, \textbf{8})
(1,2,3,4,3,2,7,8)可以看出链的总长度不可能超过数组总长度 + 重复数字数,当然也不会超过
O
(
n
)
O(n)
O(n),所以时间复杂度是
O
(
n
)
O(n)
O(n)。