位运算相关算法总结

位运算

Posted by WenlSun" on May 6, 2020

几个有趣的位运算操作

利用或操作|和空格将英文字符转换成小写

1
2
('a' | ' ') = 'a'
('A' | ' ') = 'a'

利用与操作&和下划线将英文字符转换成大写

1
2
('b' & '_') = 'B'
('B' & '_') = 'B'

利用异或操作^和空格进行英文字符大小写互换

1
2
('d' ^ ' ') = 'D'
('D' ^ ' ') = 'd'

二进制中1的个数

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

输入:n

输出:1 的个数

样例:
输入:5
输出:3

思路:利用n&(n-1)来消除二进制中1的最后一位统计1的个数

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <bits/stdc++.h>

using namespace std;

int main() {
    int n;
    while (cin >> n) {
        int res = 0;
        while (n) {
            res++;
            n = n & (n - 1);  // 消除n的二进制中的最后一个1
        }
        cout << res << endl;
    }
    return 0;
}

// 统计从1~num每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。、
/**
 * 时间复杂度:O(nk)
 */
vector<int> countBits(int num) {
    vector<int> res;
    for (int i = 0; i <= num; i++) {
        int cnt = 0;
        int n = i;
        while (n) {  // 统计每一个数字的二进制中1的个数
            n = n & (n - 1);
            cnt++;
        }
        res.push_back(cnt);
    }
    return res;
}
/**
 * x可以看成是x’左移一位再加上新添进来的一位的结果
 * 时间复杂度:O(n)
 */
vector<int> countBits(int num) {
    vector<int> res(num + 1);
    for (int i = 1; i <= num; i++) res[i] = res[i >> 1] + (i & 1);
    return res;
}

颠倒二进制位

颠倒给定的 32 位无符号整数的二进制位。

输入:00000010100101000001111010011100 // 对应的无符号整数

输出:00111001011110000010100101000000 // 对应的无符号整数

思路:从右至左,对于每一位进行颠倒,通过n&1获取每一位,然后向右移动到相应的位置和之前的结果相加。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <bits/stdc++.h>

using namespace std;

int main() {
    uint32_t n;
    while (cin >> n) {
        uint32_t res = 0, power = 31;
        while (n) {
            // 将最后一位移动到对应的位置与之前的结果相加
            res += (n & 1) << power;
            // 更新最后一位
            n = n >> 1;
            // 更新当前位移到的步数
            power--;
        }
        cout << res << endl;
    }
    return 0;
}

缺失数字

给定一个包含 0, 1, 2, …, n 中 n 个数的序列,找出 0 .. n 中没有出现在序列中的那个数。LeeCode268

思路:0-n中缺失一个数字,说明这n个序列中必定有n存在。我们将其余的数字的值和索引对应,然后分别将值和索引进行异或操作,如果索引和值可以对应上就会通过异或抵消,最终没被抵消掉的数就是缺失的数字。技巧是将初始值设置为n。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <bits/stdc++.h>

using namespace std;

int main() {
    int n;
    while (cin >> n) {
        int nums[n];
        for (int i = 0; i < n; i++) cin >> nums[i];
        int res = n;
        for (int i = 0; i < n; i++) {
            res ^= nums[i] ^ i;
        }
        cout << res << endl;
    }
    return 0;
}

只出现一次的数字I

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。LeeCode136

思路:对所有元素进行异或操作,剩余的数字就是出现一次的数字,因为出现两次的数字会被抵消

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <bits/stdc++.h>

using namespace std;

int main() {
    int n;
    while (cin >> n) {
        int nums[n];
        for (int i = 0; i < n; i++) cin >> nums[i];
        int res = 0;
        for (int i = 0; i < n; i++) {
            res ^= nums[i];
        }
        cout << res << endl;
    }
    return 0;
}

只出现一次的数字II

一个整型数组nums里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。面试题56数组中数字出现的次数

思路:前面讲到,数组中只有一个数字出现一次,可以通过异或运算求得,我们对这个思路扩展,考虑将数组分成两部分,每一部分中只有一个数字出现了一次,其余数字都出现了两次,然后可以利用异或运算分别求得每一部分中只出现一次的数字。(1)首先将所有数字进行异或运算,因为数组里面有两个数字只出现了一次,所以它们异或的结果一定不为0,即某一位一定为1,我们以这一位是否为1来划分数组,就可以保证每一部分中只有一个数字只出现了一次,因为相同的数字它们的位是一样的,所以会被分到相同的组里面;(2)对每一部分分别进行异或运算,即可求得只出现一次的数字。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <bits/stdc++.h>

using namespace std;

int main() {
    int n;
    while (cin >> n) {
        vector<int> nums(n);
        for (int i = 0; i < n; i++) cin >> nums[i];
        int n1 = 0, n2 = 0;
        int t = 0;
        for (auto n : nums) t ^= n;  // 异或得到一个不为零的数字
        int idx = 0;
        while (!(t & 1)) {  // 寻找划分标志位
            t >>= 1;
            idx++;
        }
        // 对每一部分分别进行异或
        for (auto n : nums) {
            if ((n >> idx) & 1)
                n1 ^= n;
            else
                n2 ^= n;
        }
        cout << n1 << ' ' << n2 << endl;
    }
    return 0;
}

只出现一次的数字III

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。LeeCode137

思路:有限状态自动机 + 位运算 参考题解

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <bits/stdc++.h>

using namespace std;

int main() {
    int n;
    while (cin >> n) {
        vector<int> nums(n);
        for (int i = 0; i < n; i++) cin >> nums[i];
        // 高逼格的代码 看不懂。。。。
        // int ones = 0, twos = 0;
        // for (auto n : nums) {
        //     ones = ones ^ n & ~twos;
        //     twos = twos ^ n & ~ones;
        // }
        // cout << ones << endl;

        // 通俗版本
        int m[32] = {0};  // 记录每一位1出现的次数
        for (auto n : nums) {
            // 统计每一位1出现的次数
            for (int i = 0; i < 31; i++) {
                if ((n >> i) & 1) m[i]++;
            }
        }
        int res = 0;
        // 生成只出现一次的数字
        for (int i = 31; i >= 0; i--) {
            res = res << 1;
            res += m[i] % 3;  // 判断当前位是1还是0
        }
    }
    return 0;
}

不用加减乘除做加法

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。LeeCode371剑指offer

思路:考虑采用位运算来做加法,通过实验发现,位运算做加法和正常加法一样,都是先不考虑进位做加法,再把进位的结果加上去。发现位运算的不进位加法等价于两个数异或的结果,位运算的进位只有两个数对应位全是1时才进位,可以通过两个数的与操作然后左移一位得到.

(1) 两数字做异或运算得到不进位的和; (2) 两个数字做与运算然后左移一位得到进位的结果。 (3) 将上面两个结果相加,直到没有进位停止。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <bits/stdc++.h>

using namespace std;

int main() {
    int a, b;
    while (cin >> a >> b) {
        int res = 0, carry = 0;
        res = a ^ b; // 不进位的和
        carry = (unsigned int)(a & b) << 1; // 进位的和
        int n1 = res, n2 = carry;
        while (n2) {
            res = n1 ^ n2;
            carry = (unsigned int)(n1 & n2) << 1;
            n1 = res, n2 = carry;
        }
        cout << res << endl;
    }
    return 0;
}