位运算(代码实现1)

162 阅读9分钟

剑指 Offer 15. 二进制中1的个数

知识点:数学;位运算

题目描述

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为 汉明重量).)。

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

示例
输入:n = 11 (控制台输入 00000000000000000000000000001011)
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。

输入:n = 128 (控制台输入 00000000000000000000000010000000)
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。

输入:n = 4294967293 (控制台输入 11111111111111111111111111111101,部分语言中 n = -3)
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。

解法一:位运算

直接统计每一位上1的个数,注意java中无符号右移是>>>;只要n为0后就可以停止了。

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int res = 0;
        while(n != 0){  //n为0就可以停止了
            res += n & 1;
            n = n >>> 1;  //无符号右移;
        }
        return res;
    }
}

相关链接

不用加减乘除做加法(位运算,清晰图解)

剑指 Offer 65. 不用加减乘除做加法

知识点:数学;位运算

题目描述

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

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

解法一:位运算

不能用四则运算,那其实可以用的只要逻辑运算和位运算了,这里很明显要用位运算。关键怎么用呢。

我们列一张表其实就可以发现,两个元素如果没有进位也就是不是对应位都是1的时候,相加就和异或运算的结果是一样的;如果有进位,也就是两位上都是1的时候,相加就是两者相与然后左移一位;所以我们就可以做了。

可以依次得到两个数的异或结果和与结果;
sum=a+b=sum+carry;但是求得这两个后还是要加,由于不能用加法,所以可以再求这两个结果的与结果和异或结果。直到最后进位的为0,sum就是答案了。

class Solution {
    public int add(int a, int b) {
        int sum = a^b;  //无进位的;
        int carry = (a&b) << 1;  //有进位的;
        while(carry != 0){  //进位为0的时候返回sum;
            a = sum;
            b = carry;
            sum = a^b;
            carry = (a & b) << 1;
        }
        return sum;   
    }
}

相关链接

不用加减乘除做加法(位运算,清晰图解)

136. 只出现一次的数字

知识点:哈希表;set;消消乐;位运算

题目描述

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

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

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

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

异或运算有几个很重要的性质:

1.任何数和0异或,仍为本身:a⊕0 = a
2.任何数和本身异或,为0:a⊕a = 0
3.异或运算满足交换律和结合律:a⊕b⊕a = (a⊕a)⊕b = 0⊕b = b

可以利用第2条和第3条性质将整个数组异或运算,那最后剩下的那个就是只出现一次的。

class Solution {
    public int singleNumber(int[] nums) {
        int ans = 0;
        for(Integer i : nums){
            ans ^= i;
        }
        return ans;
    }
}

体会

上面的解法整体上分为2种:一种就是将每个元素出现的次数统计出来,然后我们找到出现一次的;另一种就是将重复的两两抵消,那最后剩下的就是我们要的。其实方法2-6都是用的这种思路,两两抵消.剩下的就是。其中位运算是最快的,而且不用开辟新的空间。

掌握位运算的解决方法:这种题目往往要按位与、按位异或等操作

异或运算有几个很重要的性质:

1.任何数和0异或,仍为本身:a⊕0 = a
2.任何数和本身异或,为0:a⊕a = 0
3.异或运算满足交换律和结合律:a⊕b⊕a = (a⊕a)⊕b = 0⊕b = b

此外还会有左移<<;右移>>等;比如说:

a & 1 : a的其他位全为0,最后一位不变:即取a最后一位;
a | (1 << i) : a的其他位不变,把a的第i位置为1;
(a >> i) & 1 : 取出a第i位上的值;

目前掌握的题型中触发位运算的是:题目中含有出现次数的问题;

260. 只出现一次的数字 III(剑指 Offer 56 -I)

知识点:数组;位运算;消消乐

题目描述

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。

进阶:你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?

示例
输入:nums = [1,2,1,3,2,5]
输出:[3,5]
解释:[5, 3] 也是有效的答案。

输入:nums = [-1,0]
输出:[-1,0]

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

解法一:位运算

