YSMull
<-- home

再做《哪些数字没有出现》

给定数组A,大小为n,数组元素为1到n的数字,不过有的数字出现了多次,有的数字没有出现。请给出算法和程序,统计哪些数字没有出现,哪些数字出现了多少次。能够在O(n)的时间复杂度,O(1)的空间复杂度要求下完成么?

9年前在微博刷看到这道题,某同学给出的算法让我印象深刻,于是记录在了哪些数字没有出现这篇blog里,最近回顾这道题,发现一下子还看不太懂解法,于是决定用它来考一考同事晴神。

方法一 位运算

旋风小晴天看了这道题,思索片刻后,给出了一个位运算的思路,大致就是,如果数字范围在32位之内,我们就用64位来存储这些数字,那么每个数字的高位32位都是0,就可以用来存储某个数字出现了多少次。对于某个数字 A[i],就用 A[A[i]] 的空闲的高位来存储 A[i] 出现了多少次,最后对任意的 $k\in[1, n]$,A[k] » 32 就是他出现的次数。

实际操作时,这个解法可以优化一下,我们用低位的32位存储次数,把数字移到高位32位,这样可以简单的用 A[A[i]]++ 来累加数字出现的次数。

public static void main(String[] args) {
    long[] arr = {4, 5, 2, 2, 1, 0};

    // 64位的数字,每个数字移动到高位32位
    for (int i = 0; i < arr.length; i++) {
        arr[i] = arr[i] << 32;
    }

    // 统计每个数字出现的次数
    for (int i = 0; i < arr.length; i++) {
        arr[(int) (arr[i] >> 32)]++;
    }

    // 只取前32位,即低位存储的次数
    for (int i = 0; i < arr.length; i++) {
        arr[i] = arr[i] & 0x00000000FFFFFFFFL;
    }

    // [1, 1, 2, 0, 1, 1]
    System.out.println(Arrays.toString(arr));
}

方法二 n+1进制

然后我把这道题的解法给晴神看了看,并且讲解了算法是如何运行的,结果晴神发现,不用那么复杂的分析,可以用进制的角度去理解那个解法。

因为所有的数字都是小于等于 n 的整数,在 n+1 进制下,这些数字都是个位数,比如0~9的数字,我们可以把所有数字乘以 10,则个位数全部变成了 0。

n+1 进制下,我们把所有数字都乘以 n+1,那么这些数字在 n+1 进制下的个位数就都是 0 了,然后在个位数对这些数字进行累加,这些数字最多出现 n 次,累加也不会进位,最后对 n+1 取模,只保留个位数,就是每个数字出现的次数了

public static void main(String[] args) {
    long[] arr = {4, 5, 2, 2, 1, 0};

    int base = arr.length + 1;

    // 全部移到高位
    for (int i = 0; i < arr.length; i++) {
        arr[i] = arr[i] * base;
    }
    // 在个位数统计数字出现了多少次
    for (int i = 0; i < arr.length; i++) {
        arr[(int) (arr[i] / base)]++;
    }

    // 取模,只保留个位数
    for (int i = 0; i < arr.length; i++) {
        arr[i] = arr[i] % base;
    }

    // [1, 1, 2, 0, 1, 1]
    System.out.println(Arrays.toString(arr));
}

这就是之前那篇 blog 的解法,不需要进行那么复杂的证明和分析,思路一下子就清晰了许多,这样再去看当时记录的解法二,也就清晰了许多。