统计二进制数中 1 的个数

这个任务常常出现在哈希函数、数据压缩、权限管理等场景中

代码示例

一、逐位判断

int bitCount(unsigned int n) {
    unsigned int c = 0;
    int i;
    for (i = 0; i < sizeof(n) * 8; ++i) {
        c += (n >> i) & 1;  // 右移 i 位并与 1 做与操作,检查当前位是否为 1
    }
    return c;
}

解释

n >> i:将 n 右移 i 位,将目标位移到最低位

& 1:通过与操作判断当前最低位是否为 1

c += (n >> i) & 1;:如果当前位是 1,就加 1 到计数器 c

举例

n = 13,逐位判断的过程

  • i = 0: (13 >> 0) & 1 = 1101 & 0001 = 1 → 1 被加到 c
  • i = 1: (13 >> 1) & 1 = 1101 >> 1 = 0110 & 0001 = 0 → 0 被加到 c
  • i = 2: (13 >> 2) & 1 = 1101 >> 2 = 0011 & 0001 = 1 → 1 被加到 c
  • i = 3: (13 >> 3) & 1 = 1101 >> 3 = 0001 & 0001 = 1 → 1 被加到 c
  • i = 4 to 31: 由于这些位都是 0,因此每次 & 1 的结果都为 0

最终 c = 3,表示数字 13 中有 3 个 1

二、n &= (n - 1)

int bitCount(unsigned int n) {
    unsigned int c;
    for (c = 0; n; ++c) {
        n &= (n - 1); // 清除最低位的1
    }
    return c;
}
  • 不断清除 n 的二进制表示中最右边的 1,同时累加计数器,直至 n 为 0
  • 比起逐位判断(例如 n >> i & 1),这种写法更高效

举例

n = 13,二进制表示为 1101,可以通过这个过程逐步清除最低位的 1

  • 第一次,n = 1101n - 1 = 1100n &= (n - 1) 结果是 1100,清除了最右边的 1
  • 第二次,n = 1100n - 1 = 1011n &= (n - 1) 结果是 1000
  • 第三次,n = 1000n - 1 = 0111n &= (n - 1) 结果是 0000

最终,n 变为 0,共清除 3 次,表示数字 13 中有 3 个 1

运算次数与输入 n 的大小无关,只与 n 中 1 的个数有关

这是位操作中非常高效的技巧之一,用于 计数、布隆过滤器、掩码处理 等多种场景

三、GCC __builtin_popcount

通常编译器会优化成高效的机器指令(如 POPCNT

#include <stdio.h>

int main() {
    unsigned int x =13;
    printf("Number of 1 in %u (binary: %08b): %d\n", x,x, __builtin_popcount(x));
    return 0;
}

GCC 还提供了用于不同整数类型的版本:

函数名 类型
__builtin_popcount(x) unsigned int
__builtin_popcountl(x) unsigned long
__builtin_popcountll(x) unsigned long long

性能比较

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 手动实现 popcount
int popcount_manual(unsigned int n) {
    int count = 0;
    while (n) {
        n &= (n - 1);  // 清除最低位的 1
        count++;
    }
    return count;
}

int main() {
    unsigned int number = 0b1101010110110101; // 16 位数(含 10 个 1)

    // 比较性能的循环次数
    int iterations = 10000000;

    // 测量 __builtin_popcount 的时间
    clock_t start = clock();
    for (int i = 0; i < iterations; i++) {
        __builtin_popcount(number);
    }
    clock_t end = clock();
    double builtin_time = ((double)(end - start)) / CLOCKS_PER_SEC;

    // 测量手动实现的时间
    start = clock();
    for (int i = 0; i < iterations; i++) {
        popcount_manual(number);
    }
    end = clock();
    double manual_time = ((double)(end - start)) / CLOCKS_PER_SEC;

    // 输出时间对比结果
    printf("Time taken by __builtin_popcount: %.6f seconds\n", builtin_time);
    printf("Time taken by popcount_manual: %.6f seconds\n", manual_time);

    return 0;
}
Time taken by __builtin_popcount: 0.003116 seconds
Time taken by popcount_manual: 0.049420 seconds

位操作其他技巧

一、1 的相关技巧(统计、掩码)

技巧 说明 示例或表达式
清除最低位的 1 n & (n - 1):把最右边的 1 变成 0 n = 0b10100 → n & (n - 1) = 0b10000
只保留最低位的 1 n & -nn & (~n + 1) 提取最右边的 1
判断是否是 2 的幂 n > 0 && (n & (n - 1)) == 0 只有一个 1 的数字
统计 1 的个数(位计数) __builtin_popcount(n)(GCC)或用循环清 1
找最高位的 1 的位置 31 - __builtin_clz(n)(GCC) CLZ = Count Leading Zeros
找最低位的 1 的位置 __builtin_ctz(n)(GCC) CTZ = Count Trailing Zeros

二、加减乘除相关(通常用于优化)

技巧 等价于 示例
乘以 2 的幂 x << n x * 2^n
除以 2 的幂(向下取整) x >> n x / 2^n
模 2 的幂(取低位) x & (2^n - 1) x % 2^n
交换两个数不用中间变量 x ^= y; y ^= x; x ^= y; 适用于整数

三、掩码与位操作常见应用

功能 表达式或方法 说明
设置第 k 位为 1 n | (1 << k) 将一个 1 移动到目标位 k,然后与原数进行按位或
清除第 k 位为 0 n &= ~(1 << k)
翻转第 k 位 n ^= (1 << k)
测试第 k 位是否为 1 (n >> k) & 1(n & (1 << k)) != 0
只保留前 n 位 n &= (1 << k) - 1 清除高位

四、奇偶性判断

表达式 说明
x & 1 判断奇偶:1 是奇数,0 是偶数
x ^ 1 切换奇偶:偶数变奇数,奇数变偶数

五、位图法(Bitset/Bitmap)

当你要记录大规模的布尔标志(如是否出现过),可以用位图:

unsigned int bitmap[1000]; // 每个 int 存 32 个布尔值

// 设置第 k 个元素为 1
bitmap[k / 32] |= (1 << (k % 32));

// 查询是否为 1
bool exist = bitmap[k / 32] & (1 << (k % 32));

bool array[32000] 少用很多内存!

六、快速幂与其他优化技巧

  • 幂优化:用快速幂算法实现 a^b mod m,经常用在加密、哈希等领域
  • 反码 / 补码:理解 ~x + 1-x,用于实现取负数
  • 整除奇偶优化:比如判断是否是 4 的倍数 x & 3 == 0

📌 小结:位操作常见应用场景

  • 快速统计(如计数、权限组合)
  • 高效布尔数组(如素数筛、哈希)
  • 数据压缩、空间优化
  • 状态压缩(如动态规划中的状态编码)
  • 字符串哈希 / rolling hash
  • 多线程锁结构(比如原子变量标记)

其他场景应用(如图像处理、操作系统内核、密码学、编译器等)