我不知道的按位运算

💻计算机为什么会加减乘除?笔者是一直都没了解过,认为加减乘除就是理所当然的事情,但计算机中的万物皆为0、1,笔者的认为是不可能那么简单的。

其实说直接点,就是计算机的原理是什么?

1. 二进制

二进制就是计算机使用的“语言”。简单举例来说就是:人类使用十进制来计数,它的运算规则是“逢十进一”。二进制就是“逢二进一”的运算规则。

为什么计算机使用二进制呢?这个可以归结于一个人有十根手指,所以人类普遍认知是使用十进制。计算机是由一大堆电子电路组成的,使用二进制正好对应电路里的高低电平(大部分电子器件只有两种状态),这就好比计算机只有“两根手指”。

1.1 原码

原码是二进制的一种表现形式。其为一个整数绝对值的二进制,加上符号位(0为正,1为负)。

整数 绝对值 绝对值的二进制 原码
+3 3 000 0011 0000 00011
+3 3 000 0011 1000 00011
  • 原码是给人看的二进制,不是计算机保存的,不直接参与计算。

1.2 反码

针对负数做处理,在原码基础上,除了符号位,其它位取反。

整数 绝对值的二进制 原码 反码
+3 000 0011 0000 00011 0000 00011
+3 000 0011 1000 00011 1111 11100
  • 反码是在计算机存储的二进制,但不是真正的二进制值,它不直接参与计算。

1.3 补码

补码是真正的二进制值,主要针对负数做处理,在反码的基础上加1。

整数 原码 反码 补码
+3 0000 00011 0000 00011 0000 00011
+3 1000 00011 1111 11100 1111 11101

1.4 理解补码

假如一个时钟现在显示的是10点钟,如何将它调到6点钟?

解:有两种方法,一是向后拨8个小时,二是向前拨4个小时

在这个例子中,8 和 4 互为补数,也就是说4的补码是8,8的补码是4,而这个时钟的模就是12

注意:可能有人会想,在往后调8个小时虽然也调到了6点,但是他实际上比原来日期多了12小时。是的,的确如此,但是你的时钟有地方存储了这多余的12个小时吗?答案是没有,所以在你调完后,你没有记录这12个小时,换句话说,你把这溢出的12个小时自动舍弃了。当第二个人来查看闹钟时间的时候,他看到的时间就是准的。

二进制补码表示负数是最方便的方式,它的便利体现在,所有的加法运算(加正数、加负数)可以使用同一种电路完成。

我们以-8作为例子。

假定有两种表示方法。一种是直觉表示法,即10001000;另一种是2的补码表示法,即11111000。请问哪一种表示法在加法运算中更方便?

随便写一个计算式,16 + (-8) = ?其中,16的二进制表示是 00010000,-8的二进制表示是 10001000

  • 直觉表示法:
 00010000
+10001000
---------
 10011000

可以看到,如果按照正常的加法规则,就会得到10011000的结果,转成十进制就是-24。显然,这是错误的答案。也就是说,在这种情况下,正常的加法规则不适用于正数与负数的加法,因此必须制定两套运算规则,一套用于正数加正数,还有一套用于正数加负数。从电路上说,就是必须为加法运算做两种电路。

  • 补码表示法:
 00010000
+11111000
---------
100001000

可以看到,按照正常的加法规则,得到的结果是100001000。注意,这是一个9位的二进制数。我们已经假定这是一台8位机,因此最高的第9位是一个溢出位,会被自动舍去。所以,结果就变成了00001000,转成十进制正好是8,也就是16 + (-8) 的正确答案。这说明了,2的补码表示法可以将加法运算规则,扩展到整个整数集,从而用一套电路就可以实现全部整数的加法。

2. 按位运算

Python为例,常用位运算符包括以下6中:

  • 按位与 &
  • 按位或 |
  • 按位异或 ^
  • 按位翻转 ~
  • 左移运算 <<
  • 右移运算 >>

2.1 按位与(&)

即按照对应位置的二进制进行 and运算,其中运算规则为 1 & 1 = 11 & 0 = 00 & 1 = 00 & 0 = 0

下面的 5 & 3示意如下:

十进制     二进制
5 --> 0000 0101
  &        &&&&
3 --> 0000 0011
  =        ||||
1 <-- 0000 0001
  • 位运算判断奇偶性

n&1 == 1 # 奇数返回1,偶数返回0

2.2 按位或(|)

即按照对应位置的二进制进行 or运算,其中的运算规则为 1 | 1 = 11 | 0 = 10 | 1 = 10 | 0 = 0

