30.【必备】位运算的骚操作

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解031【必备】位运算的骚操作_哔哩哔哩_bilibili

一.2的幂

题目: 2 的幂

算法原理

  • 整体思路
    • 这个算法利用了整数在二进制表示下的特性以及补码的性质来判断一个整数是否为2的幂。
  • 具体原理
    • 对于n & -n操作
      • 在计算机中,负数是以其正数的补码形式存储的。对于一个正数(n),设(n)的二进制表示为(x_{n}x_{n - 1}\cdots x_{m}\cdots x_{1}),其中x_{m}是从右往左数第一个1(即最右侧的1)。
      • 那么(-n)的二进制表示为(\overline{x_{n}}\overline{x_{n - 1}}\cdots\overline{x_{m + 1}}1\overline{x_{m - 1}}\cdots\overline{x_{1}})(这里\overline{x}表示x按位取反)。
      • 当进行(n& - n)操作时,对于n中x_{m}左边的位,由于n中的位与(-n)中对应位是相反的(因为取反操作),所以这些位按位与的结果为0。
      • 而对于x_{m}以及其右边的位,-n中从x_{m}开始都是1和0的组合,x_{m}与n按位与后,只有这一位为1(因为x_{m}本身为1,(-n)中这一位也是1),其余右边的位都为0。所以(n& - n)的结果就是n二进制表示中最右侧的1。
    • 判断是否为2的幂
      • 如果一个数n是2的幂,那么它的二进制表示中只有一个1。例如(2^{0}=1)(二进制1),2^{1}=2(二进制10),2^{2}=4(二进制(100))等。
      • 当(n > 0)且(n=(n& - n))时,说明(n)的二进制表示中只有一个(1),即(n)是2的幂。因为如果n是2的幂,那么n的二进制表示中最右侧的1就是它本身,也就是(n& - n=n);反之,如果n不是2的幂,那么它的二进制表示中有多个1,(n& - n)的结果就不等于n。

代码实现

// Brian Kernighan算法
// 提取出二进制里最右侧的1
// 判断一个整数是不是2的幂
// 测试链接 : https://leetcode.cn/problems/power-of-two/
public class Code01_PowerOfTwo {

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

}

二.3的幂

题目:3 的幂

算法原理

  • 整体思路
    • 这个算法的核心思路是利用了3的幂在整数范围内的特性来判断一个整数是否为3的幂。通过与整数范围内最大的3的幂进行取余运算来得出结果。
  • 具体原理
    • 3的幂的特性
      • 如果一个数(n)是3的幂,那么(n)可以表示为(3^{k})((k)为整数)。
      • 在(int)型范围内,最大的3的幂是(3^{19}=1162261467)。由于3是质数,所以这个数只含有3这一个质数因子。
    • 判断方法
      • 对于给定的整数(n),首先要求(n > 0),因为3的幂必然是正数。
      • 然后通过(1162261467%n == 0)来判断。如果n是3的幂,那么n的唯一质数因子就是3,因为1162261467也只含有3这个质数因子,所以1162261467能被n整除,即(1162261467%n == 0);反之,如果(1162261467%n != 0),这就说明n一定含有除3以外的其他因子,那么n就不是3的幂。

代码实现

// 判断一个整数是不是3的幂
// 测试链接 : https://leetcode.cn/problems/power-of-three/
public class Code02_PowerOfThree {

    // 如果一个数字是3的某次幂,那么这个数一定只含有3这个质数因子
    // 1162261467是int型范围内,最大的3的幂,它是3的19次方
    // 这个1162261467只含有3这个质数因子,如果n也是只含有3这个质数因子,那么
    // 1162261467 % n == 0
    // 反之如果1162261467 % n != 0 说明n一定含有其他因子
    public static boolean isPowerOfThree(int n) {
        return n > 0 && 1162261467 % n == 0;
    }

}

三.返回大于等于n的最小的2的幂

算法原理

  • 整体思路
    • 该算法的目的是找到大于等于给定非负整数(n)的最小的(2)的幂次方数。通过一系列位运算操作来实现这个目标。
  • 具体步骤
    • 处理特殊情况
      • 当(n\leq0)时,按照要求返回(1),因为(1 = 2^{0})是最小的(2)的幂次方数。
    • 核心位运算操作(当(n > 0)时)
      • 首先将(n)减(1),这一步是关键操作。例如,如果(n = 5)(二进制(101)),(n - 1 = 4)(二进制(100))。
      • 然后进行一系列的位或(\vert)和无符号右移(\ggg)操作。
        • 第一次(n\vert n\ggg1):对于(n = 4)(二进制(100)),(n\ggg1 = 2)(二进制(010)),(n\vert n\ggg1 = 6)(二进制(110))。这个操作的目的是将最高位(1)之后的位都填充为(1)。
        • 第二次(n\vert n\ggg2):对于(n = 6)(二进制(110)),(n\ggg2 = 1)(二进制(001)),(n\vert n\ggg2 = 7)(二进制(111))。进一步将更高位的位填充为1。
        • 以此类推,经过(n\vert n\ggg4)、(n\vert n\ggg8)和(n\vert n\ggg16)这些操作后,n的二进制表示中从最高位的(1)开始到最低位都被填充为(1)。
      • 最后将结果加(1),得到大于等于(n)的最小的(2)的幂次方数。例如,经过前面操作后(n = 7)(二进制(111)),(n + 1 = 8)(二进制(1000)),(8 = 2^{3})就是大于等于5的最小的2的幂次方数。

