[Leetcode]数学运算技巧——python版本

本篇文章根据labuladong的算法小抄介绍有关数学运算技巧的常见算法,采用python3实现

常用的位操作

&:与,两个值相应位都为1则为1

|:或,两个值相应为有一个为1则为1

^:异或,两个值对应位相异则为1

~:非,对数据的每个二进位取反

<<:左移,对数据的每个二进位全部左移若干位,高位丢弃,低位补0

>>:右移,对数据的每个二进位全部右移若干位。

  1. 判断两个数是否异号

    x = -1
    y = 2
    f = ((x^y) < 0) #True
    
  2. 消除数字n的二进制表示中的最后一个1

    img
    n & (n - 1)
    

计算汉明权重,T191

输入一个无符号整数,返回其二进制表达式中数字为1的个数

def hammingWeight(n):
    res = 0
    while n != 0:
        n = n & (n - 1)
        res += 1
    return res

判断整数是否为2的幂次方,T231

如果一个数是2的指数,那么它的二进制一定只有一个1

def isPowerOfTwo(n):
    if n <= 0:
        return False
    return (n & (n - 1)) == 0

查找只出现一次的数字,T136

a ^ a = 0;a ^ 0 = a

对所有数字进行异或,那么成对数字变为0,落单数字还是它本身

def singleNumber(nums):
    res = 0
    for n in nums:
        res = res ^ n
    return res

阶乘

阶乘后的0

题目:输入一个非负整数n,请计算n!的结果末尾有几个0

思考:末尾有0,一定是有因子2和5,那么问题转化为n!最多可以分解出多少个因子2和5?

每个偶数都能分解出因子2,所以主要取决于能分解出几个因子5。

#写法1
def trailingZeroes(n):
    res = 0
    divisor = 5
    while divisor <= n:
        res += n // divisor
        divisor *= 5
    return res

#写法2
def trailingZeroes(n):
    res = 0
    d = n
    while d > 0:
        res += d // 5
        d = d // 5
    return res

对于125,首先125/5=25,那么有25个数可以提供1个5;125/25=5,那么有5个数可以提供2个5;125/125=1,那么有1个数可以提供3个5.

阶乘后的k个0

题目:输入一个非负整数K,请计算有多少个n可以满足n!末尾恰好有K个0

方法:使trailingZeroes(n) == k的n有多少个,n在[0,float(“inf”)]中,可以转换成二分搜索的左右边界。

class Solution:
    def preimageSizeFZF(self, k: int) -> int:

        def trailingZeroes(n):
            res = 0
            d = n
            while d > 0:
                res += d // 5
                d = d // 5
            return res
        
        def leftBound(k):
            left = 0
            right = sys.maxsize
            while left < right:
                mid = left + (right - left) // 2
                x = trailingZeroes(mid)
                if x >= k:
                    right = mid
                else:
                    left = mid + 1
            return left

        def rightBound(k):
            left = 0
            right = sys.maxsize
            while left < right:
                mid = left + (right - left) // 2
                x = trailingZeroes(mid)
                if x <= k:
                    left = mid + 1
                else:
                    right = mid
            return left - 1

        return rightBound(k) - leftBound(k) + 1

素数

素数:如果一个数只能被1和它本身整除,那么它就是素数

计数质数,T204

判断质数:

def isPrime(n):
    for i in range(2,sqrt(n)+1):
        if n % i == 0:
            return False
    return True

计数质数:

def countPrimes(n):
    isPrime = [True for i in range(n)]
    for i in range(2,int(sqrt(n))+1):
        if isPrime[i]:
            j = i * i
            while j < n:
                isPrime[j] = False
                j += i
    count = 0
    for i in range(2,n):
        if isPrime[i]:
            count += 1
    return count

时间复杂度O(n loglogn)

模幂

超级次方,T372

