【LeetCode | 记录】有趣的位运算

139 阅读4分钟

位运算

今天刷了几道【位运算】的入门习题,挺有趣的,同时也发现自己的基础知识还不够牢固,所以记录一下,方便复习。

1486. 数组异或操作

题目描述

image.png

输入输出

image.png

image.png

参考代码一

遍历区间 [0,n1][0, n-1] 中的每一个整数 i,令 ans 与每一个 start + 2 * i 做异或运算(^),最终返回结果即可。

class Solution {
public:
    int xorOperation(int n, int start) {
        int ans = 0;
        for(int i = 0; i<n; i++) {
            ans ^= start + 2 * i;
        }
        return ans;
    }
};

参考代码二

下面是官方的代码,时间复杂度为 O(1)O(1)

class Solution {
public:
    // 返回 0⊕1⊕2⊕⋯⊕x 的结果
    int sumXor(int x) {
        if (x % 4 == 0) return x;
        if (x % 4 == 1) return 1;
        if (x % 4 == 2) return x + 1;
        return 0;
    }

    int xorOperation(int n, int start) {
        // 也就是 s = start / 2,然后向下取整
        int s = start >> 1;
        // 根据 start 和 n 的奇偶性就可以求出最后一位的值 e,值为0或1。
        int e = n & start & 1;
        int ret = sumXor(s - 1) ^ sumXor(s + n - 1);
        return ret << 1 | e;
    }
};

记录

首先,记录异或的性质,我们设 是异或运算符,那么异或运算满足以下性质:

  1. xx=0x ⊕ x = 0;
  2. xy=yxx ⊕ y = y ⊕ x;
  3. (xy)z=x(yz)(x⊕y)⊕z = x⊕(y⊕z);
  4. xyy=xx ⊕ y ⊕ y = x;
  5. iZ,\forall i \in Z,4i(4i+1)(4i+2)(4i+3)=04i⊕(4i+1)⊕(4i+2)⊕(4i+3) = 0;

那么现在来看本题要计算的式子:start(start+2)(start+4)(start+2(n1))start⊕(start+2)⊕(start+4)⊕⋯⊕(start+2(n−1))

观察公式可以知道,这些数的奇偶性质相同,因此它们的二进制表示中的最低位或者均为 11,或者均为 00

所以这道题的核心思想就是:

将参与运算的数的二进制位的最低位提取出来单独处理。

考虑将所有参与异或的数都删除最后一位再进行运算(也就是除以 22),然后用得到的结果乘于 22(即向左移位),再加上最后一位值ee

那么要计算的式子就转化为:

  • (start/2(start/2+2/2)(start/2+4/2)(start/2+2(n1)/2))2+e(start/2⊕(start/2+2/2)⊕(start/2+4/2)⊕⋯⊕(start/2+2(n−1)/2)) * 2 + e

s = start/2,得到最后的公式:

  • (s(s+1)(s+2)(s+(n1)))2+e(s⊕(s+1)⊕(s+2)⊕⋯⊕(s+(n−1))) * 2 + e

这里记录一下,为什么最后一步要 sumXor(s1)sumXor(s+n1)sumXor(s−1)⊕sumXor(s+n−1) 然后再乘以 22ee

  • xxss+n-1 的异或结果,是我们需要的结果
  • yy0s-1 的异或结果,即sumXor(s-1)
  • 因为 xy=sumXor(s+n1)x⊕y = sumXor(s+n-1)
  • 由于 异或运算 的相反性 xyy=xx⊕y⊕y = x
  • 所以 sumXor(s+n1)sumXor(s1)sumXor(s+n-1)⊕sumXor(s-1) 就是我们要的答案
  • 所以令 res = sumXor(s+n-1) ^ sumXor(s-1)
  • 最后再乘以 2 并加上 e 即可:return ret << 1 | e;

备注:

为什么当且仅当 startn 都为奇数时,结果的二进制位的最低位才为 1

因为当 n 为偶数时:

  • 如果 start 为偶数,那么 n 个偶数做异或,最后一位一定是 0
  • 如果 start 为奇数,那么 n 个奇数做异或,最后一位一定是 0

1720. 解码异或后的数组

题目描述

image.png

输入输出

image.png

image.png

参考代码

解题思路都在代码里:

