学习目标:
复习Task01内容,并对关键内容进行总结
学习内容:
主要复习了运算符优先级、位运算两块内容,对运算符的优先级进行了背诵和记忆,对位运算相关知识进行了总结
首先说明的是,本文所有内容是本人对python客观性能的主观分析,只对python语言的宏观表现加以理解,目的在于更好更方便的推理出相关语句的行为结果,可能与实际的python解释器的底层行为大有不同。
学习时间:
2020/12/2 21:00–23:00
2020/12/3 12:00-15:00
学习产出:
运算符优先级研究
优先级
我根据自己的记忆习惯,把这些运算符分成了三个优先级梯队:
1.最优先梯队:()、x[i]、x.attr
小括号自然不必多说,在不清楚优先级顺序的情况下可以用来保证正确的运算顺序
其次是索引运算符和属性运算符,这两个运算符都是放置于操作变量的右边的,对于一个操作数来说同一方向的运算符,哪个距离它近,先执行哪个,所以这两个运算符的优先级比较没有太大意义
2.第二梯队:**、~、±(正负号)
除了乘方运算符,取反和正负号都是一元运算符,独特的乘方运算符恰好是第二梯队的最优先运算符
而由于第三梯队的运算符都是二元运算符,所以“一元运算符通常优先于二元运算符”
由于取反和正负号都在操作数左边,所以他们的优先级比较也没有太大意义,虽然规定~优先于正负号,但是还是哪个运算符距离操作数近,先执行哪一个,示例代码如下
3.第三梯队:大多数二元运算符
从高到低:算、位、比、逻、逗
算术运算符、位运算符、比较运算符、逻辑运算符、逗号运算符
通过将多种运算符分成梯队记忆,可以快速掌握知识点
相信有很多C++、java基础的同学可能在想,自增运算符(++)和自减运算符(–)呢?
值得庆幸的是,python舍弃了++和–,所以我们不需要研究这两个运算符的复杂运算顺序了
结合性
知晓这些运算符的优先级,我们便可以理解的绝大多数表达式的运算顺序,但并不是全部
还需要补充一点的知识是运算符的结合性
什么是运算符的结合性呢?
所谓结合性,就是当一个表达式中出现多个优先级相同的运算符时,先执行哪个运算符:先执行左边的叫左结合性,先执行右边的叫右结合性。
例如对于表达式对于100 / 25 * 16,/和*的优先级相同,应该先执行哪一个呢?这个时候就不能只依赖运算符优先级决定了,还要参考运算符的结合性。/和*都具有左结合性,因此先执行左边的除法,再执行右边的乘法,最终结果是 64。
Python 中大部分运算符都具有左结合性,也就是从左到右执行;只有 ** 乘方运算符、单目运算符(例如 not 逻辑非运算符)、赋值运算符和三目运算符例外,它们具有右结合性,也就是从右向左执行
上述情况也许并不能很有效地帮助我们理解运算符结合的必要性,再举一个例子:2**1**2
结果是多少呢?根据结合性分析,**具有右结合性,所以先运算1**2=1,再运算2**1=2,所以结果应为2
验证代码如下:
位运算研究
众所周知,数据在计算机内部以二进制形式存储,每一个位(bit)均有两种状态(0和 1),而位运算就是以位为单位进行的操作,处理的数据只有0和1.
1.按位与&
为了方便理解,并且与义务教育的数学知识相结合,我们可以假设0为假,1为真。按位与可以理解为“且”。对于两个命题A和B,A且B为真等价于什么?(高中数学)很显然是A为真并且B为真,同样的对于按位与运算,A&B等于1(为真)等价于A是1且B是1;换言之只要两个操作数有一个为0,那么结果为0.
代码验证如下:
2.按位或|
类似于按位与的分析理解方式,两个命题A和B,他们的组合命题A或B为真的等价于什么?显然是A为真或者B为真,对于按位或运算,只要两个操作数有一个为1,那么结果为1;换言之,两个操作数都为0时,结果才为0.
代码验证如下:
3.按位取反~
取反这个概念顾名思义,就是把状态变成反状态,一个位只有0和1两种状态,所以对0取反得1,对1取反得0.
在python中,二进制数据以补码形式存储,最高位代表符号位,0代表正,1代表负。
对任意一个数的位运算(包括与或非等等),都是对这个数的补码的所有位进行运算。上面两个运算符操作中,0和1两个数除最后一位之外,其他的位包括符号位均在运算中不会改变,因此可以演示对一位进行位运算的结果。而取反必然将使得符号位发生改变,所以很难演示只对一个位取反的结果。
因此我们在上述理论基础下,直接研究对一个多位二进制数的取反。
设a=0b110(原码,python是根据0b前面的正负号来设定符号位的,所以符号位是0而不是1,原码写全可以看成0110),a在十进制的值是6,由于位运算是对补码的运算,所以我们要研究a的补码是多少。对于正数,补码、反码、原码相同,所以对0110取反,得到1001。值得注意的是,前面提到位运算是对补码进行运算,还有一点需要加以说明,位运算的结果仍然是补码,也就是说1001是运算结果的补码。符号位为1说明这是一个负数的补码,对于负数来讲,反码是原码除符号位之外的位取反,补码是反码加一。这里我们由补码推原码,自然要先减去1,得1000,再对符号位之外的位取反,得1111(原码),即-7.
代码验证:
bin()函数是返回的带正负号的原码,所以bin(~a)应该为-0b111,代码验证:
下面我们再推理一个负数取反的例子,设a=-0b110(原码1110),反码为1001,补码为1010,对补码进行按位取反操作,得结果得补码为0101(正数),推得结果得原码为0101,即5,代码验证如下:
至此,我们对6对反得到-7,对-6取反得到5。
每一次推导取反运算都太过于繁琐,为了能快速得到取反得结果值,下面我们对取反前后的十进制数值关系做研究。
为了不失一般性,我们设参与取反运算的二进制数a包括符号位一共是n位,a的原码为a(原),a的反码为a(反),a的补码为a(补),a的十进制数值为a(10)。
取反之后结果的补码为b(补),反码为b(反),原码为b(原),十进制数值为b(10)
下面运算,我们将把第n位的符号位看作数值为参与运算,这样子可以把运算统一为二进制数的加法运算,但是最后要对结果做一个从无符号到有符号的转换
由条件定义得b(10)=~a(10)
由取反位运算是对补码操作得a(补)+b(补)=111…111(n位) (取反操作是0边1,1边0,包括符号位)
即a(补)+b(补)=2^n-1
由于取反会使符号位发生改变,所以我们不妨设a的符号位为0。
所以,a(原)=a(反)=a(补)
取反之后,b的符号位是1
所以对于b来说,b(补)=b(反)+1 b(反)+b(原)=(2^n)-1+2^(n-1)
所以,a(补)+b(补)=a(原)+b(反)+1=a(原)+(2^n)-1+2^(n-1)-b(原)+1=a(原)-b(原)+2^n+2^(n-1)=2^n-1
即b(原)=a(原)+2^(n-1)+1
值得注意的是,我们假设的a第n位的符号位是0,而2^(n-1)(无符号二进制)位1000…000(第n位是1),最后我们要做一个从无符号推理到有符号的转换,而a(原)+1000…0000,即把a(原)的符号位从0变成1,从正到负。最后还有个加一操作,这里的加一是无符号运算,比如1010+1等于1011,所以对于一个负数+00001,等价于在考虑符号情况下的数值上-1。
所以最后推理的结果应该为
b(10)=-a(10)-1
这里推理结果,符合之前两个例子,我们不妨再举一例验证:
位运算使用场景
学习位运算,不禁思考一个问题,我们什么时候使用位运算呢?
由于位运算比较复杂,二进制的运算不利于人去快速思考和推理,所以我们在一般的数值运算时都不会使用位运算符。但是,在某些特殊的情况下,位运算可以为我们带来一些性能提速和空间资源的节省。下面,我对在网上搜集到的一些使用场景做分析
1.快速2倍运算
利用<< >>可实现快速2倍运算,通过代码检验,位运算确实比*=更快速
2.交换数值
通过实际检验,我发现a,b=b,a比使用位运算更好
3.判断奇数和偶数
与1进行&,如果为1,那么该数为奇数;如果为0,那么该数是偶数
实际检验,发现%运算和位运算性能差不多,%更好一点
4.寻找列表中独一无二的数
假设一个列表中有n+1个数,其中n个数出现了2次,只有一个数出现了一次,要求找出这一个特别的数。
算法一:遍历列表,并为每个数统计出现次数,最后寻找只出现一次的数
算法二:将列表所有的数做异或运算,最后得出的结果就是特殊数
a1^a2^a3^…^a(2n+1),由于异或运算具有交换律和结合律
所以上述式子相当于(b1^b1)^(b2^b2)^…^(bn^bn)^C
由于相同数的异或为0,0和任意数的异或为该数,所以结果为C,即独一无二的数
#随机生成长列表
import random
from time import time
n=1000000
l=random.sample(range(0,n+1),n+1)
l+=l
index=random.sample(range(0,len(l)),1)
special=l[index[0]]
l.remove(special)
print("special num = ",special)
#special num = 56158
#算法一
start=time()
dic={}
result=None
for i in l:
if dic.get(i) is not None:
dic[i]+=1
else:
dic.update({i:1})
for key,value in dic.items():
if value==1:
result=key
print("Special num=",special,"result=",result,"time=",time()-start)
#Special num= 56158 result= 56158 time= 0.9763875007629395
#算法二
start=time()
result=0
for i in l:
result^=i
print("Special num=",special,"result=",result,"time=",time()-start)
#Special num= 56158 result= 56158 time= 0.37100648880004883
理论上分析,两种算法的时间复杂度都是O(n),但是算法二明显快了一倍;算法一的空间复杂度为O(n)(因为创建了一个长度和n成正比的字典),而算法二却只是用了数个变量,空间复杂度为O(1)
在一些算法问题上,使用位运算可以达到很好的效果
参考
http://c.biancheng.net/view/2190.html
https://blog.csdn.net/weixin_44786530/article/details/89737903