问题一

一个数组中有一种数出现了奇数次, 其他数都出现了偶数次, 怎么找到这个种数?

在线OJ: LeetCode136. 只出现一次的数字

解题思路:

因为 a ^ a = 0, 所以出现过偶次的数异或结果都是 0, 又因为 0^a=a, 所以把数组中所有的数进行异或以后的结果, 就是出现了奇数次的那个数.

代码实现:

class Solution {
    public int singleNumber(int[] nums) {
        int ans = 0;
        for (int num : nums) {
            ans ^= num;
        }
        return ans;
    }
}

问题二

一个数组中有两种数出现了奇数次, 其他数都出现了偶数次, 怎么找到这两种数.

在线OJ: LeetCode260. 只出现一次的数字 III

根据问题一的结论, 假设数组中 ab 这两个数出现了奇数次, 这个数组中的所有数字异或以后得到的结果一定 a^b, 但因为 ab 是两种不同的数, 所以 a^b 的结果一定不等于 0.

所以, a^b 的结果如果转换成二进制的话, 一定至少有一个二进制位是 1.

我们假设 a^b 转换成二进制后最右侧位置的 1i 位置, 由此可以得出一个结论: a 和 b 的二进制在i位置一定一个为 0, 一个为 1.

不妨假设 ai 位置为 0, bi 位置为 1.

此外, 容易得知, 整个数组中的数, i 位置为 0 的数除了 a 以外, 其他数一定有偶数个, i 位置为 1 的数除了 b 之外, 其他数一定有偶数个.

那么我们可以只对 i 位置为 1 的数求异或, 最后得到的值一定是 b, 然后通过 b^(a^b) = a, 可以得到 a 的值.

最后只剩下一个问题: 如何求一个数只有其最右侧的1对应的的数呢?

假设某个数x二进制为: 00010010, 那么其只有最右侧的 1 对应的数就是: 00000010.

算法是: 对于一个数 x 来说, 其只有最右侧的 1 对应的数等于 x & ((~x) + 1) 或者 x & (-x).

所以, 如果一个数是 a^b, 那么它只有最右侧的1对应的数就是 (a^b) & (~(a^b) + 1).

我们用 (a^b) & (~(a^b) + 1) 这个值去和数组中每个值进行与运算, 如果与完以后的结果是 0, 说明这个数 i 位置是 0, 否则说明这个数 i 位置是1; 我们前面已经得到一个结论, i 位置为0的数除了 a 以外, 其他数一定有偶数个; 所以, 用 (a^b) & (~(a^b) + 1) 这个值和每个 i 位置是 0 的数组元素做与运算以后, 最后的结果一定是 a; 得到 a 以后, 然后通过 a^(a^b) = b, 可以得到 b 的值.

代码实现:

class Solution {
    public int[] singleNumber(int[] arr) {
        int ret = 0;
        for (int num : arr) {
            ret ^= num;
        }
        // 此时ans的结果是 a ^ b 的结果
        // 随便提取出 ans 随便哪一位置的 1 进行之后过程
        // 因为 a ^ b 的结果的这一位置为1, 那么这两个数的这一二进制位置一定不不同, 就可以以此进行区分
        // 一个数 & 上 它的相反数(取反+1)就可以可以提取出它最右侧的 1, 其余二级制位为0
        int rightOne = ret & (-ret);

        int ans1 = 0;
        for (int num : arr) {
            if ((num & rightOne) != 0) {
                ans1 ^= num;
            }
        }
        int ans2 = ret ^ ans1;

        return new int[] {ans1, ans2};
    }
}

当有了如下公式可以计算一个数最右侧的 1 以后, 也可以完成这样一个题

x & ((~x) + 1)

在线OJ: LeetCode191. 位1的个数

解题思路:

可以使用上面的公式提取出最右侧的 1 以后, 与目标数进行或运算, 得到一个新的目标数, 然后继续提取新目标数的最右侧的 1, 如此往复, 直到目标值为 0, 即可把所有位置的 1 都提取出来.

代码实现:

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        if (n == 0) {
            return 0;
        }
        int count = 0;
        int rightOne = 0;
        while (n != 0) {
            rightOne = n & (-n);
            count++;
            n ^= rightOne;
        }
        return count;
    }
}