下面的 5 | 3示意如下:

十进制     二进制
5 --> 0000 0101
  |        ||||
3 --> 0000 0011
  =        ||||
7 <-- 0000 0111
  • 向上取奇数

map(lambda x:x|1, range(6)) # 结果为[1, 1, 3, 3, 5, 5]

2.3 按位异或(^)

即按照对应位置的二进制进行 xor运算,其中的运算规则为 1 ^ 1 = 01 ^ 0 = 10 ^ 1 = 10 ^ 0 = 0

下面的 5 ^ 3示意如下:

十进制     二进制
5 --> 0000 0101
  ^        ^^^^
3 --> 0000 0011
  =        ||||
6 <-- 0000 0110
  • 只出现一次的数字

reduce(lambda x,y: x^y, [1,1,3,2,4,3,4]) # 累积进行异或运算,找出只出现一次的数字为2,出现两次的数字就抵消掉了

2.4 按位翻转(~)

即1变成0,0变成1。

十进制     二进制
5 --> 0000 0101
  ~   ~~~~ ~~~~
-6<-- 1111 1010

翻转相当于跟 -1做异或运算,即~n = n ^ (-1)

十进制     二进制
5 --> 0000 0101
  ^        ^^^^
-1--> 1111 1111
  =        ||||
6 <-- 1111 1010

2.5 左移运算(<<)

左移运算是将二进制数值整体向左边移动n个位置,空出来的位置补0。

下面的 5 << 2示意如下:

十进制     二进制
5 --> 0000 0101
  <<    << 2
20<-- 0001 0100
  • 用于乘法计算
  5 * 7
= (101 * 111)_2
= (101 * (100 + 10 + 1))_2
= (101 * 100 + 101 * 10 + 101 * 1)_2
= 5 << 2 + 5 << 1 + 5 << 0
= (10100 + 01010 + 00101)_2
= 20 + 10 + 5
= 35

2.6 右移运算(>>)

左移运算是将二进制数值整体向右边移动n个位置,空出来的位置补上符号位的数值。即正数补0,负数补1。

下面的 5 >> 2-5 >> 2示意如下:

十进制     二进制
5 --> 0000 0101
  >>    >> 2
1 <-- 0000 0001

十进制     二进制
-5--> 1111 1011
  >>    >> 2
-1<-- 1111 1110
  • 用于除法
  5 / 3
= 5 >> 2
= (101 / 100)_2
= (001)_2
= 1

2.7 位运算实现加减乘除

def add(num1, num2):
    while num2 != 0:
        temp = num1 ^ num2
        num2 = (num1 & num2) << 1
        num1 = temp
    return min(max(-2147483648, num1), 2147483647)

def sub(num1, num2):
    return add(num1, add(~num2, 1))

def mul(num1, num2):
    sign = (num1 > 0) is (num2 > 0)

    if num1 < 0:
        num1 = add(~num1, 1)
    if num2 < 0:
        num2 = add(~num2, 1)

    result = 0
    while num2:
        if num2 & 1:
            result = add(result, num1)
        num1 = num1 << 1
        num2 = num2 >> 1

    if not sign:
        result = - result
    return result

def div(num1, num2):
    sign = (num1 > 0) is (num2 > 0)

    if num1 < 0:
        num1 = add(~num1, 1)
    if num2 < 0:
        num2 = add(~num2, 1)

    result = 0
    while (num1 >= num2):
        tmp, i = num2, 1
        n = 4
        while(num1 >= tmp):
            num1 -= tmp
            result += i

            tmp = tmp<<n
            i = i << n
        n = n >> 2

    if not sign:
        result = -result

    return min(max(-2147483648, result), 2147483647)

在早期版本中,如Python2.7,整数的有int和long两个类型。int类型是一个固定位数的数;long则是一个理论上可以存储无限大数的数据类型。当数大到可能溢出时,为了避免溢出,Python会把int转化为long。

而Python3.x之后整数只有一个可以放任意大数的int了。可是无论哪种,都是采用了特殊的方法实现了不会溢出的大整数。

在进行负数的按位加法时,有可能发生在最高位还要向前进一位的情形,正常来说,这种进位因为超出了一个int可以表示的最大位数,应该舍去才能得到正确的结果。但在Python中因为上述原因会产生一个不会溢出的大整数,所以在进行负数的按位加法时,结果会不断变大。

这个问题在 Java、C、C++ 中不会存在,这也是Python效率低的一个原因。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容