最近刷了几道算法题,有一些是和位运算有关的,虽然位运算在源码中较多的出现,但个人仍认为其实操意义不大。不过不得不说,基于位运算的一些算法处理,有时候确实很神奇。因此做一个记录。
备注:所有例子都以Java中int类型来说明。
大家都知道,计算机中的一切都是用二进制来表示的,Java中以int为例,一般来说的32位的二进制数字。简单来说,正数的最高位为0,负数的最高位为1,另因为一些原因(具体见参考文档1),负数是使用补码来进行表示。我们可以使用Integer.toBinaryString()方法来查看一些常见的值,做一下直观理解。
System.out.println(Integer.toBinaryString(1)); //1
System.out.println(Integer.toBinaryString(-1)); //11111111 11111111 11111111 11111111
System.out.println(Integer.toBinaryString(Integer.MAX_VALUE)); //1111111 11111111 11111111 11111111
System.out.println(Integer.toBinaryString(Integer.MIN_VALUE)); //10000000 00000000 00000000 00000000
符号 |
含义 |
详解 |
& |
位与 |
两个比特位都为 1 时,结果才为 1,否则为 0 (位与操作满足交换律和结合律,甚至分配律) |
| |
位或 |
两个比特位都为 0 时,结果才为 0,否则为 1 (位或操作满足交换律和结合律,甚至分配律) |
~ |
位非 |
即按位取反,1 变 0,0 变 1 |
^ |
异或 |
两个比特位相同时(都为 0 或都为 1)为 0,相异为 1(异或操作满足交换律和结合律,甚至分配律。任何整数和自己异或的结果为 0,任何整数与 0 异或值不变) |
<< |
左移 |
将所有的二进制位按值向←左移动若干位,左移操作与正负数无关,它只是傻傻地将所有位按值向左移动,高位舍弃,低位补 0 |
>> |
右移 |
将所有的二进制位按值向右→移动若干位,低位直接舍弃,跟正负无关,而高位补 0 还是补 1 则跟被操作整数的正负相关,正数补 0 ,负数补 1 |
>>> |
无符号右移 |
将所有的二进制位按值向右→移动若干位,低位直接舍弃,跟正负无关,高位补 0 ,也跟正负无关 |
这些操作看了都很容易理解,但自己去想,一般却想不出这些个用法。
//判断奇数偶数
public void isOddOrEven(int n){
if ((n & 1) == 1) {//n是奇数
System.out.println("Odd");
}else {//n是偶数
System.out.println("Even");
}
}
//交换两数的值,无需中间变量
public void swap(){
int a = 1, b = 2;
a ^= b;
b ^= a;//b == 1
a ^= b;//a == 2
System.out.println("a:" + a);//a:2
System.out.println("b:" + b);//b:1
}
//判断正数是不是2的幂
public boolean isPowerOfTwo(int num) {
if (num < 1) {
return false;
}
return (num & (num - 1)) == 0;
}
写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。
示例:
输入: a = 1, b = 1
输出: 2
提示:
a, b 均可能是负数或 0
结果不会溢出 32 位整数
这种题初看真的很容易僵住。但我们找几个数出来试试,还是能找到点端倪。
设两数字的二进制形式 a, b ,其求和 s = a + b ,a(i) 代表 a 的二进制第 ii位,则分为以下四种情况:
a(i) |
b(i) |
无进位和 n(i) |
进位 c(i+1) |
0 |
0 |
0 |
0 |
0 |
1 |
1 |
0 |
1 |
0 |
1 |
0 |
1 |
1 |
0 |
1 |
观察发现,无进位和 与 异或运算 规律相同,进位 和 与运算 规律相同(并需左移一位)。通过反复的异或操作,到没有进位的时候,就可以实现加法,顺势可以写出以下代码。
public static int add(int a, int b) {
if (a == 0) {
return b;
}
if (b == 0) {
return a;
}
return add(a ^ b, (a & b) << 1);
}
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出: 1
如果没有经过训练,或者提示本题目可以使用位运算,可能主要的思路还是排序或者哈希来记个数。翻翻位运算,可以很快的想到,两个相同的数异或后会清零,剩下的思路就很自然了。
public int singleNumber(int[] nums) {
int single = 0;
for (int num : nums) {
single ^= num;
}
return single;
}
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
示例 1:
输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
限制:
2 <= nums.length <= 10000
做了上面的,可能感觉有点东西,有点感觉了。这题咋一看和上一题基本一样,但凭空从一个数字变成了两个数字,如果也是全部异或一遍,只会得到两个出现一次的数字的异或,拿着似乎也没有用,着实有点下头。
这时候,我们可能有个模糊的想法,能不能把数组分成两部分,两个待找出的数字放到两个数组中,两边再分别搭配一些出现了两次的数字,这样我们只需要把上一题玩两遍,就可以拿到答案了。
public int[] singleNumbers(int[] nums) {
int x = 0, y = 0, n = 0, m = 1;
for(int num : nums) // 1. 遍历异或
n ^= num;
while((n & m) == 0) // 2. 循环左移,计算 m
m <<= 1;
for(int num: nums) { // 3. 遍历 nums 分组
if((num & m) != 0) x ^= num; // 4. 当 num & m != 0
else y ^= num; // 4. 当 num & m == 0
}
return new int[] {x, y}; // 5. 返回出现一次的数字
}
题目编号 | 题目名称 | 难度 | 链接 |
---|---|---|---|
leetcode 461 | 汉明距离 | 简单 | 力扣 |
剑指 Offer 15 | 二进制中1的个数 | 简单 | 力扣 |
面试题 17.04 | 消失的数字 | 简单 | 力扣 |
剑指 Offer 56 - II | 数组中数字出现的次数 II | 中等 | 力扣 |
leetcode 477 | 汉明距离总和 | 中等 | 力扣 |
leetcode 1879 | 两个数组最小的异或值之和 | 困难 | 力扣 |
实际编码中,显然没有人特别去使用位运算来提高所谓的执行效率。但在大部分人心中,位运算的效率理论上应高于一般的四则运算,但在笔者的实操中,并没有跑出预想中的结果,也没能查阅到确切的资料。这一点欢迎大家讨论。
1.原码、反码、补码的产生、应用以及优缺点有哪些? - 知乎
2.Java 位运算超全面总结 - antball - 博客园
3.题解来源,力扣