/*
题意:
    - 原数组:arr
    - 编码后的数组:encoded
    - 编码规则:encoded[i] = arr[i] ^ arr[i+1]
    - 已知:arr[0] = first 以及 encoded 数组
    - 要求:返回原数组 arr
思路:
    - 已知编码规则:encoded[i] = arr[i] ^ arr[i+1]
    - 那么当 i 的范围在 [1, n) 之间时:
        * 有 encoded[i-1] = arr[i-1] ^ arr[i]
        * 此时在等式两边同时异或 arr[i-1],结果为:
        * arr[i-1] ^ encoded[i-1] = arr[i-1] ^ arr[i-1] ^ arr[i]
        * 由于 任意整数 和 自身做异或运算 的结果都等于 0
        * 那么上面的式子就变为:
        * arr[i-1] ^ encoded[i-1] = 0 ^ arr[i]
        * 又因为 任意整数 和 0 做 异或运算 的结果都等于其自身
        * 那么上面的式子就变为:
        * arr[i-1] ^ encoded[i-1] = arr[i]
    - 好了,解题思路出来了:
        * 对 i 从 1 到 n-1,依次遍历,计算出 arr[i] 的值
        * 即 arr[i] = arr[i-1] ^ encoded[i-1]
        * 这样就可以解码得到原数组arr
*/
class Solution {
public:
    vector<int> decode(vector<int>& encoded, int first) {
        int n = encoded.size() + 1;
        vector<int> arr(n, 0);
        arr[0] = first;
        for(int i = 1; i<n; i++) {
            arr[i] = arr[i-1] ^ encoded[i-1];
        }
        return arr;
    }
};

注意:

  • 异或(^)的两条基本性质:
    • 任意整数自身 做异或运算 的结果都等于 00
    • 任意整数00 做 异或运算 的结果都等于其自身

693. 交替位二进制数

题目描述

image.png

输入输出

image.png

image.png

参考代码

/*
解题思路:
    - 从低位到高位,我们对数值 n 进行取模最后再除以 2,可以得到的当前位上的数
    - 如果 当前位上的数 和 前一位上的数字 相等,那么不符合条件,直接返回 false
    - 如果不同,那么符合条件,此时更新前一位上的数字(将当前位上的数字赋值到前一位上)
    - 然后指向更高一位的数字,接着循环,直到 n = 0,如果一直符合条件,则返回true
*/
class Solution {
public:
    bool hasAlternatingBits(int n) {
        int prior = 2;
        while(n != 0) {
            int now = n % 2;
            if(now == prior) return false;
            prior = now;
            n /= 2;
        }
        return true;
    }
};

191. 位 1 的个数

题目描述

image.png

输入输出

image.png

image.png

参考代码一

/*
思路:
    - 依次遍历到每一位上的值,检查位上是否为 1
    - 这里使用到了一个技巧:
        * 如果想检查数字 n 的第 i 位是否为 1
        * 那么可以让 n 与 2^i 进行 与运算(&)
        * 如果 n & (1<<i) 的值不等于 0,那么表明 n 的第 i 位为 1
*/
class Solution {
public:
    int hammingWeight(uint32_t n) {
        int ans = 0;
        for(int i = 0; i<32; i++) {
            if(n & (1 << i)) {
                ans++;
            }
        }
        return ans;
    }
};

参考代码二

/*
思路:
    - 我们可以不断让当前的 n 与 (n-1) 做 与运算(&),直到 n 变为 0
    - 原理:
        * 看这行代码: n & (n - 1)
        * 这行代码的作用是将数值 n 的二进制中末尾的 1 改为 0
        * 为什么呢?来看下面例子:
        *     n :      1 0 1 1 0 0
        *   n - 1:     1 0 1 0 1 1
        * n & (n-1):   1 0 1 0 0 0
        * 所以说,n & (n - 1) 可以让 n 的二进制数中最后一个出现的 1 改写成 0
*/
class Solution {
public:
    int hammingWeight(uint32_t n) {
        int ans = 0;
        while(n != 0) {
            n = n & n-1;
            ans++;
        }
        return ans;
    }
};

参考代码三

/*
思路:
    - 使用  n & 1 检测二进制末尾是否为 1
    - 接着,让 n 右移 1 位
    - 反复进行,直到 n 等于 0
*/
class Solution {
public:
    int hammingWeight(uint32_t n) {
        int ans = 0;
        while(n != 0) {
            ans += n&1;
            n >>= 1;
        }
        return ans;
    }
};

338. 比特位计

题目描述

image.png

输入输出

image.png

参考代码

思路同上一题:

