LeetCode刷题之位运算

374 阅读10分钟

461. 汉明距离(Easy)

两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。

给出两个整数 x 和 y,计算它们之间的汉明距离。

注意: 0 ≤ x, y < 2^31.

示例:

输入: x = 1, y = 4

输出: 2

解释:
1   (0 0 0 1)
4   (0 1 0 0)
       ↑   ↑

上面的箭头指出了对应二进制位不同的位置。

使用Integer自带的函数求1的个数

public int hammingDistance(int x, int y) {
    // 将相同位转为0
    int exclusiveOr = x ^ y;
    // 统计1的个数
    int result = Integer.bitCount(exclusiveOr);
    return result;
}

n&(n-1)去除n最低位的1

public int hammingDistance(int x, int y) {
    // 将相同位转为0
    int exclusiveOr = x ^ y;
    // 统计1的个数
    int result = 0;
    while (exclusiveOr != 0) {
        result++;
        exclusiveOr = exclusiveOr & (exclusiveOr - 1);
    }
    return result;
}

使用算术右移

public int hammingDistance(int x, int y) {
    // 将相同位转为0
    int exclusiveOr = x ^ y;
    // 统计1的个数
    int result = 0;
    while (exclusiveOr != 0) {
    	if ((exclusive & 1) == 1) {
        	result++;
        }
        exclusiveOr = exclusiveOr >> 1;
    }
    return result;
}

136. 只出现一次的数字(Easy)

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

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

示例 1:

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

示例 2:

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

利用异或的特点

异或:相同为0,不同为1,将所有的数进行异或,剩下的那个就是只出现一次的数

public int singleNumber(int[] nums) {
	int count = 0;
    for (int num : nums) {
    	count = count ^ num;
    }
    return count;
}

268. 缺失数字(Easy)

给定一个包含 0, 1, 2, ..., n 中 n 个数的序列,找出 0 .. n 中没有出现在序列中的那个数。

示例 1:

输入: [3,0,1]
输出: 2

示例 2:

输入: [9,6,4,2,3,5,7,0,1]
输出: 8

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

利用异或的特点

假设数组{0,2,4,1},n个序列的数字为“0,1,2,3,4”,如果一一对应异或,剩下的就是答案:3,根据异或的运算规则:a^b^c=a^(b^c),所以只需要这9个数字参与异或,得到的就是答案。

public int missingNumber(int[] nums) {
    int result = 0;
    for (int i = 0; i < nums.length; i++) {
        int tmp = nums[i] ^ i;
        result = result ^ tmp;
    }
    result = result ^ nums.length;
    return result;
}

260. 只出现一次的数字 III(Medium)

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。

示例 :

输入: [1,2,1,3,2,5]
输出: [3,5]

注意:

  1. 结果输出的顺序并不重要,对于上面的例子, [5, 3] 也是正确答案。
  2. 你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?

最粗暴的方法:遍历数组,用HahsMap统计每个数字出现的次数

异或

这道题是上题的“加强版”,上题只有一个数字A出现一次,所以一次异或就可以得到答案,本题有两个出现一次的数字AB,异或之后得到的是A^B之后的值C,但是并不能将这两个数字从C中分离开。思考:

  1. 这时候我们可以想到,既然我们不能对数组的所有数字进行一次异或得到答案,那可否将这个数组分为两半:每一半都由只出现一次的数字和成对的数字构成,那么对这两半分别异或,得到的就是答案了。思路是正确的,关键就看怎么将这个数组按照我们的想法分成两半
  2. 两个只出现一次的数字,这两个数字最起码在一个位置上是不同的(比如3的二进制表示0111的二进制表示001在第二位上不同),那么可以找出其中一个位置,然后按照这个将数字分为两半(分为两半并不是平均分),PS:相同的数字所有位置上的数字都是相同的
public int[] singleNumber(int[] nums) {
    // 求出异或之后的数字
    int diff = 0;
    for (int num : nums) {
        diff ^= num;
    }

    // 找出其中一位不同,使用 diff & (-diff) 求出最右边的1
    diff &= -diff;
    int[] result = new int[2];
    // 将数组分为两半,分别异或
    for (int num : nums) {
        // num跟diff在‘1’的位置相同
        if ((num & diff) == 0) {
            result[0] ^= num;
        } else {
            result[1] ^= num;
        }
    }
    return result;
}

190. 颠倒二进制位(Easy)

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

示例 1:

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

示例 2:

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

提示:

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

解法一:取模求和

