【数据结构与算法】位运算经典题
- 位运算原理
- 不适用额外空间交换两个数
- 1. 缺失数字(异或应用)
- 2. 只出现一次的数字(异或应用)
- 3. 明汉距离(异或应用)
- 4. 只出现一次的数字(进阶版)
- 5. 颠倒二进制位
- 6. 判断一个数是不是4的n次方
- 7. 交替位二进制数
- 8. 数字的补数
- 9. 两整数之和
- 10. 最大单词长度乘积
- 11. 比特位计算
- 12. 错误的集合
位运算原理
基本位运算
x ^ 0s = x x & 0s = 0 x | 0s = x
x ^ 1s = ~x x & 1s = x x | 1s = 1s
x ^ x = 0 x & x = x x | x = x
感觉这得记住
去除两个相同的数
利用 x ^ x = 0
下面的方法似乎有类似的效果(附加和删除)
res |= i;
res &= ~i;
实际上,这不能用来去除两个相同的数。如果res包含了i的一部分,那么第二步会把res中部分1给去除掉。
掩码操作
利用 x & 0s = 0 和x & 1s = x 可以实现掩码操作,例如 x & 0b0011100 仅保留x中与 1 重叠的部分
设值操作
利用 x | 1s = 1s 和 x | 0s = x 可实现设值操作,例如 x | 0b0011100 把x中与右边 1 相对应的部分设值为1
去除最右边的1
x&(x - 1)
仅获取最右边的1
x&(~x + 1)或者x&(-x)。因为~x + 1 和 x 只有一位同时为 1 ,即最右边的 1
x - x&(-x) 的效果和x&(x - 1)一样,都是去除最右边的 1 。
mask计算
- 获取全为 1 的二进制数:~0;
- 获取第 i 位为1的二进制数:1<<(i-1);
- 获取右边i位全为1的二进制数:(1<<i) - 1;
- 获取右边i位全为0,左边全为1的二进制数:~((1<<i) - 1);
java中的位操作
static int Integer.bitCount(); // 统计 1 的数量
static int Integer.highestOneBit(); // 获得最高位
static String toBinaryString(int i); // 转换为二进制表示的字符串
交换两个数
a = a ^ b;
b = a ^ b;
a = a ^ b;
缺失数字
LeetCode:缺失数字
题目描述:
给定一个包含 0, 1, 2, ..., n 中 n 个数的序列,找出 0 .. n 中没有出现在序列中的那个数。
示例:
输入: [9,6,4,2,3,5,7,0,1]
输出: 8
思想:
利用 x ^ x = 0 去除两个相同的数。
我一开始是用下面这种方法
public int missingNumber(int[] nums) {
int res = 0;
for(int i = 0;i<nums.length;++i){
res = res ^ (i+1) ^ nums[i];
}
return res;
}
效率比下面的标准做法要慢一些
代码:
class Solution {
public int missingNumber(int[] nums) {
int res = 0;
for(int i = 0;i<nums.length;++i){
res = res ^ i ^ nums[i];
}
return res ^ nums.length;
}
}
只出现一次的数字
LeetCode:只出现一次的数字
题目描述:
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例:
输入: [4,1,2,1,2]
输出: 4
思想:
利用 x ^ x = 0 去除两个相同的数
代码:
class Solution {
public int singleNumber(int[] nums) {
int m = 0;
for(int item : nums){
m ^= item;
}
return m;
}
}
明汉距离
LeetCode:明汉距离
题目描述:
两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。
给出两个整数 x 和 y,计算它们之间的汉明距离。
示例:
输入: x = 1, y = 4
输出: 2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
上面的箭头指出了对应二进制位不同的位置。
思想:
取异或,得到目标位数全为 1
通过循环移位来计算 1 的数量。
代码:
class Solution {
public int hammingDistance(int x, int y) {
int z = x ^ y;
int cnt =0;
while(z!=0){
if((z & 1) == 1) cnt++;
z >>=1;
}
return cnt;
}
}
只出现一次的数字_进阶
五星
LeetCode:只出现一次的数字_进阶
题目描述:
给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。
你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?
示例:
输入: [1,2,1,3,2,5]
输出: [3,5]
思想:
- 首先很容易想到,获得所有元素取异或之后的数,设为t;
- t &= -t;获得只包含最右边的1的数;
- 再循环一遍,t与每个元素求异或,只有两种结果,为0或者不为0,这两种情况一定可以把两个目标元素区分开;同时其它元素都有两个相同的,都能抵消。
代码:
class Solution {
public int[] singleNumber(int[] nums) {
int t = 0;
for(int num : nums) t ^= num;
t &= -t;
int[] res = new int[2];
for(int num : nums){
if((num & t) ==0) res[0] ^= num;
else res[1] ^= num;
}
return res;
}
}
颠倒二进制位
LeetCode:颠倒二进制位
题目描述:
颠倒给定的 32 位无符号整数的二进制位。
示例:
输入: 00000010100101000001111010011100
输出: 00111001011110000010100101000000
解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
思想:
注意n要无符号右移
代码:
public class Solution {
// you need treat n as an unsigned value
public int reverseBits(int n) {
int res = 0;
for(int i=0;i<32;++i){
res <<= 1;
res += (n&1);
n >>>= 1;
}
return res;
}
}
判断一个数是不是4的n次方
LeetCode:判断一个数是不是4的n次方
题目描述:
给定一个整数 (32 位有符号整数),请编写一个函数来判断它是否是 4 的幂次方。
示例:
输入: 16
输出: true
思想:
判断是否是2的幂:
- num&(num-1)==0;
- (n&(-n)) == n
4的幂有其对应的特点:0的数量位偶数;1所在的位,一定是奇数位;等等
代码:
我的方法
class Solution {
public boolean isPowerOfFour(int num) {
if(num<1 || (num&(num-1))!=0) return false;
return Integer.bitCount(num - 1)%2==0;//我想到的是后面0的数量为2的倍数,根据这一点来做
}
}
其它方法
//1所在的位,一定是奇数位
return num>0&&((num&(num-1))==0 && (num&0b10101010101010101010101010101010)==0)
//转化为4进制
return Integer.toString(num, 4).matches("10*");
交替位二进制数
LeetCode:交替位二进制数
题目描述:
给定一个正整数,检查他是否为交替位二进制数:换句话说,就是他的二进制数相邻的两个位数永不相等。
示例:
输入: 11
输出: False
解释:
11的二进制数是: 1011
思想:
简单题
int m = n+(n>>1);
和int m = n^(n>>1);效果一样
注意不需要判断n是否为奇数,直接向右移位即可
代码:
class Solution {
public boolean hasAlternatingBits(int n) {
int m = n+(n>>1);
// if(n%2==0) m = n+(n>>1);
// else m = n+(n<<1);
return (m&(m+1))==0;
}
}
数字的补数
五星
LeetCode:数字的补数
题目描述:
给定一个正整数,输出它的补数。补数是对该数的二进制表示取反。
示例:
输入: 5
输出: 2
解释: 5 的二进制表示为 101(没有前导零位),其补数为 010。所以你需要输出 2 。
思想:
这题主要是如何获取11111,通过如下方法来获取
mask |= mask>>1;
mask |= mask>>2;
mask |= mask>>4;
mask |= mask>>8;
mask |= mask>>16;
当然也可以用 Integer.highestOneBit(num) 获取仅包含最高位,且最高wei为1的数
代码:
class Solution {
public int findComplement(int num) {
int mask = num;
mask |= mask>>1;
mask |= mask>>2;
mask |= mask>>4;
mask |= mask>>8;
mask |= mask>>16;
return mask ^ num;
}
}
两整数之和
五星
LeetCode:两整数之和
题目描述:
不使用运算符 + 和 - ,计算两整数 a 、b 之和。
示例:
输入: a = 1, b = 2
输出: 3
思想:
a^b就是不考虑进位情况下a和b的和;
(a&b)<<1就是进位。
代码:
class Solution {
public int getSum(int a, int b) {
return b==0?a:getSum(a^b,(a&b)<<1);
}
}
最大单词长度乘积
五星
LeetCode:最大单词长度乘积
题目描述:
给定一个字符串数组 words,找到 length(word[i]) * length(word[j]) 的最大值,并且这两个单词不含有公共字母。你可以认为每个单词只包含小写字母。如果不存在这样的两个单词,返回 0。
示例:
输入: ["abcw","baz","foo","bar","xtfn","abcdef"]
输出: 16
解释: 这两个单词为 "abcw", "xtfn"。
思想:
思路:用一个int值的每一位来记录字母是否存在;
注意:需要两层循环来求最大乘积
代码:
class Solution {
public int maxProduct(String[] words) {
int[] arr = new int[words.length];
for(int i=0;i<words.length;++i){
for(char c : words[i].toCharArray()){
arr[i] |= 1 << (c - 'a');
}
}
int res = 0;
for(int i=0;i<words.length-1;++i){
for(int j=i+1;j<words.length;++j){
if((arr[i]&arr[j])!=0) continue;
res = Math.max(res, words[i].length()*words[j].length());
}
}
return res;
}
}
比特位计算
五星
LeetCode:比特位计算
题目描述:
给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
示例:
输入: 5
输出: [0,1,1,2,1,2]
思想:
- 线性复杂度,可以想到,随着循环的进行,后面的数由前面的数得到;
- 再想到,后面的数由前面的数加1得到;
- 每个二进制数去掉最右边的1,正好与上面思路匹配;
- 联想到,i&(i-1)可得到去掉最右边的1的数
代码:
class Solution {
public int[] countBits(int num) {
int[] res = new int[num+1];
for(int i=1;i<=num;++i){
res[i] = res[i&(i-1)] + 1;
}
return res;
}
}
错误的集合
五星
LeetCode:错误的集合
题目描述:
集合 S 包含从1到 n 的整数。不幸的是,因为数据错误,导致集合里面某一个元素复制了成了集合里面的另外一个元素的值,导致集合丢失了一个整数并且有一个元素重复。
给定一个数组 nums 代表了集合 S 发生错误后的结果。你的任务是首先寻找到重复出现的整数,再找到丢失的整数,将它们以数组的形式返回。
注意:
给定数组的长度范围是 [2, 10000]。
给定的数组是无序的。
示例:
输入: nums = [1,2,2,4]
输出: [2,3]
思想:
使用位运算,所有数异或得到m,使用m &=-m得到最右边的1,来区分两个数。
- 最后注意要再循环一次判断哪个数是重复的。
代码:
class Solution {
public int[] findErrorNums(int[] nums) {
int m=0;
for(int i=0;i<nums.length;++i){
m ^= nums[i];
m ^= i+1;
}
m &=-m;
int xor0=0,xor1=0;
for(int i=0;i<nums.length;++i){
if((nums[i]&m)==0) xor0 ^= nums[i];
else xor1 ^= nums[i];
}
for(int i=0;i<nums.length;++i){
if(((i+1)&m)==0) xor0 ^= i+1;
else xor1 ^= i+1;
}
for(int item : nums){
if(item == xor1){
return new int[]{xor1,xor0};
}
}
return new int[]{xor0,xor1};
}
}