题目:a^b对1337取模,a为正整数,b为非常大的正整数,以数组形式给出
a [ 1 , 5 , 6 , 4 ] = a 4 ∗ a [ 1 , 5 , 6 , 0 ] = a 4 ∗ ( a [ 1 , 5 , 6 ] ) 10 a^{[1,5,6,4]}=a^4*a^{[1,5,6,0]}=a^4*(a^{[1,5,6]})^{10} a[1,5,6,4]=a4a[1,5,6,0]=a4(a[1,5,6])10
模运算技巧:

对乘法的结果求模,等价于先对每个因子都求模,然后对因子相乘的结果再取模
( a ∗ b ) % k = ( a % k ) ( b % k ) % k (a*b)\%k = (a\%k)(b\%k)\%k (ab)%k=(a%k)(b%k)%k

#递归
def superPow(a,b):
    #计算(a^k)%1337=((a%1337)(a%1337)...(a%1337))%1337
    def mypow(a,k):
        res = 1
        a = a % 1337
        for x in range(k):
            res *= a
            res %= 1337
        return res
    
    if not b:
        return 1
    last = b.pop()
    part1 = mypow(a,last)
    part2 = mypow(superPow(a,b),10)
    return (part1 * part2) % 1337

如何高效求幂

img
def mypow(a,k):
    if k == 0:
        return 1
    a %= 1337
    if k % 2 == 1:
        return (a * mypow(a,k-1)) % 1337
    else:
        sub = mypow(a,k//2)
        return (sub * sub) % 1337

缺失元素

丢失的数字,T268

题目:给定一个包含[0,n]中n个数的数组nums,找出[0,n]中没有出现在数组中的那个数

法1:数组排序,遍历一遍(时间复杂度O(NlogN))

法2:用集合储存数组里出现的数字,再遍历[0,n]去集合中查询(时间复杂度O(N),空间复杂度O(N))

法3:位运算,把所有元素和索引做异或运算,成对的数字会变成0,落单的会剩下(时间复杂度O(N),空间复杂度O(1))

def missingNumber(nums):
    res = 0 ^ len(nums)
    for i in range(len(nums)):
        res ^= i ^ nums[i]
    return res

法4:等差数列求和,sum(0,1,…,n) - sum(nums)

def missingNumber(nums):
    n = len(nums)
    expect = (0 + n) * (n + 1) // 2
    return expect - sum(nums)

但有可能整型溢出,优化:

def missingNumber(nums):
    n = len(nums)
    res = 0
    res += n
    for i in range(n):
        res += i - nums[i]
    return res

缺失和重复元素

错误的集合,T645

题目:nums数组本应是1…n的整数,但其丢失一个数字并有一个数字重复,请你找到重复出现的整数,再找到丢失的整数,将它们以数组形式返回。

法1:遍历数组,记录每个数字出现的次数;遍历1…N,看哪个元素重复出现,哪个元素没有出现

法2:将每个索引对应的元素变成负数,以表示这个索引被对应过一次了

def findErrorNums(nums):
    n = len(nums)
	for num in nums:
        index = abs(num) - 1
        if nums[index] < 0:
            dup = nums[i]
        else:
            nums[index] *= -1
    for i in range(n):
        if nums[i] > 0:
            missing = i + 1
            break
    return [dup,missing]

这种数组问题,关键点在于元素和索引是成对出现的,常用方法是排序、异或、映射。

重复元素

对数组来说,尾部插入、删除元素比较高效,O(1);开头或中间插入、删除元素,涉及数据搬移,O(N)。

原地删除有序数组的重复项,T26

要求返回修改后数组的长度

def removeDuplicates(nums):
    if len(nums) == 0:
        return 0
    slow = 0
    fast = 0
    while fast < len(nums):
        if nums[fast] != nums[slow]:
            slow += 1
            nums[slow] = nums[fast]
        fast += 1
    return slow + 1

原地删除有序链表的重复项,T83

要求返回修改后的

def deleteDuplicates(head):
    if (not head) or (not head.next):
        return head
    slow = head
    fast = head
    while fast:
        if fast.val != slow.val:
            slow.next = fast
            slow = slow.next
        fast = fast.next
    slow.next = None #注意!
    return head

移除元素,T27

原地移除所有值等于val的元素,返回新长度

def removeElement(nums,val):
    n = len(nums)
    if n == 0:
        return 0
    slow = 0
    fast = 0
    while fast < n:
        if nums[fast] != val:
            nums[slow] = nums[fast]
            slow += 1
        fast += 1
    return slow

移动零,T283

将nums所有0移到末尾

def moveZeroes(nums):
    slow = 0
    fast = 0
    while fast < len(nums):
        if nums[fast] != 0:
            nums[slow] = nums[fast]
            slow += 1
        fast += 1
    for i in range(slow,len(nums)):
        nums[i] = 0

随机抽取元素

链表随机节点,T382

链表随机选取一个节点的值:

def getRandom(head):
    p = head
    i = 0
    res = 0
    while p:
        i += 1
        #生成一个[1,i]的整数,这个整数等于1的概率就是1/i
        if random.randint(1,i) == 1:
            res = p.val
        p = p.next
    return res

链表随机选取k个节点的值:(概率为k/i)

def getRandom(head,k):
    res = [0 for i in range(k)]
    p = head
    j = 0
    #前k个元素先默认选上,0..k-1
    while p and j < k:
        res[j] = p.val
        p = p.next
        j += 1
    i = k
    while p:
        i += 1
        #生成一个[0,i-1]之间的整数,小于k的概率为k/i
        j = random.randint(0,i-1)
        if j < k:
            res[j] = p.val
        p = p.next
    return res

脑筋急转弯

Nim游戏,T292

题目:你和朋友面前有一堆石子,轮流拿,一次至少一颗,最多三颗,谁拿走最后一颗获胜。假设你们都很聪明,你先第一个拿。判断n个石子你是否能赢。

思考:如果我能赢,那么到我的时候必须有1-3颗石子 → 对手必须有4颗石子 → 我必须有5 - 7颗 → 对手必须有8颗

def canWinNim(n):
    return n % 4 != 0

石子游戏,T877

题目:有偶数堆石头,piles[i]表示第i堆有多少个。你和朋友轮流拿,一人拿一堆,但只能拿最左边或最右边的。石头拿完后,谁的石头多,谁获胜。假设你们都很聪明,你先开始,返回你是否能赢。

动态规划:

def stoneGame(piles):
    n = len(piles)
    #dp[i..j]中,先手和后手能获得的最高分数
    dp = [[[0 for i in range(n)] for j in range(n)] for k in range(2)]
    for i in range(n):
        dp[i][i][0] = piles[i]
        dp[i][i][1] = 0
    for i in range(n-2,-1,-1):
        for j in range(i+1,n):
            left  = piles[i] + dp[i+1][j][1]
            right = piles[j] + dp[i][j-1][1]
            if left > right:
                dp[i][j][0] = left
                dp[i][j][1] = dp[i+1][j][0]
            else:
                dp[i][j][0] = right
                dp[i][j][1] = dp[i][j-1][0]
    return dp[0][i-1][0] - dp[0][i-1][1]

新思路:

def stoneGame(piles):
    return True

假设有四堆石头,用1,2,3,4作为索引,可以把它们按照奇偶分为1、3堆和2、4堆,这两组石头的数量一定不同(石头总数为奇数,%2 != 0)。先拿石头的人可以控制自己拿到所有偶数堆或奇数堆。

电灯开关,T319

题目:n盏电灯,开始都是关着的。现在要进行n轮操作:

(1)把每盏电灯的开关按一下(全部打开)

(2)把每2盏电灯的开关按一下(2,4,6…关闭)

(3)把每3盏电灯的开关按一下(3,6,9,…)

……直到第n轮,只按一下第n盏灯的开关。问还有多少盏灯是亮的?

def bulbSwitch(n):
    return int(sqrt(n))

思考:如果一盏灯最后是点亮的,必须按奇数次开关。假设有6盏灯,那么第6盏等第1,2,3,6轮会被按,6=1*6=2*3。一般情况下,因子成对出现,也就是说开关被按的次数一般是偶数次但有特殊情况,比如共有16盏等,那么第16盏灯会被按5次,16=1*16=2*8=4*4。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值