与反转十进制数的方法类似:

  • 十进制 ans = ans * 10 + n % 10, n = n / 10
  • 二进制 ans = ans * 2 + n % 2, n = n / 2 但是 Java 中负数除以2会向零取整,-3 / 2 = -1,而-3 >> 1 = -2,所以要转换成位运算
// you need treat n as an unsigned value
public int reverseBits(int n) {
    int result = 0;
    for (int i = 0; i < 32; i++) {
        result = (result << 1) + (n & 1);
        n >>= 1;
    }
    return result;
}

解法二:按位翻转

直接颠倒计算的每一位数字:

  • 32位 int 标记 [0 ~ 31],如果 n 的第 i 位为1,则相应的res31 - i 位应该为1
  • 类似地,如果 n 的第 i 位为0,则相应的 res31 - i 位应该为0 res初始化为0,考虑n的每一位,n的第i位只有两种可能(0或者00··1000··形式),设其为temp,然后在res的第31-i位上将temp加上去或者做运算,或者做异或(因为res初始值全部为0,所以不用担心出现两个1的情况,只有0100两种情况)。
// you need treat n as an unsigned value
public int reverseBits(int n) {
    int res = 0;
    for (int i = 0; i < 32; i++) {
        res |= (n & (1 << i)) == 0 ? 0 : 1 << (31 - i);
    }
    return res;
}

231. 2的幂(Easy)

给定一个整数,编写一个函数来判断它是否是 2 的幂次方。

示例 1:

输入: 1
输出: true
解释: 2^0 = 1

示例 2:

输入: 16
输出: true
解释: 2^4 = 16

示例 3:

输入: 218
输出: false

数学方法

判断一个数是否为2的次幂,关键是判断这个数的二进制表示是否只有一个1

这种做法需要注意的是 tmp 不断扩大的时候是否会超过 Integer 所能表示的最大值。同样的,也可以将 n 不断地除以2来求解

public boolean isPowerOfTwo(int n) {
    int tmp = 1;

    while (n >= tmp) {
        if ((n & tmp) == tmp && (tmp == n)){
            return true;
        }
        if (Integer.MAX_VALUE / 2 < tmp) {
            return false;
        }
        tmp = tmp << 1;
    }
    return false;
}

利用 n&(n-1) 去除最低的那位1的方法

public boolean isPowerOfTwo(int n) {
    return n >0 && (n & (n - 1)) == 0;
}

342. 4的幂(Easy)

给定一个整数 (32 位有符号整数),请编写一个函数来判断它是否是 4 的幂次方。

示例 1:

输入: 16
输出: true

示例 2:

输入: 5
输出: false

位运算

一个数为 4 的次幂,说明二进制表示上奇数位为 1 (有且仅有)

public boolean isPowerOfFour(int num) {
    return num > 0 && (num & (num - 1)) == 0 && (num & 0b01010101010101010101010101010101) != 0;
}

位运算+数学运算

利用 2 % 3 = 24 % 3 = 1 的区分是2的次幂还是4的次幂

public boolean isPowerOfFour(int num) {
    return (num > 0) && ((num & (num - 1)) == 0) && (num % 3 == 1);
}

693. 交替位二进制数(Easy)

给定一个正整数,检查他是否为交替位二进制数:换句话说,就是他的二进制数相邻的两个位数永不相等。

示例 1:

输入: 5
输出: True
解释:
5的二进制数是: 101

示例 2:

输入: 7
输出: False
解释:
7的二进制数是: 111

示例 3:

输入: 11
输出: False
解释:
11的二进制数是: 1011

 示例 4:

输入: 10
输出: True
解释:
10的二进制数是: 1010

与前一个数比较

遍历数字的所有位数,用一个变量存储前一位和当前位

public boolean hasAlternatingBits(int n) {
    // 初始化为 n 的第一位
    int pre = n & 1;

    while (n != 0) {
        n >>= 1;
        int cur = n & 1;
		
        // 相等返回false
        if (cur == pre) {
            return false;
        }
		
        pre = cur;
    }
    return true;
}

位运算

交替的数字错位异或必然全是1,从这个点出发解答,然后将异或之后的值加 1 再和原值做 与运算,结果为0的就是正确的

public boolean hasAlternatingBits(int n) {
	int p = n ^ (n >> 1);
    return (p & (p + 1)) == 0;
}

476. 数字的补数(Easy)

给定一个正整数,输出它的补数。补数是对该数的二进制表示取反。

示例 1:

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

示例 2:

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

注意:

  • 给定的整数保证在 32 位带符号整数的范围内。
  • 你可以假定二进制数不包含前导零位。

