一、位运算符
1、位 与(&) 运算符
位与运算符是一个二元的位运算符,也就是有两个操作数,表示为 x & y。
左操作数 | 右操作数 | 结果 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
#include <stdio.h>
int main() {
int a = 0b1010; // (1)
int b = 0b0110; // (2)
printf("%d\n", (a & b) ); // (3)
return 0;
}
- 在C语言中,以 0b 作为前缀,表示这是一个二进制数。那么 a 的实际值就是 (1010) 。
- 同样的, b 的实际值就是 (0110) ;
- 那么这里 a & b 就是对 (1010) 和 (0110) 的每一位做表格中的 & 运算。
- 所以最后输出结果为:2
因为输出的是十进制数,它的二进制表示为:(0010)_2
。
注意:这里的 前导零 可有可无,作者写上前导零只是为了对齐以及让读者更加清楚位与的运算方式。
前导零的概念通过一个例子解释:
例如,反转 2021 得到 1202 。反转 12300 得到 321 ,这里就不保留前导零 。
位与运算符的应用
1、奇偶性判定
对任何一个数,通过将它和 0b1 进行位与,结果为零,则必然这个数的二进制末尾位为0,根据以上表就能得出它是偶数了;否则,就是奇数。
if(5 & 1) {
printf("5是奇数\n");
}
if((6 & 1) == 0) {
printf("6是偶数\n");
}
2、取末五位
【例题1】给定一个数,求它的二进制表示的末五位,以十进制输出即可。
- 这个问题的核心就是:我们只需要末五位,剩下的位我们是不需要的,所以可以将给定的数 位与上0b11111,这样一来就直接得到末五位的值了。
- 代码实现如下:
#include<stido.h>
int main() {
int x;
scanf(“%d”, &x);
printf("%d\n", (x & 0b11111) );
return 0;
}
3、消除末尾五位
【例题3】给定一个 32 位整数,要求消除它的末五位。
还是根据位与的性质,消除末五位的含义,有两层:
- 1)末五位,要全变成零;
- 2)剩下的位不变;
那么,根据位运算的性质,我们需要数,它的高27位都为1,低五位都为 0,则这个数就是:
(
11111111111111111111111111100000
)
2
(11111111111111111111111111100000)_2
(11111111111111111111111111100000)2
但是如果要这么写,代码不疯掉,人也会疯掉,所以一般我们把它转成十六进制,每四个二进制位可以转成一个十六进制数,所以得到十六进制数为 0xffffffe0
。
代码实现如下:
#include <stdio.h>
int main() {
int x;
scanf("%d", &x);
printf("%d\n", (x & 0xffffffe0) );
return 0;
}
提示: f 代表 4 个1; e 代表 3个1,1个0; 0 代表 4 个 0 ;
4、消除末尾连续 1
【例题4】给出一个整数,现在要求将这个整数转换成二进制以后,将末尾连续的1都变成0,输出改变后的数(以十进制输出即可)。
我们知道,这个数的二进制表示形式一定是:
…
0
11
…
11
⏟
k
\dots 0 \underbrace{11\dots11}_{\text{k}}
…0k
11…11
如果,我们把这个二进制数加上1,得到的就是:
…
1
00
…
00
⏟
k
\dots 1 \underbrace{00\dots00}_{\text{k}}
…1k
00…00
我们把这两个数进行位与运算,得到:
…
0
00
…
00
⏟
k
\dots 0 \underbrace{00\dots00}_{\text{k}}
…0k
00…00
5、2的幂判定
【例题5】请用一句话,判断一个正数是不是2的幂。
如果一个数是 2 的幂,它的二进制表示必然为以下形式:
…
1
00
…
00
⏟
k
\dots 1 \underbrace{00\dots00}_{\text{k}}
…1k
00…00
这个数的十进制值为 2 的 k 次。
那么我们将它减一,即 2 的 k 次 减一 的二进制表示如下(参考二进制减法的借位):
…
0
11
…
11
⏟
k
\dots 0 \underbrace{11\dots11}_{\text{k}}
…0k
11…11
于是 这两个数位与的结果为零,于是我们就知道了如果一个数 x 是 2 的幂,那么 x & (x-1) 必然为零。而其他情况则不然。所以本题的答案为:
(x & (x-1)) == 0
2、右移(>>) 运算符
1、右移的二进制形态
右移运算符是一个二元的位运算符,也就是有两个操作数,表示为 x >> y。其中 x 和 y 均为整数。
x >> y 念作:“将 x 右移 y 位”,这里的位当然就是二进制位了,那么它表示的意思也就是:先将 x 用二进制表示,对于正数,右移 y 位;对于负数,右移 y 位后高位都补上 1。
举个例子:对于十进制数 87,它的二进制是 (1010111), 左移 y 位的结果就是: ( 1010 ) 2 (1010)_2 (1010)2
2、右移的执行结果
x >> y 的执行结果等价于:
⌊
x
2
y
⌋
\lfloor\frac{x}{2^y}\rfloor
⌊2yx⌋
⌊
⌋
\lfloor\rfloor
⌊⌋这个符号代表取下整。如下代码:
#include <stdio.h>
int main() {
int x = 0b1010111;
int y = 3;
printf("%d\n", x >> y);
return 0;
}
输出结果为:
10
即:
(
1010
)
2
(1010)_2
(1010)2
正好符合这个右移运算符的实际含义:
10
=
⌊
87
2
3
⌋
10 = \lfloor\frac{87}{2^3}\rfloor
10=⌊2387⌋
由于除法可能造成不能整除,所以才会有 取下整 这一步运算。
3、负数右移的执行结果
所谓负数右移,就是 x >> y 中,当 x 为负数的情况,代码如下:
#include <stdio.h>
int main() {
printf("%d\n", -1 >> 1);
return 0;
}
它的输出如下:
-1
同样满足式子:
⌊
x
2
y
⌋
\lfloor\frac{x}{2^y}\rfloor
⌊2yx⌋
这个可以用补码来解释,-1 的补码为:(补码的计算方法可参考:补码的计算 )
11111111111111111111111111111111
11111111 11111111 11111111 11111111
11111111111111111111111111111111
右移一位后,由于是负数,高位补上 1,得到:
11111111111111111111111111111111
11111111 11111111 11111111 11111111
11111111111111111111111111111111
可以理解成 - (x >> y)和 (-x) >> y 是等价的。
4、右移负数位是什么情况
刚才我们讨论了 x < 0 的情况,那么接下来,我们试下 y < 0 的情况会是如何?是否同样满足如下性质呢?
⌊
x
2
y
⌋
\lfloor\frac{x}{2^y}\rfloor
⌊2yx⌋
如果还是满足,那么两个整数的左移就有可能产生小数了。
看个例子:
#include <stdio.h>
int main() {
printf("%d\n", 1 >> -1); // 2
printf("%d\n", 1 >> -2); // 4
printf("%d\n", 1 >> -3); // 8
printf("%d\n", 1 >> -4); // 16
printf("%d\n", 1 >> -5); // 32
printf("%d\n", 1 >> -6); // 64
printf("%d\n", 1 >> -7); // 128
return 0;
}
虽然能够正常运行,但是结果好像不是我们期望的,而且会报警告如下:
[Warning] right shift count is negative [-Wshift-count-negative]
实际上,编辑器告诉我们尽量不用右移的时候用负数,但是它的执行结果不能算错误,起码例子里面对了。
右移负数位其实效果和左移对应正数数值位一致。
右移运算符的应用
1、去掉低 k 位
【例题2】给定一个数 x,去掉它的低 k 位以后进行输出。
这个问题,可以直接通过右移来完成,如下:x >> k
。
2、取低位连续 1
【例题3】获取一个数 x 低位连续的 1 并且输出。
对于一个数 x,假设低位有连续 k 个 1。如下:
(
…
0
11
…
11
⏟
k
)
2
(\dots 0 \underbrace{11\dots11}_{\text{k}})_2
(…0k
11…11)2
然后我们将它加上 1 以后,得到的就是:
(
…
1
00
…
00
⏟
k
)
2
(\dots 1 \underbrace{00\dots00}_{\text{k}})_2
(…1k
00…00)2
这时候将这两个数异或结果为:
(
11
…
11
⏟
k+1
)
2
(\underbrace{11\dots11}_{\text{k+1}})_2
(k+1
11…11)2
这时候,再进行右移一位,就得到了 连续 k 个 1 的值,也正是我们所求。
所以可以用以下语句来求:(x ^ (x + 1)) >> 1
。
3、取第k位的值
【例题4】获取一个数 x 的第 k(0 <= k <= 30) 位的值并且输出。
对于二进制数来说,第 k 位的值一定是 0 或者 1。
而 对于 1 到 k-1 位的数字,对于我们来说是没有意义的,我们可以用右移来去掉,再用位与运算符来获取二进制的最后一位是 0 还是 1,如下:(x >> k) & 1
。
3、异或(^)运算符
异或运算符是一个二元的位运算符,也就是有两个操作数,表示为 x ^ y。
异或运算会对操作数的每一位按照如下表格进行运算,对于每一位只有 0 或 1 两种情况,所以组合出来总共 4 种情况。
左操作数 | 右操作数 | 结果 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
通过这个表,我们得出一些结论:
1)两个相同的十进制数异或的结果一定为零。
2)任何一个数和 0 的异或结果一定是它本身。
3)异或运算满足结合律和交换律。
#include <stdio.h>
int main() {
int a = 0b1010; // (1)
int b = 0b0110; // (2)
printf("%d\n", (a ^ b) ); // (3)
return 0;
}
(1) 在C语言中,以 0b 作为前缀,表示这是一个二进制数。那么 a 的实际值就是 (1010) 。
(2) 同样的, b 的实际值就是 (0110) ;
(3) 那么这里 a ^ b 就是对 (1010) 和 (0110) 的每一位做表格中的 ^ 运算。
所以最后输出结果为:
12
因为输出的是十进制数,它的二进制表示为: (1100) 。
异或运算符的应用
1、标记位取反
【例题1】给定一个数,将它的低位数起的第 4 位取反,0 变 1,1 变 0。
这个问题,我们很容易联想到异或。我们分析一下题目意思,如果第 4 位为 1,则让它异或上 0b1000 就能变成 0;如果第 4 位 为 0,则让它异或上 0b1000 就能变成 1,也就是无论如何都是异或上 0b1000 ,代码如下:
int main() {
int x;
scanf("%d", &x);
printf("%d\n", x ^ 0b1000);
return 0;
}
2、变量交换
【例题2】给定两个数 a 和 b,用异或运算交换它们的值。
#include <stdio.h>
int main() {
int a, b;
while (scanf("%d %d", &a, &b) != EOF) {
a = a ^ b; // (1)
b = a ^ b; // (2)
a = a ^ b; // (3)
printf("%d %d\n", a, b);
}
return 0;
}
我们直接来看 (1) 和 (2) 这两句话,相当于 b 等于 a ^ b ^ b ,根据异或的几个性质,我们知道,这时候的 b 的值已经变成原先 a 的值了。
而再来看第 (3) 句话,相当于 a 等于 a ^ b ^ a,还是根据异或的几个性质,这时候,a 的值已经变成了原先 b 的值。
从而实现了变量 a 和 b 的交换。
3、出现奇数次的数
【例题3】输入 n 个数,其中只有一个数出现了奇数次,其它所有数都出现了偶数次。求这个出现了奇数次的数。
根据异或的性质,两个一样的数异或结果为零。也就是所有出现偶数次的数异或都为零,那么把这 n 个数都异或一下,得到的数就一定是一个出现奇数次的数了。
#include <stdio.h>
int main() {
int n, x, i, ans;
scanf("%d", &n);
ans = 0;
for(i = 0; i < n; ++i) {
scanf("%d", &x);
ans = (ans ^ x);
}
printf("%d\n", ans);
return 0;
}
4、丢失的数
【例题4】给定一个 n-1 个数,分别代表 1 到 n 的其中 n-1 个,求丢失的那个数。
int missingNumber(int* nums, int numsSize){
int i;
int res = 0;
for (i = 0; i < numsSize; ++i) {
res ^= (i + 1) ^ nums[i];
}
return res;
}
5、简单加密
基于 两个数异或为零,任何数和零 异或 为其本身 这两个特点,异或还可以用来做简单的加密。将明文异或上一个固定的数变成密文以后,可以通过继续异或上这个数,再将密文转变成明文。
二、算术运算符
二、取余
取余,也就是求余数,使用的运算符是 %,C语言中的取余运算只能针对整数,也就是说,% 两边必须是整数,不能出现小数,否则会出现编译错误。
例如:5 % 3 = 2、7 % 2 = 1。
当然,余数可以是正数也可以是负数,由 % 左边的数决定:
1)如果 % 左边的数是正数,那么余数也是正数;
2)如果 % 左边的数是负数,那么余数也是负数;
三、幂
幂用 **
表示。
其中 a**b
代表 a 的 b 次幂。如下:
当然,幂运算的右操作数,也支持负数。
四、取整数
取整除就是数学上的取下整的意思。用 // 表示,如下:
a = 10
b = 6
`` print(a//b)
输出的结果是 : 1
汉明重量
leetcode 191.编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量 )。
例如:11100011 的汉明重量是 5。
int hammingWeight(uint32_t n) {
int c = 0;
while(n){ //循环遍历 n 的每个二进制位
if(n & 1){ //如果 n 的最低二进制位为 1,则给计数器 c 加上 1;
++c;
}
n >>= 1; //将 n 的二进制位右移一位;
}
return c; //返回计数器,代表 n 中二进制位的 1 的数量;
}
汉明距离
两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
leetcode 461.
C语言版本:
int hammingDistance(int x, int y){
int z = x ^ y; //将 x 和 y 进行异或操作,得到的结果存到 z 中;
int c = 0; //c 统计变量 z 中 1 的数量;
while(z){
if(z & 1){
++c;
}
z >>= 1; //将 z 的二进制位右移一位;
}
return c;
}
python版本:
class Solution(object):
def hammingDistance(self, x, y):
"""
:type x: int
:type y: int
:rtype: int
"""
z = x ^ y
c = 0
while z:
if z & 1:
c += 1
z >>= 1
return c