问题三

一个数组中有一种数出现 k 次, 其他数都出现了 m 次 (m > 1, k < m), 找到出现了 k 次的数.

要求: 假设数组中所有数都是 int 类型, 额外空间复杂度 O(1), 时间复杂度 O(N).

在线OJ: LeetCode137. 只出现一次的数字 II

我们可以这样考虑, 设置一个 32 位的数组,

int[] help = new int[32];

遍历原始数组中每个数 num 的每一个二进制位, 伪代码如下:

for (int num : arr) {
  for (int i = 0; i < 32; i++) {
    help[i] += num的二进制中i位置的值(只能是0或者1)
  }
}

经过以上循环, help 数组就把数组中的所有数的二进制位上的信息累加起来了;

help[0] 表示数组中所有数二进制中0位置的值之和;

help[1] 表示数组中所有数二进制中1位置的值之和;

help[31] 表示数组中所有数二进制中31位置的值之和.

然后 i0 位置开始拿出 help[i] 的值, 假设 help[i]=x, 用 x % m, 如果结果是 k, 说明出现 k 次的元素在这个位置上是 1, 否则, 这个出现了 k 次的数在 i 位置上是 0, 遍历完 help 数组, 出现 k 次元素的每一位信息都拿到了, 然后还原出来即可.

通用代码:

/*一个数组中有一种数出现K次,其他数都出现了M次,
已知M > 1,K < M,找到出现了K次的数, 如果不存在出现了K次的数, 返回-1
要求额外空间复杂度O(1),时间复杂度O(N)*/
public static int onlyKTimes(int[] arr, int k, int m) {
    // 用来将 arr 数组中元素相应位置二进制位为 1 的个数
    int[] help = new int[32];
    // 遍历arr数组
    for(int num : arr) {
        // 将每一个元素二进制为 1 的个数进行累加
        for (int i = 0; i <= 31; i++) {
            // 判断每一位二进制位值是否为 1
            if (((num >> i) & 1) != 0) {
                // 为 1 就累加到相应位置
                help[i]++;
            }
            // 这个判断和 help[i] += (num >> i) & 1 等效
        }
    }
    int ans = 0;
    // 如果出现 m 次元素中的某个二进制位为 1, 那么 help 数组中相应位置的累加和一定是 m 的倍数
    // 如果出现 k 次元素中某个二进制为 1, 出现 m 次元素中的这个二进制位也为 1, 那么 help 数组中相应位置的累加和一定不能被 m 整数
    // 出现的不是 k 次, 但要比 m 小, 也是不能被 m 整除的, 返回-1
    for (int i = 0; i < 32; i++) {
        if (help[i] % m == 0) {
            continue;
        }
        // 如果找到了不能被 m 整除的, 等于k, 那么说明结果中这个二进制位一定是为 1 的
        // 不等于k, 那么除了出现 m 次的元素, 剩下的这个元素就不是出现 k 次的, 返回 -1 即可
        if (help[i] % m == k) {
            ans |= (1 << i);
        } else {
            return -1;
        }
    }
    // 出现 k 次的元素如果是0的话, 上面的遍历就会一直 continue, 就没有判断的时机了
    // 所以这里对于 0 的情况需要特殊判断一下
    if (ans == 0) {
        int count = 0;
        for (int num : arr) {
            if (num == 0) {
                count++;
            }
        }
        if (count != k) {
            return -1;
        }
    }

    // 如果题意是数组中出现 k 的数字一定存在, 可以换成下面的代码
    /*for (int i = 0; i < 32; i++) {
        if (help[i] % m != 0) {
            ans |= (1 << i);
        }
    }*/

    return ans;
}

解题代码:

class Solution {
    public int singleNumber(int[] arr) {
        int[] help = new int[32];
        for(int num : arr) {
            for (int i = 0; i <= 31; i++) {
                if (((num >> i) & 1) != 0) {
                    help[i]++;
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < 32; i++) {
            if (help[i] % 3 != 0) {
                ans |= (1 << i);
            }
        }
        if (ans == 0) {
            int count = 0;
            for (int num : arr) {
                if (num == 0) {
                    count++;
                }
            }
        }
        return ans;
    }
}
05-09 14:49