代码实现

// 已知n是非负数
// 返回大于等于n的最小的2某次方
// 如果int范围内不存在这样的数,返回整数最小值
public class Code03_Near2power {

    public static final int near2power(int n) {
        if (n <= 0) {
            return 1;
        }
        n--;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return n + 1;
    }

    public static void main(String[] args) {
        int number = 100;
        System.out.println(near2power(number));
    }

}

四.数字范围按位与

题目:数字范围按位与

算法原理

  • 整体思路
    • 这个算法的目的是求出给定区间([left, right])内所有数字按位与(&)的结果。通过不断调整(right)的值,直到(left = right),此时(right)的值就是区间内所有数字按位与的结果。
  • 具体原理
    • 关于(right-=right& - right)操作
      • (right& - right)这个操作是提取(right)二进制表示中最右侧的1(参考Brian Kernighan算法原理)。
      • 当我们从right中减去它二进制表示中最右侧的1时,相当于将right的二进制表示中最右侧的1及其后面的0都变为0。
      • 在这个区间[left, right]中,按位与操作的结果是逐渐变小的。例如,对于区间[5, 7],5的二进制为(101),7的二进制为(111)),如果我们不断地从7中减去其最右侧的1,(7 - 1=6)(二进制(110)),(6 - 2 = 4)(二进制(100)),此时4就是5和7按位与的结果。
    • 循环终止条件
      • 当(left < right)时,不断调整(right)。一旦(left = right),就说明已经将(right)调整到了区间内所有数字按位与的结果,此时返回(right)。因为在调整过程中,我们始终保证调整后的(right)与区间内的所有数字按位与的结果是不变的。

代码实现

// 给你两个整数 left 和 right ,表示区间 [left, right]
// 返回此区间内所有数字 & 的结果
// 包含 left 、right 端点
// 测试链接 : https://leetcode.cn/problems/bitwise-and-of-numbers-range/
public class Code04_LeftToRightAnd {

    public static int rangeBitwiseAnd(int left, int right) {
        while (left < right) {
            right -= right & -right;
        }
        return right;
    }

}

五.颠倒二进制位

题目:颠倒二进制位

算法原理

  • 整体原理
    • 这个算法的目的是将一个整数的二进制表示进行逆序。它通过一系列位操作逐步将二进制数的各个部分进行交换,最终实现整个二进制数的逆序。
  • 具体步骤
    • 第一次操作:n = ((n & 0xaaaaaaaa) >>> 1) | ((n & 0x55555555) << 1);
      • 这里将整数n的二进制表示看作是由相邻的两位组成的一组。
      • 0xaaaaaaaa(二进制为10101010101010101010101010101010)和n进行按位与操作后,得到的结果是每一组中的高位,然后将这个结果右移1位。
      • 0x55555555(二进制为01010101010101010101010101010101)和n进行按位与操作后,得到的结果是每一组中的低位,然后将这个结果左移1位。
      • 最后将这两个结果进行按位或操作,这样就实现了每一组相邻两位的交换。
    • 第二次操作:n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2);
      • 此时将经过第一次操作后的n的二进制表示看作是由相邻的四位组成的一组。
      • 0xcccccccc(二进制为11001100110011001100110011001100)和n进行按位与操作后,得到的结果是每一组中的高两位,然后将这个结果右移2位。
      • 0x33333333(二进制为00110011001100110011001100110011)和n进行按位与操作后,得到的结果是每一组中的低两位,然后将这个结果左移2位。
      • 最后将这两个结果进行按位或操作,实现了每一组相邻四位的内部交换。
    • 第三次操作:n = ((n & 0xf0f0f0f0) >>> 4) | ((n & 0x0f0f0f0f) << 4);
      • 把经过第二次操作后的n的二进制表示看作是由相邻的八位组成的一组。
      • 0xf0f0f0f0(二进制为11110000111100001111000011110000)和n进行按位与操作后,得到的结果是每一组中的高四位,然后将这个结果右移4位。
      • 0x0f0f0f0f(二进制为00001111000011110000111100001111)和n进行按位与操作后,得到的结果是每一组中的低四位,然后将这个结果左移4位。
      • 最后将这两个结果进行按位或操作,实现了每一组相邻八位的内部交换。
    • 第四次操作:n = ((n & 0xff00ff00) >>> 8) | ((n & 0x00ff00ff) << 8);
      • 将经过第三次操作后的n的二进制表示看作是由相邻的十六位组成的一组。
      • 0xff00ff00(二进制为11111111000000001111111100000000)和n进行按位与操作后,得到的结果是每一组中的高八位,然后将这个结果右移8位。
      • 0x00ff00ff(二进制为00000000111111110000000011111111)和n进行按位与操作后,得到的结果是每一组中的低八位,然后将这个结果左移8位。
      • 最后将这两个结果进行按位或操作,实现了每一组相邻十六位的内部交换。
    • 第五次操作:n = (n >>> 16) | (n << 16);
      • 此时将经过第四次操作后的n的二进制表示看作是由高十六位和低十六位组成的两组。
      • n右移16位得到高十六位部分,左移16位得到低十六位部分。
      • 最后将这两个结果进行按位或操作,实现了高十六位和低十六位的交换,从而完成了整个二进制数的逆序。

