𝓝𝓮𝓶𝓸&博客

【算法】位运算技巧

对于仍然不太清楚位操作符的同学们,可以看看这篇文章:位操作符

特别注意

特别注意:使用按位操作符时要注意,相等(==)与不相等(!=)的优先级在按位运算符之上!!!!
这意味着,位运算符的优先级极小,所以使用位运算符时,最好加上括号()

重要技巧

位运算相关的知识。

  • &符号,x&y,会将两个十进制数在二进制下进行与运算(都1为1,其余为0) 然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
  • |符号,x|y,会将两个十进制数在二进制下进行或运算(都0为0,其余为1) 然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
  • ^符号,x^y,会将两个十进制数在二进制下进行异或运算(不同为1,其余 为0)然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
  • ~符号,~x,按位取反。例如~101=010。
  • <<符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。
  • >>符号,是右移操作,x>>1相当于给x/2,去掉x二进制下的最右一位;有符号右移,正数用0填补,负数用1填补。
  • >>>:无符号右移,用0填补。

1.判断一个数字x二进制下第i位是不是等于1。(最低第1位)

方法:if(((1<<(i−1))&x)>0) 将1左移i-1位,相当于制造了一个只有第i位 上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0, 说明x第i位上是1,反之则是0。

2.将一个数字x二进制下第i位更改成1。

方法:x=x|(1<<(i−1)) 证明方法与1类似。

3.将一个数字x二进制下第i位更改成0。

方法:x=x&~(1<<(i−1))

4.把一个数字二进制下最靠右的第一个1去掉。

方法:x=x&(x−1)