class Solution {
public:
    int count(int val) {
        int ans = 0;
        while(val != 0) {
            val &= (val-1);
            ans++;
        }
        return ans;
    }
    vector<int> countBits(int n) {
        vector<int> res(n+1, 0);
        for(int i = 0; i<=n; i++) {
            res[i] = count(i);
        }
        return res;
    }
};

461. 汉明距离

题目描述

image.png

输入输出

image.png

参考代码

/*
思路:
    - 首先计算 x 和 y 的异或结果,即 res = x^y
    - 接着,统计 res 中 1 的个数即可
*/
class Solution {
public:
    int hammingDistance(int x, int y) {
        int ans = 0;    // 存放结果
        int res = x^y;  // x⊕y 的结果
        while(res != 0) {
            res &= (res-1);
            ans++;
        }
        return ans;
    }
};

190. 颠倒二进制位

题目描述

image.png

输入输出

image.png

参考代码一

/*
题意:
    - 给定数字 n,将其二进制数翻转,求得另一个二进制数
解题:
    - 每次取到 n 的二进制末尾的数字
    - 拼接到结果集 ans 中
    - 然后更新 n,(右移一位)
*/
class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        uint32_t ans = 0;
        for(int i = 0; i<32; i++) {
            ans = (ans << 1) | (n & 1);
            n >>= 1;
        }
        return ans;
    }
};

参考代码二

/*
进制转换
    - 十六进制      二进制
    -   f           1111
    -   c           1100
    -   a           1010
    -   5           0101
    -   3           0011
分治法:
    - 32 位无符号整数,如: 1111 1111 1111 1111 1111 1111 1111 1111 
    - 表示成 16 进制,则是:  f    f    f    f    f    f   f     f
    - 接着:
        * ffff ffff 右移 16 位,变成 0000 ffff
        * ffff ffff 左移 16 位,变成 ffff 0000
        * 它们俩相或,就可以完成低16位 与 高16位的交换
        * 接着再依次分治下去,一共需要五次
    - 注意,每次分治都需要先和上一个掩码进行交换
*/
class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        // 1. 在 32 位中,低 16 位与高 16 位交换
        n = (n >> 16) | (n << 16);

        // 2. 在 16 位中,低 8 位与高 8 位交换
        n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8);

        // 3. 在 8 位中,低 4 位与高 4 位交换
        n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4);

        // 4. 在 4 位中,低 2 位与高 2 位交换
        n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2);

        // 5. 在 2 位中,低 1 位与高 1 位交换
        n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1);

        return n;
    }
};

/*
备注:
    -  十六进制                      二进制
    - 0x00ff00ff        00000000111111110000000011111111
    - 0x0f0f0f0f        00001111000011110000111100001111
    - 0x33333333        00110011001100110011001100110011
    - 0x55555555        01010101010101010101010101010101
*/

记录

总结一些位运算的入门小技巧:

首先,是求解 012x0⊕1⊕2⊕⋯⊕x 的函数。

定义如下:

int sumXor(int x) {
    if (x % 4 == 0) return x;
    if (x % 4 == 1) return 1;
    if (x % 4 == 2) return x + 1;
    return 0;
}

接着,是 异或 运算的性质:

  1. xx=0x ⊕ x = 0;
  2. 0x=x0 ⊕ x = x;
  3. xy=yxx ⊕ y = y ⊕ x;
  4. (xy)z=x(yz)(x⊕y)⊕z = x⊕(y⊕z);
  5. xyy=xx ⊕ y ⊕ y = x;
  6. iZ,\forall i \in Z,4i(4i+1)(4i+2)(4i+3)=04i⊕(4i+1)⊕(4i+2)⊕(4i+3) = 0;

如果想要将一个数,比如 val 的最后一位删除掉,那么只需 右移 1 位即可,比如 val >> 1


另外,如果我们想知道在数字 nn 的二进制表达中,第 ii 位是否为 11,那么可以让 nn2i2^i 进行 与运算(&)。

如果 nn & 2i2^i 不等于00,那么表明 n 的第 i 位为 1,用代码表示则是:n & (1<<i) != 0


代码 n & (n - 1) 可以让 n 的二进制数中最后一个出现的 1 改写成 0


如果你想知道两个数值的对应二进制位有几个不同,那么可以先计算二者的异或结果,再来统计结果中二进制位上 1 的个数即可。


当然,位运算的技巧不止这一点点,感兴趣的朋友可以自行百度。


本文正在参加「金石计划」