在编程世界里,我们日常操作的整数、布尔值,在计算机底层最终都会被拆解成一串由 0 和 1 组成的二进制序列——这就是“位”(bit),计算机最基本的运算单元。位运算,就是直接对这些二进制位进行操作的运算方式,它跳过了高层的数值转换,直接与硬件底层交互,因此具备极高的执行效率和空间优化能力。
很多开发者觉得位运算晦涩难懂、实用性不强,实则不然。它广泛应用于底层开发、算法优化、嵌入式编程、数据压缩、权限控制等场景:LeetCode 上的高频算法题(如找只出现一次的数)用位运算可实现最优解,嵌入式开发中用位运算操作寄存器,甚至我们常用的权限系统(读/写/执行)也能通过位运算高效实现。
今天,我们从基础到进阶,从原理到实战,彻底吃透位运算,让这个“底层神器”成为你的编程加分项。
一、前置基础:读懂二进制与位运算的核心意义
在学习位运算前,我们先明确两个核心前提,避免后续理解偏差:
1.1 二进制与十进制的快速关联
计算机中,整数以二进制补码形式存储(正数补码=原码,负数补码=反码+1),我们无需深入补码的底层实现,但要记住一个关键逻辑:每一位二进制数的权重都是 2 的幂次。
示例:十进制 5 的二进制是 101,对应权重计算为:1×2² + 0×2¹ + 1×2⁰ = 4 + 0 + 1 = 5。
后续所有位运算,本质都是对这些“权重位”的操作——要么翻转,要么移位,要么判断。
1.2 位运算的核心优势
为什么要用位运算?相比常规算术运算(如加减乘除),它有三个不可替代的优势:
- 性能极致:位运算直接操作 CPU 逻辑门,仅需 1 个 CPU 时钟周期,比算术运算(如 n%2 判断奇偶)更快。
- 空间优化:可通过一个整数存储多个布尔状态(如用 32 位整数存储 32 个开关状态),大幅节省内存。
- 场景专属:很多底层场景(如寄存器操作)只能通过位运算实现,无替代方案。
提示:位运算的可读性较差,实际开发中建议添加注释,平衡效率与可维护性。
二、核心解析:六大基础位运算(必掌握)
位运算的核心是 6 个基础操作:按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移(<<)、右移(>>)。我们逐个拆解,结合“二进制示例+核心特性+应用场景+代码示例”,确保一看就懂、一用就会。
2.1 按位与(&):“同时为 1 才放行”
逻辑规则:两个二进制位同时为 1 时,结果为 1;否则为 0(可类比为“两道门都打开,才能进入仓库”)。
二进制示例:12(1100) & 10(1010) = 8(1000)
核心特性:
- 任何数 & 0 = 0(清零作用);
- 任何数 & 自身 = 自身(无变化);
- 任何数 & 1 = 该数的二进制最低位(判断奇偶的核心)。
高频应用场景:
- 判断奇偶数(比 n%2 更高效):n & 1 == 1 → 奇数;n & 1 == 0 → 偶数。
- 提取二进制特定位(掩码操作):如提取 IP 地址的子网掩码,或提取一个数的低 4 位(n & 0xF)。
- 清零特定位:将第 k 位(从 0 开始计数)清零,公式:n & ~(1 << k)。
代码示例:
#include <stdio.h>
// 1. 判断奇偶数(比 n%2 更高效)
int is_odd(int n) {
return n & 1 == 1; // 1为奇数,0为偶数
}
// 2. 提取低4位(等价于 n % 16)
int get_low4(int n) {
return n & 0xF; // 0xF 二进制是 1111,仅保留低4位
}
int main() {
printf("5是否为奇数:%d(1=是,0=否)\n", is_odd(5)); // 输出1(5二进制101,101&1=1)
printf("18的低4位:%d(对应二进制0010)\n", get_low4(18)); // 输出2(18二进制10010,10010&1111=0010)
return 0;
}
2.2 按位或(|):“有一个为 1 就放行”
逻辑规则:两个二进制位中至少有一个为 1 时,结果为 1;否则为 0(可类比为“应急通道,只要有一扇门打开就能进入”)。
二进制示例:12(1100) | 10(1010) = 14(1110)
核心特性:
- 任何数 | 0 = 自身(无变化);
- 任何数 | 1 = 该位变为 1(置 1 作用);
- 任何数 | 自身 = 自身(无变化)。
高频应用场景:
- 设置二进制特定位:将第 k 位设为 1,公式:n | (1 << k)。
- 合并标志位(权限控制):如 READ=1(0001)、WRITE=2(0010),同时拥有读写权限则为 READ | WRITE = 3(0011)。
代码示例:
#include <stdio.h>
// 1. 将第k位(从0开始)设为1
int set_bit(int n, int k) {
return n | (1 << k);
}
int main() {
// 2. 合并权限(位掩码应用)
const int READ = 1 << 0; // 0001(读权限)
const int WRITE = 1 << 1; // 0010(写权限)
const int EXECUTE = 1 << 2; // 0100(执行权限)
int user_perm = READ | WRITE; // 0011,拥有读写权限
printf("将5(二进制101)的第2位设为1:%d(二进制1001)\n", set_bit(5, 2));
printf("读写权限组合结果:%d(二进制0011)\n", user_perm);
return 0;
}
2.3 按位异或(^):“不同为 1,相同为 0”
逻辑规则:两个二进制位不同时,结果为 1;相同时为 0(最具“程序员特性”的操作,可类比为“找不同”)。
二进制示例:12(1100) ^ 10(1010) = 6(0110)
核心特性(重中之重,面试高频) :
- 交换律:a ^ b = b ^ a;
- 结合律:a ^ (b ^ c) = (a ^ b) ^ c;
- 自反性:a ^ a = 0(两个相同的数异或,结果为 0);
- 零特性:a ^ 0 = a(任何数异或 0,结果为自身)。
高频应用场景:
- 交换两个变量(无需临时变量,面试常考):a ^= b; b ^= a; a ^= b;
- 找只出现一次的数(LeetCode 136 题):数组中所有数异或,出现偶数次的数会抵消(a^a=0),最终结果就是只出现一次的数。
- 数据加密/校验和计算:通过异或一个密钥实现简单加密,解密时再次异或该密钥即可(a^key ^key = a)。
代码示例:
#include <stdio.h>
// 1. 交换两个变量(无需临时变量,面试常考)
void swap(int *a, int *b) {
if (a != b) { // 避免同一地址清零
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
}
// 2. 找只出现一次的数(LeetCode 136 题)
int single_number(int nums[], int size) {
int res = 0;
for (int i = 0; i < size; i++) {
res ^= nums[i];
}
return res;
}
int main() {
// 测试交换变量
int a = 5, b = 8;
swap(&a, &b);
printf("交换后:a=%d, b=%d\n", a, b); // 输出8 5
// 测试找只出现一次的数
int nums1[] = {2, 2, 1};
int nums2[] = {4, 1, 2, 1, 2};
printf("只出现一次的数(nums1):%d\n", single_number(nums1, 3)); // 输出1
printf("只出现一次的数(nums2):%d\n", single_number(nums2, 5)); // 输出4
return 0;
}
2.4 按位取反(~):“翻转所有位”
逻辑规则:将二进制的每一位都翻转(0→1,1→0),包括符号位(这是最容易踩坑的点)。
关键注意:不同语言的取反表现不同,核心原因是整数的存储位数不同:
- Java:int 是 32 位有符号整数,取反后会改变符号位,示例:~10 = -11(10 二进制 000...1010,取反后 111...0101,对应补码 -11)。
- Python:整数是无限位补码模型,取反结果遵循公式:~x = -(x + 1),示例:
10 = -11,-5 = 4。
高频应用场景:
- 生成掩码:如 ~0 是全 1 二进制(Python 中是无限位,但实际使用时会结合移位限制位数)。
- 数值取反(无分支优化):通过 ~x + 1 可得到 x 的负数(x 为正数时)。
代码示例:
#include <stdio.h>
// 辅助函数:打印二进制(简化版,仅展示低16位)
void print_binary(int n) {
for (int i = 15; i >= 0; i--) {
printf("%d", (n & (1 << i)) ? 1 : 0);
}
printf("\n");
}
int main() {
// 按位取反特性验证(C语言int通常为32位有符号整数)
printf("~10 = %d\n", ~10); // 输出-11(遵循 ~x = -(x+1))
printf("~-5 = %d\n", ~-5); // 输出4(-( -5 + 1 ) = 4)
// 生成低4位全1的掩码(等价于 0xF)
int mask = ~(~0 << 4);
printf("低4位全1掩码(十进制):%d\n", mask); // 输出15
printf("低4位全1掩码(二进制):");
print_binary(mask); // 输出0000000000001111
return 0;
}
2.5 左移(<<):“整体左移,低位补 0”
逻辑规则:将二进制的所有位向左移动 n 位,右边补 0(可类比为“整排箱子往左挪,右边补空箱”)。
二进制示例:10(1010) << 2 = 40(101000)
核心特性:
- 数学意义:a << n = a × 2ⁿ(比乘法运算更快,底层优化常用);
- 注意:左移可能导致数值溢出(但 Python 中整数无限位,不会溢出,只会变大)。
高频应用场景:
- 快速乘法(2 的幂次):如 n × 8 = n << 3(比 n*8 更高效)。
- 生成指定位的 1:如 1 << k 表示第 k 位为 1,其余为 0(用于掩码、位设置)。
2.6 右移(>>):“整体右移,高位补符号位”
逻辑规则:将二进制的所有位向右移动 n 位,左边补符号位(正数补 0,负数补 1),称为“算术右移”。
二进制示例:10(1010) >> 2 = 2(0010);-10(补码 111...0110) >> 2 = -3(111...1101)
核心特性:
- 数学意义:a >> n = a // 2ⁿ(向下取整,比除法运算更快);
- 语言差异:Java 有两个右移(>> 算术右移、>>> 逻辑右移,高位补 0),Python 只有算术右移(无 >>>)。
高频应用场景:
- 快速除法(2 的幂次):如 n // 16 = n >> 4。
- 提取高位:如将一个 8 位二进制数的高 4 位提取,可右移 4 位(n >> 4)。
代码示例:
#include <stdio.h>
// 辅助函数:打印二进制(简化版,仅展示低16位)
void print_binary(int n) {
for (int i = 15; i >= 0; i--) {
printf("%d", (n & (1 << i)) ? 1 : 0);
}
printf("\n");
}
int main() {
// 左移:快速乘法(2的幂次)
printf("10 << 3 = %d\n", 10 << 3); // 输出80(10 × 2³ = 80)
// 右移:快速除法(向下取整)
printf("10 >> 2 = %d\n", 10 >> 2); // 输出2(10 // 4 = 2)
printf("-10 >> 2 = %d\n", -10 >> 2); // 输出-3(-10 // 4 = -3,向下取整)
// 提取高4位(假设n是8位整数,用unsigned char确保无符号)
unsigned char n = 0xD3; // 0xD3 二进制是 11010011(十进制211)
unsigned char high4 = n >> 4;
printf("8位整数0xD3的高4位(十进制):%d\n", high4); // 输出13(0xD)
printf("8位整数0xD3的高4位(二进制):");
print_binary(high4); // 输出0000000000001101
return 0;
}
三、进阶技巧:位运算实战高频用法(面试/工作必备)
掌握基础运算后,我们结合实际场景,总结 6 个高频进阶技巧,这些技巧是算法优化和底层开发的核心,也是面试常考考点。
3.1 快速判断一个数是否是 2 的幂次
原理:2 的幂次的二进制只有一个 1(如 2=10、4=100、8=1000),n & (n-1) 会消去最低位的 1,若结果为 0,则 n 是 2 的幂次(注意 n>0,避免 n=0 的特殊情况)。
代码示例:
#include <stdio.h>
// 快速判断一个数是否是2的幂次(n>0)
int is_power_of_two(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
int main() {
printf("8是否为2的幂次:%d(1=是,0=否)\n", is_power_of_two(8)); // 输出1
printf("6是否为2的幂次:%d(1=是,0=否)\n", is_power_of_two(6)); // 输出0
return 0;
}
3.2 统计二进制中 1 的个数(汉明重量)
原理:利用 n & (n-1) 消去最低位的 1,每消去一次,计数加 1,直到 n 变为 0,计数即为 1 的个数(Brian Kernighan 算法,比逐位遍历更高效)。
代码示例:
#include <stdio.h>
// 统计二进制中1的个数(汉明重量)Brian Kernighan 算法
int hamming_weight(int n) {
int count = 0;
while (n) {
n &= n - 1; // 消去最低位的1
count++;
}
return count;
}
int main() {
printf("5(二进制101)中1的个数:%d\n", hamming_weight(5)); // 输出2
printf("0xD(二进制1101)中1的个数:%d\n", hamming_weight(0xD)); // 输出3
return 0;
}
3.3 位掩码技术(权限/状态管理)
原理:用一个整数的每一位表示一个独立的状态(如权限、开关),通过与、或、异或运算实现状态的设置、检查、清除。
代码示例(权限管理) :
#include <stdio.h>
// 定义权限位(宏定义更贴合C语言开发习惯)
#define READ (1 << 0) // 0001(读权限)
#define WRITE (1 << 1) // 0010(写权限)
#define EXECUTE (1 << 2) // 0100(执行权限)
int main() {
// 1. 设置权限(给用户添加写权限)
int user_perm = READ; // 初始只有读权限
user_perm |= WRITE; // 添加写权限
printf("添加写权限后(二进制):");
for (int i = 3; i >= 0; i--) { // 打印低4位
printf("%d", (user_perm & (1 << i)) ? 1 : 0);
}
printf("(十进制:%d)\n", user_perm); // 输出0011(3)
// 2. 检查权限(判断用户是否有执行权限)
int has_execute = (user_perm & EXECUTE) != 0;
printf("是否有执行权限:%d(1=是,0=否)\n", has_execute); // 输出0
// 3. 清除权限(移除读权限)
user_perm &= ~READ;
printf("移除读权限后(二进制):");
for (int i = 3; i >= 0; i--) {
printf("%d", (user_perm & (1 << i)) ? 1 : 0);
}
printf("(十进制:%d)\n", user_perm); // 输出0010(2)
return 0;
}
3.4 无分支实现绝对值(高效优化)
原理:负数的补码是反码+1,利用右移获取符号位(mask = n >> 31,正数 mask=0,负数 mask=-1),再通过异或和减法实现绝对值:abs_n = (n ^ mask) - mask。
代码示例:
#include <stdio.h>
// 无分支实现绝对值(高效优化,适用于int类型)
int abs_without_branch(int n) {
int mask = n >> 31; // 获取符号位(正数mask=0,负数mask=-1)
return (n ^ mask) - mask;
}
int main() {
printf("|-10| = %d\n", abs_without_branch(-10)); // 输出10
printf("|25| = %d\n", abs_without_branch(25)); // 输出25
return 0;
}
3.5 位图(BitMap):海量数据去重/判存
原理:用一个整数数组存储位集合,每个位对应一个数据的存在状态(0=不存在,1=存在),适用于海量整数去重(如 1 亿个整数去重,仅需约 12MB 内存)。
代码示例(简单位图实现) :
#include <stdio.h>
#include <stdlib.h>
// 位图(BitMap)结构体
typedef struct {
int *bits; // 存储位集合的整数数组
int size; // 数组长度(每个int存储32位)
} BitMap;
// 初始化位图(max_num:最大待存储数字)
BitMap* bitmap_init(int max_num) {
BitMap *bm = (BitMap*)malloc(sizeof(BitMap));
if (bm == NULL) return NULL;
// 计算所需数组长度:max_num / 32 + 1
bm->size = (max_num >> 5) + 1;
bm->bits = (int*)calloc(bm->size, sizeof(int)); // 初始化为0
return bm;
}
// 标记数字存在
void bitmap_set(BitMap *bm, int num) {
if (bm == NULL || num < 0) return;
int idx = num >> 5; // 确定所属数组下标(num // 32)
int pos = num & 0x1F; // 确定在整数中的位位置(num % 32)
bm->bits[idx] |= (1 << pos);
}
// 判断数字是否存在
int bitmap_exists(BitMap *bm, int num) {
if (bm == NULL || num < 0) return 0;
int idx = num >> 5;
int pos = num & 0x1F;
return (bm->bits[idx] & (1 << pos)) != 0;
}
// 释放位图内存
void bitmap_free(BitMap *bm) {
if (bm != NULL) {
free(bm->bits);
free(bm);
}
}
int main() {
// 测试:1亿个整数去重判存(简化测试,实际可支持到1亿)
BitMap *bm = bitmap_init(100000000);
if (bm == NULL) {
printf("位图初始化失败\n");
return 1;
}
bitmap_set(bm, 123456);
printf("123456是否存在:%d(1=是,0=否)\n", bitmap_exists(bm, 123456)); // 输出1
printf("654321是否存在:%d(1=是,0=否)\n", bitmap_exists(bm, 654321)); // 输出0
bitmap_free(bm); // 释放内存
return 0;
}
3.6 交换两个变量(无需临时变量,面试常考)
原理:利用异或的自反性(a^a=0)和零特性(a^0=a),无需临时变量即可交换两个数(注意:a 和 b 不能指向同一内存地址,否则会清零)。
代码示例:
#include <stdio.h>
// 交换两个变量(无需临时变量,面试常考)
void swap(int *a, int *b) {
if (a != b) { // 避免同一地址清零(如swap(&x, &x))
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
}
int main() {
int x = 100, y = 200;
printf("交换前:x=%d, y=%d\n", x, y); // 输出100 200
swap(&x, &y);
printf("交换后:x=%d, y=%d\n", x, y); // 输出200 100
return 0;
}
四、避坑指南:位运算常见错误与注意事项
位运算虽高效,但容易因细节出错,总结 4 个常见坑点,帮你避开陷阱:
4.1 符号位陷阱(取反、右移)
负数的取反和右移会涉及符号位,比如 ~10 = -11,-10 >> 2 = -3,不要想当然认为是 5(10>>2=2,负数向下取整)。
4.2 语言差异陷阱
Java 和 Python 的位运算表现不同:Java 有逻辑右移(>>>),Python 没有;Python 整数无限位,不会溢出,Java 整数有固定位数(如 int 32 位),会溢出。
4.3 优先级陷阱
位运算的优先级低于算术运算和比较运算,比如 n & 1 == 1 不能写成 n & (1 == 1),需注意括号的使用(虽 Python 中无需额外括号,但建议加上,提升可读性)。
4.4 可读性陷阱
位运算的可读性较差,比如 n & ~(1 << k) 是清零第 k 位,建议添加注释,避免后续维护时看不懂。
五、总结:位运算的学习与应用建议
位运算的核心是“操作二进制位”,它的优势是高效、省内存,短板是可读性差。学习位运算,不要死记硬背规则,要抓住“二进制位的权重”和“运算的逻辑意义”,结合示例多练习,才能真正掌握。
最后给大家两个应用建议:
- 新手入门:先掌握 6 个基础运算的规则和简单应用(判断奇偶、交换变量),再逐步学习进阶技巧;
- 实战提升:做 LeetCode 上的位运算专题(136、137、260 等),结合底层开发、权限控制等场景练习,让位运算真正融入你的编程习惯。
位运算看似底层、晦涩,但一旦吃透,你会对计算机的运行逻辑有更深刻的理解,也能在面试和工作中脱颖而出。希望这篇详解能帮你打开位运算的大门,让这个“底层神器”为你所用~
如果觉得有收获,欢迎点赞、收藏,也可以在评论区分享你的位运算实战经验哦!