【位运算】速算密钥:位运算探秘

发布于:2025-03-18 ⋅ 阅读:(16) ⋅ 点赞:(0)


在这里插入图片描述

前言

什么是位运算算法呢?

位运算算法是以位运算为核心操作,设计用来高效解决特定问题的一系列计算步骤集合。它巧妙利用位运算直接对二进制位操作的特性,构建独特的逻辑流程,从而实现比常规算法更优的时间复杂度或空间复杂度。​
例如在判断一个数是否为 2 的幂次方的问题上,常规思路可能是不断除以 2 并检查余数。但位运算算法则利用二进制特性:一个数若是 2 的幂次方,其在二进制表示下只有一位是 1,其余全为 0。这种位运算算法避免了循环除法操作,极大提升了运算效率。与普通位运算不同,位运算算法不是孤立的单个操作,而是一系列位运算操作的有机组合,根据问题特性精心编排,以达成复杂问题的高效求解,在数据压缩、加密解密、图形处理等诸多对性能敏感的领域发挥着关键作用。

位运算都有哪几种呢?

按位与(&)
对两个操作数对应的二进制位进行逐位比较,只有当两个对应位都为 1 时,结果位才为 1,否则为 0。

示例代码:

public class BitwiseAndExample {
    public static void main(String[] args) {
        int a = 5; // 二进制: 0101
        int b = 3; // 二进制: 0011
        int result = a & b; // 二进制结果: 0001,十进制为 1
        System.out.println(result);
    }

按位或(|)
对两个操作数对应的二进制位进行逐位比较,只要两个对应位中有一个为 1,结果位就为 1,只有当两个对应位都为 0 时,结果位才为 0。

示例代码:

public class BitwiseOrExample {
    public static void main(String[] args) {
        int a = 5; // 二进制: 0101
        int b = 3; // 二进制: 0011
        int result = a | b; // 二进制结果: 0111,十进制为 7
        System.out.println(result);
    }
}

按位异或(^)
对两个操作数对应的二进制位进行逐位比较,当两个对应位不同时,结果位为 1,相同时结果位为 0。

示例代码:

public class BitwiseXorExample {
    public static void main(String[] args) {
        int a = 5; // 二进制: 0101
        int b = 3; // 二进制: 0011
        int result = a ^ b; // 二进制结果: 0110,十进制为 6
        System.out.println(result);
    }
}

按位取反(~)
对操作数的每个二进制位进行取反操作,即 1 变为 0,0 变为 1。

示例代码:

public class BitwiseNotExample {
    public static void main(String[] args) {
        int a = 5; // 二进制: 0101
        int result = ~a; // 结果为 -6
        System.out.println(result);
    }
}

左移(<<)
将操作数的二进制位向左移动指定的位数,右边空出的位用 0 填充。左移 n 位相当于将原数乘以 2 的 n 次方。

示例代码:

public class LeftShiftExample {
    public static void main(String[] args) {
        int a = 5; // 二进制: 0101
        int result = a << 2; // 二进制结果: 010100,十进制为 20
        System.out.println(result);
    }
}

右移(>>)
将操作数的二进制位向右移动指定的位数,左边空出的位根据原数的符号位进行填充(正数补 0,负数补 1)。右移 n 位相当于将原数除以 2 的 n 次方并向下取整。

代码示例:

public class RightShiftExample {
    public static void main(String[] args) {
        int a = 5; // 二进制: 0101
        int result = a >> 1; // 二进制结果: 0010,十进制为 2
        System.out.println(result);
    }
}

接下来,本篇文章将通过几道位运算的例题来详细展开位运算这一算法!

例题

一、判定字符是否唯一

  1. 题目链接:判定字符是否唯一
  2. 题目描述:

实现⼀个算法,确定⼀个字符串 s 的所有字符是否全都不同。
示例 1:
输⼊: s = “leetcode”
输出: false
示例 2:
输⼊: s = “abc”
输出: true
限制:0 <= len(s) <= 100
s[i]仅包含小写字目
如果你不使⽤额外的数据结构,会很加分。

  1. 解法(位图的思想):

算法思路:
利⽤「位图」的思想,每⼀个「比特位」代表⼀个「字符,⼀个 int 类型的变量的 32 位⾜够 表示所有的小写字母。比特位里面如果是 0 ,表⽰这个字符没有出现过。比特位里面的值是 1 , 表⽰该字符出现过。
那么我们就可以用⼀个「整数」来充当「哈希表」。

  1. 代码示例:
  public boolean isUnique(String astr) {
  		//利用鸽巢原理来进行优化
        if(astr.length()>26) return false;
        int bitMap =0;
        for(int i = 0 ;i<astr.length();i++){
            int x = astr.charAt(i)-'a';
            if(((bitMap>>x)&1)==1) return false;
            bitMap = (1<<x)|bitMap;
        }
        return true;
    }

二、丢失的数字

