位操作
有趣的位运算技巧
1、利用或操作 | 和空格将英文字符转换为小写
空格的ASCII码是32,大小写的ASCII码差32
1 | ('a' | ' ') = 'a' |
2、利用与操作 & 和下划线将英文字符转换为大写
1 | ('b' & '_') = 'B' |
3、利用异或操作 ^ 和空格进行英文字符大小写互换
1 | ('d' ^ ' ') = 'D' |
以上操作能够产生奇特效果的原因在于 ASCII 编码。ASCII 字符其实就是数字,恰巧空格和下划线对应的数字通过位运算就能改变大小写。有兴趣的读者可以查 ASCII 码表自己算算,本文就不展开讲了。
4、不用临时变量交换两个数
1 | int a = 1, b = 2; |
5、加一
1 | int n = 1; |
6、减一
1 | int n = 2; |
7、判断两个数是否异号
1 | int x = -1, y = 2; |
如果说前 6 个技巧的用处不大,这第 7 个技巧还是比较实用的,利用的是补码编码的符号位。整数编码最高位是符号位,负数的符号位是 1,非负数的符号位是 0,再借助异或的特性,可以判断出两个数字是否异号。
当然,如果不用位运算来判断是否异号,需要使用 if else 分支,还挺麻烦的。你可能想利用乘积来判断两个数是否异号,但是这种处理方式容易造成整型溢出,从而出现错误。
算法题常用的位运算技巧
index & (arr.length - 1)的运用
模运算 % 对计算机来说其实是一个比较昂贵的操作,所以我们可以用 & 运算来求余数。
注意:
这个技巧只适用于数组长度是 2 的幂次方的情况,比如 2、4、8、16、32 以此类推。
简单说,& (arr.length - 1) 这个位运算能够替代 % arr.length 的模运算,性能会更好一些。
n & (n-1) 的运用「重要」
n & (n-1) 这个操作在算法中比较常见,作用是消除数字 n 的二进制表示中的最后一个 1。
思路:
(n-1) 将会把 n 的最右边的 1 及其右边的所有位都取反,而其他位保持不变。然后,将 n 和 (n-1) 进行按位与操作,结果就是将 n 最右边的 1 变为 0,而其他位保持不变。
常见应用:
1、判断一个数是否是 2 的幂:如果 n & (n-1) 的结果为 0,那么说明 n 只有一个位为 1,即 n 是 2 的幂。
2、统计一个数的二进制表示中有多少个 1:可以使用一个循环,每次执行 n = n & (n-1),并计数循环的次数,直到 n 变为 0。
3、清除一个整数的最右边的 1:可以使用 n = n & (n-1) 来清除最右边的 1。
a ^ a = 0 的运用
牢记:
一个数和它本身做异或运算结果为 0,即 a ^ a = 0;一个数和 0 做异或运算的结果为它本身,即 a ^ 0 = a。
典型算法题
191. 位1的个数
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。
提示:
- 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
- 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在 示例 3 中,输入表示有符号整数
-3。
示例 1:
1 | 输入:n = 00000000000000000000000000001011 |
示例 2:
1 | 输入:n = 00000000000000000000000010000000 |
示例 3:
1 | 输入:n = 11111111111111111111111111111101 |
提示:
- 输入必须是长度为
32的 二进制串 。
进阶:
- 如果多次调用这个函数,你将如何优化你的算法?
思路和算法
统计一个数的二进制表示中有多少个 1:可以使用一个循环,每次执行 n = n & (n-1),并计数循环的次数,直到 n 变为 0。
1 | public class Solution { |
231. 2的幂
给你一个整数 n,请你判断该整数是否是 2 的幂次方。如果是,返回 true ;否则,返回 false 。
如果存在一个整数 x 使得 n == 2x ,则认为 n 是 2 的幂次方。
示例 1:
1 | 输入:n = 1 |
示例 2:
1 | 输入:n = 16 |
示例 3:
1 | 输入:n = 3 |
示例 4:
1 | 输入:n = 4 |
示例 5:
1 | 输入:n = 5 |
提示:
-231 <= n <= 231 - 1
**进阶:**你能够不使用循环/递归解决此问题吗?
思路和算法
判断一个数是否是 2 的幂:如果 n & (n-1) 的结果为 0,那么说明 n 只有一个位为 1,即 n 是 2 的幂。
1 | class Solution { |
131. 只出现一次的数字
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
1 | 输入:nums = [2,2,1] |
示例 2 :
1 | 输入:nums = [4,1,2,1,2] |
示例 3 :
1 | 输入:nums = [1] |
提示:
1 <= nums.length <= 3 * 104-3 * 104 <= nums[i] <= 3 * 104- 除了某个元素只出现一次以外,其余每个元素均出现两次。
思路和算法
把所有数字进行异或,成对儿的数字就会变成 0,落单的数字和 0 做异或还是它本身,所以最后异或的结果就是只出现一次的元素:
1 | class Solution { |
286. 丢失的数字
给定一个包含 [0, n] 中 n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。
示例 1:
1 | 输入:nums = [3,0,1] |
示例 2:
1 | 输入:nums = [0,1] |
示例 3:
1 | 输入:nums = [9,6,4,2,3,5,7,0,1] |
示例 4:
1 | 输入:nums = [0] |
提示:
n == nums.length1 <= n <= 1040 <= nums[i] <= nnums中的所有数字都 独一无二
**进阶:**你能否实现线性时间复杂度、仅使用额外常数空间的算法解决此问题?
思路和算法
1、先把索引补一位,让每个元素和自己相等的索引相对应
2、把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下。
1 | class Solution { |
解析 res ^= i ^ nums[i];
res ^= n;:这一步将res与数组的长度n进行异或操作。这是因为在缺失一个数字的情况下,数组的长度就是缺失的那个数字,所以将res与n异或可以将缺失的数字存储在res中。res ^= i ^ nums[i];:这一步是用来处理数组中的其他元素和索引。对于每个索引i和对应的元素nums[i],我们将i和nums[i]进行异或操作,然后再与res进行异或操作。这样做的目的是将数组中的元素和索引都进行异或操作,最终得到的结果就是缺失的数字。
通过对数组中的每个元素和索引进行异或操作,重复的元素和索引会互相抵消,而只有缺失的数字会留下来,最终存储在 res 中。最后,将 res 返回作为缺失的数字。