目录
位运算概述
位运算符
位运算的优先级
位运算常见应用
1. 给定一个数n,判断其二进制表示中的第x位是0还是1
2. 将数n的二进制表示中的第x位修改为1
3. 将数n的二进制表示中的第x位修改为0
4. 位图
例题:判断字符是否唯一
5. 提取数n的二进制表示中的最右侧的1(lowbit)
6. 去掉数n的二进制表示中的最右侧的1
例题:位1的个数
7. 异或(^)运算的运算律
例题:只出现一次的数字
位运算练习
练习1:两整数之和
练习2:只出现一次的数字II
练习3:只出现一次的数字III
练习4:消失的两个数字
位运算概述
位运算:计算机中的数据在内存中是以二进制形式进行存储的,而位运算是对二进制数进行操作的运算,能够按位对数字进行操作
位运算符
(1)与运算符(&):如果两个二进制数的同一位都为1,则结果为1,否则为0。(有0为0)
(2)或运算符(|):如果两个二进制数的同一位至少有一个为1,则结果为1,否则为0。(有1为1)
(3)异或运算符(^):如果两个二进制数的同一位不相同,则结果为1,否则为0。也可以理解为 无进位相加(即0 + 0 = 0;0 + 1 = 1;而当1+1时,结果为0,但不进位)
(4)取反运算符(~):将二进制数的每一位取反,即0变为1,1变为0。
(5)左移运算符(<<):将二进制数向左移动指定的位数,右边补0。
(6)右移运算符(>>):将二进制数向右移动指定的位数,左边补0或者补1(取决于原数字的符号位)。
(7)复合赋值运算符:
&= a&=b 同 a=a&b
|= a|=b 同 a=a|b
>>= a>>=b 同 a=a>>b
<<= a<<=b 同 a=a<<b
注:对于负数的位操作,一般会使用补码表示法。在补码表示法中,最高位为符号位,0表示正数,1表示负数。位操作对于负数的结果仍然是以补码形式表示的。
位运算的优先级
常见位运算的优先级顺序:
(1)括号(()):括号可以用来改变运算符的结合性和优先级。
(2)取反运算符(~):取反运算符的优先级较高。
(3)左移运算符(<<)、右移运算符(>>)、无符号右移运算符(>>>):左移和右移运算符的优先级相同,且比与、或、异或运算符低。
(4)与运算符(&):与运算符的优先级较低。
(5)异或运算符(^):异或运算符的优先级较低于与运算符。
(6)或运算符(|):或运算符的优先级最低。
注:位运算符的优先级较低,因此在进行位运算操作时,可以使用括号来明确运算符的优先级和结合性
位运算常见应用
1. 给定一个数n,判断其二进制表示中的第x位是0还是1
例如:
在这里,我们从右侧第零位开始计数
要判断一个数的第x位是0还是1,只需要将x向右移动x位(>> x),再与(&) 上1,即可判断,
即 (n >> x) & 1
2. 将数n的二进制表示中的第x位修改为1
例如:
要将第x位上的0修改为1,我们可以想到或(|)运算符,当两个二进制数的同一位至少有一个为1,则该位结果为1
因此,我们可以将1左移x位再让n与其进行或运算,即n |= (1 << x)
3. 将数n的二进制表示中的第x位修改为0
与将第x位修改为1类似,我们可以想到与(&)运算符,当两个二进制数的同一位至少有一个为0,则该位结果为0
因此,我们将1左移x位,再对其按位取反,这样就使得第x位为0,其他位都为1,然后再进行与运算,即 n &= (~ (1 << x))
4. 位图
位图是一种数据结构,类似于数组,在数组中每个下标标识一个元素,而位图使用每个位来标识一种状态
以上图为例,int类型的整数有32个比特位,每个比特位标识一个元素,一个元素有两种状态(0或1)
接下来我们以一道例题来进一步了解和使用位图:
例题:判断字符是否唯一
题目链接:
面试题 01.01. 判定字符是否唯一 - 力扣(LeetCode)
题目描述:
实现一个算法,确定一个字符串 s
的所有字符是否全都不同。
示例 1:
输入: s
= "leetcode"
输出: false
示例 2:
输入: s
= "abc"
输出: true
限制:
0 <= len(s) <= 100
s[i]
仅包含小写字母- 如果你不使用额外的数据结构,会很加分。
思路分析:要判断字符串中是否有字符相同,我们可以使用哈希表来统计每个字符的个数,由于字符串中仅包含小写字母,我们可以使用int类型的数组(int[26])来标识26个小写字母(即0下标位置表示a的个数,1下标位置表示b的个数...),然后统计每个字符的个数。
由于只需要判断是否有字符相同,我们也可以使用位图,使用int类型的变量bitMap来作为“数组”,其中26位来标识26个小写字母,当该字符未出现过时(即对应二进制位为0),将对应二进制位修改为1,而当该字符出现过时(即对应二进制位为1),则可判断有字符相同,返回false
代码实现:
class Solution {public boolean isUnique(String astr) {char[] s = astr.toCharArray();if(s.length > 26) return false;int bitMap = 0;for(char x : s){int index = x - 'a';if((bitMap & (1 << index)) == 0){bitMap |= (1 << index);}else{return false;}}return true;}
}
5. 提取数n的二进制表示中的最右侧的1(lowbit)
要想提取出n中最右侧的1,只需将 n & (-n)
我们可以发现:将 n 转换为 -n(补码) 后,最右侧1 的左边区域(不包括最右侧1)全部变相反,此时,再将n & (-n),就只剩下最右侧的1
6. 去掉数n的二进制表示中的最右侧的1
去掉最右侧的1,只需将 n & (n - 1)
我们可以发现:将 n 转换为 n-1 后,最右侧1 的右边区域(包括最右侧1)全部变相反,此时,再将n & (n-1),就能够去掉最右侧的1了
我们以一道例题进行练习:
例题:位1的个数
题目链接:
191. 位1的个数 - 力扣(LeetCode)
题目描述:
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。
示例 1:
输入:n = 00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:
输入:n = 00000000000000000000000010000000 输出:1 解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:
输入:n = 11111111111111111111111111111101 输出:31 解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。
思路分析:
题目要求我们计算整数n的二进制表示中的1的个数,可以通过判断每一位是否为1((n >> x) & 1)求出1的个数,但我们知道n & (n-1)能够去掉最右侧的1,因此我们只需计算能够去掉多少个1,即可求出1的个数
代码实现:
public class Solution {// you need to treat n as an unsigned valuepublic int hammingWeight(int n) {int sum = 0;while(n != 0){n = n & (n-1);sum ++;}return sum;}
}
而题目:
338. 比特位计数 - 力扣(LeetCode)
461. 汉明距离 - 力扣(LeetCode)
也是类似的解题思路,大家可以自行练习
7. 异或(^)运算的运算律
a ^ 0 = a
a ^ a = 0
a ^ b ^ c = a ^ (b ^ c)
我们以一道例题来进一步掌握异或运算的运算律:
例题:只出现一次的数字
题目链接:
136. 只出现一次的数字 - 力扣(LeetCode)
题目描述:
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1] 输出:1
示例 2 :
输入:nums = [4,1,2,1,2] 输出:4
示例 3 :
输入:nums = [1] 输出:1
思路分析:题目告诉我们数组中只有一个元素出现了一次,其他的都出现了两次,且a ^ a = 0,因此我们只需要将数组所有元素异或,出现两次的元素异或后都变为0,最后只剩下出现一次的元素a ^ 0 = a
代码实现:
class Solution {public int singleNumber(int[] nums) {int ret = nums[0];for(int i = 1; i < nums.length; i++){ret = ret ^ nums[i];}return ret;}
}
位运算练习
练习1:两整数之和
题目链接:
371. 两整数之和 - 力扣(LeetCode)
题目描述:
给你两个整数 a
和 b
,不使用 运算符 +
和 -
,计算并返回两整数之和。
示例 1:
输入:a = 1, b = 2 输出:3
示例 2:
输入:a = 2, b = 3 输出:5
思路分析:不能使用运算符 + 和 - ,则我们可以考虑使用位运算来解决这个问题
异或(^)运算相当于无进位相加,在无进位的情况下(0 + 0 = 0,0 + 1 = 1)
接下来,我们考虑进位的情况:当该位上出现 1 + 1 时,则需要进位(即 1 + 1 = 10)而有需要进位的位为(a & b),进位后为(a & b)<< 1
因此,我们可以考虑将整数a 和 b的和,分为无进位加法的结果和进位结果的和
具体过程为:
以15和9为例:
1.先将每一位相加,且不考虑进位。二进制满2进位,即1 + 1 = 10,此时不考虑进位,因此,可以通过异或实现不考虑进位的相加,即 1 + 1 = 0,0 + 0 = 0,1 + 0 = 1,此时1111 + 1001 = 0110
2.找出进位的数。当出现1 + 1时,产生进位,我们可以通过两个数按位与找到这些存在进位的数,再将其向左移动1位,实现进位
3.将1和2得到的数字相加,由于相加不能使用加法,即重复1、2步骤,模拟实现加法,当进位为0时,即不再产生进位,此时异或的结果即为两个数相加的结果
代码实现:
class Solution {public int getSum(int a, int b) {while(b != 0){int carry = (a & b) << 1;//进位a = a ^ b;//无进位b = carry;}return a;}
}
练习2:只出现一次的数字II
题目链接:
137. 只出现一次的数字 II - 力扣(LeetCode)
题目描述:
给你一个整数数组 nums
,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。
示例 1:
输入:nums = [2,2,3,2] 输出:3
示例 2:
输入:nums = [0,1,0,1,0,1,99] 输出:99
提示:
1 <= nums.length <= 3 * 104
-231 <= nums[i] <= 231 - 1
思路分析:由于只有一个元素出现了一次,其他n个元素元素都出现了三次,那么对于每一位都有以下四种情况:
1. 3n个0 + 1个0
2. 3n个0 + 1个1
3. 3n个1 + 1个0
4. 3n个1 + 1个1
对其模3(%3)可得:
1. 3n个0 + 1个0 (%3) -> 0
2. 3n个0 + 1个1 (%3) -> 1
3. 3n个1 + 1个0 (%3) -> 0
4. 3n个1 + 1个1 (%3) -> 1
即只出现一次的元素的二进制位,因此,我们只需求出出现一次的元素的每一位,即可求出该元素
代码实现:
class Solution {public int singleNumber(int[] nums) {int ret = 0;for(int i = 0; i < 32; i++){int count = 0;for(int j = 0; j < nums.length; j++){count += ((nums[j] >> i) & 1);}count %= 3;ret = ret | (count << i);}return ret;}
}
练习3:只出现一次的数字III
题目链接:
260. 只出现一次的数字 III - 力扣(LeetCode)
题目描述:
给你一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。
你必须设计并实现线性时间复杂度的算法且仅使用常量额外空间来解决此问题。
示例 1:
输入:nums = [1,2,1,3,2,5] 输出:[3,5] 解释:[5, 3] 也是有效的答案。
示例 2:
输入:nums = [-1,0] 输出:[-1,0]
示例 3:
输入:nums = [0,1] 输出:[1,0]
提示:
2 <= nums.length <= 3 * 104
-231 <= nums[i] <= 231 - 1
- 除两个只出现一次的整数外,
nums
中的其他数字都出现两次
思路分析:与只出现一次的数字类似,但此题是两个元素只出现了一次,我们同样可以利用异或的运算律,将所有元素异或,此时得到两个只出现一次的元素的异或结果
如何得到这两个元素呢?
由于这两个元素一定不相同(若相同则异或结果为0,不符合题目中描述),我们可以通过提取结果中最右侧的1(lowbit),并将数组按照该位是否为1分为两组元素,这样,两个只出现一次的数字就被分到了不同的组里,此时再进行异或,就可得到这两个元素
代码实现:
class Solution {public int[] singleNumber(int[] nums) {int ret = nums[0];for(int i = 1; i < nums.length; i++){ret ^= nums[i];}//得到两个数的异或结果int ret1 = ret & (-ret);//找到最低位1int ret2 = 0;for(int i = 0; i < nums.length; i++){if((nums[i] & ret1) == 0){ret2 ^= nums[i];}}ret1 = ret2 ^ ret;return new int[] {ret1, ret2};}
}
练习4:消失的两个数字
题目链接:
面试题 17.19. 消失的两个数字 - 力扣(LeetCode)
题目描述:
给定一个数组,包含从 1 到 N 所有的整数,但其中缺了两个数字。你能在 O(N) 时间内只用 O(1) 的空间找到它们吗?
以任意顺序返回这两个数字均可。
示例 1:
输入: [1]
输出: [2,3]
示例 2:
输入: [2,3]
输出: [1,4]
提示:
nums.length <= 30000
思路分析:
数组中缺失两个数字,设数组的长度为len,则所有元素为1 到 len+2,由于其中缺失两个数字,若将数组中所有元素和1 - len + 2看做一个整体,则此时就变为两个数字只出现一次,其他数字出现两次,此时就和 只出现一次的数字III 相同了,接下来按照 只出现一次的数字III 的解题思路来解题就可以解决该问题
代码实现:
class Solution {public int[] missingTwo(int[] nums) {int len = nums.length;int ret = (len + 2) ^ (len + 1);//求出两个数异或的结果for(int i = 0; i < nums.length; i++){//所有元素异或结果ret ^= nums[i];ret ^= (i+1);}int ret1 = ret & (-ret);//找到最低位1int ret2 = 0;for(int i = 0; i < len; i++){if((nums[i] & ret1) == 0){ret2 ^= nums[i];}}for(int i = 1; i <= len + 2; i++){if((i & ret1) == 0){ret2 ^= i;}}ret1 = ret2 ^ ret;return new int[] {ret1, ret2};}
}