
名称:Doubletful的博客 💠算法专栏 签名:路漫漫其修远兮,吾将上下而求索
文章目录
- 一、二进制计算简介
- 二、题目
-
- [解法一:>> 操作符遍历统计](#解法一:>> 操作符遍历统计)
- [解法二:Brian Kernighan 算法遍历统计](#解法二:Brian Kernighan 算法遍历统计)
- 解法三:掩码分治统计
- 三、总结
- 四、练习
前言
在数字世界的底层,万物皆为二进制,当我们比较两个整数时,一个有趣的度量方式不是看它们数值差多少,而是看它们在二进制形态下"长得有多不像",这种度量在信息论中被称为汉明距离。
文章概述:
- 本文将使用 c 语言通解 LeetCode 题目:461. 汉明距离。
- 将提供三种逐步优化的解法使时间复杂度缩短至 O(1)。
- 提供基础位运算简介以便初学者阅读,可跳过。
一、二进制计算简介
1.二进制操作符
1.1.位操作符
🔹按位与 &:按照对应的二进制位进行与运算,同时为1才为1,
二进制例:1100 & 0011 == 0000,1010 & 1000 == 1000。
🔸按位或 |:按照对应的二进制位进行或运算,同时为0才为0,
二进制例:1100 | 0011 == 1111,1010 | 1000 == 1010。
🔹按位异或 ^:按照对应的二进制位进行异或运算,相同为0,相异为1,
二进制例:1100 ^ 0011 == 1111,1010 ^ 1000 == 0010。
🔸按位取反 ~:按照对应的二进制位进行取反运算,将0和1互换,
二进制例:~1100 == 0011,~1010 == 0101,~1000 == 0111。
1.2.移位操作符
🔹左移操作符 <<:
语法为:被移动整数 << 移动位数,规则是左边丢弃,右边填充0。
二进制例:00011000 << 1 == 00110000。
🔸右移操作符 >>:
语法为:被移动整数 >> 移动位数,规则分为两种情况,逻辑右移和算术右移,大部分编译器采用算术右移。逻辑右移:左边填充0,右边丢弃。算术右移:左边用该值的符号位填充,右边丢弃。
二进制例:00011000 >> 1 == 00001100。
注:位操作符和移位操作符都只能对整数进行操作。
2.进制转换
⇒二进制转八进制:
从二进制的低位开始每3个二进制位转换为一个八进制位,剩余直接转化。
例:二进制为 10111,八进制为 27,十进制为 23。
⇒二进制转十六进制:
从二进制的低位开始每4个二进制位转换为一个十六进制位,剩余直接转化。
例:二进制为 1010011,十六进制为 53,十进制为 83。
接下来,是正题部分:
二、题目
先看题目:

我们被要求计算两个 32 位整数的汉明距离,即 x 与 y 不同二进制位的个数。
解法一:>> 操作符遍历统计



首先计算 x ^= y。异或运算的本质是"相同为0,不同为1",因此新 x 的二进制表示中有多少个 1,就代表原数有多少个不同的位。利用 >> 操作符遍历 x 的每一位并使用 & 操作符判断 x 的每一位是否为 1 记录不同位的个数。
解法二:Brian Kernighan 算法遍历统计

有一个极其巧妙的位运算性质:n & (n - 1) 的结果会将 n 的二进制中最右边的 1 变为 0,图例如下:

设有一个二进制数为 10000,当这个数减 1 时结果是 01111,10000 & 01111 正好等于 0,因为每次减 1 都会使二进制数上最右边的 1 退位,使在该数位前的全 0 位变为全 1 位并将该数位变为 0,& 操作正好就将减 1 前的二进制数与减 1 后的二进制数有 1 的位全部对应有 0 的位,抹除最右边的 1。
解法三:掩码分治统计
这是本篇文章的重头戏,也是计算机底层架构(如 CPU 指令 POPCNT)实现统计 1 个数的数学原理。它不再依赖循环,而是通过二分法将位数累加。

初看代码会非常奇怪,这些十六进制数代表什么意思,为什么 x 要 & 这些数后又将 x 右移并再次 & 它们,为什么这样就能计算新 x 中为 1 的二进制位个数了------我们逐步拆解,先看这些十六进制数代表什么:
一个十六进制位等于四个二进制位,将这些十六进制数转换为二进制后:
- 0x55555555 = 0101 0101 0101 0101 0101 0101 0101 0101
- 0x33333333 = 0011 0011 0011 0011 0011 0011 0011 0011
- 0x0f0f0f0f = 0000 1111 0000 1111 0000 1111 0000 1111
- 0x00ff00ff = 0000 0000 1111 1111 0000 0000 1111 1111
- 0x0000ffff = 0000 0000 0000 0000 1111 1111 1111 1111
可以观察到有一定规律,它们的二进制特征分别是:
- 0x55555555(单个位交替,用于隔离相邻的奇偶位)
- 0x33333333(两位交替,用于隔离相邻的 4 位中的低 2 位)
- 0x0F0F0F0F(四位交替,用于隔离相邻的 8 位中的低 4 位)
- 0x00FF00FF(八位交替,用于隔离相邻的 16 位中的低 8 位)
- 0x0000FFFF (十六位交替,用于隔离高 16 位和低 16 位)
这些位中的 0 都用于隔离,1 都用于保留计算,0x55555555 的作用有些特殊,它先以每 1 位统计 x 中 1 的个数并将个数存储进对应的两位中,也就是先转换,将代表 x 与 y 在哪一位不同的 1 转换为以每两位分隔的实际不同位的数量,注意该意义转换目前只在每两位中生效 。
而除 0x55555555 以外的值都在做被分隔的实际不同位数量的合并操作,最终合并为 32 位表示的实际不同位数量,也就是正确的十进制表示。

& 运算、>> 运算与加法解读:
在理解了这些数字在代码中的作用后,再看 x 分别直接进行 & 运算和 x 先右移再进行 & 运算就非常通俗易懂了,如下所示:
❇️2 位一组:将 x 分为每 2 位一组,计算每组中 1 的个数。
x & 0x55555555 取出奇数位的 1,右移后取出偶数位的 1,二者相加,结果以二进制形式保存在原来的 2 位空间中。
❇️4 位一组:将上一步的结果每 4 位一组,计算每组中 1 的个数。
x & 0x33333333 先取出前两位的个数,右移后再取出后两位的个数,最后相加,结果以二进制形式保存在原来的 4 位空间中。
........................
❇️归并到 32 位:最终得到整个 32 位整数中 1 的总数。
三、总结
通过这道经典的汉明距离题目,我们清晰地看到了算法演进的三个层次:
| 解法 | 核心思想 | 优点 | 缺点 |
|---|---|---|---|
| 解法一 | 逐位右移检查 | 直观易懂,符合第一反应 | 固定循环 32 次,存在冗余操作 |
| 解法二 | 清除最低位的 1 | 循环次数仅与 1 的个数有关,实战效率高 | 依然依赖循环,无法利用现代 CPU 并行特性 |
| 解法三 | 二进制并行加法,树状归并 | 无分支、无循环,性能极致,体现了位运算的数学之美 | 可读性较差,需要深刻理解二进制分治原理 |
四、练习
推荐手动在LeetCode网站上实现三种解法:
- 汉明距离 && 链接:link.
⚛️EL PSY CONGROO,十分感谢你的阅读
本期不确定:
是否应该添加掩码分治统计执行图例