常见的位操作bit manipulation

本文深入讲解位操作的各种实用技巧,包括但不限于位翻转、位移、位与、位或等基本操作的应用,以及如何通过位操作解决实际问题,如查找缺失数字、计算两数之和等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

常见的位操作-bit manipulation

  1. 将第bit位置1
A |= 1 << bit
  1. 将bit位置0
    A &= ~(1 << bit)
  1. 测试bit位是否为1
(A & 1 << bit)!= 0
  1. 提取从右边起第一个为1的bit位,其余置0
A & ~A

A & ~(A - 1)

A^(A&(A - 1))
  1. 删除从右边起第一个为1的bit
A & (A - 1)
  1. 得到所有为1的bit
~0

判断一个数是否是2的幂次方:

if(n & n - 1 == 0) return true;
else return false;

判断一个数是否是4的幂次方

  bool isPowerOfFour(int n) {
           return !(n&(n-1)) && (n&0x55555555);
    }

说明:上边与式子左边的是判断n是否为2的幂次方,因为4的幂次方一定是2的幂次方,如果2的幂次方都不是,那就一定不是2的四次方了。第二个条件就是0x55555555,5的二进制位0101,又因为该数与n进行与运算,因此,也就是奇数位全部为与0进行与——置0,偶数位全部与1进行与——保留原本比特位。

判断一个十进制的数转化为二进制时包含的1的个数:(因为每次N&(N-1)操作都是将n最末尾的1反转为0,因此反转个数即为1的个数)

public int hammingWeight(int n) {
        int sum = 0;
        while(n != 0) {
            sum ++;
            n &= n - 1;         
        }   
        return sum;
    }

将一个32位数字前后颠倒,即reverse:

class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        n = (n >> 16) | (n << 16);
        n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8);
        n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4);
        n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2);
        n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1);
        return n;
    }
};

下边是它翻转的顺序:

abcdefgh -> efghabcd -> ghefcdab -> hgfedcba

下面为另一种解法,该解法的主要思路是:一个32位的mask,并且其32位中只有一个位为1,开始时,使其置1位位于最左端,并依次向最右移动,同时,从最右端开始检测n,如果n的对应位为1,则将结果或上mask。

public void reverseBits(int n) {
    int mask = 1<<31, res = 0;
    for(int i = 0; i < 32; ++i)  {
        if(n & 1) res |= mask;
        mask >>= 1;
        n >>= 1;
    }
    return res;
}

找到从右边起,第一个为1的位,其余位置0(注:主要利用补码 = 反码 + 1)

n &= -n;

两个数相加减不使用+ -

递归方法

int getSum(int a, int b) {
    return b==0? a:getSum(a^b, (a&b)<<1); 
}

int getSubstract(int a, int b) {
     return b==0 ? a : getSubstract(a^b, (~a & b)<<1); 
}

循环方法

public int getSum(int a, int b) {
        if(a == 0) {
            return b;
        }
        if(b == 0) {
            return a;
        } 
        int carry = 0;
        while(b != 0) {
            // If both bits are 1, we set the bit to the left (<<1) to 1 -- this is the carry step
            carry = (a & b) << 1; 
            // If both bits are 1, this will give us 0 (we will have a carry from the step above)
            // If only 1 bit is 1, this will give us 1 (there is nothing to carry)
            a = a ^ b;
            b = carry;
        }
        
        return a;
    }

让我们以2+3为例来看一下:
0010(2)
0011(3)

在我们的解释过程中,如果不清楚的地方,试着类比一下10进制数的加法。首先看最右边的第一位,0+1为1,不进位,接着1+1=2,要进位。

我们如何判断相加的两个bit只有一个1?答案是使用异或操作。
我们如何判断相加的两个bit为两个1?答案是使用与操作。

因此 a&b的结果使得所有该进位的位置为1, a^b的结果使得所有该进位的位置0,不该进位的位置为其应该有的数。在进行初步的计算之后,我们需要把该进位的位置加上,因此进位之后,是在其前面的bit位加1,因此,需要将b左移1位,然后与初步计算结果相加,也就是我们递归方法的核心。

