备战蓝桥杯,第八章:位运算符和操作符属性

一.操作符的分类

|---------|-------------------------------------------|
| 分类 | 操作符 |
| 算术操作符 | + - * % / |
| 移位操作符 | << >> |
| 位操作符 | & | ^ |
| 赋值操作符 | = += -= *= /= %= <<= >>= &= |= ^= |
| 单目操作符 | ! ++ -- & * + - ~ sizeof (类型) |
| 关系操作符 | > >= < <= == != |
| 逻辑操作符 | && || |
| 条件操作符 | ? : |
| 下标引用操作符 | [ ] |
| 函数调用操作符 | ( ) |

上述的操作符,以前章节有些许涉及,本章节继续介绍一组和位运算相关的操作符。这些操作符和二进制的知识有关系,下面先讲解二进制的相关知识。

二.二进制和进制转换

我们经常听到的二进制、八进制、十进制、十六进制是什么意思呢?

其实所有进制只是数值的不同表示形式而已。比如:数值15的各种进制的表示形式:

15的二进制:1111

15的八进制:17//八进制数值的前面写0

15的十进制:15

15的16进制:F//16进制的数值前面写0X

下面重点介绍一下二进制:十进制是我们日常生活中的最常用的进制。下面普及一下进制的基本常识:

  • 十进制中满10进1
  • 十进制的数字是由0~9的数字组成的
  • 二进制是满2进1
  • 二进制是由0和1数字组成的

(1)二进制转十进制

其实十进制的123表示的是一百二十三,为什么呢?

十进制的每一位数字都有自己对应的权重。十进制的数字从右向左是个位、十位、百位......,分别每一位的权重是1、10、100......如下图所示:

二进制和十进制是类似的,只不过二进制的每一位的权重,从右向左是:1、2、4、8......,下面是二进制的权重理解:

八进制转十进制数,方法类似,只是权重是从8开始,然后依次是:1、8、......16。16进制也是类似的道理。

(2)十进制转二进制

(3)二进制转八进制

八进制的数字每一位是0~7的数字,0~7的数字中,各自写成二进制,最多有3个二进制位组成,比如7的二进制是111,所以在二进制转8进制的时候,从二进制序列中右边低位开始向左每3个二进制位换算一个八进制位,剩余不够3个二进制位的直接换算。如二进制的01101011,换算成8进制为0153。(0开头的数字,编译器会被认为是八进制数字)

八进制数字转换为二进制数,需要将每一个八进制为转换为3个二进制位即可。

(4)二进制转十六进制

十六进制的数字每一位是0~9、a~f。每位各自写成二进制数字,最多有4个二进制位就足够了,比如f的二进制是1111,所以在二进制转十六进制时,从二进制序列中右边低位开始向左每4个二进制位会换算一个十六进制位,剩余不够4个二进制位的直接换算。如二进制的01101011换成十六进制为0X6b,十六进制表示应该在数字之前加上0X。

十六进制转换为二进制应该,每一位16进制位转换位3个二进制位就行。

(5)原码、反码、补码

二进制在计算机内部都是以二进制的补码形式存储的。下面讲解下二进制的表示形式:

移位运算符:>>

位运算符 : & | ^

整数的二进制表示方法有三种,即原码、反码、补码;整数分为有符号整数和无符号整数。

有符号整数的原码、反码和补码的二进制表示均由符号位和数值位两部分组成,二进制序列中,最高位的那一位被当作符号位,剩余位都是数值为。符号位是0表示正,用1表示负。正整数的原码、反码、补码相同,负数的三种表示方法各不相同,需要计算:

原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。

反码:将原码的符号位不变,其余位依次按位取反就可以得到反码。

补码:反码+1就得到了补码。

cpp 复制代码
int a = -10;
//原码:10000000 00000000 00000000 00001010
//反码:11111111 11111111 11111111 11110101
//补码:11111111 11111111 11111111 11110110
int a = 10;
//原码:10000000 00000000 00000000 00001010
//反码:10000000 00000000 00000000 00001010
//补码:10000000 00000000 00000000 00001010

无符号整数的三种二进制表示相同,没有符号位,每一位都是数值位

整数在内存中以补码的形式存储,整数在参与位运算时,是使用内存中的补码进行计算的,计算的结果也是补码,需要转换成原码才是真实显示出来的真实值。

为什么整型在内存中存放的是补码呢?

在计算机系统中,整数的数值一律用补码来表示和存储。原因在于使用补码计算时,可以将符号位和数值位统一进行处理。此外补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