代码实现

// 逆序二进制的状态
// 测试链接 : https://leetcode.cn/problems/reverse-bits/
public class Code05_ReverseBits {

    // 逆序二进制的状态
    // 是不是看着头皮发麻啊?代码看着很魔幻吧?别慌
    public static int reverseBits(int n) {
        n = ((n & 0xaaaaaaaa) >>> 1) | ((n & 0x55555555) << 1);
        n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2);
        n = ((n & 0xf0f0f0f0) >>> 4) | ((n & 0x0f0f0f0f) << 4);
        n = ((n & 0xff00ff00) >>> 8) | ((n & 0x00ff00ff) << 8);
        n = (n >>> 16) | (n << 16);
        return n;
    }

}

六.汉明距离

题目:汉明距离

算法原理

  • 整体思路
    • 这个算法的目标是计算两个整数x和y之间的汉明距离。首先通过x和y的异或(x ^ y)得到一个新的整数,这个整数的二进制表示中,1的位置就是x和y二进制位不同的位置。然后计算这个新整数二进制表示中1的个数,这个个数就是汉明距离。而计算一个整数二进制中1的个数是通过一系列的位运算和加法运算来实现的。
  • 具体原理
    • 计算汉明距离
      • 对于(hammingDistance)方法,首先计算(x ^ y)。异或操作的特性是,当两个对应位不同时结果为1,相同时结果为(0)。所以(x ^ y)的结果中1的位置就是x和y二进制位不同的位置。然后调用(cntOnes)方法计算这个结果中1的个数。
    • 计算整数二进制中(1)的个数((cntOnes)方法)
      • 第一步
        • 使用掩码(0x55555555)(二进制(01010101010101010101010101010101))。
        • (n&0x55555555)得到(n)二进制表示中奇数位的值,((n\ggg1)&0x55555555)得到n二进制表示中偶数位的值(经过右移后),然后将这两个结果相加。这一步相当于将相邻的两位看作一组,计算每组中(1)的个数,因为每组两位相加后,结果就是这组中1的个数(例如(01 + 10 = 11),表示有2个1)。
      • 第二步
        • 使用掩码(0x33333333)(二进制(00110011001100110011001100110011))。
        • (n&0x33333333)得到每2位中的高位组的值,((n\ggg2)&0x33333333)得到每2位中的低位组的值(经过右移后),然后将这两个结果相加。这一步相当于将相邻的两组(每组2位)看作一组,计算每组中1的个数。
      • 第三步
        • 使用掩码(0x0f0f0f0f)(二进制(00001111000011110000111100001111))。
        • 类似前面的操作,以4位为一组计算每组中1的个数。
      • 第四步
        • 使用掩码(0x00ff00ff)(二进制(00000000111111110000000011111111))。
        • 以8位为一组计算每组中1的个数。
      • 第五步
        • 使用掩码(0x0000ffff)(二进制(00000000000000001111111111111111))。
        • 以16位为一组计算每组中1的个数。最后得到的结果n就是原整数n二进制表示中1的个数。
  • 代码实现

// 返回n的二进制中有几个1
// 两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
// 给你两个整数 x 和 y,计算并返回它们之间的汉明距离
// 测试链接 : https://leetcode.cn/problems/hamming-distance/
public class Code06_CountOnesBinarySystem {

    public static int hammingDistance(int x, int y) {
        return cntOnes(x ^ y);
    }

    // 返回n的二进制中有几个1
    // 这个实现脑洞太大了
    public static int cntOnes(int n) {
        n = (n & 0x55555555) + ((n >>> 1) & 0x55555555);
        n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
        n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f);
        n = (n & 0x00ff00ff) + ((n >>> 8) & 0x00ff00ff);
        n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff);
        return n;
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值