python算法之位运算

python算法之位运算

本文主要介绍python中的位运算的一些技巧。

本文会配合练习题让大家更快更好的掌握python位运算的知识,同时也会不断的更新文章。

位运算的基本知识

首先,位运算分:

  1. 与:& 只有全为一的时候才是1
  2. 或:| 有1取1
  3. 非:~ 二进制中取反
  4. 异或:^ 在二进制中,相同为0,不同为1
  5. 左移:<<
  6. 右移: >>

位运算的算法技巧

找出重复的数(异或技巧)

这里我们用到了异或的运算规则,及相同为0,不同为1。

我们来看一个公式:在异或中我们可以将数用这个公式表示:
A ⨁ B ⨁ B = A = > A ⨁ 0 = A A\bigoplus B\bigoplus B=A => A \bigoplus 0 = A ABB=A=>A0=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 AABBCCC...=>C0=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 A10+B10+C10+...+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 AK+BK+CK+...+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) AABB...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社区的居民抽奖,以稍慰藉心中愧疚。
麻烦的是,他有个很奇怪的要求:

  1. 100万元必须被正好分成若干份(不能剩余)。
    每份必须是7的若干次方元。
    比如:1元, 7元,49元,343元,…

  2. 相同金额的份数不能超过5份。

  3. 在满足上述要求的情况下,分成的份数越多越好!

请你帮忙计算一下,最多可以分为多少份?

题解

我们假设有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)

代码中的两种都可以,读者自行采纳。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值