位运算总结
在大学学习点亮LED灯时第一次接触到位运算,通过将0x0001连续左移,赋值给对应IO口,实现LED灯的循环点亮,相比于 置位与复位,位运算简化了编程,也更容易理解程序内部数据如何变化。
本文章是对位运算的部分总结,希望和朋友交流想法。
了解位运算之前,需要对有符号数,无符号数,补码进行了解。
无符号数,有符号数
深入理解计算机系统一书中采用一个函数B2U (binary to unsign) 将一个二进制向量转化为无符号数, 比如0x0111=7; 0x1001=9。在一个32位的编译器中,一个unsigned int型变量有4个比特,32位二进制数构成,其所所能表示范围是2^31-1=42 9496 7295 将近43亿,由于unsigned int所表示的数据有上限,所以经常看到某些游戏过程中 怪的血量或者攻击叠到一定程度会突然变为0。有符号数将
数据最高为看作符号位 0表示整数,1表示负数。 0111表示+7,1111表示-1.
1 1 1 1
-8 +4 +2 +1 = -1
对于有符号数0xFFFFFFF就表示-1. 由于 0 用二进制表示为0x00000,所以
~0(非零) = -1; 你计算6-1也可以写成6+~0.当然谁也不愿意这么去写,但当你
键盘“-”不小心坏掉,你就可以for (int i = 5; i>0; i += ~0) 继续了(狗头)
获取 int 表示的最大值方法:
cout << (1 << 31) ; //int 型最小值 0x100...000
cout << (1 << 31) - 1 ; // int型最大值 0x011111...11 最小值减1得到最大值
cout << ~(1 << 31) ; // int型最大值 0x011111...11
cout << (((unsigned int)-1) >> 1) //
补码
现代计算机中保存数据均用补码表示,及解决负数加减问题…我们常听到补码
网上搜会有两种 1’s complement&2’s complement 及1补数和2补数。两者区别是
1补数是按位取反,2补数在1补数基础上+1.2补数方便了计算机电路设计,一种电路
就可以计算正负数加减法。举个例子:
5 - 3=2 -6-3=-9
0101 +5 1010 -6
1101 -3 1101 -3
=0010 +2 = 10111 -9
补码的计算在数值输入时,编译器就已经计算好。
位运算
1位运算操作符有 & | ~ ^(和&&、||、!相区分)以及>> <<。
1、^异或操作及相同取0,不同取1。可以看作不进位加法,eg 01^11=10;
异或性质如下,熟记编程过程会渐变很多。
i、 任何数与0异或等于该数 1001^0=10001
ii、 异或满足交互率 abc=cab
iii、相同的数异或结果为0 a^a=0;
leetcode上有很多位运算题目可以应用此性质方便解出。基于以上性质,我们可以重新实现swap()函数;如果我们想交换AB的值,通常会新建一个tmp中间变量。可以通过
a=a+b;b=a-b;a=a-b 实现。使用异或操纵与第二种类似,即
a=a^b;
b=a^b;
a=a^b;
上面说异或可以看作不进位加法,与运算可以求取进位,所以两者结合可计算 a+b;
int add(int a, int b) {
if (0 == b) {
return a;
}
return add(a ^ b, (a & b) << 1);
}
有补码性质,-a=~a+1 同样我们可以实现减法; a-b=a+1+~b;
判断a,b是否异号
bool issame(int a,int b){return (a^b)<0;} //小于0即首位不同
2、统计二进制中1的个数
此问题时二进制的经典问题,网上有很多解答,在此做一下总结。最简单的,将二进制每一位与0x1做与运算,对于一个int型数据,需要循环32次。第二种常用方法是通过 num&(num-1)计算 num中1的个数,该算式每计算一次,num中最右边一个1变为0 .所以该方法运行num中1的个数次**(一个数
字减一,最右边一个1之后所有数字将取反)**。
对于方法2的延伸可以判断一个数是否是2的整数次幂,2的整数次幂满足该数的最左边一位为1,所以 计算num&(num-1)结果为0,则num位2的整数次幂。
int cnt_one(int num)
{
int cnt = 0;
while (num != 0)
{
num = num&(num - 1);
cnt++;
}
return cnt;
}
还有一种方法,代码如下:
int bitCount(int x) {
int mask1=0x55555555;
int mask2=0x33333333;
int mask3=0x0f0f0f0f0f;
int mask4=0x00ff00ff;
int mask5=0x0000ffff;
int ans=(x&mask1)+((x>>1)&mask1);
ans=(ans&mask2)+((ans>>2)&mask2);
ans=(ans&mask3)+((ans>>4)&mask3);
ans=(ans&mask4)+((ans>>8)&mask4);
ans=(ans&mask5)+((ans>>16)&mask5);
return ans;
}
怎么理解呢,mask1计算每两位1的个数,mask2则计算每4位1的个数,最后
计算整个32为数1的个数。
3.求一个序列的子集。
二进制每一位只有两个值0,1。好处就是可以判断每一位的有无,比如求abc的子集。即可用三位二进制数表述 000->111。
4.计算m的n次幂
常用方法是用一个n次循环,时间复杂度位O(n),下面考虑用位运算
eg 计算3的11次幂 311=3^0x1011=3 ^0x1000+3 ^0x0010+3 ^0x0001
code:
int pow(int m,int n)
{
int sum=1;
while(n!=0)
{
if(n & 1 == 1)
{
sum *= m;
}
m*= m;
n = n >> 1;
}
return sum;
}
5.算log2
32位数字计算对数,最大值就是32。可以用5位二进制数表示:
int ilog2(int x) {
int ans=0;
ans=(!!(x>>(16)))<<4; // 16~31位是否有值,有的话ans=10000
ans=ans+((!!(x>>(8+ans)))<<3); // 01000
ans=ans+((!!(x>>(4+ans)))<<2); // 00100
ans=ans+((!!(x>>(2+ans)))<<1); // 00010
ans=ans+((!!(x>>(1+ans)))<<0); // 00001
return ans;
}
6.取绝对值
int abs(int n)
{
return (n ^ (n >> 31)) - (n >> 31);
}
解释一下,n>>31 位判断数字n的正负号(针对32位int数据),如果n为正数,
原式等于 n^0 - 0 = n;当n为负数时,原式等于(n ^-1)-1 ,这不就是取相反数过程嘛。
7.大小写字符转换
小写字母 a~z 97 ~ 122 小写字符减0x20即可。
观察其二进制 0b0110_0001~0b0111_1010 只要将第六位1置零即可
大写字母A~Z 65 ~ 90 二进制表示为 0b0100_0001 ~ 0b0101_1010
将第六位0置一即可。
统一转成大写:ch & 0b11011111 简写:ch & 0xDF
统一转成小写:ch | 0b00100000 简写:ch | 0x20
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1="your str";
string s2;
for (auto str:s1)
{
str^= 0x20;
s2+=str;
}
cout << s2 << endl;
}
- 获取最低位的1
x & -x == x & (~x + 1)
当x为奇数,他取反后肯定是个偶数,再加一只有最低为变化,其余都没变,最后结果为1.
结论:x是奇数, 那x & -x 的结果一定是1
当x为偶数,有两个重要特性
这个结果只有一位值是1, 其他位均是0
这个值的末位0的个数与原值保持一致
最后结论:当一个数与其取负后的值相与, 如果这个数是偶数, 则结果是能整除这个偶数的最大的2的幂(即: m = n & -n , 则 n % m = 0, 且 m = 2 ^ k), 如果这个数是奇数, 则结果必为1
参考博客
用途:获取某个二进制位的低位
leetcode题目
9.n&(n-1)的用途
判断一个是否是2 的幂 参考3
10 dd