基本的操作我就直接略过了。下面是我认为必须掌握的技巧:(注意,我把一些生僻的技巧都已经砍掉了,留下来的,就是我认为应该会的)

  1. 使用 x & 1 == 1 判断奇偶数。(注意,一些编辑器底层会把用%判断奇偶数的代码,自动优化成位运算&

  2. 不使用第三个数,交换两个数。x = x ^ yy = x ^ yx = x ^ y。(早些年喜欢问到,现在如果谁再问,大家会觉得很low)

  3. 两个相同的数异或的结果是 0(n ^ n = 0),一个数和 0 异或的结果是它本身(n ^ 0 = n)。(对于找数这块,异或往往有一些别样的用处。)

  4. x & (x - 1) ,可以将最右边的 1 设置为 0。(这个技巧可以用来检测 2的幂,或者检测一个整数二进制中 1 的个数,又或者别人问你一个数变成另一个数其中改变了多少个bit位,统统都是它)

  5. i + (~i) = -1,i 取反再与 i 相加,相当于把所有二进制位设为1,其十进制结果为-1。

  6. 对于int32而言,使用 n >> 31取得 n 的正负号。并且可以通过 (n ^ (n >> 31)) - (n >> 31) 来得到绝对值。(n为正,n >> 31 的所有位等于0。若n为负数,n >> 31 的所有位等于1,其值等于-1)

  7. 使用 (x ^ y) >= 0 来判断符号是否相同。(如果两个数都是正数,则二进制的第一位均为0,x^y=0;如果两个数都是负数,则二进制的第一位均为1;x^y=0 如果两个数符号相反,则二进制的第一位相反,x^y=1。有0的情况例外,^相同得0,不同得1)

  8. “异或”是一个无进位加法,说白了就是把进位砍掉。比如01^01=00

  9. “与”可以用来获取进位,比如 01&01=01,然后再把结果左移一位,就可以获取进位结果。

使用掩码遍历二进制位

这个方法比较直接。我们遍历数字的 32 位。如果某一位是 1 ,将计数器加一。

我们使用 位掩码 来检查数字的第 ith 位。一开始,掩码 m=1 因为 1 的二进制表示是

0000 0000 0000 0000 0000 0000 0000 0001

0000 0000 0000 0000 0000 0000 0000 0001

显然,任何数字跟掩码 11 进行逻辑与运算,都可以让我们获得这个数字的最低位。检查下一位时,我们将掩码左移一位。

0000 0000 0000 0000 0000 0000 0000 0010

0000 0000 0000 0000 0000 0000 0000 0010

并重复此过程,我们便可依次遍历所有位。

Java

public int hammingWeight(int n) {
    int bits = 0; // 用来储存 1 的个数

    int mask = 1;  // 掩码,从最低位开始

    for (int i = 0; i < 32; i++) {
        // 注意这里是不等于0,而不是等于1,因为我们的位数是不断在变化的,可能等于2、4、8...
        // 使用按位操作符时要注意,相等(==)与不相等(!=)的优先级在按位运算符之上!!!!
        // 使用按位运算符时,最好加上括号()
        if ((n & mask) != 0) {
            bits++;
        }
        mask <<= 1;
    }
    return bits;
}

注意:这里判断 n & mask 的时候,千万不要错写成 (n & mask) == 1,因为这里你对比的是十进制数。(我之前就这么写过,记录一下...)

无符号右移遍历二进制位

逐位判断
根据 与运算 定义,设二进制数字 n ,则有:

  • n&1=0,则 n 二进制 最右一位 为 0 ;
  • n&1=1,则 n 二进制 最右一位 为 1 。

根据以上特点,考虑以下 循环判断 :

  1. 判断 n 最右一位是否为 1 ,根据结果计数。
  2. 将 n 右移一位(本题要求把数字 n 看作无符号数,因此使用 无符号右移 操作)。

算法流程:

  1. 初始化数量统计变量 res = 0。
  2. 循环逐位判断: 当 n = 0 时跳出。
  3. res += n & 1 : 若 n&1=1 ,则统计数 res 加一。
  4. n >>= 1 : 将二进制数字 n 无符号右移一位( Java 中无符号右移为 ">>>" ) 。
  5. 返回统计数量 res。
public class Solution {
    public int hammingWeight(int n) {
        int res = 0;
        while(n != 0) {
            res += n & 1;  // 遍历
            n >>>= 1;  // 无符号右移
        }
        return res;
    }
}

反转最后一个 1

对于特定的情况,如 只有 1 对我们有用时,我们不需要遍历每一位,我们可以把前面的算法进行优化。

我们不再检查数字的每一个位,而是不断把数字最后一个 1 反转,并把答案加一。当数字变成 0 的时候偶,我们就知道它没有 1 的位了,此时返回答案。

注意:这里我们说的是最后一个 1而不是最后一位 1,这个 1 可能在任何位上。

这里关键的想法是对于任意数字 n ,将 n 和 n - 1 做与运算,会把最后一个 1 的位变成 0 。为什么?考虑 n 和 n - 1 的二进制表示。

巧用 n&(n1)
(n1) 解析: 二进制数字 n 最右边的 1 变成 0 ,此 1 右边的 0 都变成 1 。
n&(n1) 解析: 二进制数字 n 最右边的 1 变成 0 ,其余不变。


图片 1. 将 n 和 n-1 做与运算会将最低位的 1 变成 0

在二进制表示中,数字 n 中最低位的 1 总是对应 n - 1 中的 0 。因此,将 n 和 n - 1 与运算总是能把 n 中最低位的 1 变成 0 ,并保持其他位不变。

比如下面这两对数:

肯定有人又是看的一脸懵逼,我们拿 11 举个例子:(注意最后一位 1 变成 0 的过程)

使用这个小技巧,代码变得非常简单。

Java

public int hammingWeight(int n) {

    int sum = 0;  // 用来存放 1 的个数

    while (n != 0) {
        sum++;
        n &= (n - 1);
    }
    return sum;
}

Java内置函数:1 的个数

public int hammingWeight(int n) {
    return Integer.bitCount(n);
}

异或相消(异或运算的特性)

我们先来看下异或的数学性质(数学里异或的符号是 ):

  • 交换律:pq=qp
  • 结合律:p(qr)=(pq)r
  • 恒等率:p0=p
  • 归零率:pp=0

异或运算有以下三个性质。

  1. 任何数和 0 做异或运算,结果仍然是原来的数,即 a0=a
  2. 任何数和其自身做异或运算,结果是 0,即 aa=0
  3. 异或运算满足交换律和结合律,即 aba=baa=b(aa)=b0=b

假设数组中有 2m+1 个数,其中有 m 个数各出现两次,一个数出现一次。令 a1a2…、am 为出现两次的 m 个数,am+1 为出现一次的数。根据性质 3,数组中的全部元素的异或运算结果总是可以写成如下形式:

(a1a1)(a2a2)(amam)am+1

根据性质 2 和性质 1,上式可化简和计算得到如下结果:

000am+1=am+1

因此,数组中的全部元素的异或运算结果即为数组中只出现一次的数字。

下面我们来举个例子吧:
假如我们有 [21,21,26] 三个数,是下面这样:

回想一下,之所以能用“异或”,其实我们是完成了一个 同一位上有 2 个 1 清零 的过程。上面的图看起来可能容易,如果是这样 (下图应为 26^21):

Java

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

利用二进制数的每一位表示两种选择状态

面试题 08.04. 幂集

幂集。编写一种方法,返回某集合的所有子集。集合中不包含重复的元素。

说明:解集不能包含重复的子集。

示例:

 输入: nums = [1,2,3]
 输出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

位运算解决

本题的一般方法是采用回溯法解决,我们这里可以用位运算方法解决。

数组中的每一个数字都有不选两种状态,我们可以用0和1表示,0表示不选,1表示选择。如果数组的长度是n,那么子集的数量就是2n。比如数组长度是3,就有8种可能,分别是

  1. [0,0,0]
  2. [0,0,1]
  3. [0,1,0]
  4. [0,1,1]
  5. [1,0,0]
  6. [1,0,1]
  7. [1,1,0]
  8. [1,1,1]

这里参照示例画个图来看下
image

代码如下

public static List<List<Integer>> subsets(int[] nums) {
    //子集的长度是2的nums.length次方,这里通过移位计算
    int length = 1 << nums.length;
    List<List<Integer>> res = new ArrayList<>(length);
    //遍历从0到length中间的所有数字,根据数字中1的位置来找子集
    for (int i = 0; i < length; i++) {
        List<Integer> list = new ArrayList<>();
        for (int j = 0; j < nums.length; j++) {
            //如果数字i的某一个位置是1,就把数组中对
            //应的数字添加到集合
            if (((i >> j) & 1) == 1)
                list.add(nums[j]);
        }
        res.add(list);
    }
    return res;
}

实例

231. 2 的幂

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

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

示例 1:

输入:n = 1
输出:true
解释:20 = 1

示例 2:

输入:n = 16
输出:true
解释:24 = 16

示例 3:

输入:n = 3
输出:false

示例 4:

输入:n = 4
输出:true

示例 5:

输入:n = 5
输出:false

答案

class Solution {
    public boolean isPowerOfTwo(int n) {
		// 因为是2的幂,得是正数才行
        return n > 0 && (n & (n - 1)) == 0;
    }
}

位1的个数

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

提示:

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

进阶:

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

示例 1:

输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。

示例 2:

输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。

示例 3:

输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。

提示:
输入必须是长度为 32 的 二进制串。


答案

方法 1:循环和位移动

算法

这个方法比较直接。我们遍历数字的 32 位。如果某一位是 1 ,将计数器加一。

我们使用 位掩码 来检查数字的第 ith 位。一开始,掩码 m=1 因为 1 的二进制表示是

0000 0000 0000 0000 0000 0000 0000 0001

0000 0000 0000 0000 0000 0000 0000 0001

显然,任何数字跟掩码 11 进行逻辑与运算,都可以让我们获得这个数字的最低位。检查下一位时,我们将掩码左移一位。

0000 0000 0000 0000 0000 0000 0000 0010

0000 0000 0000 0000 0000 0000 0000 0010

并重复此过程。

Java

public int hammingWeight(int n) {
    int bits = 0;
    int mask = 1;
    for (int i = 0; i < 32; i++) {
        if ((n & mask) != 0) {
            bits++;
        }
        mask <<= 1;
    }
    return bits;
}

复杂度分析

  • 时间复杂度:O(1)。运行时间依赖于数字 n 的位数。由于这题中 n 是一个 32 位数,所以运行时间是 O(1) 的。

  • 空间复杂度:O(1)。没有使用额外空间。


逐位判断
根据 与运算 定义,设二进制数字 n ,则有:

  • n&1=0,则 n 二进制 最右一位 为 00 ;
  • n&1=1,则 n 二进制 最右一位 为 11 。

根据以上特点,考虑以下 循环判断 :

  1. 判断 n 最右一位是否为 1 ,根据结果计数。
  2. 将 n 右移一位(本题要求把数字 n 看作无符号数,因此使用 无符号右移 操作)。

算法流程:

  1. 初始化数量统计变量 res = 0。
  2. 循环逐位判断: 当 n = 0 时跳出。
  3. res += n & 1 : 若 n&1=1 ,则统计数 res 加一。
  4. n >>= 1 : 将二进制数字 n 无符号右移一位( Java 中无符号右移为 ">>>" ) 。
  5. 返回统计数量 res。
public class Solution {
    public int hammingWeight(int n) {
        int res = 0;
        while(n != 0) {
            res += n & 1;  // 遍历
            n >>>= 1;  // 无符号右移
        }
        return res;
    }
}

方法 2:位操作的小技巧

算法

我们可以把前面的算法进行优化。我们不再检查数字的每一个位,而是不断把数字最后一个 1 反转,并把答案加一。当数字变成 0 的时候偶,我们就知道它没有 1 的位了,此时返回答案。

这里关键的想法是对于任意数字 n ,将 n 和 n - 1 做与运算,会把最后一个 1 的位变成 0 。为什么?考虑 n 和 n - 1 的二进制表示。

巧用 n&(n1)
(n1) 解析: 二进制数字 n 最右边的 1 变成 0 ,此 1 右边的 0 都变成 1 。
n&(n1) 解析: 二进制数字 n 最右边的 1 变成 0 ,其余不变。


图片 1. 将 n 和 n-1 做与运算会将最低位的 1 变成 0

在二进制表示中,数字 n 中最低位的 1 总是对应 n - 1 中的 0 。因此,将 n 和 n - 1 与运算总是能把 n 中最低位的 1 变成 0 ,并保持其他位不变。

使用这个小技巧,代码变得非常简单。

Java

public int hammingWeight(int n) {
    int sum = 0;
    while (n != 0) {
        sum++;
        n &= (n - 1);
    }
    return sum;
}

复杂度分析

  • 时间复杂度:O(1)。运行时间与 n 中位为 1 的有关。在最坏情况下,n 中所有位都是 1 。对于 32 位整数,运行时间是 O(1) 的。

  • 空间复杂度:O(1)。没有使用额外空间。

只出现一次的数字

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

说明:

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

示例 1:

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

示例 2:

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

答案

方法一:位运算
如果不考虑时间复杂度和空间复杂度的限制,这道题有很多种解法,可能的解法有如下几种。

  • 使用集合存储数字。遍历数组中的每个数字,如果集合中没有该数字,则将该数字加入集合,如果集合中已经有该数字,则将该数字从集合中删除,最后剩下的数字就是只出现一次的数字。

  • 使用哈希表存储每个数字和该数字出现的次数。遍历数组即可得到每个数字出现的次数,并更新哈希表,最后遍历哈希表,得到只出现一次的数字。

  • 使用集合存储数组中出现的所有数字,并计算数组中的元素之和。由于集合保证元素无重复,因此计算集合中的所有元素之和的两倍,即为每个元素出现两次的情况下的元素之和。由于数组中只有一个元素出现一次,其余元素都出现两次,因此用集合中的元素之和的两倍减去数组中的元素之和,剩下的数就是数组中只出现一次的数字。

上述三种解法都需要额外使用 O(n) 的空间,其中 n 是数组长度。

如何才能做到线性时间复杂度和常数空间复杂度呢?

答案是使用位运算。对于这道题,可使用异或运算 。异或运算有以下三个性质。

任何数和 0 做异或运算,结果仍然是原来的数,即 a0=a
任何数和其自身做异或运算,结果是 0,即 aa=0
异或运算满足交换律和结合律,即 aba=baa=b(aa)=b0=b

假设数组中有 2m+1 个数,其中有 m 个数各出现两次,一个数出现一次。令 a1a2…、am 为出现两次的 m 个数,am+1 为出现一次的数。根据性质 3,数组中的全部元素的异或运算结果总是可以写成如下形式:

(a1a1)(a2a2)(amam)am+1

根据性质 2 和性质 1,上式可化简和计算得到如下结果:

000am+1=am+1

因此,数组中的全部元素的异或运算结果即为数组中只出现一次的数字。

Java

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

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组长度。只需要对数组遍历一次。

  • 空间复杂度:O(1)

剑指 Offer 56 - II. 数组中数字出现的次数 II

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:

输入:nums = [3,4,3,3]
输出:4

示例 2:

输入:nums = [9,1,7,9,7,9,7]
输出:1

位运算答案

// 虽然位运算很麻烦,但是我也得试试啊
class Solution {
    public int singleNumber(int[] nums) {

        // 位运算有点麻烦

        // 把所有数的二进制位相加,对每一位取余3,那么剩下来的就是我们需要找的数字了
        int[] counts = new int[32]; // 用来存储答案数字的32位二进制
        
        // 遍历数组中的所有数,将他们的二进制位相加,存起来
        for(int num : nums) {
            // 从0到31,从低位到高位
            for(int j = 0; j < 32; j++) {
                counts[j] += num & 1;
                num >>>= 1;
            }
        }

        // 从高位开始,把每一位二进制 % 3
        int res = 0;    // 结果答案
        for(int i = 31; i >= 0; i--) {
            // 先移位再加个位,就如 sum += sum * 10 + 个位
            res <<= 1;
            res |= counts[i] % 3;
        }
        return res;

    }
}

哈希表答案

class Solution {
    public int singleNumber(int[] nums) {

        // 位运算有点麻烦

        // 哈希表
        Map<Integer, Integer> map = new HashMap<>();

        for (int i = 0; i < nums.length; i++) {
            
            int count = map.getOrDefault(nums[i], 0) + 1;

            map.put(nums[i], count);
        }

        for (int i = 0; i < nums.length; i++) {
            
            int count = map.getOrDefault(nums[i], 0);
            if (count == 1) {
                return nums[i];
            }
        }

        return 0;
    }
}

两数之和

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

答案

位运算的题,大部分都有一些特别的技巧,只要能掌握这些技巧,对其拼装组合,就可以破解一道道的题目。很多人说那些技巧想不到,我觉得是因为没有认真的去学习和记忆。你要相信,基本上所有人最开始都是不会的,只是后来他们通过努力学会了记住了,而你因为没努力一直停留在不会而已。不要觉得那些一眼看到题就能想到解法的人有多么了不起。“无他,唯手熟尔!”

下面这两个技巧大家需要记住,这也是讲解本题的目的:

  • “异或”是一个无进位加法,说白了就是把进位砍掉。比如01^01=00。

  • “与”可以用来获取进位,比如01&01=01,然后再把结果左移一位,就可以获取进位结果。

根据上面两个技巧,假设有 12+7:

根据分析,完成题解:

//JAVA
class Solution {
    public int getSum(int a, int b){
        while(b != 0){
            int temp = a ^ b;
            b = (a & b) << 1;
            a = temp;
        }
        return a;
    }
}

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

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

示例:

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

答案

//JAVA
class Solution {
    public int add(int a, int b) {
        // 该位都为1,&,则进位
        // 异或运算,^,非进位加

        // 我们使用temp来记录进位的位数二进制
        // 每次我们都将 非进位和 与 进位二进制 做非进位加法运算,直到没有进位为止(进位为0)
        while(b != 0) { // 当进位为 0 时跳出
            int temp = (a & b) << 1;  // temp = 进位
            a ^= b; // a = 非进位和
            b = temp; // b = 进位
        }
        return a;
    }
}

一个数变成另一个数其中改变了多少个bit位

答案

将整数A转换为B,如果A和B在第i(0<=i<32)个位上相等,则不需要改变这个BIT位,如果在第i位上不相等,则需要改变这个BIT位。所以问题转化为了A和B有多少个BIT位不相同。联想到位运算有一个异或操作,相同为0,相异为1,所以问题转变成了计算A异或B之后这个数中1的个数。


int bit_count(int number1, int number2) {
  int temp = number1 ^ number2;
  int count = 0;

  while (temp != 0) {
    temp = temp & (temp - 1);
    count++;
  }
  return count;
}
posted @   Nemo&  阅读(4316)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验
点击右上角即可分享
微信分享提示