三.位运算操作符

<< //左移操作符

>> //右移操作符

& //按位与操作符

| //按位或操作符

^ //按位异或操作符

~ //按位取反操作符

上面的运算符只适合于整数的计算,不能应用于其他数据类型的计算。

(1)左移位操作符

左移位操作符是双目操作符,形式如下:

cpp 复制代码
int num = 10;
num << i; //将num的⼆进制表⽰,左移i位 

代码演示:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 int num = 10;
 int n = num << 1;
 cout << "n = " << n << endl;
 cout << "num = " << num << endl;
 return 0;
}

左移位操作符的移位规则:左边丢弃,右边补0。下面是运算过程:

(2)右移位操作符

右移位操作符也是双目操作符,运算形式如下:

cpp 复制代码
num >> i;//将num的⼆进制表⽰右移i位 

代码演示:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 int num = -1;
 int n = num >> 1;
 cout << "n = " << n << endl;
 cout << "num = " << num << endl;
 return 0;
}

右移位操作符分为两种运算形式:逻辑右移和算数右移,具体那种右移方式取决于编译器,大部分的编译器采用的是算数右移。两种移位操作如下:

  • 逻辑右移:左边用0填充,右边丢弃。
  • 算术右移:左边用符号位数字填充,右边丢弃。

下面是相关的运算操作:

逻辑右移:

算数右移:

对于移位操作符来说,操作数不能是负数,这种行为是未定义的行为。编译器会报错的。下面是必须避免的错误示范:

cpp 复制代码
int num = 10;
num >> -1;//error

(3)按位与操作符

按位与操作符是双目操作符,形式如下:

cpp 复制代码
a & b; //a和b按位与运算 

规则:对于两个数的二进制位进行与运算,只有对应的两个二进制位都为1时,结果才为1。计算详情如下:

代码演示:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 int a = -5;
 int b = 7;
 int c = a & b;
 cout << c << endl;
 return 0;
}

解析:

-5的原码:10000000 00000000 00000000 00000101

-5的反码:11111111 11111111 11111111 11111010

-5的补码: 11111111 11111111 11111111 11111011

7的补码:00000000 00000000 00000000 00000111

两数的按位与运算过程如下:

计算得到的二进制数字是: 00000000 00000000 00000000 00000011,这个二进制序列当作补码时,最高位是1,说明是正数,所以原码、反码、补码相同,最终化为十进制数位3。

(4)按位或操作符

按位或操作符是双目操作符,形式如下:

cpp 复制代码
a | b; //a和b按位或运算 

规则:对于两个数的对应二进制位进行或运算,对应的两个二进制只要有1,按位或的结果就为1。

代码演示:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 int a = -5;
 int b = 7;
 int c = a | b;
 cout << c << endl;
 return 0;
}

解析:

-5的原码:10000000 00000000 00000000 00000101

-5的反码:11111111 11111111 11111111 11111010

-5的补码:11111111 11111111 11111111 11111011

7的补码:00000000 00000000 00000000 00000111

两者按位或的计算过程如下:

计算得到的二进制序列是11111111 11111111 11111111 11111111,这个二进制序列当作补码时,且最高位是1,说明是负数,计算的原码是:10000000 00000000 00000000 00000001,所以最终十进制的结果为-1。

(5)按位异或操作符

按位异或操作符i是双目操作符,形式如下:

cpp 复制代码
a ^ b; //a和b按位异或运算 

规则:对两个数的对应二进制进行异或运算,对应的两个二进制相同则为0,相异则为1。下面是相关计算过程:

代码演示:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 int a = -5;
 int b = 7;
 int c = a ^ b;
 cout << c << endl;
 return 0;
}

解析:

-5的原码:10000000 00000000 00000000 00000101

-5的反码:11111111 11111111 11111111 11111010

-5的补码:11111111 11111111 11111111 11111011

7的补码:00000000 00000000 00000000 00000111

-5的补码和7的补码按位异或运算如下:

计算得到的二进制序列是:11111111 11111111 11111111 11111100,这个二进制序列当作补码时,且最高位是1,说明是负数,求的原码是:10000000 00000000 00000000 00000100,所以最终计算的二进制是-4。

(6)按位取反操作符

按位取反操作符是单目操作符,形式如下:

cpp 复制代码
~a; //对a的⼆进制位按位取反 

规则:对操作数的二进制位进行按位取反运算,二进制数字是0变成1,1变成0。

代码演示:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 int a = -5;
 int b = ~a;
 cout << b << endl;
 return 0;
}

解析:

-5的原码:10000000 00000000 00000000 00000101

-5的反码:11111111 11111111 11111111 11111010

-5的补码:11111111 11111111 11111111 11111011

对-5的补码按位取反运算过程如下:

计算得到的二进制序列是:00000000 00000000 00000000 00000100,这个二进制序列当作补码时,且最高位是0,说明是正数,所以原码与补码相同,最终化成十进制的结果是-4。

四.位运算符的应用

(1)判断奇偶数

所有偶数的二进制表示中,最低位一定是0,所有奇数的二进制表示中,最低位一定是1。所以将一个数字与1进行按位与运算,即可判断这个数是奇数还是偶数。

(x&1) == 1 说明x是奇数

(x&1) == 0 说明x是偶数

代码演示:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 int n = 0;
 cin >> n;
 if ((n & 1) == 1)
 cout << "odd" << endl;
 else
 cout << "even" << endl;
 return 0;
}

(2)保留二进制的指定位

有时候需要从一个整数x的二进制中取出某个位或某几个位,使取出的位置上保留原来的值,其他位置为0,这时候可以使用一个值m,使m的二进制位中对应的位置为1,其他位为0。然后使这两个数按位与即可。计算详情如下:

比如:取出x的最低四位,则只需要将x与末尾四位是1,其余为0,进行按位与运算。计算过程如下:

再比如:取出x的最低2~4位,则只需要将x与最低2~4是1,其余位是0的数进行按位与计算。计算过程让如下:

假设我们有一个权限标志位,存储在一个变量中,我们可以通过按位与操作来检查某个特定位是否被设置。

(3)获取二进制的指定位

我们需要获取一个整数x的二进制序列的第i位(从低到高,以最低位为第0位)的具体数字时,可以对x进行下面的运算:

cpp 复制代码
class Solution {
public:
 uint32_t reverseBits(uint32_t n) {
 int i = 0;
 uint32_t ret = 0;
 for (i = 0; i < 32; i++)
 {
 int b = (n >> i) & 1; //从低往⾼获取每⼀个⼆进制位 
 b <<= (31 - i); //从低位移动到⾼位 
 ret |= b; //将这⼀位设置到ret中 
 }
 return ret;
 }
};

如果结果是0,表示第i位是0;如果结果是1,表示第i位是1。比如:确定x的二进制序列的第三位的值,需要将x右移3位,第三位就是最低位,然后和1进行按位与计算即可。

x: 00101011 右移三位后:

00000101

& 00000001


00000001

(4)将指定二进制位设置为1

有时候需要将一个整数x的二进制表示中的某一位设置为1,其余位保留原值。就可以使用另外一个数m,使m的二进制上对应的位置为1,其余位置为0。然后使两个数进行按位或运算,就可以得到想要的值。比如:将x的后四位二进制序列设置为1,其余位保持不变,只需要将x与后四位为1,其余位为0的数字进行按位或运算。

x: 00101011

| 00001111


00101111

当然也可以只设置x二进制序列的某一位为1,也就是将x二进制的第i位置为1,就只需要进行下面的运算:

将x=00101011的第四位二进制序列置为1,其余位保持原有的值不变。只需要将x与00010000进行按位或运算:

x: 00101011

| 00010000 //这个数字可以通过 1得到


00111011

(5)将指定的二进制位设置为0

有时候需要将一个整数的二进制序列中某一位设置为0,其余位保留原值。也就是将x的二进制的第i位置为0,其余值保持不变。下面举出相关例子进行计算:

将x=00001011的第三位置为0,其余位保持不变。则只需要将x=00001011与m=11110111进行按位与运算。下面是运算过程:

x: 00001011

& 11110111 //这个数字可以通过 1,后然后按位取反得到


00000011

(6)反转指定的二进制位

有时候需要将一个整数的二进制表示中的第i位反转,也就是从原来的1变为0,从原来的0变为1。这种操作只需要将使用另一个数m,m的二进制序列中第i位为1,其余位为0.然后让两个数进行按位异或运算,就可以得到想要的数。比如:

将x=00101011的第二位反转,只需要将x=00101011与m=00000100进行按位异或运算,计算过程如下:

00101011

^ 00000100


00101111

(7)将二进制中最右边的1变为0

有时候需要将一个整数的二进制表示中最右边的1变成0,这时候就可以使用这个数字的二进制表示x与x-1进行合取运算,比如:

将x=00101100二进制中最右边的1变为0,只需要x=00101100&(x-1)=00101100即可,下面是详细的计算过程:

00101100 (x)

