目录
一、位运算的应用
1.1 判断奇数和偶数
所有偶数的 2 进制表示中,最低位⼀定是 0 ;所有奇数的 2 进制表示中,最低位⼀定是 1 ;所以将⼀个数字与 1 进行按位与运算,即可判断这个数是奇数还是偶数。
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
int n;
cin >> n;
if( n & 1)
cout << "odd" << endl;
else
cout << "even" << endl;
return 0;
}
1.2保留二进制位中的指定位
有时候需要从⼀个整数 x 的 2 进制中取出某个位或者某几个位 (想取位), 使取出的位置上保留原来的值,其他位置为 0 ;方法 : 这时候可以使用⼀个值 m ,使 m 的 2 进制位中对应取出的位置为 1 ,其他位为 0 。 然后使这两个数按位与( x & m )即可。简单点就是 , 造一个二进制数m( 想取那几位 , 那几位就为1 , 其余都为0 ) ,m & 原二进制数 , 就好了。
下面有几个例子:
1.3 获取二进制位中的指定位
当我们需要获取⼀个整数 x 的二进制中第 i 位( 从低到高,以最低位为第 0 位 )是 1 还是 0 的时候,我们可以对 x 做这样的运算: (x >> i) & 1 ,结果是 0 ---> 第 i 位是 0结果是 1 ---> 第 i 位是 1
练习:
public class Solution {
public int reverseBits(int n) {
int ret = 0;
for(int i = 0; i < 32 ; i++)
{
int b = (n >> i) & 1 ;
b <<= (31 - i);
ret |= b;
}
return ret;
}
}
1.4 将指定二进制位设置为1
将⼀个整数 x 的⼆进制表示中的某⼀位(几位)设置为 1 , 其余位置保留原值:方法:设⼀个数 m ,使 m 的二进制上对应位置为 1 ,其余位置为 0 。然后使两个数进行按位或运算 ( x | m ),即可得到想要的数。
当然也可以只设置 x 二 进制中的某 1 位,也就是将 x 二 进制中的第 i 位(从低到高,以最低位为第 0 位)置为 1 ,方法: x |= (1<<i) ;
练习 :数字的补救
class Solution {
public:
int findComplement(int num) {
int ret = 0;
int i = 0;
while(num)
{
if((num & 1) == 0)
ret |= (1 << i);
num >>= 1;
i++;
}
return ret;
}
};
1.5 将指定二进制位设置为0
将⼀个整数 x 的二进制表示中 的某 1 位设置为 0 ,其余位置保留原值 。也就是将 x 二 进制中的第 i 位(从低到高,以最低位为第 0 位)置为 0 ,其他位保持不变,方法: x &= ~(1<<i) ;
应用场景:
在嵌⼊式开发的时候,假设我们要控制⼀个LED灯的开关,某⼀个寄存器的第2位用于控制这个LED灯 的开关。我们可以通过将该位设置为0来关闭LED。
#include <cstdio>
#define LED_CONTROL_BIT 0x04 // 00000100,第2位控制LED
// 11111011
int main()
{
unsigned char Register = 0x04; // 初始状态为⾼电平,LED亮
Register &= ~LED_CONTROL_BIT; // LED灭
printf("Register: 0x%02X\n", Register ); // 输出: 0x00
return 0;
}
1.6 反转指定二进制位
将⼀个整数 x 的⼆进制表示中的第 i 位反转( 从低到高,以最低位为第 0 位 ),也就是原来是 1 的变成 0 ,原来是 0 的变成 1 。1)用⼀个数 m , 使得 m 的二进制中第 i 位为1 ,其余位置为 0 。2 )两个数进行按位 异或运算( x ^ m )
1.7 将二进制位最右边的1变0
有时候,我们需要将一个整数 x 的⼆进制表示中最右边的 1 变为 0 ,这时候就可以使用x & ( x- 1) 来得到想要的数字。
应用场景:这种运算通常应用 到求一个数的二进制序列中有几个 1 。
为啥?
因为 x = x & (x-1) 这种运算,每计算 一次就会将 x 的二进制表示中最右边的 1 置为 0 ,那么不断的执行 x = x & (x-1) 这样的运算,就会将 x 的二进制中的 1 逐个都置 0 ,这时候 x 也就变成了 0 ,当 x 变成 0 之前,这个运算被执行多少次就说明x的二进制中有多少个 1
1.7.1 练习一:位1的个数
方法一:借助 x & ( x - 1 ) , 有多少个1 , 就执行几次
class Solution {
public:
int hammingWeight(int n) {
int c = 0;
while(n)
{
n = n & (n - 1 );
c++;
}
return c;
}
};
方法二:取二进制的每一位 , 然后判断是否为1 。
---> 为1 , 则计数器c 加一下
但是这个方法的效率没有这么高 , 因为无论你有多少个1 , 程序都会循环32次 ,并且判断32次 ( 就算一个1都没有)
class Solution {
public:
int hammingWeight(int n) {
int c = 0;
for(int i = 0 ;i < 32 ; i++ )
{
if((n >> i) & 1 == 1)
c++;
}
return c;
}
};
1.7.2 练习二:2的幂
class Solution {
public:
bool isPowerOfTwo(int n) {
return ((n >= 1) &&( n & (n - 1 )) == 0) ;
}
};
记住该加括号要加括号 , 这里涉及操作符优先级的问题 , 想深入了解 , 继续往下看噢~
1.7.3 应用场景
1) 位图算法:分配和释放资源位图(bitmap)是操作系统、文件系统等经常使用的数据结构,用于高效管理资源,如内存块、磁盘块等。通过将最右边的1变为0,可以快速释放资源。 假设位图用于管理内存块 ,每个位表示⼀个内存单元的状态:1表示已分配,0表示未分配。要释放最右侧已分配的块,只需将最右边的1变为0。2) 计数算法:统计二进制数中的1的个数在统计⼆进制数中1的个数时,通过不断将最右边的1变为0,可以显著优化计算的速度。这种方法被称为 Brian Kernighan 算法 ,广泛用于计算机科学的各种优化场景。
1.8 只保留二进制位最右边的1变0
将一个整数 x 的二进制中 最右边的 1 保留下来,其他位都置为 0 :x & -x 就可以得到想要的数字。
回过来 , 看一下这道题:
class Solution {
public:
bool isPowerOfTwo(int n) {
return ((n >= 1) &&( n & ( -n )) == n) ;
}
};
1.9 异或的巧用
异或运算符的特点 :
1) x ^ x = 0 , 相同的数字异或的结果是0
2)0 ^ x = x , 0 和 x 异或还是 x
3) a ^ b ^ a = a ^ a ^ b , 异或是支持交换律的。
练习一:交换两个整数的值
方法一:创建一个临时变量
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
int a = 0;
int b = 0;
cin >> a >> b;
//交换
int t = 0;
cout << "交换前:a = " << a << ", b = " << b << endl;
t = a;
a = b;
b = t;
cout << "交换后:a = " << a << ", b = " << b << endl;
return 0;
}
如果不允许创建临时变量 , 那如何交换呢?
方法二:
a = a + b ; //此时的a 是和 , b 不动 ( a = a + b , b = b )
b = a - b ; //此时 b = a+ b - b = a , b 的值变了 ( b = a ) ,此时a还是和 ( a = a + b )
a = a - b ; // 此时a 是 a = a+ b -a = b
这样就实现了两个数字交换 , 不使用变量
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
int a = 0;
int b = 0;
cin >> a >> b;
//交换
cout << "交换前:a = " << a << ", b = " << b << endl;
a = a + b;
b = a - b;
a = a - b;
cout << "交换后:a = " << a << ", b = " << b << endl;
return 0;
}
这种方法 仅仅可以适用部分情形 , 如果数据太大 , 可能存在溢出的风险
方法三:使用异或
a = a ^ b;
b = a ^ b;
a = a ^ b;
使用异或交换两个数的值 , 只能使用于整形类型 , 因为 异或运算仅适用于整型类型
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
int a = 0;
int b = 0;
cin >> a >> b;
//交换
cout << "交换前:a = " << a << ", b = " << b << endl;
a = a ^ b;
b = a ^ b;
a = a ^ b;
cout << "交换后:a = " << a << ", b = " << b << endl;
return 0;
}
练习二:找出单身狗
int singleNumber(int* nums, int numsSize) {
int singer = 0;
for(int i = 0 ; i < numsSize ; i++)
{
singer ^= nums[i];
}
return singer;
}
练习三:丢失的数字
int missingNumber(int* nums, int numsSize) {
int miss = 0;
//产生nums 的数据异或在一起
for(int i = 0 ; i < numsSize ; i++)
{
miss ^= nums[i];
}
//将0~numsSize的数字依次异或到 miss 上
for(int i = 0; i<= numsSize ; i++)
{
miss ^= i;
}
return miss;
}
二、操作符的属性
C/C++ 语言的操作符有两个重要的属性 : 优先级 、 结合性 。
---> 这两个属性 , 决定了表达式求值的计算顺序
2.1 优先级
如果在写代码的时候 , 不清楚操作符的优先级 , 建议改加括号!!!
2.2 结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了。则根据运算符是左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执⾏),少数运算符是右结合(从右到左执行),比如赋值运算符( = )。