位运算

根据异或的特点:相同为0,不同为1,要求num的补数,可以让num和全是1的数进行异或(长度相同)

public int findComplement(int num) {
    // 得到一个和 num 长度相同的全是1的数
    int tmp = 1;
    while (tmp < num) {
        tmp <<= 1;
        tmp += 1;
    }
    return tmp ^ num;
}

371. 两整数之和(Easy)

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

示例 1:

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

示例 2:

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

位运算

题目规定不能使用运算符,那么自然地就会想到使用位运算来替代了,数学运算也是通过位运算来实现的,两数转换为二进制表示,对应的位上做异或运算,与运算的结果做进位,当进位为0的时候说明计算完成。

先看十进制是如何做加法的: 123+298

  1. 各位相加,先不算进位,得到 311
  2. 计算进位,得到110,如果进位为0,说明计算结束
  3. 重复上述两个步骤,得到521 用同样的方法计算二进制相加
  4. 各位相加,二进制每位相加就相当于各位做异或操作
  5. 计算进位,相当于各位进行与操作,然后左移一位,结束条件仍然是进位为0
  6. 重复上述步骤
public int getSum(int a, int b) {
    while(b != 0){
        int temp = a ^ b;
        b = (a & b) << 1;
        a = temp;
    }
    return a;
}

318. 最大单词长度乘积(Medium)

给定一个字符串数组 words,找到 length(word[i]) * length(word[j]) 的最大值,并且这两个单词不含有公共字母。你可以认为每个单词只包含小写字母。如果不存在这样的两个单词,返回 0。

示例 1:

输入: ["abcw","baz","foo","bar","xtfn","abcdef"]
输出: 16 
解释: 这两个单词为 "abcw", "xtfn"

示例 2:

输入: ["a","ab","abc","d","cd","bcd","abcd"]
输出: 4 
解释: 这两个单词为 "ab", "cd"

示例 3:

输入: ["a","aa","aaa","aaaa"]
输出: 0 
解释: 不存在这样的两个单词。

位操作

最粗暴的方法是逐个检查第一个单词字母是否出现在第二个单词,但是这种方式太过耗时,可以参考布隆过滤器的做法,将26个字母转换为一个长度为26的二进制数字,单词有a字母的时候,第0位为1,b对应第2位,以此类推,判断两个单词是否有相同字母,只需做操作即可。

如何计算一个单词的位掩码?遍历单词的每个字母,计算该字母在掩码中的位置 n = (int)ch - (int)'a',然后创建一个第n位为 1 的掩码 n_th_bit = 1 << n,通过或操作将该码合并到位掩码中 bitmask |= n_th_bit

// 将字符转换为 a 的相对位置
private int bitNumber(char ch) {
    return ch - 'a';
}

// 比较两个单词是否有相同字母
public boolean noCommonLetters(String s1, String s2){
    int bitMarks1 = 0;
    int bitMarks2 = 0;

    for (char c : s1.toCharArray()) {
        bitMarks1 |= 1 << bitNumber(c);
    }

    for (char c : s2.toCharArray()) {
        bitMarks2 |= 1 << bitNumber(c);
    }

    return (bitMarks1 & bitMarks2) == 0;
}

public int maxProduct(String[] words) {
    int n = words.length;
    int maxProd = 0;
    
    // 遍历所有的组合,求出最值
    for (int i = 0; i < n; ++i)
        for (int j = i + 1; j < n; ++j)
            if (noCommonLetters(words[i], words[j]))
                maxProd = Math.max(maxProd, words[i].length() * words[j].length());

    return maxProd;
}

338. 比特位计数(Medium)

给定一个非负整数 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)来执行此操作。

汉明权重

利用 x & (x - 1) 去掉x的最低位的1,计算需要几步能将x归零,x就有几个1

public int[] countBits(int num) {
    int[] res = new int[num + 1];
    for (int i = 0; i < num + 1; i++) {
        res[i] = popCount(i);
    }
    return res;
}

private int popCount(int x) {
    int count = 0;
    while (x != 0) {
        count++;
        x &= x - 1;
    }
    return count;
}

找规律

public int[] countBits(int num) {
    int[] res = new int[num + 1];
    
    for (int i = 1; i < num + 1; i++) {
        // i 为偶数,一个数乘以2,相当于左移一位,1的个数不变
        if (i % 2 == 0) {
            res[i] = res[i / 2];
        } else {
            // 奇数比前一个偶数多1个1
            res[i] = res[i - 1] + 1;
        }
    }
    return res;
}