【位运算】力扣338:比特位计数

给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。
示例:

输入:n = 2
输出:[0,1,1]
解释:
0 --> 0
1 --> 1
2 --> 10

  1. 暴力:遍历+统计
    部分编程语言有相应的内置函数用于计算给定的整数的二进制表示中的 1 的数目。
class Solution:
    def countBits(self, n: int) -> List[int]:
        res = []
        for i in range(n + 1):
            res.append(bin(i).count('1'))
        return res

一行代码

class Solution:
    def countBits(self, n: int) -> List[int]:
        return  [bin(i).count('1') for i in range(n+1)]

作者:Jam007
链接:https://leetcode.cn/problems/counting-bits/solution/yi-xing-dai-ma-si-lu-jian-dan-xing-neng-sctro/

时间复杂度: O(N * sizeof(int))
空间复杂度:O(1),返回数组不计入空间复杂度中

  1. Brian Kernighan 算法
    最直观的做法是对从 0 到 n 的每个整数直接计算「一比特数」。每个 int 型的数都可以用 32 位二进制数表示,只要遍历其二进制表示的每一位即可得到 1 的数目。
    利用 Brian Kernighan 算法,可以在一定程度上进一步提升计算速度。Brian Kernighan 算法的原理是:对于任意整数 x,令 x = x & (x−1),该运算将 x 的二进制表示的最后一个 1 变成 0。因此,对 x 重复该操作,直到 x 变成 0,则操作次数即为 x 的「一比特数」。
class Solution:
    def countBits(self, n: int) -> List[int]:
        def countOnes(x: int) -> int:
            ones = 0
            while x > 0:
                x &= (x - 1)
                ones += 1
            return ones

        bits = [countOnes(i) for i in range(n + 1)] # 数组哦
        return bits

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/counting-bits/solution/bi-te-wei-ji-shu-by-leetcode-solution-0t1i/

时间复杂度:O(nlogn)。需要对从 0 到 n 的每个整数使用计算「一比特数」,对于每个整数计算「一比特数」的时间都不会超过 O(logn)。
空间复杂度:O(1)。除了返回的数组以外,空间复杂度为常数。

直接从结果分析
把第 i 个数分成两种情况:

  • i 是偶数,那么它的二进制 1 的位数与 i/2 的二进制 1 的位数相等;因为偶数的二进制末尾是 0,右移一位等于 i/2,而二进制中 1 的个数没有变化。
  • i 是奇数,那么【i 的二进制 1 的位数 = i - 1 的二进制位数 + 1】;因为奇数的二进制末尾是 1,如果把末尾的 1 去掉就等于 i - 1。又 i - 1 是偶数,所以奇数 i 的二进制 1 的个数等于 i/2 中二进制 1 的位数 +1。

因此可以从递归方面考虑,从而想到记忆化搜索,从而动态规划。

  1. 递归
class Solution(object):
    def countBits(self, n: int) -> List[int]:
        res = []
        for i in range(n + 1):
            res.append(self.count(i))
        return res

    def count(self, n):
        if n == 0:
            return 0
        if n % 2 == 1: # 奇数
            return self.count(n - 1) + 1
        return self.count(n // 2)

作者:fuxuemingzhu
链接:https://leetcode.cn/problems/counting-bits/solution/yi-bu-bu-fen-xi-tui-dao-chu-dong-tai-gui-3yog/

时间复杂度: O(N ^ 2),因为遍历了一次,每次求解最多需要递归 N/2次。
空间复杂度:O(N),递归需要调用系统栈,栈的大小最多为 N/2。

  1. 记忆化搜索
    在递归解法中,其实有很多重复的计算,比如当 i = 8 的时候,需要求 i = 4, 2, 1, 0 的情况,而这些取值已经计算过了,此时可以使用记忆化搜索。
    所谓记忆化搜索,就是在每次递归函数结束的时候,把计算结果保存起来。这样的话,如果下次递归的时候遇到了同样的输入,则直接从保存的结果中直接查询并返回,不用再次递归
    举个例子,比如 i = 8 的时候,需要求 i = 4 的情况,而 i = 4 的情况在之前已经计算过了,因此直接返回 memo[4] 即可。
class Solution(object):
    def countBits(self, n: int) -> List[int]:
        self.memo = [0] * (n + 1)
        res = []
        for i in range(n + 1):
            res.append(self.count(i))
        return res

    def count(self, n):
        if n == 0:
            return 0
        if self.memo[n] != 0:
            return self.memo[n]
        if n % 2 == 1:
            res = self.count(n - 1) + 1
        else:
            res = self.count(n // 2)
        self.memo[n] = res # 能看出此题可以不用 memo 数组,直接利用 res 保存结果
        return res

作者:fuxuemingzhu
链接:https://leetcode.cn/problems/counting-bits/solution/yi-bu-bu-fen-xi-tui-dao-chu-dong-tai-gui-3yog/

时间复杂度:O(N),因为遍历了一次,每次求解都可以从之前的记忆化结果中找到。
空间复杂度:O(N),用到了辅助的空间保存结果,空间的结果是 O(N)。

  1. 动态规划
    很多时候,动态规划的方法都是从记忆化搜索中优化出来的。本题也可以如此。
    在记忆化搜索过程中,我们看到其实每次调用递归函数的时候,递归函数只会运行一次,就被 memo 捕获并返回了。那么其实可以去除递归函数,直接从 res 数组中查结果。
    此外,运用位运算的思想优化:
  • 对于第 i 个数字,如果它二进制的最后一位(最低位)为 1,那么它含有 1 的个数则为 dp[i-1] + 1;
  • 如果它二进制的最后一位(最低位)为 0,那么它含有 1 的个数和其算术右移结果相同,即 dp[i>>1]。
class Solution:
    def countBits(self, n: int) -> List[int]:
        dp = [0] * (n + 1)
        for i in range(1, n + 1):
            if i & 1: # 最低位 i & 1 == 1
                dp[i] = dp[i - 1] + 1
            else:
                dp[i] = dp[i >> 1]
        return dp

简单一点就是:
i 的二进制中 1 的个数 = (i 右移一位后 1 的个数) + (i 的最低位)

class Solution:
    def countBits(self, n: int) -> List[int]:
        dp = [0] * (n + 1)
        for i in range(1, n + 1):
            dp[i] = dp[i >> 1] + (i & 1)
        return dp

时间复杂度:O(N),因为遍历了一次。
空间复杂度:O(1),返回结果占用的空间不计入空间复杂度中。

  1. 规律技巧型
    列出0-8的二进制数:
    0 0
    1 1
    2 10
    3 11
    4 100
    5 101
    6 110
    7 111
    8 1000
    观察可以发现一个规律:例如3和7,7比3大4,体现在二进制中,111就是在11的首位添上1。自然1的个数也要加一。
    那这个规律就是,任何一个大于等于1的数字n,它的二进制数中一的个数实际就是之前某个数n-k的一的个数+1。
    k很显然就是不大于n的2的整数次幂中的最大值。
    在进行遍历的时候只需要额外维护一个k值,每当n>=k,k更新为k*2。
class Solution:
    def countBits(self, num: int) -> List[int]:
        dp = [0] * (num + 1)
        k = 1
        for i in range(1,num+1):
            if i >= k * 2:
                k = k * 2
            dp[i] = dp[i-k]+1
        return dp

作者:eager-6oldstine
链接:https://leetcode.cn/problems/counting-bits/solution/ling-lei-de-dong-tai-gui-hua-si-lu-by-ea-tm64/
posted @ 2022-05-15 20:11  Vonos  阅读(122)  评论(0)    收藏  举报