运算与编程息息相关。我们编写的每个程序都可能包括加减乘除。当然,这是最基本的操作。大一的时候学了第一门编程语言C,后来还学了余数(%)和三元运算符(?:)。当时觉得(?:)真的很牛逼,但是在编程的时候却很少用到它,因为if和else已经刻在脑海里了。不同语言的运算符也有一些偏差。例如Python中的整数除法(//)在C中是没有的。C中的三元运算符在Python中也有不同的表达方式,如np.where和if、else组合。下面介绍一下我个人认为比较高大上的位运算符。说它高大上是因为它很方便用位运算来解决某些问题,另一个小的方面是因为它可以安装Bi。如果你从来没有接触过位运算,那么即使是一小部分关于位运算的代码也会让你看得一头雾水。了解位运算后,你会发现这种方法确实比较巧妙。我们知道,所有的数字,包括字母、符号等,在计算机中都是以二进制的形式存储的,位运算都是直接对二进制进行运算。常见的位运算有以下几种:按位与:&按位或:|按位异或:^左移:<<右移:>>取反:~这些位运算符号按优先级排序如下:12345~<<,>>&^按位还是为了便于理解和查看,下面的例子中只列出了8位二进制按位与(&)。按位与运算算法可以概括为“相同则为真,否则为假”。0和1之间的运算有如下形式:1&1=11&0=00&0=0例如数字5和数字6之间使用按位与运算,两个数组的8位二进制形式的每一位是对应的,利用上述规则可以得到一个新的数字4,即5&6=4。按位或(|)按位或运算算法可以概括为“如果相同为假,则为假,否则为真”,0和1之间的运算有如下形式:1|1=11|0=10|0=0也以数字5和数字6为例。使用与上述相同的方法对两者进行按位或运算,可以得到一个新的数字7,即5|6=7。按位异或(^)按位异或算法可以概括为“相同为假,不同为真”,0和1之间的运算有如下形式:1|1=01|0=10|0=0还是以数字5和数字6为例。用和上面一样的方法对两者进行按位异质运算,可以得到一个新的数3,即5^6=3。左移(<<)左移操作是将a的二进制形式移动整个左边的数,然后用0填充右边的虚位。具体动作见下图:这个例子就是2<<2=8的体现,可以看到1本来是在右边第2位,如果整体向左移动两位,那么1会移动到右边第4位,空出的两位可以用0填充。右移(>>)右移操作为与左移操作相同,方向相反。具体移动如下图所示:本例是6>>2=1的体现,只需要整体向右移动两个位置,然后左边的虚位加0即可,不需要担心1是否会被覆盖。这两个符号比较容易混淆,这里有一个自己的小技巧:箭头移动到哪里,左移值就增加,左移值就减少。倒数(~)倒数运算比较简单,只需要记住这么一个小公式:~x=-(x+1),比如数字5的倒数运算等于-6,即~5=-(5+1)=-6。我已经介绍了这六位运算符是如何对二进制进行运算的,但是简单的介绍并不能体现出位运算的高层次。接下来,我将使用位运算的技巧来解决一些问题。这些问题并不难,但是我们从中可以体会到位运算的便捷性,加深对位运算的印象。交换两个数对于这道题,我们可能想到的第一个方法就是利用第三个变量来帮助交换。比如要交换a和b两个变量的值,就需要定义一个临时变量temp来辅助。a=1;b=2temp=0temp=a;一=b;b=temp这个需要额外的存储空间,那么有没有办法只在原地交换a和b这两个变量的值呢?可以进行位运算,而且非常简单。a=a^b#(1)b=a^b#(2)a=a^b#(3)如果你没有接触过位运算,你一定对这三行代码很迷茫,明白这三行代码你还需要知道按位异或的几个性质:1.按位异或满足交换律,即a^b=b^a2。按位异或满足结合律,即(a^b)^c=a^(b^c)3。任何与0异或的数都等于它本身,比如a^0=a。将式(1)代入式(2),将上述性质结合按位异或“同为假”的规则,则式(2)可变为如下形式:b=a^b^b=a^0=a这样就把a的值赋给了b。如果再将计算出的式(2)代入式(3),则式(3)变为:a=a^a^b=0^b=b这样,变量a和b的交换就完成了。是不是已经有牛逼的感觉了?继续下一个。2的幂假设给定一个数,需要判断它是否是2的幂,如果是则需要返回true,否则返回false。这道题用loop的方法也不错。设置一个循环条件,然后将输入的整数n除以2。最后只需要判断n是否等于1即可,因为n==1只有当n是2的幂时才有效。满足。whilen>1:n/=2returnn==1这个方法应该算是很常用的方法了,体现不出自己的风格怎么办?让我们看看如何使用位运算来解决它。returnn>0andn&(n-1)==0若n为2的幂,则二进制最高位应为1,其余位应为0,例如8的二进制值为1000;对于n-如果为1,除了最高位为0,其他都应该为1。比如7的二进制值为0111,那么此时n&(n-1)=0。求不同如果给定一个只包含小写字母的字符串s1,那么字符串s2会打乱s1并在其中插入一个新的小写字母,并希望您找出新插入的字母。例如,s1='abc',s2='badc',输出d。第一个思路是使用哈希表来统计和存储每个元素出现的次数。新添加的元素只出现一次,使用哈希表意味着需要空间来创建新的字典。而使用按位异或运算只需要引入第三个变量就可以解决这个问题,利用上述异或的性质1和3,以及“相同则为假”的规则。m=s1+s2first=0foriinrange(0,len(m)):first^=ord(m[i])returnchr(first)这里ord是将字母转成对应的Ascii码,chr是转Ascii代码数字被转换回相应的字母形式。翻转二进制,先以8位二进制为例。例如输入二进制为000100111,要求输出为11001000。整数翻转使用字符串可以很好解决,但是二进制不同于整数,因为二进制中有很多0,翻转转换后可能会擦掉一些0。这里可以使用位运算实现二进制翻转,具体代码如下:res=0foriinrange(8):res<<=1res+=n&1n>>=1returnbin(res)这里使用按位与“相同则为真,否则为假”的规则是每次将输入二进制的最后一位与1进行比较,将结果加到res中,然后将n右移一位,因为最后一位已经进行了比较。每次遍历开始时,注意将res左移一位,因为无论n&1的结果是0还是1,在二进制中都是有意义的,需要为数字提供一个空格获得,这是不可能省略的。这里建议自己push一下,简单易懂。1个数的奇偶性仍然给出一个8位的二进制,统计8个数中1个数的奇偶性。如果1个数是奇数,则返回1;如果1个数是偶数,则返回0。比如00110111里面有5个1,所以应该返回1。这道题用位运算的思路和上一道有点类似。也是用bitwise和“同理为真,反义为假”的规则循环8个数,用计数器count统计1的个数,最后用count&1可以判断count的奇偶性,奇数返回1,偶数返回0。count=0foriinrange(8):count+=n&1n>>=1returncount&1但这还不是最牛逼的位操作,下面这个方法确实很强,但是可读性可能不如以上方法。n=n^(n>>1)n=n^(n>>2)n=n^(n>>4)returnn&1还是以00110111为例,对n和n>>进行异或运算1、得到一个新的二进制,如下:新二进制中0的含义表示该位置与上一个位置有偶数个1,1表示该位置与上一个位置有奇数个1.比如New中第6位的1,表示Num1中的第5、6位有奇数个1。可以看到Num1中对应的位置是01,是一致的。同理,你可以用这个属性来比较其他位置。同样,移动2位表示该位置与前三个位置的奇偶性,移动4位表示该位置与前七位的奇偶性,所以移动4位后的最后一个数表示整个8中的1的奇偶性-bit二进制,如果最后一位是1,表示有奇数个1,如果最后一位是0,表示有偶数个1。总结位运算可能很多人都知道,但是在解决问题的时候却很少用到,因为有很多通俗易懂的方法可以代替位运算,但是位运算的功能非常强大,值得学习。通过上面的例子我们也可以发现,位运算在解决某些问题上确实很方便,但是不懂的人的可读性会比较差,这也是位运算可以装逼的地方。关注公众号【奶糖猫】第一时间获取更多精彩好文