  1. 题目链接:丢失的数字
  2. 题目描述:

给定⼀个包含 [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 中。
题示:n == nums.length
1 <= n <= 10^4
0 <= nums[i] <= n
nums 中的所有数字都 独⼀⽆二
进阶:你能否实现线性时间复杂度、仅使用额外常数空间的算法解决此问题?

  1. 解法(位运算)
    算法思路:
    设数组的大小为 n ,那么缺失之前的数就是 [0, n] ,数组中是在 [0, n] 中缺失⼀个数形成 的序列。 如果我们把数组中的所有数,以及 [0, n] 中的所有数全部「异或」在⼀起,那么根据「异或」 运算的「消消乐」规律,最终的异或结果应该就是缺失的数

  2. 代码示例:

		//法一:哈希表 大家可以自行尝试

>  public int missingNumber1(int[] nums) {
        int n =nums.length,ret = 0;
        int[] hash = new int[n+1];
        for(int i = 0;i<nums.length;i++){
            hash[nums[i]]++;
        }
        for(int i = 0;i<hash.length;i++){
            if(hash[i]==0)
                ret = i;
        }
        return ret;
    }
    //法二 :位运算
    public int missingNumber(int[] nums) {
        int ret = 0;
        for(int x :nums) ret ^= x;
        for(int i = 0;i<nums.length;i++) ret ^= i;
        return ret;
    }

三、两整数之和

  1. 题目链接: 两整数之和
  2. 题目描述:

给你两个整数 a 和 b ,不使⽤ 运算符 + 和 - ,计算并返回两整数之和。
示例 1:
输入:a = 1, b = 2
输出:3
示例 2:
输入:a = 2, b = 3
输出:5
提示:
-1000 <= a, b <= 1000

  1. 解法(位运算):
    算法思路:
    ◦ 异或 ^ 运算本质是「无进位加法」;
    ◦ 按位与 & 操作能够得到「进位」;
    ◦ 然后⼀直循环进行,直到「进位」变成 0 为止。

4.代码示例:

  public int getSum(int a, int b) {
        while (b != 0) {
            int x = a ^ b; // 先算出⽆进位相加的结果
            int carry = (a & b) << 1; // 计算进位
            a = x;
            b = carry;
        }
        return a;
    }

四、只出现⼀次的数字 II

  1. 题目链接:只出现一次的数字||
  2. 题目描述:

给你⼀个整数数组 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 中,除某个元素仅出现⼀次 外,其余每个元素都恰出现 三次

  1. 解法(比特位计数):

算法思路: 设要找的数位 ret 。 由于整个数组中,需要找的元素只出现了「⼀次」,其余的数都出现的「三次」,因此我们可以根据所有数的「某⼀个比特位」的总和 %3 的结果,快速定位到 ret 的「⼀个比特位上」的值是 0 还是 1 。 这样,我们通过 ret 的每⼀个比特位上的值,就可以将 ret 给还原出来。

  1. 代码示例:
  public int singleNumber(int[] nums) {
        int ret = 0;
        for (int i = 0; i < 32; i++) // 依次修改 ret 中的每⼀个⽐特位
        {
            int sum = 0;
            for (int x : nums) // 统计 nums 中所有的数的第 i 位的和
                if (((x >> i) & 1) == 1)
                    sum++;
            sum %= 3;
            if (sum == 1) ret |= 1 << i;
        }
        return ret;
    }

五、消失的两个数字

  1. 题目链接:消失的两个数字
  2. 题目描述:

给定⼀个数组,包含从 1 到 N 所有的整数,但其中缺了两个数字。你能在 O(N) 时间内只⽤ O(1) 的 空间找到它们吗?以任意顺序返回这两个数字均可。
示例 1:
输⼊: [1]
输出: [2,3]
示例 2:
输⼊: [2,3]
输出: [1,4]
提示:
nums.length <= 30000

  1. 解法(位运算):
    算法思路:
    和上述中消失一个数字中题解法一样,首先我们需要将该数组中的所有元素与连续情况下的数组同时异或,经过此操作后,只剩下消失的两个数字的异或,此时我们通过对找出得到结果的比特位为一,即找出两个数比特位不同的那一位,在之后,将所有的数按照位置不同,分为两类异或即可得到a,b值。

  2. 代码示例

 public int[] missingTwo2(int[] nums) {
        // 1. 先把所有的数异或在⼀起
        int tmp = 0;
        for (int x : nums) tmp ^= x;
        for (int i = 1; i <= nums.length + 2; i++) tmp ^= i;
        // 2. 找出 a,b 两个数⽐特位不同的那⼀位
        int diff = 0;
        while (true) {
            if (((tmp >> diff) & 1) == 1) break;
            else diff++;
        }
        // 3. 将所有的数按照 diff 位不同,分两类异或
        int[] ret = new int[2];
        for (int x : nums)
            if (((x >> diff) & 1) == 1) ret[1] ^= x;
            else ret[0] ^= x;
        for (int i = 1; i <= nums.length + 2; i++)
            if (((i >> diff) & 1) == 1) ret[1] ^= i;
            else ret[0] ^= i;
        return ret;
    }

结语

本文到这里就结束了,主要介绍了位运算法则,并通过几道例题更加深入了解位运算相关题目的应用。希望能够对你有帮助。

以上就是本文全部内容,感谢各位能够看到最后,创作不易,希望大家多多支持!

最后,大家再见!祝好!我们下期见!