这个题是LeetCode 136的升级,从一个元素变成了两个元素,所以我们可以采用分组的方法来解决,那么在每个组上再采用和136题一样的思路按位异或就可以了,分组的关键在于两点:

  • 1.两个只有一个的分在不同组;
  • 2.重复的数字要在一个组;
    所以问题的关键就在于如何分组:将整个数组异或一遍后的结果其实就是那两个数字的异或结果,然后可以看这两个异或后的结果哪一位是1,是1的话就证明这两个数在这位上是不等的,那就可以根据这位进行分组,就能满足以上两个条件;
  • 1 两个相同的数字对应位是相等的,一定被分到一组里;
  • 2 两个只有一个的数字在这位一个是1,一个是0,肯定被分到不同组里;
    整体流程如下图:

clipboard.png

class Solution {
    public int[] singleNumber(int[] nums) {
        int n = 0; //计算整个数组异或后的结果;
        int m = 1; //分组依据,看异或后结果在哪位上为1;
        int x = 0; //第一组;
        int y = 0; //第二组;
        for(Integer i : nums){
            n ^= i;
        }
        //n要是在第i位为0,证明两个数字在第i位上相同,与m进行与操作后为0
        //n要是在第i位为1,证明两个数字在第i位上不同,与m进行与操作后为1
        while((n & m) == 0){
            m = m << 1; //找到n在哪位上为1;以此为依据分组;
        }
        for(Integer i : nums){
            if((i & m) == 0) x ^= i; //第一组;
            else y ^= i; //第二组;
        }
        return new int[]{x,y};
    }
}

时间复杂度:O(N);
空间复杂度:O(N);

137. 只出现一次的数字 II(剑指offer 56-II)

知识点:哈希表;位运算

题目描述

给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。

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

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

输入:nums = [0,1,0,1,0,1,99]
输出:99

解法一:位运算

这题不能再用异或去解了,因为变成了三个没有办法抵消,但是想象一下,如果有三个一模一样的数字,那这三个数字二进制相加后,所有位上要么是0;要么全是3的倍数;然后我们的多余元素,要么加上去为0;要么加上去多了一个1,所以可以依次求每位的和,然后%3,如果值为1,那证明我们在这位上的值为1;否则为0;
如下图所示;

clipboard (1).png

class Solution {
    public int singleNumber(int[] nums) {
        //在java中int类型是32位,我们需要统计所有数字在某一位置的和能不能被3整除,
        // 如果不能被3整除,说明那个只出现一次的数字的二进制在那个位置是1……把32位全部统计完为止
        int ans = 0;
        for(int i = 0; i < 32; i++){
            int count = 0; //统计1的个数;
            for(int j = 0; j < nums.length; j++){
                count += (nums[j] >> i) & 1; //统计所有数在第i位上1的个数;
            }
            if(count % 3 != 0){
                ans = ans | (1 << i); //其他位不变,第i位置为1;
            }
        }
        return ans;
    }
}

时间复杂度:O(N);
空间复杂度:O(1);

体会

掌握位运算的解决方法:这种题目往往要按位与、按位异或等操作;
此外还会有左移<<;右移>>等;比如说:

a & 1 : a的其他位全为0,最后一位不变:即取a最后一位;
a | (1 << i) : a的其他位不变,把a的第i位置为1;
(a >> i) & 1 : 取出a第i位上的值;

389.找不同

知识点:哈希表;抵消思想

题目描述

给定两个字符串 s 和 t,它们只包含小写字母。
字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。
请找出在 t 中被添加的字母。

示例
输入;s='abcdac'; t='adaccbb';
输出;'b';
解释;因为b是被添加的元素,不一定在最后位置;

异或;

这种思路是很巧妙的:这道题可以转换思路,将s和t合为一个字符串,然后呢,每两个字符两两按位取异或,如果两个字符一样,那取完异或就是0(字符0)。因为异或运算具有交换律,所以这样都按位异或完之后肯定有一个多余的,0和任意字符异或都等于那个字符,这样就把那个字符取出来了。其实这也是抵消思想的一种,就是按位异或去抵消,只要相同,求完就为0;

class Solution {
    public char findTheDifference(String s, String t) {
        char flag = 0;
        for(char c : (s+t).toCharArray()){
            flag ^= c;
        }
        return flag;
    }
}

以上两种解法都只需要将两个字符串遍历一遍就可以;

体会

一定要学会这种抵消的思想,很常用;
记住两个元素按位异或后为0;0与元素A按位异或后为A;

作者:[curryxin]

链接:位运算 - 标签 - Curryxin - 博客园 (cnblogs.com)