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

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

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

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

什么是位运算?

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

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

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

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

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

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

左移(<<):将二进制数的所有位向左移动指定的位数,左边移出的位丢弃,右边补0。

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

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

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

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;  // 左移运算,结果为 1010,即二进制的 10

        int h = b >> 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,我们要交换它们的值。我们可以使用异或运算的特性来实现,具体步骤如下:

  1. 对 a 和 b 进行异或运算,得到一个新的值 c,即 c = a ^ b。
  2. 对 c 和 a 进行异或运算,得到一个新的值 b,即 b = c ^ a。
  3. 对 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到2n-1总共2n个整数,正好对应集合的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<List<Integer>> getAllSubsets(int[] nums) {

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

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

        List<Integer> subset = new ArrayList<>();

        // 将二进制数中的 1 对应的整数加入到临时集合中

        for (int j = 0; j < n; j++) {
            if ((i & (1 << j)) != 0) {
                subset.add(nums[j]);
            }
        }
        subsets.add(subset);
    }

    return subsets;
}

位运算的高级应用

空间压缩

可以用整型的每一位表示一个状态或者标记。这样一个int就可以存储32个状态或标记。

例如一个用户要获取指定优惠信息,优惠信息有很多,例如:红包、优惠券、活动等等。可以将int从低位到高位依次表示红包、优惠券、活动等等。如果该位置为1就表示需要这个优惠信息。通过判断某一位是否为1即可。

// x从低位到高位依次表示红包、优惠券、活动
boolean needCoupon = ((x & 2) == 2);
boolean needAct = ((x & 4) == 4);

位图算法

位图算法是一种使用位运算处理大量数据的技巧。位图算法可以将数据压缩到一个二进制数中,然后使用位运算进行处理,从而实现高效的算法。

在位图算法中,通常使用一个数组(也称为位图)来存储数据。对于一个数据集合,我们将每个数据的值作为数组的下标,在对应位置上设置一个标记,表示该数据存在于数据集合中。由于标记只有 0 和 1 两种取值,因此可以用一个二进制数来表示一个数据集合,从而实现对数据集合的高效处理。

位图算法在计算机科学中有广泛的应用,例如文本处理、图像处理、网络路由、数据压缩等领域。由于位图算法具有高效、简单、易于实现等优点,因此在大数据处理、大规模计算等场景下得到广泛的应用。

下面是一个使用位图算法实现的代码示例,用于对一个数据集合进行查找、插入、删除操作:

public class BitMap {

    private static final int BIT_PER_WORD = 32; // 每个整数有 32 位
    private int[] bitmap;

    public BitMap(int size) {
        bitmap = new int[(size + BIT_PER_WORD - 1) / BIT_PER_WORD]; // 计算需要多少个整数
    }

    public boolean get(int pos) {
        int wordIndex = pos / BIT_PER_WORD; // 计算整数下标
        int bitIndex = pos % BIT_PER_WORD; // 计算位下标
        return (bitmap[wordIndex] & (1 << bitIndex)) != 0; // 判断位是否为 1
    }

    public void set(int pos) {
        int wordIndex = pos / BIT_PER_WORD; // 计算整数下标
        int bitIndex = pos % BIT_PER_WORD; // 计算位下标
        bitmap[wordIndex] |= (1 << bitIndex); // 设置位为 1
    }

    public void clear(int pos) {
        int wordIndex = pos / BIT_PER_WORD; // 计算整数下标
        int bitIndex = pos % BIT_PER_WORD; // 计算位下标
        bitmap[wordIndex] &= ~(1 << bitIndex); // 清除位
    }

    public static void main(String[] args) {

        int[] nums = {1, 3, 5, 7, 9};
        BitMap bitMap = new BitMap(10);

        // 插入数据
        for (int num : nums) {
            bitMap.set(num);
        }

        // 查找数据  
        System.out.println(bitMap.get(5)); // true  
  
        // 删除数据  
        bitMap.clear(5);  
        System.out.println(bitMap.get(5)); // false  
    }  
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值