说说为什么最后b会变成0:
首先,carry << 1一定会使得carry中多一个0,因为左移之后最右边的位置一定为0,但是其前面可能舍弃了一个1,因此carry<<1使得carry的bit位的0的个数逐渐增加。
其次,carry = a & b 这个使得b中的0得以保存,因为0 & 任何数都为0
因此,这两个条件使得最后b一定为0.

同理,两个数相减也是一样的道理。首先a^b得到bit位上位数相同的位置全部为0,不同的位置为1,因为0-0=0, 1-1=0;其次,~a&b得到需要借位的bit位,因此只有0-1需要借位,1-0不需要借位,因此~a & b刚好符合

寻找丢失的数

一个正常数组包括0,1,2,3,4,5…n-1,但是现在这个数组中少了一个数,求这个数是多少?例如:[0,1,3]输出结果为2

   public int getMissingNumber(int[] nums) {
    	int res = 0;
    	for(int i = 0; i < nums.length; i++) {
    		res ^= i;
    		res ^= nums[i];
    	}
    	return res ^ nums.length;
    }

我们根据两个相同的数异或结果为0这个结论,可以很容易想到上边的办法。因为数组少一个数,因此,最后记得把数组长度也异或上,保证了我们参与异或运算的数字个数为奇数个。也刚好能找到丢失的数。

范围[m,n]之间所有数字与的结果

例如[5,7]之间包括的数字为5,6,7,这三个数字与完之后,结果为4,因此,输出的结果就是4。在leetcode上的题目为Bitwise AND of Numbers Range

我们知道,多个数字相与,只要其中有一个数字为0,结果就是0,而且,我们知道在数字递增的过程中,低位的数字总是在不断地变化,因此,我们只需要找到m与n相同的高位数字就行。例如,5,6,7的二进制分别为:
101
110
111
全部一样的部分为第三位,都为1,因此最后输出的结果为4

class Solution {
public:
    int rangeBitwiseAnd(int m, int n) {
        int i = 0;
        while (m != n) {
            m >>= 1;
            n >>= 1;
            ++i;
        }
        return (m << i);
    }
};

找到数组中超过1半的数字,众数

假设数组的长度为n,则找出该数组中出现次数超过n/2的元素的个数。

下面的方法是bit位的操作方法,代码写的很清楚,比较容易明白。

int majorityElement(vector<int>& nums) {
    int len = 32, size = nums.size();
    int count = 0, mask = 1, ret = 0;
    for(int i = 0; i < len; ++i) {
        count = 0;
        for(int j = 0; j < size; ++j)
            if(mask & nums[j]) count++;
        if(count > size/2) ret |= mask;
        mask <<= 1;
    }
    return ret;
}

还有一种方法是o(n)时间的方法,该方法在编程珠玑上曾经出现过。
思想如下:超过一半的数字,我们可以和未超过一半的数字进行抵消,抵消完之后,剩下的数字一定是众数,因为非众数一定小于n/2个。假设我们的数组为[1,2,2,2,2,3,4]4个2中的3个可以分别和1,3,4进行抵消,因此,最后剩下的2就是我们要的答案。但是我们如何实现这个抵消的过程呢?

过程如下:依次遍历数组,并设置两个变量res和cnt,如果当前的数字和候选者相同,则cnt++,如果不相同,则cnt–,相当于进行了抵消。就这样,最后的res一定为众数,但是该方法的前提是,这个数组中一定存在众数,如果不能保证这个前提,则最后结果可能是错误的。

public class Solution {
    public int majorityElement(int[] nums) {
        int res = 0, cnt = 0;
        for (int num : nums) {
            if (cnt == 0) {res = num; ++cnt;}
            else if (num == res) ++cnt;
            else --cnt;
        }
        return res;
    }
}

求模运算

求一个数n模m的值,也就是求n除以m之后的余数,这里m为2的幂次方。
则普通的求余运算为

n % m

优化之后为

n & (m - 1)

取余操作中如果除数是2的幂次方则等价于其除数减一的与操作

也就是说hash % length == hash & (length - 1) 的前提是length 是 2的n次方,并且采用二进制位操作&,相对于%能够提高运算效率,这就解释了hashmap的长度为什么是2的幂次方。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值