位运算

本文详细介绍了位运算的各种使用姿势,包括判断奇偶性、求最大值、无加法实现相加等,揭示了位运算在编程中的高效性和灵活性。通过位运算可以实现不使用比较和算术运算符的复杂逻辑,如求绝对值、快速乘法等。位运算在解决LeetCode等算法题目中也展现出独特优势。
摘要由CSDN通过智能技术生成

位运算的各种姿势

LeetCode官方解释:

位操作(Bit Manipulation)是程序设计中对位模式或二进制数的一元和二元操作。在许多古老的微处理器上,位运算比加减运算略快,通常位运算比乘除法运算要快很多。在现代架构中,情况并非如此:位运算的运算速度通常与加法运算相同(仍然快于乘法运算)。

位操作包括:

  • ¬ 取反(NOT)
  • ∩ 按位或(OR)
  • ⊕ 按位异或(XOR)
  • ∪ 按位与(AND)
  • 移位,
    移位是一个二元运算符,用来将一个二进制数中的每一位全部都向一个方向移动指定位,溢出的部分将被舍弃,而空缺的部分填入一定的值。
    移位又分为
    • 算术移位
    • 逻辑移位

位运算是计算机中非常重要的运算方式,位运算的符号有(&、|、^、~、>>、<<)

注:以下True也可以是1,False也可以是0

  • &:这是一个与运算符号,效果是两者都为True才是True。1 & 1 = 1, 1 & 0 = 0
  • |:或运算符号,效果是两者其一为True,就是True。1 | 1 = 1, 1 | 0 = 1
  • ^:异或运算符号,效果是相同为False,相反为True。1 ^ 1 = 0, 1 ^ 0 = 1
  • ~:取反运算符号,效果是True为False,False为True。
  • >>:右移运算符号,效果是将一个数的二进制形式向右移动。7 = 111, 7 >> 1 = 11 = 3(移动一位等于除以2向下取整,两位就是除以4)
  • <<:左移运算符号,效果是将一个数的二进制形式向左移动。7 = 111, 7 << 1 = 1110 = 14(移动一位等于乘以2,两位就是乘以4)

姿势1(x & 1):

x & 1 == 0表示判断是否是偶数

x & 1 == 1表示判断是否是奇数

因为偶数的二进制形式最后一位是0,奇数最后一位是1

那么7 = 111 & 001 = 1,而6 = 110 & 001 = 0

姿势2(x & -x):

如果x是偶数,那么x & -x表示截取这个偶数二进制形式下从最低位的1开始到最后的值,也就是100…0。

比如100 = 1100100,运算后4 = 100
用法,如果这个数是偶数,我们可以x - (x & -x),这样这个数的最低位的1就变成了0

如果x是奇数,那么x & -x是多少呢?答案是:任意奇数算出的答案都是1。

原理-x在二进制下等于所有位取反(~)后再加1。比如-100 = 0011011 + 1 = 0011100。首先,这个数字的最低位的1的右边所有的值全部都取反了,所以做&运算一定都是0,而最低位的1虽然也被取反,但是由于它前面的0全部变成1,+1之后进位,导致前面的1又变为0,自己又变成了1。比如01111 + 1 = 10000,所以在偶数情况下只为保留最低位的1。

那如果是奇数呢?这样的话最低位就是最右边的那一位,所以只有1。

姿势3(0 ^ x):

0 ^ x = x(x为任意整数)

姿势4(1 ^ x):

两种情况:

当x为偶数时 1 ^ x = x + 1

当x为偶数时 1 ^ x = x - 1

x同样为任意整数。

姿势5((var ^ (var >> bit)) - (var >> bit)):

如果不用if-else和比较运算符,那么可以使用这个来求解绝对值

其中bit是位,比如int是32位的,long是64位的,但注意要-1,32位最多只能移31位

摘自别人的解释:

  • var >= 0: var >> 7 => 0x00,即:(var ^ 0x00) - 0x00,异或结果为var
  • var < 0: var >> 7 => 0xFF,即:(var ^ 0xFF) - 0xFF,var ^ 0xFF是在对var的全部位取反,-0xFF <=> +1, 对signed int取反加一就是取其相反数。

举个栗子🌰:var = -3 <=> 0xFD,(var ^ 0xFF) - 0xFF= 0x02 - 0xff= 0x03

作者:dexin

链接:https://leetcode-cn.com/problems/maximum-lcci/solution/ji-yu-wei-yun-suan-shi-xian-da-xiao-bi-jiao-by-dex/

姿势6(两个数的最大值):

求两个数的最大值,这不是很简单嘛。蛋式,你不可以使用if-else和比较运算符。

这是LeetCode上的一道题:https://leetcode-cn.com/problems/maximum-lcci/solution/gen-ju-ti-mu-de-ti-shi-wan-cheng-wei-yun-suan-qiu-/

下面给出题解:

我们要得到一个数k,当这个数为1时,b大,当这个数为0时,a大。得到后,我们使用b * k + a * (k ^ 1)得出答案,是不是很巧妙!!!那么怎么得到k呢?

我们只要得到a - b的符号位就可以了,符号位在第一位上,所以如果是int类型,我们要右移31位,如果是long类型我们要右移63位。在这里我们使用long类型,因为a - b是有可能会溢出的。

