位运算总结

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;

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值