比特位计数

📌 定义

给定一个非负整数 n,对于每个数字 i(0 ≤ i ≤ n),计算其二进制表示中 1 的个数,并返回一个长度为 n+1 的数组。

示例:
输入: n = 5
输出: [0,1,1,2,1,2]
解释:
0 → 000 → 0个1
1 → 001 → 1个1
2 → 010 → 1个1
3 → 011 → 2个1
4 → 100 → 1个1
5 → 101 → 2个1

核心思路

使用动态规划结合位运算,利用已计算的结果避免重复计算。

关键观察:

  • i & (i - 1) 可以将 i 的最右边的 1 变为 0
  • bits[i] = bits[i & (i - 1)] + 1

或者:

  • i >> 1 将 i 右移一位
  • bits[i] = bits[i >> 1] + (i & 1)
示例: n = 5

方法1: i & (i - 1)
bits[0] = 0 (base case)
bits[1] = bits[1 & 0] + 1 = bits[0] + 1 = 1
bits[2] = bits[2 & 1] + 1 = bits[0] + 1 = 1
bits[3] = bits[3 & 2] + 1 = bits[2] + 1 = 2
bits[4] = bits[4 & 3] + 1 = bits[0] + 1 = 1
bits[5] = bits[5 & 4] + 1 = bits[4] + 1 = 2

结果: [0, 1, 1, 2, 1, 2]

复杂度分析

方法时间复杂度空间复杂度说明
暴力法O(n log n)O(1)每个数字遍历其二进制位
DP + i&(i-1)O(n)O(n)利用已计算结果
DP + i>>1O(n)O(n)右移动态规划
查表法O(n)O(256)预计算8位表

Go 代码

Go 实现

package main
 
import "fmt"
 
// 方法1: i & (i - 1)
func countBits(n int) []int {
    bits := make([]int, n+1)
 
    for i := 1; i <= n; i++ {
        bits[i] = bits[i&(i-1)] + 1
    }
 
    return bits
}
 
// 方法2: 右移
func countBitsShift(n int) []int {
    bits := make([]int, n+1)
 
    for i := 1; i <= n; i++ {
        bits[i] = bits[i>>1] + (i & 1)
    }
 
    return bits
}
 
// 方法3: 最高有效位
func countBitsMSB(n int) []int {
    bits := make([]int, n+1)
    highBit := 0
 
    for i := 1; i <= n; i++ {
        if i&(i-1) == 0 {
            highBit = i
        }
        bits[i] = bits[i-highBit] + 1
    }
 
    return bits
}
 
// 方法4: 查表法
func countBitsTable(n int) []int {
    // 预计算0-255的比特数
    table := make([]int, 256)
    for i := 0; i < 256; i++ {
        table[i] = table[i>>1] + (i & 1)
    }
 
    bits := make([]int, n+1)
    for i := 0; i <= n; i++ {
        // 将32位整数拆分成4个8位
        bits[i] = table[i&0xFF] +
            table[(i>>8)&0xFF] +
            table[(i>>16)&0xFF] +
            table[(i>>24)&0xFF]
    }
 
    return bits
}
 
func main() {
    n := 5
    result := countBits(n)
    fmt.Printf("n = %d, 结果: %v\n", n, result)
}

思路展开

方法1详解:i & (i - 1)

i & (i - 1) 的作用:去掉 i 最右边的1

示例:
i = 5 (101)
i - 1 = 4 (100)
i & (i - 1) = 101 & 100 = 100 (4)

i = 6 (110)
i - 1 = 5 (101)
i & (i - 1) = 110 & 101 = 100 (4)

递推关系:
bits[i] = bits[i & (i - 1)] + 1
         ↑                    ↑
      去掉一个1后的结果      被去掉的那个1

计算过程(n=5):
i=0: bits[0] = 0 (初始)
i=1: bits[1] = bits[0] + 1 = 1
i=2: bits[2] = bits[0] + 1 = 1  (2&1=0)
i=3: bits[3] = bits[2] + 1 = 2  (3&2=2)
i=4: bits[4] = bits[0] + 1 = 1  (4&3=0)
i=5: bits[5] = bits[4] + 1 = 2  (5&4=4)

方法2详解:右移

观察规律:
i >> 1 是 i 除以2的整数部分
i & 1 是 i 的最低位(0或1)

递推关系:
bits[i] = bits[i >> 1] + (i & 1)
         ↑             ↑
      i除以2的比特数  i的最低位

示例:
i = 5 (101)
i >> 1 = 2 (10)
i & 1 = 1
bits[5] = bits[2] + 1 = 1 + 1 = 2

计算过程(n=5):
i=0: bits[0] = 0
i=1: bits[1] = bits[0] + 1 = 1  (1>>1=0, 1&1=1)
i=2: bits[2] = bits[1] + 0 = 1  (2>>1=1, 2&1=0)
i=3: bits[3] = bits[1] + 1 = 2  (3>>1=1, 3&1=1)
i=4: bits[4] = bits[2] + 0 = 1  (4>>1=2, 4&1=0)
i=5: bits[5] = bits[2] + 1 = 2  (5>>1=2, 5&1=1)

方法3详解:最高有效位

观察规律:
2^k 到 2^(k+1) - 1 的数字可以分解为:
数字 i = 2^k + j (其中 0 ≤ j < 2^k)
bits[i] = 1 + bits[j]

示例(n=7):
范围 [0, 1]: 最高位 = 1
  bits[1] = 1

范围 [2, 3]: 最高位 = 2
  bits[2] = bits[0] + 1 = 1
  bits[3] = bits[1] + 1 = 2

范围 [4, 7]: 最高位 = 4
  bits[4] = bits[0] + 1 = 1
  bits[5] = bits[1] + 1 = 2
  bits[6] = bits[2] + 1 = 2
  bits[7] = bits[3] + 1 = 3

经典题目

LeetCode 问题

扩展问题

  • 统计二进制字符串中1的个数
  • 二进制中连续1的最大个数

⚖️ 优缺点

优点

  • ✅ 时间高效:O(n) 线性时间
  • ✅ 空间合理:O(n) 用于存储结果
  • ✅ 避免重复计算:动态规划思想
  • ✅ 多种实现:可根据场景选择

缺点

  • ❌ 需要额外空间:存储所有结果
  • ❌ 不适合稀疏查询:如果只查询少数几个数,暴力法更好

🎨 应用场景

💡 优化技巧

💡 三种方法对比

方法递推公式优点缺点
i&(i-1)bits[i] = bits[i&(i-1)] + 1直观易懂需要理解位操作
右移bits[i] = bits[i>>1] + (i&1)简洁高效不太直观
最高位bits[i] = bits[i-highBit] + 1规律性强需要维护highBit

相关主题


返回:位运算 | 算法学习导航