class Solution {
public:
    int maximum(int a, int b) {
        long k = (((long)a - (long)b) >> 63) & 1;
        return b * k + a * (k ^ 1);
    }
};

最后给出一个写的很不错的文章:https://www.csdn.net/gather_2b/MtzaIg4sOTctYmxvZwO0O0OO0O0O.html

姿势7:两数相加

在不使用算术运算符的情况下完成两数相加。

同样也是LeetCode上的一道题:https://leetcode-cn.com/problems/add-without-plus-lcci/comments/

这题很有意思,我们要想做出这道题,就要先了解二进制的加法。二进制中逢二进一,所以二进制中的每一位1 + 1 = 0(进位) 1 + 0 = 1 0 + 0 = 0都有这么三种情况。我们发现,除了需要进位,和异或操作(^)一模一样。

那么剩下我们就是要考虑进位情况了,怎么拿出所有要进位的位置呢?我们发现只有两个都为1的情况才需要进位,那么我们只要用与运算(&)就可以取出所有进位的情况。然后把它们向左移动一位,再进行异或操作,就可以完成进位了。

可是现在还有一个问题,进位的过程中也有可能需要进位,所以你需要递归完成这个步骤,直到进位值都是0,也就是不需要进位为止。

class Solution {
public:
    int add(int a, int b) {
        int res = a ^ b;
        if (b == 0) return res;
        return add(res, (unsigned int)(a & b) << 1);
    }
};

我们还可以精简到一句话

class Solution {
public:
    int add(int a, int b) {
        return b == 0 ? a : add(a ^ b, (unsigned int)(a & b) << 1);
    }
};

姿势8:(num & x)

x可以是一个全为1的二进制数。

比如1111 = 15,它可以将一个数的前4位取出。取出来的数字就是num % 16的结果,而剩下的值就是num / 16的结果。

同理,我们有111 = 7,11 = 3, 1 = 1它们分别可以对8,4,2取模。

姿势9:(求区域异或)

求一个数组中其中一段的异或值。这个很简单,一个一个算即可。但如果操作数量很多显然一个一个算不是好办法。

求区域和可以用前缀和,求区域异或一样可以用区域异或。

class Solution {
public:
    vector<int> xorQueries(vector<int>& arr, vector<vector<int>>& queries) {
        int size =  arr.size();
        vector<int> ans, pre_xor; pre_xor.push_back(0);
        for (int i = 0; i < size; i++) pre_xor.push_back(pre_xor.back() ^ arr[i]);
        for (auto query : queries) {
            ans.push_back(pre_xor[query[1] + 1] ^ pre_xor[query[0]]);
        }
        return ans;
    }
};

姿势10:(提取最高位的1)

先来看代码

public static int highestOneBit(int i) {
    // HD, Figure 3-1
    i |= (i >>  1);
    i |= (i >>  2);
    i |= (i >>  4);
    i |= (i >>  8);
    i |= (i >> 16);
    return i - (i >>> 1);
}

返回的答案就是i只保留最高位的1其余都为0的值。

怎么做到的呢?其实很简单。我们设最高位的1是目标位,整个数是001xxx..xx我们假设x可以是1也可以是0。

  • 第一个或运算必定会把目标位右边一位变成1,不管是0是1。即0011xxxx..xx
  • 第二个或运算必定会把目标位右边3位变成1,即001111xxx..x
  • 第三个或运算必定会把目标位右边7位变成1,即0011111111xx...x
  • 第四个或运算必定会把目标位右边15位变成1,即001111...1111xx...x
  • 第五个或运算必定会把目标位右边31位变成1,即00111111...11111,因为目标位右边没有31位,所以目标位右边被全部填成了1

那么最后这句又是什么意思呢?return i - (i >>> 1);

>>>java的无符号位右移,这样如果是负数那么符号位依然补0不会补1。

最后返回值的答案就是0011111...111 - 00011111..111。后者少了一个1。所以相减后就是001000..000。这样就只保留了目标位。

姿势11:(1的所有子集)

假设有一个数101010,我想按照它1的排列将它输出(比如111 110 101 100 011 010 001 000),这怎么办呢?

for (int i = num; i != 0; i = i - 1 & num) {
    cout << i;
}

这样,输出的i就可以满足我们的要求了。

姿势12:(快速乘)

如果在不使用乘号的情况下将两数相乘?

我们可以将两个数排成二进制,然后使用二进制下的竖式计算来让两数相乘。

为什么要用二进制呢?

很简单,第一,计算机底层就是用二进制存储的,所以我们可以直接使用位运算求解。第二,二进制只有0和1,所以二进制下的竖式计算是不需要使用到乘法的,因为要么乘以1,要么乘以0,而乘1就相当于没有乘。

int quickMulti(int A, int B) {
    int ans = 0;
    for ( ; B; B >>= 1, A <<= 1) if (B & 1) ans += A;
    return ans;
}

LeetCode面试题64. 求1+2+…+n

要么乘以0,而乘1就相当于没有乘。

int quickMulti(int A, int B) {
    int ans = 0;
    for ( ; B; B >>= 1, A <<= 1) if (B & 1) ans += A;
    return ans;
}

LeetCode面试题64. 求1+2+…+n

这道题就可以使用上面说的方法求解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值