python算法之位运算
本文主要介绍python中的位运算的一些技巧。
本文会配合练习题让大家更快更好的掌握python位运算的知识,同时也会不断的更新文章。
位运算的基本知识
首先,位运算分:
- 与:& 只有全为一的时候才是1
- 或:| 有1取1
- 非:~ 二进制中取反
- 异或:^ 在二进制中,相同为0,不同为1
- 左移:<<
- 右移: >>
位运算的算法技巧
找出重复的数(异或技巧)
这里我们用到了异或的运算规则,及相同为0,不同为1。
我们来看一个公式:在异或中我们可以将数用这个公式表示:
A
⨁
B
⨁
B
=
A
=
>
A
⨁
0
=
A
A\bigoplus B\bigoplus B=A => A \bigoplus 0 = A
A⨁B⨁B=A=>A⨁0=A
也就是说,自己和自己异或是0,同时,自己本身和0异或就是自身。
总结下来就是,如果存在偶数个自身相异或,那么最终,我们的结果是0.
题目
在连续的自然数当中,有一个数重复了,如何设计算法将它找到。不用辅助空间
题解
按照题目的意思,只有一个数的个数是双数。其它数都是单数。
我们可以构造一个一摸一样的连续自然数列表,该列表没个值都是唯一的,然后与原列表相加。此时,我们得到的列表就变成了:重复的数为单数,而非重复数为双数的一个列表。
这个时候,我们就可以根据异或的性质,将这个列表中的所有元素做异或运算,最终的结果就是我们要的答案。因为双数的数异或后为0,而单数的数就相当于0和自己本身异或,最终得到了答案。
公式如下(假设C为重复的):
A
⨁
A
⨁
B
⨁
B
⨁
C
⨁
C
⨁
C
⨁
.
.
.
=
>
最
终
可
以
变
成
:
C
⨁
0
=
C
A\bigoplus A\bigoplus B\bigoplus B\bigoplus C \bigoplus C\bigoplus C \bigoplus... => 最终可以变成:C \bigoplus 0=C
A⨁A⨁B⨁B⨁C⨁C⨁C⨁...=>最终可以变成:C⨁0=C
代码
# -*- coding:utf-8 -*-
"""
题目:
在连续的自然数当中,有一个数重复了,如何设计算法将它找到。不用辅助空间
"""
# 思路,可以用位运算中的异或运算。这个运算是相同为0不同为1的特点。再生成一个连续的等长数组加到后面然后异或
begin = [i for i in range(1, 11)] # 我们假设是1-10的连续自然数
# 算法开始
begin += begin # 构造了一个一模一样的连续数组
begin.append(9) # 假设我们的重复数字是9
repeat_num = begin[0] # 异或准备
for i in range(1, len(begin)):
repeat_num = repeat_num ^ begin[i] # 开始连续异或
print(repeat_num) # 输出答案
需要改变几位(异或技巧)
题目
输入两个数,告诉我们,从从一个数变到另一个数需要改变几位二进制才可以实现。
如:10 8
输出:1
题解
这个题目总结下来就是找不同,也就是对比两个数中,不同的二进制位数有几个。
由于异或的特性是相同位置不同的数异或结果为1,所以,我们可以想到,将两个数异或,然后去数结果中1的个数,这个就是答案。因为相同的都变成0了,不同的变成1留下了。
代码
def transToDouble(num): # 将数字二进制化
result = []
while True:
if num//2 == 0:
result.append(str(num % 2))
break
else:
result.append(str(num % 2))
num = num//2
result.reverse()
return ''.join(result)
a = eval(input("第一个数"))
b = eval(input("第二个数"))
print(transToDouble(a^b).count('1'))
判断2的整数次幂(与运算技巧)
题目
如何判断一个数是否为2的整数次幂,如:32是,12不是。
题解
这个其实很简单,根据二进制的性质,没进一位就是2的n-1次方,所以,我们可以去判断所给的数中是否只包含一位2进制即可。
实现思路也很简单,就是用与运算。通过n&(n-1)相与,如果仅包含1个1的话,那么,他们的结果必然是0.
代码
"""
题目:
本题目让我们写算法判断是否为2的整数次幂
"""
# 思路: 由于计算机是二进制,我们可以观察里面1的个数,如果是2的整数次幂,只能有一个1存在
while True:
num = eval(input("输入一个数"))
count_3 = 1
while True:
if (num & (num-1)) != 0: # 这句话逻辑是说,如果只有一个1存在,那么该二进制数减1和本身相与必为0。这个读者可以举个例子验证。比如32
#和31相与。
count_3 += 1 # 标志位
num = num & (num-1)
else:
break
if count_3 != 1:
print("不是")
else:
print("是")
# print(count_3)
互换奇偶位(与运算和异或运算技巧)
题目
现在给定一个数,我们希望将这个数的奇偶位互换,如何设计算法将其实现。
题解
这个思路我们是比较难以想到的。
我们需要记住这个解题技巧。
由于,我们可以先将数和十六进制表示的0xaaaaaaaa
相与,然后再将数与十六进制表示的0x55555555
相与,将两个相与的结果分别向右和向左移动一位后,最终的结果进行异或运算,就得到了我们要的结果。
代码
num = eval(input("输入一个要互换的整数"))
first = int("0xaaaaaaaa", 16)
second = int("0x55555555", 16)
a = num & first
b = num & second
print((a >> 1) ^ (b << 1))
从多个重复k次的数中找出落单的数字(无进位加法技巧)
题目
现在有一组数,里面的数除了一个落单的数之外,其它数均只出现了k次,请问如何将这个数找出来。(时间复杂度位n,空间复杂度为1)
题解
我们从十进制开始引入,我们发现,十进制中,一个数字乘以10后面会多个0,比如1乘以10就会变成10.那是由于,十进制中逢十进一,乘以10相当于加了自己十次。也就是说,十进制中,数最多加十次肯定会进1位。加十次必进一位。进位就相当于当前位置变成0
我们接着上面的来,这样就是说,如果这组数,除了落单的数,每个数字出现10次,那么,将列表里的所有数字相加,最终得到的一定是
A
∗
10
+
B
∗
10
+
C
∗
10
+
.
.
.
+
N
+
(
N
+
1
)
∗
10
A*10+B*10+C*10+...+N+(N+1)*10
A∗10+B∗10+C∗10+...+N+(N+1)∗10
由于我们是做不进位加法,所以,该式子最终变成了:
0
+
0
+
0
+
.
.
.
+
N
+
0...
=
N
0+0+0+...+N+0... = N
0+0+0+...+N+0...=N
那如果换成K次呢,那么我们将上面的结论变形成,在K进制下,数字相加必进位。所以,上面的公式变成了
A
∗
K
+
B
∗
K
+
C
∗
K
+
.
.
.
+
N
+
(
N
+
1
)
∗
K
A*K+B*K+C*K+...+N+(N+1)*K
A∗K+B∗K+C∗K+...+N+(N+1)∗K
最终还是为:
0
+
0
+
0
+
.
.
.
+
N
+
0...
=
N
0+0+0+...+N+0... = N
0+0+0+...+N+0...=N
所以,本题要将所给列表中的数转换成K进制,然后做不进位加法,最终得到答案。
代码
def trans_map(cint):
# cint: int
# return: str
# 把数字转化为相应字符,比如10-a, 11-b
if cint < 0:
return None
elif cint < 10: # 数字转为字母
return str(cint)
elif cint >= 10: # ASCII码转为字母A-Z
return chr(cint - 10 + 65)
def tenToAny(n, origin):
"""
十进制转任意进制
:param n:
:param origin:
:return:
"""
# n进制: int
# origin: int
# return: str
res = ''
while origin:
res = trans_map(origin % n) + res # 需要逆序
origin = origin // n # 一定要整除,不然除3会进入死循环
return res
def noCarryNum(first, second, k):
"""
两数的不进位加法
"""
result_list = []
if len(first) < len(second):
first, second = second, first # first一定是最长的
add_list = list(second)
add_list.reverse() # 最后一位开始加,所以要翻转
for index, i in enumerate(add_list):
result = int(i, k) + int(first[-1-index], k) # 对应位置相加
while result >= k:
result = int(i, k) + int(first[-1-index], k)-k # 对应位置相加,有进位就减掉
trans_map(result)
result_list.append(str(result))
result_remain = []
if len(first) != len(second):
result_remain = list(first[0:len(first)-len(second)])
result_list.reverse()
result_list = result_remain + result_list
return ''.join(result_list)
if __name__ == '__main__':
k = 4
a = [3, 3, 3, 2, 3, 1, 2, 2, 2]
a = [tenToAny(k, i) for i in a] # 转换成对应的进制
sums = noCarryNum(str(a[0]), str(a[1]), k) # 列表内部求和
for index, i in enumerate(a[2:]):
sums = noCarryNum(sums, str(a[index+2]), k)
print(int(sums, k))
题目进阶
找出两个落单的数
其它数出现了两次,有两个落单的数在列表中,我们如何将其找出。空间复杂度要求为1,时间复杂度为O(n)
题解
看到这道题,我们可以想到之前的异或运算,之前我们要找一个落单的数,这次要找两个。
这道题还是采用异或运算,解题方法类似,只不过加了一个分组的概念。
由于异或只能解出一个落单的数,如果有两个落单的数,那么最终结果会变成:
A
⨁
A
⨁
B
⨁
B
⨁
.
.
.
⨁
N
⨁
(
N
+
1
)
⨁
.
.
.
=
N
⨁
(
N
+
1
)
A\bigoplus A \bigoplus B \bigoplus B \bigoplus...\bigoplus N \bigoplus (N+1)\bigoplus ... = N\bigoplus (N+1)
A⨁A⨁B⨁B⨁...⨁N⨁(N+1)⨁...=N⨁(N+1)
我们发现是两个数的异或。
虽然到目前为止,我们没有真正的解出答案,但是离解出答案。但是,离解出答案已经很近了。
异或的结果中,1,一定是不同位数运算的结果。所以,我们通过上式得出的结果中,1的位置可以作为分组的依据。讲两个单独的数分到不同的组中,然后再运用异或来求出最终的两个落单的数到底是什么。
举个例子:现在有一个列表是[1,1,2,2,3,4,4,11,6,6]
通过第一个公式,我们可以得出,异或的结果是3^11
,化成二进制就是1000
也就是说,他们只有第一位是不一样的。
我们可以根据第一位来划分组,如果第一位是1,就划分为第一组,否则就为第二组。
然后分别异或,最终求出两个落单的数。
当然,如果异或结果是10111
,我随便举一个例子,那么,我们可以按照第一位,第三位等分组,分组条件自己定,但是目的是要根据这个条件,分开两个数。
代码
import random
def transToDouble(num):
result = []
while True:
if num//2 == 0:
result.append(str(num % 2))
break
else:
result.append(str(num % 2))
num = num//2
result.reverse()
return ''.join(result)
a = [2,2,1,1,4,4,69,69,77,77,5, 45, 111, 111]
if __name__ == '__main__':
double_list = []
begin = a[0]
for i in range(1, len(a)):
begin = begin ^ a[i]
for index, i in enumerate(a):
a[index] = transToDouble(i)
max_len = 0
for i in a:
if max_len < len(i):
max_len = len(i)
print(max_len)
result = ('{:0>%s}'%max_len).format(transToDouble(begin)) # 让所有二进制长度相等
for index, i in enumerate(a):
if len(i)<max_len:
tmp = '{:0>%s}'%max_len
a[index] = tmp.format(i) # 让所有二进制长度相等
first = 0
second = 0
for i in a: # 由于空间复杂度要求为1,所以分组异或,不采用新的辅助空间
if i.index('1') == result.index('1'):
first = first^int(i,2)
else:
second = second^int(i, 2)
print(first, second)
真题训练
进制数也可以用来解关于数的组合
奇怪的捐赠
地产大亨Q先生临终的遗愿是:拿出100万元给X社区的居民抽奖,以稍慰藉心中愧疚。
麻烦的是,他有个很奇怪的要求:
-
100万元必须被正好分成若干份(不能剩余)。
每份必须是7的若干次方元。
比如:1元, 7元,49元,343元,… -
相同金额的份数不能超过5份。
-
在满足上述要求的情况下,分成的份数越多越好!
请你帮忙计算一下,最多可以分为多少份?
题解
我们假设有123456790元,按照十进制来讲,可以化成9*10+7*100+…+10**8
所以,这题说要化成7的次方组合,我们可以想到用7进制来做。第三个条件是唬人的,其实只有一种情况。
代码
# 数的进制话可以表示一个数的组成
a = 1000000
def transToSeven(num):
result = []
while True:
if num//7 == 0:
result.append(str(num % 7))
break
else:
result.append(str(num % 7))
num = num//7
result.reverse()
return ''.join(result)
if __name__ == '__main__':
count = 0
print(transToDouble(a)) # 输出的求和
while(a>0):
c = a%7
a = a // 7
count += c
print(count)
代码中的两种都可以,读者自行采纳。