Bit Manipulation - Single Number I, II, III

Single Number

Difficulty: Medium

数组中的数两两出现,只有一个出现了一次,把它找出来

异或即可;也可使用Hash Table,但效率不如异或;或者对每一位进行计数,需要大小为 32 的 int 型数组

// Runtime: 20 ms
class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ret = 0;
        for (auto num: nums) {
            ret ^= num;
        }
        return ret;
    }
};

Single Number II

Difficulty: Medium

数组中的元素出现三次,只有一个出现了一次,把它找出来

考虑对每一位的出现次数进行计数,0–>1–>2–>3(0),由于出现三次则清零,因此,用三个变量来表示三种状态。ones表示每一位出现一次的情况,twos表示每一位出现两次的情况,reset表示某一位尚未出现或出现三次的情况。参考文章

// Runtime: 12 ms
class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ones = 0, twos = 0, reset = 0;
        for (auto num : nums) {
            twos = twos | (ones & num);
            ones = ones ^ num;
            reset = ones & twos;
            ones = ones ^ reset;
            twos = twos ^ reset;
        }
        return ones;
    }
};

另一篇参考文章
考虑对每一位的出现次数进行计数,00–>01–>10–>11(00),因此,次数可以用两位二进制数来表示,twos ones

对于 ones,ones = (ones ^ num),if (twos == 1) then ones = 0,因此,ones* = (ones ^ num) & ~twos

同样的,对于 twos,twos = (twos ^ num),if (ones* == 1) then twos = 0,因此,twos* = (twos ^ num) & ~ones*。注意,ones* 是 ones 的新值,因此,twos 需要后计算

// Runtime: 12 ms
class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ones = 0, twos = 0;
        for (auto num : nums) {
            ones = (ones ^ num) & ~twos;
            twos = (twos ^ num) & ~ones;
        }
        return ones;
    }
};

Single Number III

Difficulty: Medium

数组中的数两两出现,只有两个出现了一次,把它们找出来

如果可以把数组中的数分成两类,则转化为已经解决过的问题。考虑到数组中所有元素相异或,得到 a ^ b,必有一位数字为 1,该位表明,a 和 b 在这一位是不同的,因此,可以按照这一位来分类。遍历两遍数组即可实现。

// Runtime: 20 ms
class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        int all = 0;
        for (auto n : nums) {
            all ^= n;
        }
        int bit1 = 1;
        while ((all & 1) == 0) { // Operator precedence, & is less than ==
            all >>= 1;
            bit1 <<= 1;
        }
        vector<int> ret;
        int a = 0, b = 0;
        for (auto n : nums) {
            if (n & bit1) {
                a ^= n;
            } else {
                b ^= n;
            }
        }
        ret.push_back(a);
        ret.push_back(b);
        return ret;
    }
};

扩展

5n + 1

https://leetcode.com/discuss/6632/challenge-me-thx
000–>001–>010–>011–>100–>101(000)
threes(nc) twos(nb) ones(na)

对于 nb,在 A[i] 和 na 的相应位均为 1 的情况下,nb 的相应位取反,若 A[i] 和 na 的相应位至少有一个 0,则 nb 保持。因此,nb = nb ^ (A[i] & na)

对于 na,na = na ^ A[i],但是,如果 nc 为 1,则 na 还需要继续保持为 0。因此,na = (na ^ A[i]) & ~nc

对于 nc,当新的 nb,nc 均为 0,并且 A[i] 为 1 时,nc 取反。因此,nc = nc ^ (A[i] & ~na & ~nb)

略显复杂,一定要结合每一位的变化来考虑。另外,LeetCode 的讨论中还给出一种算法,暂时还没看懂,也一并放到这里。

// 貌似这个更好理解一些
int singleNumber(int A[], int n) {
    int na = 0, nb = 0, nc = 0;
    for(int i = 0; i < n; i++){
        nb = nb ^ (A[i] & na);
        na = (na ^ A[i]) & ~nc;
        nc = nc ^ (A[i] & ~na & ~nb);
    }
    return na & ~nb & ~nc;
}
int singleNumber(int A[], int n) {
    int twos = 0xffffffff, threes = 0xffffffff, ones = 0;
    for(int i = 0; i < n; i++){
        threes = threes ^ (A[i] & twos);
        twos = (twos ^ A[i]) & ~ones;
        ones = ones ^ (A[i] & ~twos & ~threes);
    }
    return ones;
}

mn + 1

写完后发现也有人写了 general 的方法

再观察一下 5n + 1 的算法,可以看出,nb 和 na 都是由老的 nb 和 na 算出来的,而 nc 则是由新的 nb 和 na 算出来的,自然地,它也可以由老的 nb 和 na 算出来。如果我们保留原值,思考新值的变化将会容易很多。
即 nc = (nc ^ (na & nb & x)) & ~(nc & x),异或也可以改为或。na,nb,x 全为 1 时,nc 为 1,nc 和 x 为 1 时,nc 为 0。

写到这里,自然会想到,是否有通解?

我们再来回顾一下 3n + 1, 5n + 1, 7n + 1

c b a count
0 0 0 0
0 0 1 1
0 1 0 2
0 1 1 3
1 0 0 4
1 0 1 5
1 1 0 6

3n + 1,取 count = 0, 1, 2

a = (old_a ^ x) & ~(old_b & x);
b = (old_b ^ (old_a & x)) & ~(old_b & x);

5n + 1,取 count = 0, 1, 2, 3, 4

a = (old_a ^ x) & ~(old_c & x);
b = (old_b ^ (old_a & x)) & ~(old_c & x);
c = (old_c ^ (old_b & old_a & x)) & ~(old_c & x);

7n + 1,取 count = 0, 1, 2, 3, 4, 5, 6

a = (old_a ^ x) & ~(old_c & old_b & x);
b = (old_b ^ (old_a & x)) & ~(old_c & old_b & x);
c = (old_c ^ (old_b & old_a & x)) & ~(old_c & old_b & x);

基本上一目了然了,根据 count 的最大取值可能,模拟 count 的每一位。某一位之后的所有位以及输入全为 1 时,则进位(注意,此处为异或);当计数器为 max count 并且输入为 1 时,则清零(reset)。因此,每一位的计算均由两部分构成。进位项 & 清零项。

需要注意的是,最高位用 异或 或者 或 均可,不会出现同时为 1 的情况。

实现上的考虑:根据 max count 确定的位数构造数组,进位项很容易实现,清零项可通过判断 max count 的相应为为 0 为 1 来决定是否使用相应的 old 项,然后再与 x 想与即可。

是否可以不依赖于 old 项有待进一步思考,暂时作为一个 open problem。

看到了解决方案……其实就是从高位到低位进行运算 (c–>b–>a),等所有的新值都计算出来之后,再计算 reset 项,这时的 reset 项不需要考虑 x 了。最后,每个新值都需要与 reset 项相与。其实就是 Single Number II 中的第一个算法。

3n + 2

由于所有元素出现的次数均为奇数,因此无法通过按位相与对元素分类。可以通过 Hash Table 记录元素出现的次数。

你可能感兴趣的:(位操作,落单的数,通解)