位运算刷题总结
- 29. 两数相除(数学知识+位运算知识)
- 136. 只出现一次的数字(哈希表+位运算)
- 137. 只出现一次的数字 II(哈希表+位运算)
- 190. 颠倒二进制位(&1和|和<<和>>的使用)
- 191. 位1的个数(实际上还是在考察基本的位运算符)
- 201. 数字范围按位与(公共前缀的求法:敲1法和移位法)
- 231. 2 的幂(敲1法 n & (n- 1)以及正负与( n & (-n)))
- 260. 只出现一次的数字 III(哈希表+)
- 371. 两整数之和(异或运算符)
- 405. 数字转换为十六进制数(补码反码,进制转换)
- 461. 汉明距离(比较对应位,敲1法)
- 476. 数字的补数(敲1法和正负与)
- 477. 汉明距离总和(数学上的理解)
- 1318. 或运算的最小翻转次数(分位技巧+数学分析)
29. 两数相除(数学知识+位运算知识)
注意:下面的解法,时间超时了!!!因为,如果被除数是最大值,除数是1,那么循环了O(n),最大值的时间复杂度达到了10^10,超过10 ^8 ,O(n)算法大概率超时。而且下面的算法还使用了long
class Solution {
public int divide(int dividend, int divisor) {
/**
分析:
题目中要求很明确了,不能使用乘法,除法,和mod运算符。那么就是思考能不能用减法了!!!
按照除法公式:(被除数-余数)÷除数=商,记作:被除数÷除数=商...余数,是一种数学术语。
* 在一个除法算式里,被除数、余数、除数和商的关系为:(被除数-余数)÷除数=商,记作:被除数÷除数=商...余数,
* 进而推导得出:商×除数+余数=被除数。
如果两个数都是正数或者负数,以上操作是可行的,如果是一正一负呢? 这时候第一个想法就是都转换为正数
*/
// 边界条件的判断
if( dividend == Integer.MIN_VALUE && divisor == -1){
return Integer.MAX_VALUE;
}
// 使用异或判断两个数是否同号
int sign = (dividend > 0) ^ (divisor > 0) ? -1 : 1;
// 边界条件 使用了long(题目其实规定不可以的)
long la = Math.abs((long) dividend);
long lb = Math.abs((long) divisor);
int res = 0;
while( la >= lb){
la -= lb;
res++;
}
// 不能使用乘法,所以使用了三目运算符
return sign == 1 ? res : -res;
}
}
优化版:
class Solution {
public int divide(int dividend, int divisor) {
if(dividend == 0)
return 0;
// 这些条件真的是。。。
if (dividend == Integer.MIN_VALUE && divisor == -1) {
return Integer.MAX_VALUE;
}
if(divisor == Integer.MIN_VALUE && dividend == Integer.MIN_VALUE)
return 1;
if(divisor == Integer.MIN_VALUE)
return 0;
//****进入常规题解*****//
// 先确定最终结果正负号
int op = dividend < 0 ? -1 : 1;
op = divisor < 0 ? -op : op;
// 全部转为负数计算
dividend = -Math.abs(dividend);
divisor = -Math.abs(divisor);
// 思路:****除法的其实就是是循环减法****
int res = 0;
while(dividend <= divisor){
dividend -= divisor;
res++;
}
return res*op;
}
}
再优化,位运算(有点难理解)
class Solution {
public int divide(int dividend, int divisor) {
/**
分析:
题目中要求很明确了,不能使用乘法,除法,和mod运算符。那么就是思考能不能用减法了!!!
按照除法公式:(被除数-余数)÷除数=商,记作:被除数÷除数=商...余数,是一种数学术语。
* 在一个除法算式里,被除数、余数、除数和商的关系为:(被除数-余数)÷除数=商,记作:被除数÷除数=商...余数,
* 进而推导得出:商×除数+余数=被除数。
如果两个数都是正数或者负数,以上操作是可行的,如果是一正一负呢? 这时候第一个想法就是都转换为正数
*/
// 边界条件的判断
if( dividend == Integer.MIN_VALUE && divisor == -1){
return Integer.MAX_VALUE;
}
// 使用异或判断两个数是否同号
int sign = (dividend > 0) ^ (divisor > 0) ? -1 : 1;
// 边界条件 使用了long(题目其实规定不可以的)
// long la = Math.abs((long) dividend);
// long lb = Math.abs((long) divisor);
// 转换为正数,这里要注意边界条件 -2^31转换不变(因为会越界)
dividend = Math.abs(dividend);
divisor = Math.abs(divisor);
int res = 0;
// while( la >= lb){
// la -= lb;
// res++;
// }
// 使用位运算
for(int i = 31; i >= 0; i--){
// 若使用左移运算,则有可能越界,所以采用右移运算
// 其次,要采用无符号右移,将 -2147483648 看成 2147483648
// 注意,这里不能是(a >>> i) >= b 而应该是 (a >>> i) - b >= 0
// 这个也是为了避免 b = -2147483648,如果 b = -2147483648
// 那么 (a >>> i) >= b 永远为 true,但是 (a >>> i) - b >= 0 为 false
if( (dividend >>> i) - divisor >= 0){
// 更新
dividend -= (divisor << i);
res += (1 << i);
}
}
return sign == 1 ? res : -res;
}
}
136. 只出现一次的数字(哈希表+位运算)
注意:以下解法为哈希表,空间复杂度为o(n),不满足题意
class Solution {
public int singleNumber(int[] nums) {
/**
分析:
涉及到重复问题,第一想法就是使用哈希表,但是空间复杂度为O(n)
*/
Set<Integer> set = new HashSet<>();
for(int num:nums){
if(set.contains(num)){
set.remove(num);
}else{
set.add(num);
}
}
// 获取set中的元素,使用迭代器,迭代器.next()代表取出元素
return set.iterator().next();
}
}
注意:以下代码的思想是使用了位运算异或。其中0和任何数异或都等于它本身,两个相同数之间异或等于0,那么遍历异或结束后,就只剩下单个的值了!
class Solution {
public int singleNumber(int[] nums) {
/**
分析:
涉及到重复问题,第一想法就是使用哈希表,但是空间复杂度为O(n)
不需要额外空间,那么第一想法就是往位运算那边思考,或者双指针等等。
这里是一个异或的应用。
i ^ 0 = i
i ^ i = 0;
且异或满足交换律和结合律,那么遍历循环后,最后的异或结果就是我们的答案(这了可以将相同元素的个数拓展到3.4.5.6...n个)
*/
int res = 0;
for(int num:nums){
// 对数字进行异或,0和任何数字异或都是它本身
// 相同数字之间异或就是0
res ^= num;
}
// 返回最后的结果
return res;
}
}
137. 只出现一次的数字 II(哈希表+位运算)
注意:以下代码是使用了哈希表的记忆功能,其中取键值的方法还需要继续练习!相关的api都忘的差不多了,毕竟不经常使用啊
class Solution {
public int singleNumber(int[] nums) {
/**
本题是上一题的进阶,出现三次,那么第一想法还是使用哈希表,不过这里要准备储存次数了
*/
Map<Integer,Integer> map = new HashMap<>();
for(int num:nums){
// 储存个数 键为数组中的数字 , 值为 个数
map.put( num,map.getOrDefault(num,0)+1);
}
// 遍历map,这里使用了 Map.Entry<Integer, Integer>的加强for循环遍历输出键key和值value
for(Map.Entry<Integer,Integer> temp : map.entrySet()){
if( temp.getValue() == 1){
return temp.getKey();
}
}
// 下面这种也可以
// for (int x : map.keySet()) {
// if (map.get(x) == 1) return x;
//}
return 0;
}
}
注意:以下代码使用了位运算。这道题有很多心得体会。
首先,int类型可以表示为32位的01数组,其次,32位中,每个位置1的计数可以遍历使用右移运算i,然后&1来确定当前位是1还是0
对于不同位置的1,要使用左移运算符来还原,二进制1之间的拼接采用|运算符
class Solution {
// https://leetcode-cn.com/problems/single-number-ii/solution/ti-yi-lei-jie-wei-yun-suan-yi-wen-dai-ni-50dc/
public int singleNumber(int[] nums) {
int res = 0;
//int类型有32位,统计每一位1的个数
for (int i = 0; i < 32; i++) {
//统计第 i 位中 1 的个数
int oneCount = 0;
for (int num : nums) {
// 统计对应位的1的个数
oneCount += (num >> i) & 1;
}
//如果1的个数不是3的倍数,说明那个只出现一次的数字
//的二进制位中在这一位是1
if (oneCount % 3 == 1) {
// 将不同位使用 | 拼接起来!!!
res |= 1 << i;
}
}
return res;
}
}
190. 颠倒二进制位(&1和|和<<和>>的使用)
注意:本题重点理解&1和|和<<和>>的使用场景,并且要清楚int类型是32位的二进制数
public class Solution {
// you need treat n as an unsigned value
public int reverseBits(int n) {
/**
分析:
其实本题就是整数数字的反转,在开发中直接调用api函数就可以,但是这里是算法题。而且题目也已经暗示了使用二进制,所以这里使用位运算。
首先,将res左移一位(初始化为0),取n的最后一位,将n的最后一位拼接到res的末尾,最后n右移一位。
*/
// return Integer.reverse(n);
int res = 0;
for(int i = 0; i < 32; i++){
// 这里的 | 或运算符就是达到拼接1的功效
// 一个数和1进行&与运算,实际上就是取其末尾的数字
res = (res << 1) | (n & 1);
// 记得将n右移,这样后续才能取低位的数字
n = n >> 1;
}
return res;
}
}
191. 位1的个数(实际上还是在考察基本的位运算符)
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
/**
分析:
第一想法就是遍历,和1运算然后累加起来
*/
int res = 0;
for(int i = 0; i < 32; i++){
// 从低位到高位开始累加1
res += (n & 1);
// 右移
n >>= 1;
}
return res;
}
}
注意:关键是学习敲1法, n & (n - 1)
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
/**
分析:
第一想法就是遍历,和1运算然后累加起来.
看了题解后学到了一个新的位运算算法,敲1法,n &( n - 1)就可以将n中最后一个1敲掉!,循环遍历,计算敲掉1的个数!
*/
int count = 0;
while(n != 0){
// 使用敲1法
n = n & (n - 1);
// 计数
count++;
}
return count;
}
}
201. 数字范围按位与(公共前缀的求法:敲1法和移位法)
注意:一下代码是最常规的思路,没有任何套路,同时也意味着时间会超时!!!舍弃!
class Solution {
public int rangeBitwiseAnd(int left, int right) {
/**
分析:
按位与 实际上就是对应位置上都是1,才能是1.
下面使用循环遍历,依次与运算,发现时间超时了...
*/
// 特判
if(left == right){
return left;
}
int i = left,j = left + 1;
for(; j <= right; j++){
// 定义一个临时储存按位与的结果
int temp = 0;
temp = i & j;
i = temp;
}
return i;
}
}
注意:下面的解法是移位法!!!一个注意点,连续区间中的剩余部位必然存在一个0(因为这是连续的)
class Solution {
public int rangeBitwiseAnd(int left, int right) {
/**
分析:
使用暴力遍历失败后,开始思考其底层数学逻辑。
解法一:所有数字按位与,其实就是在求最长的公共前缀(背景知识:0&1=0,0&0=0),由于这个是连续的区间,那么公共前缀后面的剩下部位,必然存在一个数的当前位为0(因为这是连续区间,连续数之间必然间隔为1),所以所有数的剩下部位&后必然是0,那么只要将公共前缀部分还原为数字就可以(<<运算,左移剩余部位的个数)
*/
// 用于计数,公共前缀外的计算剩余部位的个数
int count = 0;
// 循环条件是left < right,意味着当left == right时候,找到了公共前缀
while(left < right){
// 两边开始移位
left >>= 1;
right >>= 1;
// 计算移位个数
count++;
}
// 最后是left的值就是公共前缀,将其还原
return left << count;
}
}
注意:下面的解法是敲1法,敲1法的思路和上一题思路一样 n & (n -1)
class Solution {
public int rangeBitwiseAnd(int left, int right) {
/**
分析:
使用暴力遍历失败后,开始思考其底层数学逻辑。
解法一:所有数字按位与,其实就是在求最长的公共前缀(背景知识:0&1=0,0&0=0),由于这个是连续的区间,那么公共前缀后面的剩下部位,必然存在一个数的当前位为0(因为这是连续区间,连续数之间必然间隔为1),所以所有数的剩下部位&后必然是0,那么只要将公共前缀部分还原为数字就可以(<<运算,左移剩余部位的个数)
解法二:使用敲1法,把right中除公共前缀外的1全都敲掉,结果就是公共前缀的值了
*/
// 敲1法
while(left < right){
right = right & (right - 1);
}
return right;
}
}
231. 2 的幂(敲1法 n & (n- 1)以及正负与( n & (-n)))
注意:以下代码,按照题干是不符合要求的
class Solution {
public boolean isPowerOfTwo(int n) {
/**
分析:
第一想法是循环
*/
if( n < 0){
// 小于0,必不存在
return false;
}
for(int i = 0; i < 32; i++){
// 依次判断
if(Math.pow(2,i) == n){
return true;
}
}
return false;
}
}
注意:以下的代码重要的是分析过程,以及敲1法的使用和正负与的使用。
x&(-x):保留二进制下最后出现的1的位置,其余位置置0(即一个数中最大的2的n次幂的因数
x&(x-1):消除二进制下最后出现1的位置,其余保持不变
class Solution {
public boolean isPowerOfTwo(int n) {
/**
分析:
第一想法是循环。但是题目说不能使用循环,那么就要思考能不能使用位运算。
在int类型中,是2的幂次方,无非就是寻找二进制中1的位置规律,比如 2^0 2^1 2^2....2^30
观察这些数的二进制位会发现,他们只有一个1,其余都是0,随机取一个数4来进行测试,若 4 & 3则为0(因为4和3之间的二进制就差1),所以就可以用敲1法,敲掉最后一个1,敲完后是0,那么就满足题意了。
或者使用正负与,若正负与后等于它本身,那么就说明只有1个1,满足了题意
*/
if( n <= 0){
return false;
}
// 使用敲1法,判断是否敲完1为0,是的话就满足题意
return (n & (n - 1)) == 0;
// 使用正负与运算
//return (n &( -n)) == 0;
}
}
260. 只出现一次的数字 III(哈希表+)
注意:以下解法是使用哈希表,在面试的时候是不允许的,明确规定使用空间复杂度为O(1)
class Solution {
public int[] singleNumber(int[] nums) {
/**
分析:
哈希表具有记忆功能,首先就是考虑哈希表。
*/
Map<Integer,Integer> map = new HashMap<>();
for(int num:nums){
map.put( num,map.getOrDefault(num,0) + 1);
}
int[] res = new int[2];
int count = 0;
// 取出数据,遍历键对
for(int x :map.keySet()){
// 只出过一次
if( map.get(x) == 1){
res[count++] = x;
}
}
return res;
}
}
注意:前置知识。。。
x&(-x):保留二进制下最后出现的1的位置,其余位置置0(即一个数中最大的2的n次幂的因数,其中-a的意思是取a的负数,而负数的补码是对应的正数各位取反,末位加一)
x&(x-1):消除二进制下最后出现1的位置,其余保持不变
位运算中异或运算具有交换律,也就是
A^ B^ C=A^ C^B
我们还知道 一个数字和自己异或,结果是0,也就是
A^A=0;
任何数字和0异或结果还是他自己
A^0=A;
一句话异或yyds
class Solution {
public int[] singleNumber(int[] nums) {
/**
分析:
哈希表具有记忆功能,首先就是考虑哈希表。但是题目要求常数空间时间复杂度,第一想法就是双指针,但是本题使用双指针要先排序,这样时间复杂度又不满足要求了
*/
// 把所有元素进行异或操作,最终得到一个异或值。因为是不同的两个数字,所以这个值必定不为0
int bitmask = 0;
for(int num : nums){
bitmask ^= num;
}
// 取异或值最后一个二进制位为1的数字作为diff,如果是1,则表示两个数字在这一位上不同
int diff = bitmask & (-bitmask);
// 通过与这个diff进行与操作,如果为0的分为一组,为1的分为另一组
// 这样就把问题降低成了:“有一个数组每个数字都出现两次,有一个数字只出现了一次,求出该数字”
int[] res = new int[2];
for(int num : nums){
if((num & diff) != 0){
res[0] ^= num;
}else{
res[1] ^= num;
}
}
return res;
}
}
题解中mask的主要作用是能将数组中只出现一次的那两个数区分开来,至于其他的数字,均是出现两次,所以一定会被分在同一数组。如此就达到了降维的目的。从这个意义上将,mask的取值是不唯一的,只要能区分要寻找的那两个数即可。
371. 两整数之和(异或运算符)
本题目由于不能使用运算符,所以使用位运算来进行求解。
由位运算性质可以看出,异或运算(^)可以得到两数的无进位之和,
进位可以通过与运算(&)得到,即( a & b ) << 1
例如:
4(0100) + 6(0110) = 10(1010)
无进位之和为: 0100 ^ 0110 = 0010
进位为: (0100 & 0110) << 1 = 1000
进位之后结果为:0010 ^ 1000 = 1010,即10
class Solution {
public int getSum(int a, int b) {
while (b != 0) { //当b为0,即无位可进时,break
int carry = (a & b) << 1; // carry为需要进的数位
a = a ^ b; //新计算后的无进位之a和与进位b进行异或运算并赋值给a
b = carry; //把carry赋值给b
}
return a;
}
}
注意:上一版代码理解起来好难,下面的代码比较好理解,通过不断判断进位,进行迭代更新 a 和 b的值,其中a代表着无进位间的和,b代表着进位,最终将进位异或一下既可以获取答案。
class Solution {
public int getSum(int a, int b) {
/**
分析:
题目暗示的很明显了:使用位运算实现加法.
本题目由于不能使用运算符,所以使用位运算来进行求解。
由位运算性质可以看出,异或运算(^)可以得到两数的无进位之和,
进位可以通过与运算(&)得到,即( a & b ) << 1
例如:
4(0100) + 6(0110) = 10(1010)
无进位之和为: 0100 ^ 0110 = 0010
进位为: (0100 & 0110) << 1 = 1000
进位之后结果为:0010 ^ 1000 = 1010,即10
*/
// 定义一个进位
int carry = 0;
// 判断有无进位,若为0说明无进位,不进入循环体,直接return 无进位之间的异或运算
carry = a & b;
while(carry != 0){
// 若有进位,先进行无进位之和
a = a ^ b;
// 进位移动一位,确保在二进制正确的位置,同时赋值给b
b = carry << 1;
// 将无进位之和 & 进位,为新的进位判断条件
carry = a & b;
}
// 返回的是最后的 无进位和 异或 进位的值
return a ^ b;
}
}
405. 数字转换为十六进制数(补码反码,进制转换)
下面的解法是错误的,天真的以为java是可以提供无符号整形的。
class Solution {
public String toHex(int num) {
/**
分析:
在数学上,使用辗转相除法,每次保留余数,最后将余数倒序输出,即完成了进制的转换。
本题的是十六进制,并且明确表示十六进制中的字母是小写的,所以可以提前定义好一个char数组。
然后不断对数取余16,再除以16,拼接到字符串,最后反转字符串即可。
但是有一个问题,如果是负数?这时候就得提前将负数转换为无符号整数(最高位不代表符号了,而代码数字,而且是以补码的形式存在)
如int 类型的 -1(1000 0000 0000 0001) 转换为 unsigned int 类型后 就是(1111 1111 1111 1110)这个答案就是254,也就是-1转换成了254
*/
// 特判
if(num == 0){
return "0";
}
// 定义一个数组
char[] s = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
// 定义结果字符串用于拼接
StringBuilder sb = new StringBuilder();
// 将num 变换为无符号整形,我滴妈呀!!!java居然不提供无符号类型!!!奔溃
unsigned int n = num;
while(n != 0){
// 拼接
sb.append(s[n % 16]);
// 更新
n /= 16;
}
return sb.reverse().toString();
}
}
那么核心问题就是将上面的算法改成无符号位运算(也就是补码形式),想到了java中的<<< 和 >>>这两个就完美了解决了负数的问题。那么如果是一般的有符号位在移动时,负数的符号位为1,右移过程中就陷入死循环(最高位1不变,后面的位置一直增加1)
class Solution {
public String toHex(int num) {
/**
分析:
在数学上,使用辗转相除法,每次保留余数,最后将余数倒序输出,即完成了进制的转换。
本题的是十六进制,并且明确表示十六进制中的字母是小写的,所以可以提前定义好一个char数组。
然后不断对数取余16,再除以16,拼接到字符串,最后反转字符串即可。
但是有一个问题,如果是负数?这时候就得提前将负数转换为无符号整数(最高位不代表符号了,而代码数字,而且是以补码的形式存在)
如int 类型的 -1(1000 0000 0000 0001) 转换为 unsigned int 类型后 就是(1111 1111 1111 1110)这个答案就是254,也就是-1转换成了254
*/
// 特判
if(num == 0){
return "0";
}
// 定义一个数组
char[] s = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
// 定义结果字符串用于拼接
StringBuilder sb = new StringBuilder();
// 将num 变换为无符号整形,我滴妈呀!!!java居然不提供无符号类型!!!奔溃
// 但是呢,java又提供了一种位运算来解决缺失无符号位的问题 <<< 和 >>>(最高位移动后是补0的)
// unsigned int n = num;
while(num != 0){
// 拼接
// 在而进制中 取低位数字是 &1 然后右边移动一位,那么在十六进制中就是 &15 右边移动四位
sb.append(s[num & 15 ]);
// 更新,注意这里是无符号位移动
num >>>= 4;
}
return sb.reverse().toString();
}
}
这里补充一下原码、补码、反码的知识。
原码 = 最高位(符号:0正1负) + 低位(数值)
比如: 7的原码为 0000 0000 0000 0111
-7的原码为 1000 0000 0000 0111
反码:正数的反码就是原码,负数的反码是除了最高位(符号位)外,其余位置取反。
以-7为例子,-7的反码是 1111 1111 1111 1000
补码:正数的补码就是原码,负数的补码就是在反码的基础上加1(记忆:反码基础上补刀1)
以-7为例子,在反码基础上加1 就是 1111 1111 1111 1001
所以说,反码就是原码和补码的一个过渡阶段。
461. 汉明距离(比较对应位,敲1法)
注意:一下代码为比较对应位置
class Solution {
public int hammingDistance(int x, int y) {
/***
分析:
其实就是比较对应位置是否不同,然后计数
*/
int count = 0;
while( x != 0 || y != 0){
int posX = x & 1;
int posY = y & 1;
if((posX == 1 ? true : false) ^ (posY == 1 ? true : false) ){
count++;
}
x >>= 1;
y >>= 1;
}
return count;
}
}
注意:以下使用敲1法
class Solution {
public int hammingDistance(int x, int y) {
/***
分析:
其实就是比较对应位置是否不同,然后计数.
或者先异或,然后敲1,敲掉1的个数就是汉明距离。
*/
// 敲1法
int res = 0;
int temp = x ^ y;
while(temp != 0){
temp = temp &(temp - 1);
res++;
}
return res;
}
}
476. 数字的补数(敲1法和正负与)
class Solution {
public int findComplement(int num) {
/**
分析:
思路很简单,对每个二进制位数和全是1异或,然后就可以转换为十进制。
但是,实际上数字储存二进制是32位储存的,跟32位全是1的数异或,显然不合理。
很明显,我们要求的是最后一个1的位置,然后将最后一个1的位置左移一位再减1,最后就实现了合理位数全是1的需求了。
这里还是使用了敲1法,正负与求最右边1
*/
int temp = num;
// 定义最高位是1的数
int highBit = 0;
while(temp != 0){
// 储存最右边的最高位为1的数字
highBit = temp & (-temp);
// 敲掉1
temp = temp & (temp - 1);
}
// num和 最后一个1的位置左移一位再减1,最后就实现了合理位数全是1
return num ^ ((highBit << 1) - 1);
}
}
477. 汉明距离总和(数学上的理解)
注意:以下代码暴力法,时间超时。要去思考怎么降低时间复杂度。一般来说位运算O(n^2)降低时间复杂读就是使用一个32位的for循环降低为O(n)
class Solution {
public int totalHammingDistance(int[] nums) {
/**
分析:
定义一个汉明距离的函数,然后遍历叠加距离,问题解决。呜呜呜,最后时间超时了。。。。O(n^2)
汉明距离函数:
两个数每个位置的数字进行与运算,与的结果是0,则计数。
现在是要把时间复杂度降低下来,在位运算题目中,把复杂度降低为O(n)的操作一般都是写一个32次的for循环,恰如“仅包含字母”的题目一般都是写一个26次的for循环。
*/
int total = 0;
for(int i = 0; i < nums.length - 1; i++){
for(int j = i + 1; j < nums.length; j++){
total += distance(nums[i],nums[j]);
}
}
return total;
}
public int distance(int x, int y){
int count = 0;
while( x != 0 || y != 0 ){
int temp1 = x & 1;
int temp2 = y & 1;
if( (temp1 == 1 ? true : false) ^ (temp2 == 1 ? true : false)){
// 计数
count++;
}
// 移动位置
x >>= 1;
y >>= 1;
}
return count;
}
}
注意:以下算法的核心是理解,x位的0和y位的1,总共可以贡献出 x * y 的汉明距离,这里仔细推理,是不难理解的!那么就用一个32位数组存储第i位所有数字1的个数,那么0的个数就是数组长度-第i位所有数字1的个数。
核心:假设现在我们有 xx 位数的最后一位比特位是 0,然后有 yy 位数的最后一位比特位是 1。那么这个比特位贡献的汉明总距离则为 x * y。
为什么?一个人可以买 10 根冰棒,那么 5 个人自然就可以买 5*10 冰棒。同理一个0对应 yy 个 1 则贡献1 * y的汉明距离,那么 xx 个0对应 yy 个1自然就贡献 x * y的汉明距离。不难理解吧。
class Solution {
public int totalHammingDistance(int[] nums) {
/**
分析:
定义一个汉明距离的函数,然后遍历叠加距离,问题解决。呜呜呜,最后时间超时了。。。。O(n^2)
汉明距离函数:
两个数每个位置的数字进行与运算,与的结果是0,则计数。
现在是要把时间复杂度降低下来,在位运算题目中,把复杂度降低为O(n)的操作一般都是写一个32次的for循环,恰如“仅包含字母”的题目一般都是写一个26次的for循环。
*/
// 使用数学思想。
// 用于储存第i位1的个数
int[] cnt = new int[32];
// 定义结果
int total = 0;
// 遍历计算所有数字
for(int num : nums){
int i = 0;
// 对每个数字的分位进行判断
while( num != 0){
if((num & 1) == 1){
// 计数1
cnt[i] += 1;
}
// 数字右移
num >>= 1;
// 位置前进
i++;
}
}
// 使用算法,每个分位的0的个数*每个分位1的个数
for(int res:cnt){
total += res * (nums.length - res);
}
return total;
}
}
1318. 或运算的最小翻转次数(分位技巧+数学分析)
注意:这道题非常好!!!能学到一些分析技巧和分位技巧。
class Solution {
public int minFlips(int a, int b, int c) {
/**
分析:
刚开始见到这题有点不知所措,完全没有思路。看完题解后才恍然大悟,这是一道位运算基础运算符、基础运算技巧和数学的结合体。
解决这道题得有意识:
对于十进制整数 x,我们可以用 x & 1 得到 x 的二进制表示的最低位,它等价于 x % 2:
例如当 x = 3 时,x 的二进制表示为 11,x & 1 的值为 1;
例如当 x = 6 时,x 的二进制表示为 110,x & 1 的值为 0。
对于十进制整数 x,我们可以用 x & (1 << k) 来判断 x 二进制表示的第 k 位(最低位为第 0 位)是否为 1。如果该表达式的值大于零,那么第 k 位为 1:
例如当 x = 3 时,x 的二进制表示为 11,x & (1 << 1) = 11 & 10 = 10 > 0,说明第 1 位为 1;
例如当 x = 5 时,x 的二进制表示为 101,x & (1 << 1) = 101 & 10 = 0,说明第 1 位不为 1。
对于十进制整数 x,我们可以用 (x >> k) & 1 得到 x 二进制表示的第 k 位(最低位为第 0 位)。如果 x 二进制表示的位数小于 k,那么该表达式的值为零:
例如当 x = 3 时,x 的二进制表示为 11,(x >> 1) & 1 = 1 & 1 = 1,说明第 1 位为 1;
例如当 x = 5 时,x 的二进制表示为 101,(x >> 1) & 1 = 10 & 1 = 0,说明第 1 位为 0。
例如当 x = 6 时,x 的二进制表示为 110,(x >> 3) & 1 = 0 & 1 = 0,说明第 3 位为 0。
=================================================================================
有了上面的意识后,那么逆向分析 c的分位是0,那么翻转个数就是 a的分位 + b的分位
c的分位是1,那么翻转个数就是 a的分位 + b的分位 == 0 ? 1 : 0
*/
int res = 0;
for(int i = 0; i < 32; i++){
// 依次取分位
int bit_a = (a >> i) & 1;
int bit_b = (b >> i) & 1;
int bit_c = (c >> i) & 1;
// 判断c的分位
if(bit_c == 0){
res += bit_a + bit_b;
}else{
res += (bit_a + bit_b == 0 ? 1 : 0);
}
}
return res;
}
}