位运算练习

只出现一次的数字

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

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:
输入: [2,2,1]
输出: 1

示例 2:
输入: [4,1,2,1,2]
输出: 4

题目来源:https://leetcode-cn.com/problems/single-number/

思路:由于这里的重复数字只出现了两次,而我们可以想到异或有这样一些性质:

  • x ^ x = 0(即自身和自身进行异或,得到的是0
  • x ^ 1 = ~x(x和1进行异或得到x的相反状态,比如x原本是1,那么和1进行异或之后,变成了0状态,同理,x原本是0,和1进行异或之后变成了1)
  • x ^ 0 = x(x和0进行异或,得到的依旧是自身这个状态

基于上面的性质,由于重复数字只出现了两次,所以重复数字进行异或之后变成了0,当0和只出现一次的数字进行异或时,得到的就是这个只出现过一次的数字。因此可以得出我们本题的思路:遍历整个数组,不断进行异或,最后异或的结果就是数组中只出现过一次的数字(只适用于重复数字只出现2次的情况)

class Solution {
    public int singleNumber(int[] nums) {
       /*
       由于在这个数组中只有一个元素出现一次,其他的元素
       都出现了两次,那么我们将可以知道相同的数字进行异或,那么
       得到的结果是0,所以这时候我们只需要将数组的所有数字进行
       异或,那么最后得到的结果必然是只出现过一次的数字
       */
       int diff = 0;
       for(int num : nums){
           diff ^= num;
       }
       return diff;
    }
}

运行结果:
在这里插入图片描述

只出现一次的数字 II

给你一个整数数组 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
nums 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次
进阶:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

题目来源:https://leetcode-cn.com/problems/single-number-ii/

可能有些小伙伴看到求只出现一次的数字,就会毫不犹豫使用异或运算(方法和上面一题一样),但是最后提交却出现了错误,原来这个题目和上面一题是有区别的。上一题中的重复数字只出现了2次,所以重复数字进行异或之后得到的是0,0和只出现一次的数字进行异或,得到的就是只出现一次的数字了。然而,在本题中,重复数字不只出现了2次,而是出现了3次,这时候我们不可以在使用异或运算了,因为进行异或运算,只能将重复数字中的两个变成0,最后还剩下1个重复数字,例如[2,2,3,2],如果进行异或运算,那么还剩下2,3两个数字要进行异或运算,前两个2已经进行异或变成了0,这时候在将他们进行异或运算最后得到的结果位1,而不是我们想要得到的3.

所以在本题中,不可以使用异或了,那么我们应该怎样通过位运算来解决呢?
我们来思考一下,由于是重复数字,所以对应的二进制数字必然是相同的,因此重复数字中第 i 个二进制数字的和ans必然是0或者3(因为重复3次,1 + 1 + 1 = 3),所以要求的最后只出现一次数字res,那么它的第 i 个二进制数字就是重复数字第 i 个二进制数字的和 % 3(必然为0)+ 只出现1次的数字的第 i 个二进制数字,进而等于数组的所有元素的第 i 个二进制数字的和 % 3
基于此,我们的思路就有了:

  • 因为可能数字有32个二进制数字,所以定义i从0开始,直到31,一共32个二进制数字
  • 在第 i 个二进制数字中,我们都需要获取整个数组元素的第 i 个二进制的数字之和ans。其中一个数字第 i 个二进制数字可以表示为(num >> i) & 1.向将这个数字右移 i ,使得这个数字的第 i 个二进制数字变成最后一个,然后再和1进行与运算,从而得到最后一个数字,达到我们获取这个数字第 i 个二进制数字的目的
  • 当ans % 3不等于0的时候,我们需要更新res的第 i 个二进制数字,使其变成1,同时还需要保证res的其他二进制数字不变,所以需要进行或运算,即res |= (1 << i),即将res和1左移i为之后进行或运算。之所以需要先将1左移i为,是因为1这个数字最后一个二进制数字为1,其他的二进制数字为0,而在此处,我们需要将第 i 个二进制变成1,所以需要将1的最后一个二进制数字1移动到第 i 位,所以是左移。

对应的代码:

class Solution {
    public int singleNumber(int[] nums) {
         /*
         由于重复出现的数字不再是出现两次,所以不可以向上一个题那样,
         通过遍历这个数组,然后不断进行异或运算,最后的结果就是只出现一次的
         数字。

         因此这时候我们需要尝试另一种思路:由于重复元素出现了3次,那么重复元素
         的第i个二进制数相加的和要么是0,要么是3,因此和除3最后的余数都是0,因此
         我们可以通过将所有的数字的最后一个进行相加
         */
         int ans = 0,res = 0,i = 0;
         for(i = 0; i < 32; ++i){
             ans = 0; 
             for(int num: nums){
             //获取数组中所有元素第 i 个二进制数字的和
                 ans += (num >> i) & 1;
             }
             if(ans % 3 != 0){
                 /*
                 如果当前的ans % 3不等于0,那么说明在第i位二进制数必然位1,所以需要
                 将res当前第i个二进制数变成1,所以需要和(1左移i位之后)进行或运算
                 才可以更新res的第i个二进制数,之所以是进行或运算,是为了保持res的其他
                 二进制数不变。
                 而不是通过将res往右移或者往左移来更新,这样会破坏res其他的二进制数字
                 */
                 res |= (1 << i);
             }
         }
         return res;
    }
}

运行结果:
在这里插入图片描述

只出现一次的数字 III

给定一个整数数组 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 中的其他数字都出现两次

题目来源:https://leetcode-cn.com/problems/single-number-iii/

通过观察本题,我们可以知道,重复数字出现了两次,并且数组中有两个只出现一次的数字。那么这时候我们需要进行分组进行异或运算,因为含有两个只出现一次的数字,如果我们向第一道题一样,直接遍历整个数组来进行异或运算的话,那么得到的结果就是数组中两个只出现一次的数字进行异或的结果了。所以这时候我们需要进行分组异或,在每一组中都分配一个只出现一次的数字,然后对每一组进行异或运算即可

所以本题的关键在于,怎样分组?

我们分组的依据主要是根据两个只出现过一次的数字x和y第一位不相同的二进制数字所对应的十进制数字div来进行分组的。因为最后异或的结果等于x和y异或的结果,所以其中一个数字和div进行与运算,得到的不是0,另一个必然为0,基于此,从而实现分组。

下面我们来看一下详细分析过程:
在这里插入图片描述
运行代码:

class Solution {
    /*
    利用哈希表解决。
    public int[] singleNumber(int[] nums) {
        int[] res = new int[2];
        int i = 0;
        Boolean flag;
        HashMap<Integer,Boolean> map = new HashMap<Integer,Boolean>();
        for(int num: nums){
            flag = map.containsKey(num) == true ? false : true;
            map.put(num,flag);
        }
        for(int num: nums){
            if(map.get(num))
              res[i++] = num;
        }
        return res;
    }
    */
    public int[] singleNumber(int[] nums) {
        int[] res = new int[2];
        int diff = 0;
        for(int num: nums){
            //获取所有数字的异或结果
            diff ^= num;
        }
        int div = 1;
        while((diff & div) == 0){
            /*
            找到第一个数组中两个只出现一次的数字中第一个不相同的二进制数字1
            此时对应的数字位div
            */
            div <<= 1;
        }
        for(int num: nums){
            /*
            这时候已经将数组依据div进行分组了,因为就在div这个数字的1那里分组的
            如果和div进行与运算等于0,说明这个数字可能是只出现过一次的数字中
            这个二进制数字位0的,(当然也可能是重复出现的数字)
            否则,如果进行与运算等于1,说明这个数字是只出现过一次的数字中二进制
            数字位1的(也可能是重复出现的数字),因此需要在if语句里面进行了异或运算
            从而获得每一组中只出现过一次的数字.
            */
            if((num & div) == 0){
               res[0] ^= num;
            }else{
               res[1] ^= num;
            }
        }
        return res;
    }
}

对应结果:
在这里插入图片描述

找不同

给定两个字符串 s 和 t,它们只包含小写字母。字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。请找出在 t 中被添加的字母。
示例 1:
输入:s = “abcd”, t = “abcde”
输出:“e”
解释:‘e’ 是那个被添加的字母。

示例 2:
输入:s = “”, t = “y”
输出:“y”

示例 3:
输入:s = “a”, t = “aa”
输出:“a”

示例 4:
输入:s = “ae”, t = “aea”
输出:“a”

提示:
0 <= s.length <= 1000
t.length == s.length + 1
s 和 t 只包含小写字母

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-the-difference
本题可以有三种方法解决:

  • 方法一:计数。统计字符串s中的所有字符出现的次数,并且存放到数组count中,然后遍历字符串t,每获取到一个字符ch,就将count对应下标的值减1,如果减1之后它的值小于0,那么说明这个字符ch就是添加的,直接将ch返回即可。
  • 方法二:求和。首先获取字符串t的所有字符的ASCII码之和res,然后再次遍历字符串s,每遍历到一个字符,就用res减去这个字符的ASCII码,这样,遍历结束之后,res就是添加字符的ASCII码了,只需要将res转成char类型就是添加的字符了。
  • 方法三:位运算。我们观察题目,t是在s的基础上添加一个字母,将t和s的所有字母合并之后s的所有字母必然出现偶数次,那么合并之后出现奇数次的就是在添加的字母,直接将这个字母返回即可。这时候这个题目就转成了寻找出现奇数次的字母,所以和上面的题目一样的道理,重复的字母都出现了偶数次,所以我们可以通过连续异或来求得最后的结果

对应的代码:

class Solution {
    /*
    方法一:
    public char findTheDifference(String s, String t) {
        int[] count1,count2;
        count1 = new int[26];
        count2 = new int[26];
        getCount(count1,s);
        getCount(count2,t);
        char ch = 'a';
        while(ch < 'a' + 26){
            if(count1[ch - 'a'] != count2[ch - 'a'])
               return ch;
            ++ch;
        }
        return '0';
    }
    public void getCount(int[] count,String s){
        char[] chars = s.toCharArray();
        for(char ch: chars){
            count[ch - 'a']++;
        }
    }
    */
    /*
    方法一的优化:
    相对于上面的方法,这里只需要定义一个数组,并且是s的,然后遍历字符串t
    每遍历到一个字符,那么就将数组对应的下标的值减1,如果小于0,说明
    这个字符就是在t中添加的字母
    public char findTheDifference(String s, String t) {
        int[] count = new int[26];
        char[] chars1,chars2;
        chars1 = s.toCharArray();
        chars2 = t.toCharArray();
        for(char ch: chars1){
            count[ch - 'a']++;
        }
        for(char ch: chars2){
            count[ch - 'a']--;
            if(count[ch - 'a'] < 0){
                return ch;
            }
        }
        return ' ';
    }
    */
    /*
    方法二:
    求和,首先将t字符串中所有字母的ASCII码之和统计出来,然后
    在遍历字符串s,将去对应的字符的ASCII码,这样最后的结果就是添加的字符
    的ASCII码,只需要将其转成char类型即可。
    public char findTheDifference(String s, String t) {
       int res = 0;
       char[] chars1,chars2;
       chars1 = s.toCharArray();
       chars2 = t.toCharArray();
       for(char ch: chars2){
           res += ch;
       }
       for(char ch: chars1){
           res -= ch;
       }
       return (char)res;
    }
    */
    /*
    方法三:位运算
    利用异或的方式进行求解:因为只有一个不同的字母,那么将两个字符串合并之后,
    重复的字符出现了偶数次,那么异或之后必然是0,所以最后异或的结果是那个被添加的字母。
    所以这样的形式就转成了求解只出现一次的字母,和只出现一次的数字是一样道理,运用
    异或的方式求解。
    */
    public char findTheDifference(String s, String t) {
       int res = 0;
       char[] chars1,chars2;
       chars1 = s.toCharArray();
       chars2 = t.toCharArray();
       for(char ch: chars1){
           res = res ^ ch;
       }
       for(char ch: chars2){
           res = res ^ ch;
       }
       return (char)res;
    }
   
}

运行结果:
在这里插入图片描述

总结:
寻找一个数组中只出现一次的数字,而重复数字出现了n次,这时候我们通过位运算进行求解的时候需要分几种情况进行求解:

  • 当规定重复数字只出现偶数次(即n为偶数)的时候,那么我们可以通过遍历数组,不断进行异或运算,异或运算最后的结果就是只出现一次的数字
  • 规定重复数字出现奇数次(即n为奇数)时,那么数组所有元素的第 i 个二进制数字的和 % n就是只出现一次的数字的第 i 个二进制数字

丢失的数字

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

进阶:
你能否实现线性时间复杂度、仅使用额外常数空间的算法解决此问题?
示例 1:
输入:nums = [3,0,1]
输出:2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。

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

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/missing-number

方法一:使用两次异或运算
第一次:遍历[0,n]中的数字进行异或运算diff
第二次:遍历数组中元素,每遍历到一个元素,就将其和diff进行异或运算。这样就可以实现将重复数字异或变成0,这样在遍历完数组元素之后,diff就是丢失的数字了。

方法二:进行高斯求和公式
首先将[0,n]中所有数字的和利用高斯求和方式(n + 1) * n / 2来求出,然后遍历数组,用他们的和sum减去遍历得到的元素,遍历完毕之后,剩余的sum就是丢失的数字。

对应的代码:

class Solution {
    /*
    //首先统计[0,n]的异或的结果diff,然后再用diff和数组中的元素进行异或,最后得到的结果就是丢失的数字
    public int missingNumber(int[] nums) {
       int diff = 0;
       for(int i = 0; i <= nums.length; ++i){
           diff ^= i;
       }
       for(int num: nums){
           diff ^= num;
       }
       return diff;
    }
    */
    public int missingNumber(int[] nums) {
        /*
        利用数学方法:先统计[0,n]的数字之和,然后遍历数组,将和减去对应的元素,遍历结束字
        后,剩余的和就是缺失的数字(因为所有的数字都是独一无二的)
        */
       int sum = (nums.length + 1) * nums.length / 2;
       for(int num: nums){
           sum -= num;
       }
       return sum;
    }
}

运行结果:
在这里插入图片描述

颠倒二进制位

颠倒给定的 32 位无符号整数的二进制位。
提示:

请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -1073741825。

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

示例 1:

输入:n = 00000010100101000001111010011100
输出:964176192 (00111001011110000010100101000000)
解释:输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
示例 2:

输入:n = 11111111111111111111111111111101
输出:3221225471 (10111111111111111111111111111111)
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。

提示:

输入是一个长度为 32 的二进制字符串

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reverse-bits

由于是翻转二进制数字,所以我们需要将这个数字的最后一个二进制数字放到第一个即可。所以我们应该怎样获取一个数字的最后一个二进制数字呢?只需要将这个数字和 1 进行与运算即可得到这个数字的最后一个二进制数字
所以要实现数字的最后一个二进制数字放在最开始的位置,只需要不断左移即可

因此本体的思路是通过与运算(获取数字最后一个二进制数字) + 左移即可实现。
对应代码:

public class Solution {
    // you need treat n as an unsigned value
    public int reverseBits(int n) {
      int ans = 0,i = 0;
      while(i < 32){
          //因为有32个二进制数字,所以需要循环32次
          /*
          将ans往左移1个数字.值得注意的是,不可以将ans <<= 1这一步放在
          ans += n & 1的后面。为什么?因为当在执行最后一次循环的时候,已经
          将n的第一个数字放在了ans的后面了,此时已经成功实现了翻转,如果将
          ans <<= 1放在ans += n & 1后面,那么就会导致在已经实现了翻转的数字
          ans的基础再往左移1位,从而导致错误。
          */
          ans <<= 1;
          ans += n & 1; //获取n的最后一个数字
          n >>= 1;//需要将n往右移,从而更新n的最后一个二进制数字
          ++i;
      }
      return ans;
    }
}

运行结果:
在这里插入图片描述

但是看到代码的时候,有些伙伴可能不明白的是:为什么将ans <<= 1放在ans += n & 1之后不可以?一开始我也是这样做的,但是后来仔细思考之后,就会发现为什么错误了。因为在执行最后一次翻转之后,ans已经是正确的结果了,如果我们将ans <<= 1放在ans += n & 1的后面,就会导致ans在已经是正确结果的基础上再次往左移动1位,从而导致错误。这也是为什么需要将ans <<= 1放在ans += n & 1前面的原因。

比特位计数

给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。

示例 1:
输入: 2
输出: [0,1,1]

示例 2:
输入: 5
输出: [0,1,1,2,1,2]

进阶:
给出时间复杂度为O(n*sizeof(integer))的解答非常容易。但你可以在线性时间O(n)内用一趟扫描做到吗?
要求算法的空间复杂度为O(n)。
你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount)来执行此操作。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/counting-bits

方法一:利用的是简单的位运算
每遍历到一个元素,就将调用getCount(int n)来统计n中的二进制数字中有多少个比特1,然后将其返回。

方法二:利用动态规划 + 位运算
状态方程:dp[ i ]表示数字 i 对应的二进制数字有多少个比特1.
转移方程:dp[ i ] = dp[ i >> 1] + (i & 1),也就是如果当前数字 i 的二进制数字第一个比特为0,例如4,它的二进制数字为0100,第一个比特为0(从右边开始算的),那么dp[ 4 ] = dp[ 4 >> 1] = dp[ 2 ],否则,如果当前数字 i 的二进制数字第一个比特为 1 ,那么它的二进制数字dp[ i ] = dp[ i >> 1] + 1.所以为了减少判断直接就是dp[ i ] = dp[ i >> 1] + (i & 1)。
值得注意的是,+的优先级大于&,所以需要在进行 i & 1的时候需要用括号括起来,否则就会出现错误
在这里插入图片描述

返回值:dp

class Solution {
    /*
    方法一:利用位运算,每遍历到一个元素,就调用方法getCount,将这个元素和1相与之后,获取
    这个元素二进制数字中的1的数目
    public int[] countBits(int n) {
       int[] ans = new int[n + 1];
       for(int i = 0; i <= n; ++i){
         ans[i] = getCount(i);
       }
       return ans;
    }
    public int getCount(int n){
        int k = 0,res = 0;
        while(k < 32){
            res += n & 1;
            n >>= 1;
            ++k;
        }
        return res;
    }
    */
    /*
    方法二:利用动态规划 + 位运算
    状态方程dp[i]表示数字i对应的二进制数字中1的个数
    转移方程:dp[i] = dp[i >> 1] + (i & 1) ==》根据这个式子,我们可以得出
    状态方程转移的意思:如果当前数字第一个二进制数字为1(从右边开始算起),那么
    dp[i] = dp[i >> 1] + 1,否则,如果当前数字的第一个二进制数字不为1,那么dp[i] = dp[i 
    >> 1]
    返回值:dp
    */
    public int[] countBits(int n) {
       int[] dp = new int[n + 1];
       for(int i = 0; i <= n; ++i){
         dp[i] = dp[i >> 1] + (i & 1);//注意计算 i & 1的时候需要加上括号,因为存在优先级的问题
       }
       return dp;
    }

}

运行结果:
在这里插入图片描述
总结:
要获取一个数字的二进制数字,那么可以将位运算来实现,将这个数字和1进行与运算,这一步仅仅是获取最后一个比特数字,所以还需要将这个数字右移才可以

public class Main{
    public static void main(String[] args) {
        int[] nums = getBit(4);
        for(int num : nums){
        //输出00000000 00000000 00000000 00000100
            System.out.print(num);
        }
    }
    //获取数字n的二进制,并将其存放到一个数组中
    public static int[] getBit(int n){
        int k = 0,j = 31;
        int[] num = new int[32];
        while(k < 32){
        /*
        因为n & 1获得的是n这个数字最后一个比特数字,所以j是从31开始,然后执行j--操作
        由于在Java中右移>>需要注意,如果是正数,那么就会在高位补0,否则,如果是负数,在高位补1
        所以这里考虑到了n是一个负数的情况,这时候如果循环结束条件为n != 0,那么需要使用无符号右移
        >>>,否则,如果还是使用>>的话,容易陷入死循环中。
        
        因为一个数字的二进制数字有32个比特,无论正负数,所以使用>>的时候,是通过判断k = 32来结束
        循环的。
        */
            num[j--] = n & 1;
            n >>= 1;
            ++k;
        }
        return num;
    }
}

汉明距离

两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目
给你两个整数 x 和 y,计算并返回它们之间的汉明距离。

示例 1:
输入:x = 1, y = 4
输出:2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
上面的箭头指出了对应二进制位不同的位置。

示例 2:
输入:x = 3, y = 1
输出:1

提示:

0 <= x, y <= 231 - 1
题目来源:https://leetcode-cn.com/problems/hamming-distance/

判断两个数字的对应的二进制数字是否相同,只需要进行异或运算,如果相同,就为0,否则为1。所以基于此,本体思路为:
1、我们先获取两个数字的异或结果diff。
2、遍历diff的每一个二进制数字,如果diff中第 i 个二进制数字为0,那么表示x 和 y第 i 个二进制数字相同,否则不同。那么我们将如何实现遍历diff的每一个二进制数字呢?其实我们知道diff有32个二进制数字,只需要获取diff的最后一个二进制数字,然后通过右移 1 个二进制数字来更新diff的最后一个二进制数字就可以实现遍历diff的每一个二进制数字了

对应的代码:

class Solution {
    public int hammingDistance(int x, int y) {
          /*
          判断两个数字对应的二进制是否相同,那么需要运用异或运算进行判断。
          如果两个数字对应的数位相同,那么进行异或运算之后为0,否则为1.
          得到异或运算结果之后,我们只要将这个结果和1进行与运算,这时候我们
          就可以知道最后一个比特是否为1,如果为1,就表示不相同,需要将结果ans+1
          否则ans + 0。之后需要将异或结果右移。
          */
          int res = 0,diff = x ^ y;
          while(diff != 0){
              res += diff & 1;
              diff >>= 1;//右移,更新最后一个二进制数字(正数的右移,高位补0,否则补1)
          }
          return res;
    }
}

运行结果:
在这里插入图片描述

汉明距离总和

  1. 汉明距离总和
    两个整数的 汉明距离 指的是这两个数字的二进制数对应位不同的数量。
    给你一个整数数组 nums,请你计算并返回 nums 中任意两个数之间汉明距离的总和。

示例 1:
输入:nums = [4,14,2]
输出:6
解释:在二进制表示中,4 表示为 0100 ,14 表示为 1110 ,2表示为 0010 。(这样表示是为了体现后四位之间关系)
所以答案为:
HammingDistance(4, 14) + HammingDistance(4, 2) + HammingDistance(14, 2) = 2 + 2 + 2 = 6

示例 2:
输入:nums = [4,14,4]
输出:4

题目来源:https://leetcode-cn.com/problems/total-hamming-distance/

基于上面的题目,我们只要使用嵌套循环,再第二层循环中调用getDistance(int n,int m)来获取n和m这两个数字的汉明距离就行了。尽管思路正确,但是由于数据过大,从而导致超时。那么再本题中我们将如何解决呢?
本题中主要是利用逐位统计的方式。
我们先假设数字的二进制形式为XiXi - 1Xi - 2…X1X0,并且Xi上最多有两种值:要么是1,要么是0,那么我们遍历整个数组的元素,统计在Xi这一位二进制数字中的和sum,那么sum最多等于nums.length,此时整个数组的元素在第 Xi 位二进制数字都为1,那么数组中在第 Xi位二进制数字为0就有nums.length - sum。我们再来看一下汉明距离的定义:汉明距离 指的是这两个数字的二进制数对应位不同的数量,所以在第Xi位二进制数字中算到的汉明距离为sum * (nums.length - sum)(sum表示有sum个第 Xi 位二进制数字位1的元素,nums.length - sum表示有nums.length - sum个第Xi位二进制数字为0的元素)。因为元素最多有32位,所以需要执行32次上面的操作,才可以得到我们想要的结果。

对应的代码:

class Solution {
    public int totalHammingDistance(int[] nums) {
        int ans = 0,tmp;
        for(int i = 0; i < 32; ++i){
            //因为一个数字有32个比特
            tmp = 0;
            for(int num : nums){
                tmp += (num >> i) & 1;//获取所有元素第i个二进制数字的和
            }
            /*
            因为tmp表示的是所有元素第 i 个二进制数字的和,所以最多tmp = nums.length 
            但是因为可能存在第 i 个二进制数字为0,所以第 i 个二进制数字为0的元素个数
            为nums.length - tmp,那么其中一个二进制数字为1到第 i 个二进制数字为0的
            距离为 1 * (nums.length - tmp),从而可以得知当前再第 i 个二进制数字
            中算到的汉明距离为 tmp * (nums.length - tmp)
            */
            ans += tmp * (nums.length - tmp);
        }
        return ans;
    }
}

运行结果:
在这里插入图片描述

2 的幂

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

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

示例 1:
输入:n = 1
输出:true

示例 2:
输入:n = 16
输出:true

示例 3:
输入:n = 3
输出:false

示例 4:
输入:n = 4
输出:true
示例 5:
输入:n = 5
输出:false

提示:

-231 <= n <= 231 - 1

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

题目来源:https://leetcode-cn.com/problems/power-of-two/

判断n是否为2的幂次方,就是判断n中是否能有ans个2,并且2的ans次方刚好等于n。所以我们可以通过循环的方式实现本题:

对应代码:

class Solution {
    public boolean isPowerOfTwo(int n) {
        if(n <= 0)//因为是2的幂次方,所以必然大于0,所以当n小于等于0的时候可以直接返回false
           return false;
        int ans = 0,k = n;//表示幂次方
        while(k >= 2){
        //统计n中有多少个整数2
            k /= 2;
            ++ans;
        }
        //如果2的ans次方刚好等于n,说明n是2的幂次方,否则不是
        return (int)Math.pow(2,ans) == n;
    }
}

运行结果:
在这里插入图片描述
但是我们这里讲的是位运算,当然是要使用位运算来解决啦。那么我们怎么使用位运算来解决咧?
我们来仔细观察一下2的幂次方对应的二进制数字的形式,可以发现n & (n - 1) = 0的时候,n就是2的幂次方,否则不是
所以本题就可以很轻松通过位运算来解决了:

class Solution {
    public boolean isPowerOfTwo(int n) {
       if(n <= 0)
          return false;
       int ans = n & (n - 1);
       return ans == 0;
    }
}

运行结果:
在这里插入图片描述

4的幂

给定一个整数,写一个函数来判断它是否是 4 的幂次方。如果是,返回 true ;否则,返回 false 。

示例 1:
输入:n = 16
输出:true

示例 2:
输入:n = 5
输出:false

示例 3:
输入:n = 1
输出:true

提示:

-231 <= n <= 231 - 1

进阶:你能不使用循环或者递归来完成本题吗?
题目来源:https://leetcode-cn.com/problems/power-of-four/

同样的,我们可以像上一题一样,使用循环的方式来解决,这里重点讲解使用位运算来解决:

方法一:
通过列举1,4,16,64等数字对应的二进制数字,我们可以发现,4的幂次方对应的二进制数字只有一个二进制数字1,并且这个二进制数字1刚好在下标为偶数的位置(从右边开始算,并且下标是从0开始的)。所以判断一个数字是否为4的幂次方,只要判断这个数字的二进制数字1是否在下标为偶数的位置即可,如果不在,说明当前的数字不是4的幂次方,否则,如果这个数字的二进制数字有可能是4的幂次方。那么如果判断这个数字的二进制数字1是否出现在下标为偶数的位置呢?
我们上面说到,4的幂次方对应的二进制数字只有一个二进制数字1,除了这个二进制数字1之外,其他的二进制数字都为0,并且出现在下标为偶数的位置,例如16,对应的二进制数字为10000,所以我们通过将这个数字n和一个数字mask进行相与,根据结果来判断这个数字是否存在二进制数字1,并且是否只有一个二进制数字1,并且这个二进制数字1刚好出现在下标为偶数的位置。其中这个mask数字对应的二进制位(10101010 10101010 10101010 10101010).如果结果为0,表示当前的数字可能是4的幂次方,如果结果不为0,表示当前数字n的二进制数字有二进制数字1落在了下标为奇数的位置,所以可以直接返回false。

mask数字的选择有两种:
①下标为偶数的二进制数字为1,为奇数的二进制数字为0,即01010101 01010101 01010101 01010101,对应的十进制数字为0x55555555,那么这时候最后返回的结果ans不再是ans是否等于0,因为进行相与,所以应该是ans != 0,如果ans不等于0,表示有可能是4的幂次方,否则不是
②下标为偶数的二进制数字为0,为奇数的二进制数字为1,即10101010 10101010 10101010 10101010,对应的十进制数字为0xaaaaaaaa,这时候只需要判断ans是否为0,如果为0,表示有可能是4的幂次方,否则不是

但是为什么结果为0了,不可以直接返回true?因为还可能存在当前数字存在多个二进制数字1,并且这些二进制数字1也落在了下标为偶数的位置。例如5,对应的二进制数字为00000000 00000000 00000000 00000101,此时如果将5和mask进行相与运算得到的结果就是0,但是5并不是4的幂次方,所以我们在进行这一步(和mask进行相与运算)之前需要判断当前数字n是否为2的幂次方,因为当前的数字是4的幂次方,必然也是2的幂次方

class Solution {
    /*
    public boolean isPowerOfFour(int n) {
          
          if(n <= 0)//如果n小于等于0,必然为false,因为4的幂次方必然是大于0的数
             return false;
          if(n == 1)//如果等于1,那么必然时4的0次方,返回true
             return true;
          int ans = 0,k = n;
          while(k >= 4){
              
              值得注意的是,循环条件必须是在k大于等于4进行,否则,如果
              循环条件为k != 1的时候,那么可能会陷入死循环,例如n 等于 3时,
              就陷入了死循环。同样地,循环条件不可以为k != 0,因为先不考虑
              是否会陷入死循环,在计算ans的时候,就会出现错误。
              
             k /= 4;
             ++ans;
          }
          return (int)Math.pow(4,ans) == n;
    }
    */
    public boolean isPowerOfFour(int n) {
          if(n <= 0)//如果n小于等于0,必然为false,因为4的幂次方必然是大于0的数
             return false;
          if(n == 1)//如果等于1,那么必然时4的0次方,返回true
             return true;
          int num = -1431655766;
          int ans = n & (n - 1);//判断是否为2的幂次方,如果为0,表示n是2的幂次方,否则不是
          if(ans != 0)
            return false;//如果不是2的幂次方,直接返回false
          ans =  n & num;
          /*
          因为如果是4的幂次方,它对应的二进制中只有一个1,并且这个1出现在偶数个二进制
          数字的位置(在右边从0开始算的),因此我们只需要判断这个1的位置是否在第偶数
          个二进制数字的位置即可。num的对应的二进制数字为10101010 10101010 10101010 
          10101010 (即第偶数个二进制数字为0,第奇数个二进制数字为1),所以如果是4的幂次
          方,那么n & num结果必然为0,否则,如果不为0,那么不是4的幂次方。

          在进行ans = n & num判断是否为0之前,还需要判断当前的n是否为2的幂次方,因为
          4的幂次方必然是2的幂次方,如果没有判断的话,那么就会有可能出现错误。例如5,对应的
          二进制数字为0101,那么没有判断是否为2的幂次方,直接进行下一步,那么ans = n & num 
          = 0,这时候就返回true,从而导致结果错误
          */
          return ans == 0;
          
    }
}

运行结果:
在这里插入图片描述

方法二:
由于是判断当前的数字是否为4的幂次方,所以我们只需要判断(当前这个数字 - 1)能否%3等于0即可,如果为0,表示当前的数字有可能是4的幂次方,否则不是
为什么模3之后可以进行判断呢?
在这里插入图片描述
但是等于0之后为什么不一定是4的幂次方呢?比如7,(7 - 1) % 3 == 0,但是7并不是4的幂次方,所以在进行这一步之前同样需要判断当前的数字是否为2的幂次方。

class Solution {
    public boolean isPowerOfFour(int n) {
          if(n <= 0)//如果n小于等于0,必然为false,因为4的幂次方必然是大于0的数
             return false;
          if(n == 1)//如果等于1,那么必然时4的0次方,返回true
             return true;
          int ans = n & (n - 1);//判断是否为2的幂次方
          return ans == 0 && (n - 1) % 3 == 0;
    }
}

运行结果:
在这里插入图片描述

数字的补数

  1. 数字的补数
    给你一个 正 整数 num ,输出它的补数。补数是对该数的二进制表示取反。

示例 1:
输入:num = 5
输出:2
解释:5 的二进制表示为 101(没有前导零位),其补数为 010。所以你需要输出 2 。

示例 2:
输入:num = 1
输出:0
解释:1 的二进制表示为 1(没有前导零位),其补数为 0。所以你需要输出 0 。

提示:
给定的整数 num 保证在 32 位带符号整数的范围内。
num >= 1
你可以假定二进制数不包含前导零位。

题目来源:https://leetcode-cn.com/problems/number-complement/

解题思路:
一个正整数和它的补数的关系:

正整数 ^ 补数 = 111…11(正整数有多少个二进制数字,异或结果diff就有多少个1)

所以我们要求这个正整数的补数,我们只需要将正整数和异或结果diff进行异或就可以得到这个正整数的补数了。

class Solution {
    public int findComplement(int num) {
      /*
      一个正整数和它的补数的关系:
      正整数 ^ 补数 = 11111(正整数有多少个二进制数字,异或结果diff就有多少个1)
      所以我们要求这个正整数的补数,我们只需要将正整数和异或结果diff进行异或
      就可以得到这个正整数的补数了。
      */
      int diff = 0,k = 0,j = num;
      while(j != 0){
          diff |= 1 << k;//将diff和左移k之后的1进行或运算,从而使得diff的第k个二进制数字位1,也保证其他的二进制数字不变
          ++k;
          j >>= 1;//统计diff有多少个二进制数字1
      }
      return diff ^ num;
    }
}

运行结果:
在这里插入图片描述

两整数之和

不使用运算符 + 和 - ​​​​​​​,计算两整数 ​​​​​​​a 、b ​​​​​​​之和。

示例 1:

输入: a = 1, b = 2
输出: 3
示例 2:

输入: a = -2, b = 3
输出: 1

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sum-of-two-integers

这时候,我们学习了数字逻辑与电路这门课,我们就会很容易的解决这个问题了。
下面请看一下分析过程:

在这里插入图片描述
基于上面的分析,我们就可以书写代码了:
对应代码:

class Solution {
    public int getSum(int a, int b) {
         int carry = 0,sum = 0,res = 0,k = 0,num1,num2;
         while(k < 32){
            num1 = (a >> k) & 1;
            num2 = (b >> k) & 1;
            sum = num1 ^ num2 ^ carry;//获取当前的和
            carry = (num1 & num2) | (num1 & carry) | (num2 & carry);//获取当前加法产生的进位
            res |= (sum << k);
            ++k;
         }
         return res;
    }
}

运行结果:
在这里插入图片描述

数字范围按位与

给你两个整数 left 和 right ,表示区间 [left, right] ,返回此区间内所有数字 按位与 的结果(包含 left 、right 端点)。

示例 1:

输入:left = 5, right = 7
输出:4
示例 2:

输入:left = 0, right = 0
输出:0
示例 3:

输入:left = 1, right = 2147483647
输出:0

提示:

0 <= left <= right <= 231 - 1

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/bitwise-and-of-numbers-range

我们观察left、right的范围,可以发现这两个数字的范围极大,所以如果每遍历到一个数字就进行与运算,就会发现超时,那怕是按照逐位相与,其中一个比特数字为0,那么最后结果在该比特数字同样为0的做法,同样会发生超时
所以我们将不采用逐个遍历的方式,而是采用公共前缀 + 位运算来解决。
对于[left,right]范围中,所有数字的公共前缀必定是等于left、right这两个端点的公共前缀,因为[left + 1,right - 1]在两者之间
但是知道这个条件又如何呢?我们再来看一下与运算的规则:

x & x = x(x和它自身相与,必定还是自身)
x & ~x = 0(x和它的相反状态相与,必定为0)

所以,这时候我们看到上面的规则,是否有所顿悟呢?既然是公共前缀,那么不管数字有多少,进行与运算之后,这些公共前缀依旧没有发生改变,相反,如果不是公共前缀,那么说明在对应的比特数字上面必定存在着不同的状态(例如A的第i位比特数字为1,B的第i位比特数字位0,此时相与位0),,此时相与必然为0,所以不是公共前缀的比特数字置为0即可

那么问题来了,我们如何得到left和right这两个数字的公共前缀呢?
我们只需要在left不等于right的基础上,不断往右移,并且统计右移了move位,右移之后两者的值相等了,那么这时候left = right = 公共前缀(除去前导0,因为右移正数在高位补0)。其中move表示left、right两者不是公共前缀有多少个比特数字,所以将公共前缀左移move之后就是所有数字与运算的结果了

对应代码:

class Solution {
    /*超时
    public int rangeBitwiseAnd(int left, int right) {
        /*
        if(left <= 1)
          return 0;
        int ans = left;
        while(left <= right){
            ans &= left;
            ++left;
        }
        return ans;
        
    }
    */
    /*
    依旧会发生超时
    public int rangeBitwiseAnd(int left,int right){
        
        //利用逐个比特进行运算
        int k,j,ans = 0,tmp;
        for(k = 0; k < 32; ++k){
            tmp = (left >> k) & 1;//第k个比特的初始值
            if(tmp != 0){
                j = left + 1;
                while(j <= right){
                    if(((j >> k) & 1) == 0){
                        tmp = 0;
                        break;
                    }
                    tmp = 1;
                    ++j;
                }
            }
            ans |= (tmp << k);
        }
        return ans;

    }
    */
    public int rangeBitwiseAnd(int left,int right){
        /*
        思路:利用公共前缀,两个数字相与,那么
        相与之后的结果就是保持两者的公共前缀不变,其余的比特数字都变成了0,
        所以这时候我们在找到公共前缀之后,将这个公共前缀往左移动相应的比特位即可
        得到这两个数字的与运算结果。

        但是这是[left,right]是一个范围,所以我们需要遍历整个数组来获取公共前缀?
        并不需要,我们只需要获取left和right的公共前缀即可。因为left是最小值,right是最大值
        所以最小值和最大值都含有了公共前缀了,那么[left + 1,right - 1]这些数字也必然含有
        这些前缀。

        那么我们如何获取前缀呢?只需要通过将left、right相等的时候表示含有前缀。
        因此需要将两者左移1,每左移1次,就判断两者的值是否相等,如果相等,那么表示获取了公共前缀,
        然后将前缀右移<<shift位,从而将前缀后面的比特数字置为0
        */
        int shift = 0;
        while(left != right){
            left >>= 1;
            right >>= 1;
            ++shift;
        }
        return left << shift;


    }
}

运行结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值