& 00101011 (x-1)


00101000

这种运算通常应用到求一个数的二进制序列中有几个1,因为x=x&(x-1)这种运算,每计算一次就会将x的二进制序列的最右边的1置为0。那么只要循环不断执行该操作,直到该数的二进制序列变为全0即可。此时的循环次数就是该数的二进制序列的1的个数。

(8)只保留二进制最右边的1

有时候需要将一个整数的二进制序列表示的最右边的1保留下来,其余位置为0,那么只需要将x&(-x)即可。比如:

将x=00101100的最右边的1保留,其余位置的数字置为0,就只需要将其与-x=00101100进行合取得到的结果就是想要的结果。下面是详细的计算过程:

x的⼆进制:00101100

-x的原码 :10101100

-x的反码 :11010011

-x的补码 :11010100

00101100(x)

& 11010100(-x)


00000100

这种运算的性质可以帮助我们判断一个数是否是2的幂数?下面是相关代码演示:

cpp 复制代码
bool isPowerOfTwo(int n) 
{
 return (n > 0) && (n & -n) == n;
}

因为2的幂次方的数字的二进制序列中只有一个1,n&-n会将这个1保留下来,其实还是n,所以只要完成上述变化后值没有改变的数就是2的幂次方数。

(9)异或的运用

下面是整理的异或运算的特点:

  1. x ^ x = 0 两个相同数字异或的结果
  2. 0 ^ x = x 0和任何数异或的结果等于任何数本身
  3. a ^ b ^ a = a ^ a ^ b 异或操作符支持交换律

下面是异或运算符的运用示例:

练习:交换两个整数的值

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 int a = 0;
 int b = 0;
 cin >> a >> b; 
 cout << "交换前:a = " << a << " b = " << b << endl;
 a = a ^ b; //a' = a ^ b
 b = a ^ b; //b' = a' ^ b == a ^ b ^ b == a
 a = a ^ b; //a = a' ^ b' == a ^ b ^ a == b
 cout << "交换后:a = " << a << " b = " << b << endl;
 return 0;
}

上述代码利用异或运算符的第三条性质,将两个整数的位置进行交换。使用异或交换两个数的值,只能适用于整型类型的数,因为异或运算仅适用于整型类型。

五.操作符的属性

1.优先级

优先级指的是:如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。比如:我们小学就学过的先乘除后加减。

3 + 4 * 5;

上面的例子中,表达式中有加法和乘法。由于乘法运算符的优先级高于加法运算符的优先级,所以会先计算4*5,而不是先计算3+4。

2.结合性

如果两个运算符的优先级相同,优先级没办法确定先计算哪个,这时候就由结合性决定了。大部分运算符是左结合(从左向右执行),少数运算符遵循右结合(从右向左执行)

5 * 6 / 2;

上面示例中,乘法操作符和除法操作符的优先级相同,且都是左结合运算符,所以都是从左到右执行,先计算5*6,后计算6/2。下面是结合性优先级的相关表格:

3.总结

当有了操作符的优先级和结合性,我们就可以确定一个表达式计算的顺序,但是如果一个表达式写的过于复杂,即使有了操作符的优先性和结合性,也可能无法确定表达式唯一的计算路径。所以在今后写代码时,应该避免写过于复杂的表达式。

相关推荐
iAkuya1 天前
(leetcode)力扣100 76数据流的中位数(堆)
算法·leetcode·职场和发展
程序员潇潇1 天前
pytest 参数化测试用例构建
自动化测试·软件测试·功能测试·程序人生·职场和发展·测试用例·pytest
小白菜又菜1 天前
Leetcode 221. Maximal Square
算法·leetcode·职场和发展
我命由我123451 天前
Photoshop - Ps还原和历史记录
学习·ui·职场和发展·求职招聘·职场发展·学习方法·photoshop
我命由我123451 天前
Photoshop - Ps工作界面
学习·ui·职场和发展·求职招聘·职场发展·学习方法·photoshop
菜鸡儿齐1 天前
leetcode-电话号码的字母组合
算法·leetcode·职场和发展
小白菜又菜1 天前
Leetcode 229. Majority Element II
算法·leetcode·职场和发展
yzx9910132 天前
蓝桥杯备考智能体:构建高并发、智能化编程竞赛助手的深度实践
职场和发展·蓝桥杯
期末考复习中,蓝桥杯都没时间学了2 天前
力扣刷题23
算法·leetcode·职场和发展
菜鸡儿齐2 天前
leetcode-括号生成
算法·leetcode·职场和发展