从现在开始准备跳槽位运算

2023年 8月 23日 83.9k 0

今天在复习hashMap时又碰到了这句:

HashMap内部的bucket数组长度一直都是2的整数次,这样的优点是:第一,可以通过(table.length - 1) & key.hash()这样的位运算快速寻址,第二,在HashMap扩容的时候可以保证同一个桶中的元素均匀地散列到新的桶中,具体一点就是同一个桶中的元素在扩容后一半留在原先的桶中,一半放到了新的桶中。

那么就来从头到尾复习下位运算。

什么是位运算?

位运算是一种对二进制数进行操作的运算方式,它可以直接对二进制数的位进行操作,而不需要转换成十进制数。

在计算机中,数据都是以二进制形式存储和处理的。位运算是根据二进制数的每一位进行逻辑运算或位操作的运算方法,常见的位运算操作符包括:

按位与(&):将两个二进制数的对应位同时为1时,结果的对应位才为1,否则为0。

按位或(|):将两个二进制数的对应位只要有一个为1,结果的对应位就为1。

按位异或(^):将两个二进制数的对应位不同的情况下,结果的对应位为1,相同则为0。

按位取反(~):将二进制数的每一位取反,即1变为0,0变为1。

左移():将二进制数的所有位向右移动指定的位数,右边移出的位丢弃,左边的位根据符号位进行填充。

位运算主要用于在底层编程、位操作优化和位掩码等场景中,例如处理图像数据、位图算法、位字段操作等。位运算可以提高运算速度和节省空间,因为位运算可以在底层直接操作二进制位,而不需要通过转换为十进制数进行计算。

以下是一个示例代码,演示了一些常见的位运算操作:

public class Example {

    public static void main(String[] args) {

        int a = 5;  // 二进制表示为 101

        int b = 3;  // 二进制表示为 011

 

        int c = a & b;   // 按位与运算,结果为 001,即二进制的 1

        int d = a | b;   // 按位或运算,结果为 111,即二进制的 7

        int e = a ^ b;   // 按位异或运算,结果为 110,即二进制的 6

        int f = ~a;      // 按位取反运算,结果为 11111111111111111111111111111010,即二进制的 -6

        int g = a > 1;  // 右移运算,结果为 001,即二进制的 1

 

        System.out.println(c);

        System.out.println(d);

        System.out.println(e);

        System.out.println(f);

        System.out.println(g);

        System.out.println(h);

    }

}

hashMap中位运算的应用

在 HashMap 数据结构中,当插入或查找一个元素时,需要计算该元素的哈希值(hashCode)和它在数组中的索引位置。Java 采用的哈希表是一个数组加链表的结构,其中哈希值计算后通过位运算映射到数组的索引位置。

在 HashMap 中,为了计算元素在数组中的索引位置,通常采用的方法是取 hashCode 值和数组长度的模运算(%)来计算,即 index = hashCode % table.length。但是,这种方法存在一个问题:当数组长度为 2 的幂次方时,取模运算等价于对数组长度进行按位与操作,即 index = hashCode & (table.length - 1)。而位运算比取模运算更快,因此采用位运算可以提高 HashMap 的性能。

例如,当数组长度为 16 时,取模运算需要执行 hashCode % 16,而位运算则可以执行 hashCode & 0x0f,其计算结果相同,但是位运算比取模运算要快得多。

以下是一个示例代码,演示了使用位运算计算 HashMap 中元素索引的方法:


public class Example {

    public static void main(String[] args) {

        int hashCode = "hello".hashCode();

        int tableLength = 16;

        int index = hashCode & (tableLength - 1);

        System.out.println(index);

    }

}

在上面的示例中,我们定义了一个字符串 "hello",并获取它的哈希值。然后,我们定义了一个数组长度为 16,并使用位运算计算字符串在数组中的索引位置,并将其打印到控制台上。

输出结果为:7,这是字符串 "hello" 的哈希值在数组中的索引位置,通过位运算计算得到的。

位运算为什么比模运算更快

主要有两个方面:算术运算和硬件优化。

算术运算:位运算是直接对二进制位进行操作的运算方式,而取模运算涉及到除法运算,除法运算通常比位运算需要更多的指令和计算量。计算机底层对于位运算有特定的硬件支持,使得位运算可以在更短的时间内完成。

硬件优化:现代计算机的处理器在设计时通常会对位运算进行了优化,主要体现在 CPU 的指令集和硬件电路设计中。

