【Leetcode】442. Find All Duplicates in an Array

题目地址:

https://leetcode.com/problems/find-all-duplicates-in-an-array/

给定一个长 n n n的int数组,其中每个数字都属于 1 ∼ n 1\sim n 1n,其中有些数字出现一次,有些两次。返回那些出现过两次的数。

基本思路是,边遍历数组,边把当前的数 k k k给swap到数组下标 k − 1 k-1 k1的位置上直到不需要继续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[k1]=k。由于遍历是从左到右一个一个扫描过去的,所以 n u m s nums nums中的每一个数必然会被扫描到,而一旦被扫描到,算法就会保证它应该呆的位置上的数正好等于它。所以结论成立。接下来分情况讨论:
1、如果某个数 k k k只出现过一次,那么由于第一个for循环结束后 n u m s [ k − 1 ] = k nums[k-1]=k nums[k1]=k,所以第二个for循环里 n u m s [ i ] = k nums[i]=k nums[i]=k当且仅当 i = k − 1 i=k-1 i=k1,换句话说 k k k不会被加进list里去;
2、如果某个数 k k k出现过两次,那么由于第一个for循环结束后 n u m s [ k − 1 ] = k nums[k-1]=k nums[k1]=k,所以第二个for循环里满足 n u m s [ i ] = k nums[i]=k nums[i]=k i i i至少有两个,其中一个是 k − 1 k-1 k1,另一个不妨设为 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[k1],我们可以想象成swap完后 n u m s [ k − 1 ] nums[k-1] nums[k1]被染了色,显然如果进行了染色,每个位置只会被染色一次,所以染色次数显然不超过数组长度,所以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 47323
( 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 813 ( 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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值