位操作

位操作

有趣的位运算技巧

1、利用或操作 | 和空格将英文字符转换为小写

空格的ASCII码是32,大小写的ASCII码差32

1
2
('a' | ' ') = 'a'
('A' | ' ') = 'a'

2、利用与操作 & 和下划线将英文字符转换为大写

1
2
('b' & '_') = 'B'
('B' & '_') = 'B'

3、利用异或操作 ^ 和空格进行英文字符大小写互换

1
2
('d' ^ ' ') = 'D'
('D' ^ ' ') = 'd'

以上操作能够产生奇特效果的原因在于 ASCII 编码。ASCII 字符其实就是数字,恰巧空格和下划线对应的数字通过位运算就能改变大小写。有兴趣的读者可以查 ASCII 码表自己算算,本文就不展开讲了。

4、不用临时变量交换两个数

1
2
3
4
5
int a = 1, b = 2;
a ^= b;
b ^= a;
a ^= b;
// 现在 a = 2, b = 1

5、加一

1
2
3
int n = 1;
n = -~n;
// 现在 n = 2

6、减一

1
2
3
int n = 2;
n = ~-n;
// 现在 n = 1

7、判断两个数是否异号

1
2
3
4
5
int x = -1, y = 2;
boolean f = ((x ^ y) < 0); // true

int x = 3, y = 2;
boolean f = ((x ^ y) < 0); // false

如果说前 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
2
3
输入:n = 00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。

示例 2:

1
2
3
输入:n = 00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。

示例 3:

1
2
3
输入:n = 11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。

提示:

  • 输入必须是长度为 32二进制串

进阶

  • 如果多次调用这个函数,你将如何优化你的算法?

思路和算法

统计一个数的二进制表示中有多少个 1:可以使用一个循环,每次执行 n = n & (n-1),并计数循环的次数,直到 n 变为 0。

1
2
3
4
5
6
7
8
9
10
11
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int res = 0;
while(n != 0){
n = n & (n-1);
res++;
}
return res;
}
}

231. 2的幂

给你一个整数 n,请你判断该整数是否是 2 的幂次方。如果是,返回 true ;否则,返回 false

如果存在一个整数 x 使得 n == 2x ,则认为 n 是 2 的幂次方。

示例 1:

1
2
3
输入:n = 1
输出:true
解释:20 = 1

示例 2:

1
2
3
输入:n = 16
输出:true
解释:24 = 16

示例 3:

1
2
输入:n = 3
输出:false

示例 4:

1
2
输入:n = 4
输出:true

示例 5:

1
2
输入:n = 5
输出:false

提示:

  • -231 <= n <= 231 - 1

**进阶:**你能够不使用循环/递归解决此问题吗?

思路和算法

判断一个数是否是 2 的幂:如果 n & (n-1) 的结果为 0,那么说明 n 只有一个位为 1,即 n 是 2 的幂。

1
2
3
4
5
6
7
8
class Solution {
public boolean isPowerOfTwo(int n) {
if(n <= 0){
return false;
}
return (n & (n-1)) == 0;
}
}

131. 只出现一次的数字

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

示例 1 :

1
2
输入:nums = [2,2,1]
输出:1

示例 2 :

1
2
输入:nums = [4,1,2,1,2]
输出:4

示例 3 :

1
2
输入:nums = [1]
输出:1

提示:

  • 1 <= nums.length <= 3 * 104
  • -3 * 104 <= nums[i] <= 3 * 104
  • 除了某个元素只出现一次以外,其余每个元素均出现两次。

思路和算法

把所有数字进行异或,成对儿的数字就会变成 0,落单的数字和 0 做异或还是它本身,所以最后异或的结果就是只出现一次的元素:

1
2
3
4
5
6
7
8
9
class Solution {
public int singleNumber(int[] nums) {
int res = 0;
for(int i = 0; i < nums.length; i++){
res ^= nums[i];
}
return res;
}
}

286. 丢失的数字

给定一个包含 [0, n]n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。

示例 1:

1
2
3
输入:nums = [3,0,1]
输出:2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。

示例 2:

1
2
3
输入:nums = [0,1]
输出:2
解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。

示例 3:

1
2
3
输入:nums = [9,6,4,2,3,5,7,0,1]
输出:8
解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。

示例 4:

1
2
3
输入:nums = [0]
输出:1
解释:n = 1,因为有 1 个数字,所以所有的数字都在范围 [0,1] 内。1 是丢失的数字,因为它没有出现在 nums 中。

提示:

  • n == nums.length
  • 1 <= n <= 104
  • 0 <= nums[i] <= n
  • nums 中的所有数字都 独一无二

**进阶:**你能否实现线性时间复杂度、仅使用额外常数空间的算法解决此问题?

思路和算法

1、先把索引补一位,让每个元素和自己相等的索引相对应

2、把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int missingNumber(int[] nums) {
int n = nums.length;
int res = 0;
// 先和新补的索引异或一下
res ^= n;
// 和其他的元素、索引做异或
for(int i = 0; i < n; i++){
res ^= i ^ nums[i];
}
return res;
}
}

解析 res ^= i ^ nums[i];

  1. res ^= n;:这一步将 res 与数组的长度 n 进行异或操作。这是因为在缺失一个数字的情况下,数组的长度就是缺失的那个数字,所以将 resn 异或可以将缺失的数字存储在 res 中。
  2. res ^= i ^ nums[i];:这一步是用来处理数组中的其他元素和索引。对于每个索引 i 和对应的元素 nums[i],我们将 inums[i] 进行异或操作,然后再与 res 进行异或操作。这样做的目的是将数组中的元素和索引都进行异或操作,最终得到的结果就是缺失的数字。

通过对数组中的每个元素和索引进行异或操作,重复的元素和索引会互相抵消,而只有缺失的数字会留下来,最终存储在 res 中。最后,将 res 返回作为缺失的数字。