在 CPU 的指令集中,通常会包括一些专门用于位运算的指令,例如按位与(AND)、按位或(OR)、按位异或(XOR)、按位取反(NOT)、左移(SHIFT)和右移(SHIFT)等。这些指令可以直接对二进制位进行操作,而不需要通过逐位计算来实现,从而提高了运算速度。

此外,现代 CPU 的架构中通常都包括一个位运算逻辑单元(ALU),用于处理位运算操作。ALU 是 CPU 中最基本的运算单元之一,它负责执行算术和逻辑运算,其中就包括位运算操作。ALU 通常被设计为高度并行的硬件电路,可以同时处理多个二进制位的运算,从而提高运算速度。

另外,计算机内存中的存储单元也是按照二进制位来组织的,每个存储单元都包含一定数量的二进制位。在对存储单元进行位运算时,计算机可以直接在存储单元中进行运算,而不需要将数据从存储单元中取出来,这也提高了运算速度。

并且,位运算的操作数通常保存在寄存器中,而不是内存中,这也减少了访问内存的开销,进一步提高了速度。

总之,CPU 中的专用指令和运算电路,以及内存中的二进制位存储结构,都为位运算提供了高效和快速的支持。

需要注意的是,位运算比取模运算快并非在所有情况下都成立。在一些情况下,编译器和优化器可以通过优化算法和硬件指令自动选择最优的计算方式。因此,性能差异可能因系统、编译器和具体的运算场景而异。在实际编程中,应根据具体情况选择适当的运算方式,并进行性能测试和优化。

位运算可能比模运算慢吗?

虽然位运算通常比取模运算快,但在一些特定的情况下,位运算也可能比取模运算慢。以下是一些可能导致位运算比取模运算慢的情况:

1、操作数不在寄存器中:位运算需要操作数在寄存器中,才能发挥其最大的效能,但如果操作数在内存中,则需要先将其加载到寄存器中,这会增加访问内存的时间。而取模运算则不受此限制。

2、位运算不支持负数:在进行位运算时,如果操作数为负数,则需要将其先转换为正数,这会增加额外的运算时间。而取模运算则可以直接对任何整数进行操作。

3、操作数长度不同:在进行位运算时,如果两个操作数的位数不同,则需要将短操作数的高位补0,才能进行运算。这会增加额外的运算时间和内存开销。而取模运算则不受此限制。

4、操作数过大:在进行位运算时,如果操作数过大,则需要将其分割成多个部分进行运算,这会增加额外的运算时间和内存开销。而取模运算则通常可以直接对操作数进行操作。

JAVA开发中位运算的奇技淫巧

判断奇偶数


/**
 * 返回1表示奇数,返回0表示偶数
 *
 * @param number
 * @return
*/

public static int checkOddEven(int number) {
    return number & 1;
}

位运算判断奇偶数的原理基于二进制数的特性。在二进制表示中,奇数的最低位(最右边的一位)为 1,而偶数的最低位为 0。

通过使用位运算中的按位与(&)操作符,我们可以将一个整数的二进制表示与二进制数 1 进行按位与运算。按位与运算的规则是,如果两个相应位都为 1,则结果为 1;否则,结果为 0。

因此,如果一个整数与二进制数 1 进行按位与运算后的结果为 1,则说明该整数的最低位为 1,即为奇数。如果结果为 0,则说明最低位为 0,即为偶数。

交换两个数

优点

交换两个数的操作经常被用于排序算法中,通常的做法是利用临时变量 或是 运算符的“逆运算”特性(加法和减法、乘法和除法),比如:

a = a + b;

b = a - b;

a = a - b;

a = a * b;

b = a / b;

a = a / b;

但在加减和乘除的逆运算过程中会面临数值溢出的风险,其实位运算中的异或运算也是一种逆运算,且异或运算的逆运算是他本身且可以规避数值溢出的风险。

原理

位运算交换两个数的原理基于异或运算的特性。异或运算(^)的规则是:如果两个相应的二进制位值不同,则该位结果为 1;否则,该位结果为 0。

假设现在有两个整数 a 和 b,我们要交换它们的值。我们可以使用异或运算的特性来实现,具体步骤如下:

  • 对 a 和 b 进行异或运算,得到一个新的值 c,即 c = a ^ b。
  • 对 c 和 a 进行异或运算,得到一个新的值 b,即 b = c ^ a。
  • 对 c 和 b 进行异或运算,得到一个新的值 a,即 a = c ^ b
  • 假设有两个整数 a = 5 和 b = 7。它们的二进制表示分别为:

    a = 0101

    b = 0111

    执行交换操作,具体步骤如下:

    对 a 和 b 进行异或运算,得到 c = a ^ b = 0010。

    对 c 和 a 进行异或运算,得到 b = c ^ a = 0111。

    对 c 和 b 进行异或运算,得到 a = c ^ b = 0101。

    现在,a 和 b 的值已经被交换了,a 的值变为 7,b 的值变为 5。

    通过这种方式,我们可以使用位运算快速、简单地交换两个数的值。

    代码示例:

    public static void swap(int[] array) {
        if (array == null || array.length < 2) {
            return;
        }
    
        int a = array[0];
        int b = array[1];
        a ^= b;
        b ^= a;
        a ^= b;
        array[0] = b;
        array[1] = a;
    }
    

    获取一个数的相反数

    public static int getOppositeNumber(int number) {
        return (~number + 1);
    }
    

    这个案例利用了原码和补码的知识。

    例如,32位系统中正数13(二进制表示为00000000 00000000 00000000 00001101),

    取反为(11111111 11111111 11111111 11110010),

    然后再加1为(11111111 11111111 11111111 11110011),计算机中数是用二进制的补码表示的,

    因此取反加1的结果(11111111 11111111 11111111 11110011)即为-13的二进制补码表示形式。

    常见的一个等式:-n = ~(n - 1) = ~n + 1

    两个数求平均值

    对于两个整数x,y,如果用 (x+y)/2 求平均值,可能会产生溢出,因为 x+y 可能会大于Integer.MAX_VALUE,但是它们的平均值是肯定不会溢出的。

    根据位运算的特性,可以将除法运算转化为位运算来提高计算效率和规避这一风险。具体步骤如下:

    1、首先,通过位与运算符 & 对 x 和 y 进行按位与运算。位与运算符的特性是,当两个数的对应二进制位都为 1 时,结果的对应二进制位才为 1;否则,结果的对应二进制位为 0。这一步的目的是保留 x 和 y 的二进制表示中相同的部分。

    2、然后,通过异或运算符 ^ 对 x 和 y 进行按位异或运算。异或运算符的特性是,当两个数的对应二进制位不同时,结果的对应二进制位为 1;当两个数的对应二进制位相同时,结果的对应二进制位为 0。这一步的目的是将 x 和 y 的二进制表示中不同的部分提取出来。

    3、接下来,通过右移运算符 >> 对异或结果进行右移一位。右移运算符的特性是,将一个数的二进制表示向右移动指定的位数,空出的位数用符号位(即最高位)填充。这一步的目的是将异或结果的二进制表示向右移动一位,相当于将不同的部分除以 2。

    4、最后,将按位与结果和右移结果相加,得到的结果就是 x 和 y 的平均数。

    public static int getAverage(int x, int y) {
        return (x & y) + ((x ^ y) >> 1);
    }
    

    判断一个整数是否是2的幂次

    如果一个整数是2的幂次,则该整数满足两个条件。

    ① 大于0;② 该整数的二进制表示中只有一个1 ;

    所以使用N & (N - 1)将N唯一的一个1消去,应该返回0。

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

    求一个给定整数集合的所有子集

    思路就是使用一个正整数二进制表示的第i位是1还是0,代表集合的第i个数取或者不取。

    所以从0到2^n-1总共2^n个整数,正好对应集合的2^n个子集。

    例如,对于集合 {1, 2, 3},二进制数 101 表示子集 {1, 3},二进制数 111 表示子集 {1, 2, 3}。

    具体步骤如下:

    1、对于一个 n 个元素的集合,可能的子集个数为 2^n。因此,我们可以使用一个循环,循环变量 i 从 0 到 2^n-1,表示所有可能的子集对应的二进制数。

    2、对于每个循环变量 i,我们可以将它转换成对应的二进制数,将二进制数中的 1 对应的整数加入到一个临时集合中。例如,对于集合 {1, 2, 3},当 i = 5 时,对应的二进制数为 101,表示子集 {1, 3}。

    3、将临时集合添加到一个结果集合中,表示找到一个子集。

    4、循环结束后,结果集合存储了所有子集。

    public static List getAllSubsets(int[] nums) {

        List subsets = new ArrayList();
        int n = nums.length;

        // 枚举所有可能的子集对应的二进制数
        for (int i = 0; i < (1

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论