• 编程之美:位运算应用集萃


    Bits are everything.


    引子

    位是程序世界的本源。一切软件计算不过是位的相加与复制。位向量(位的顺序数组)的空间效率以及位运算的高效,使得位运算深受资深程序员的喜爱。看见几行位运算,不由得会生出几分崇敬之情。本文来盘点一下位运算的应用。

    基本操作

    位的基本操作包括:

    • 位的检测与置位:检测位是否为 0 或 1 , 将某一位置 0 或 置 1。通常用于位向量中,可以用来排序或者过滤。
    • 与或非、异或。与或非运算通常用来实现掩码,获取某个或多个标识位;异或可用于计算、去重、交换。
    • 算术左移或算术右移,逻辑左移或逻辑右移:结合与或非运算,用于定位某个位。
    public class BitOp {
    
        // 取 n 的第 m 位
        public static int getBit(int n, int m){
            return (n >> (m-1)) & 1;
        }
    
        // 将 n 的第 m  位置 1
        public static int setBitToOne(int n, int m){
            return n | (1 << (m-1));
        }
    
        // 将 n 的第 m 位置 0
        public static int setBitToZero(int n, int m){
            return n & ~(1 << (m-1));
        }
    
        public static String toBinaryString(int n) {
            StringBuilder s = new StringBuilder();
            for (int i=32; i >0; i--) {
                s.append(getBit(n, i));
            }
            return s.toString();
        }
    
        static class BitOpTester {
            public static void main(String[]args) {
                test(999);
                test(-999);
            }
        }
    
        public static void test(int n) {
            testUnit(n);
            int n1 = setBitToOne(n, 29);
            testUnit(n1);
            int n2 = setBitToZero(n, 6);
            testUnit(n2);
        }
    
        public static void testUnit(int num) {
            String standardBinaryStr = Integer.toBinaryString(num);
            System.out.println("Standard: " + standardBinaryStr);
            String myOwnBinaryStr = toBinaryString(num);
            System.out.println("My Own: " + myOwnBinaryStr);
            assert standardBinaryStr.equals(myOwnBinaryStr);
        }
    }
    
    

    获取不小于 cap 的 2 的幂次数(java.util.HashMap):

        /**
         * Returns a power of two size for the given target capacity.
         */
        static final int tableSizeFor(int cap) {
            int n = cap - 1;
            n |= n >>> 1;
            n |= n >>> 2;
            n |= n >>> 4;
            n |= n >>> 8;
            n |= n >>> 16;
            return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
        }
    

    状态位组合标识

    位的最常见使用,莫过于状态位组合标识了。 每个位代表某个含义,而这些含义是可组合的。


    文件权限

    Linux 系统的文件读/写/执行权限,使用了三位来标识:

    • 读取权限: r = 100
    • 写入权限 w = 010
    • 执行权限 x = 001

    Java 文件读写权限封装在 FilePermission 这个类里。获取 mask 对应的读写执行权限描述在 getActions 这个方法里。

    public final class FilePermission extends Permission implements Serializable {
    
        private final static int EXECUTE = 0x1;
        private final static int WRITE   = 0x2;
        private final static int READ    = 0x4;
        private final static int DELETE  = 0x8;
        private final static int READLINK    = 0x10;
        private final static int NONE    = 0x0;
    
        private transient int mask;
    
        private final static int ALL     = READ|WRITE|EXECUTE|DELETE|READLINK;
    

    方法标识

    Java 方法的修饰符标识是采用位来标识。

        public static final int PUBLIC           = 0x00000001;
        public static final int PRIVATE          = 0x00000002;
        public static final int PROTECTED        = 0x00000004;
        public static final int STATIC           = 0x00000008;
        public static final int FINAL            = 0x00000010;
        public static final int SYNCHRONIZED     = 0x00000020;
        public static final int VOLATILE         = 0x00000040;
        public static final int TRANSIENT        = 0x00000080;
        public static final int NATIVE           = 0x00000100;
        public static final int INTERFACE        = 0x00000200;
        public static final int ABSTRACT         = 0x00000400;
        public static final int STRICT           = 0x00000800;
    

    要知道某位是否存在,可以使用与运算,因为与运算有检测位的作用。

        public static boolean isStrict(int mod) {
            return (mod & STRICT) != 0;
        }
    

    要知道有哪些组合含义,可以使用或运算,因为或运算有保留位的作用。

       /**
         * The Java source modifiers that can be applied to a class.
         * @jls 8.1.1 Class Modifiers
         */
        private static final int CLASS_MODIFIERS =
            Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE |
            Modifier.ABSTRACT       | Modifier.STATIC       | Modifier.FINAL   |
            Modifier.STRICT;
    

    联合或运算与左移或右移操作,可以用来对多个位进行拼接,比如 SnowFlake 算法里的实现(可参阅 “Snowflake Java”):

    
        /**
         * 起始的时间戳
         */
        private final static long START_STMP = 1480166465631L;
    
        /**
         * 每一部分占用的位数
         */
        private final static long SEQUENCE_BIT = 12; //序列号占用的位数
        private final static long MACHINE_BIT = 5;   //机器标识占用的位数
        private final static long DATACENTER_BIT = 5;//数据中心占用的位数
    
        /**
         * 每一部分的最大值
         */
        private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
        private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
        private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
    
        /**
         * 每一部分向左的位移
         */
        private final static long MACHINE_LEFT = SEQUENCE_BIT;
        private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
        private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
    
        private long datacenterId;  //数据中心
        private long machineId;     //机器标识
        private long sequence = 0L; //序列号
        private long lastStmp = -1L;//上一次时间戳
       /**
         * 产生下一个ID
         *
         * @return
         */
        public synchronized long nextId() {
            long currStmp = getNewstmp();
            if (currStmp < lastStmp) {
                throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
            }
    
            if (currStmp == lastStmp) {
                //相同毫秒内,序列号自增
                sequence = (sequence + 1) & MAX_SEQUENCE;
                //同一毫秒的序列数已经达到最大
                if (sequence == 0L) {
                    currStmp = getNextMill();
                }
            } else {
                //不同毫秒内,序列号置为0
                sequence = 0L;
            }
    
            lastStmp = currStmp;
    
            return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                    | datacenterId << DATACENTER_LEFT       //数据中心部分
                    | machineId << MACHINE_LEFT             //机器标识部分
                    | sequence;                             //序列号部分
        }
    
    

    标签

    在应用程序里,常常用位的枚举来实现标签作用。

    打包数据

    比如 Java 线程池实现 ThreadPoolExecutor 将线程池状态和工作线程数打包在一个整数里。

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
        private static final int COUNT_BITS = Integer.SIZE - 3;
        private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
    
        // runState is stored in the high-order bits
        private static final int RUNNING    = -1 << COUNT_BITS;
        private static final int SHUTDOWN   =  0 << COUNT_BITS;
        private static final int STOP       =  1 << COUNT_BITS;
        private static final int TIDYING    =  2 << COUNT_BITS;
        private static final int TERMINATED =  3 << COUNT_BITS;
    
        // Packing and unpacking ctl
        private static int runStateOf(int c)     { return c & ~CAPACITY; }
        private static int workerCountOf(int c)  { return c & CAPACITY; }
        private static int ctlOf(int rs, int wc) { return rs | wc; }
    
    

    检测存在性

    位图排序

    位图可以将大量 key 映射到一个占用空间较少的位向量上,从而根据位向量来判断 key 的存在性。一个典型应用是,可以用来对稠密的很大的不重复整数列表进行排序,可节省不少空间。位图实现可阅 “位图排序(位图技术应用)”

    由于 Redis 提供了位数组功能,实现不重复数组排序的代码如下所示:

        public void sort(int[] list) {
            for (int i: list) {
               jedis.setbit("arr", i, true);
            }
            int max = max(list);
            for (int i=0; i <= max; i++) {
                if (jedis.getbit("arr", i)) {
                    System.out.printf(String.format("%d ", i));
                }
            }
        }
    

    布隆过滤器

    可以使用位运算来实现布隆过滤器。可以确定性地判断某个 key 不在某个集合里。布隆过滤器使用多个哈希函数,将 key 映射在多个位上(将相应位置一)。查找 key 时,如果发现某个位为 0 ,则表示该 key 不存在。

    com.google.common.hash 包下的 BloomFilter 和 BloomFilterStrategies 给出了一个实现。BloomFilter 主要是起封装和入口作用,而 BloomFilterStrategies 是核心实现。注意到,由于 bitArray 是 long 型数组,因此使用 data[index >> 6] & (1L << index) 来获取 index 在这个 long 型数组所代表的位向量中的位置。这里给出了在一个整数数组代表的位向量里如何定位某个位的技巧。

    enum BloomFilterStrategies implements BloomFilter.Strategy {
      /**
       * See "Less Hashing, Same Performance: Building a Better Bloom Filter" by Adam Kirsch and
       * Michael Mitzenmacher. The paper argues that this trick doesn't significantly deteriorate the
       * performance of a Bloom filter (yet only needs two 32bit hash functions).
       */
      MURMUR128_MITZ_32() {
        @Override public <T> boolean put(T object, Funnel<? super T> funnel,
            int numHashFunctions, BitArray bits) {
          long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
          int hash1 = (int) hash64;
          int hash2 = (int) (hash64 >>> 32);
          boolean bitsChanged = false;
          for (int i = 1; i <= numHashFunctions; i++) {
            int nextHash = hash1 + i * hash2;
            if (nextHash < 0) {
              nextHash = ~nextHash;
            }
            bitsChanged |= bits.set(nextHash % bits.bitSize());
          }
          return bitsChanged;
        }
    
        @Override public <T> boolean mightContain(T object, Funnel<? super T> funnel,
            int numHashFunctions, BitArray bits) {
          long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
          int hash1 = (int) hash64;
          int hash2 = (int) (hash64 >>> 32);
          for (int i = 1; i <= numHashFunctions; i++) {
            int nextHash = hash1 + i * hash2;
            if (nextHash < 0) {
              nextHash = ~nextHash;
            }
            if (!bits.get(nextHash % bits.bitSize())) {
              return false;
            }
          }
          return true;
        }
      };
    
      // Note: We use this instead of java.util.BitSet because we need access to the long[] data field
      static class BitArray {
        final long[] data;
        int bitCount;
    
        BitArray(long bits) {
          this(new long[Ints.checkedCast(LongMath.divide(bits, 64, RoundingMode.CEILING))]);
        }
    
        // Used by serialization
        BitArray(long[] data) {
          checkArgument(data.length > 0, "data length is zero!");
          this.data = data;
          int bitCount = 0;
          for (long value : data) {
            bitCount += Long.bitCount(value);
          }
          this.bitCount = bitCount;
        }
    
        /** Returns true if the bit changed value. */
        boolean set(int index) {
          if (!get(index)) {
            data[index >> 6] |= (1L << index);
            bitCount++;
            return true;
          }
          return false;
        }
    
        boolean get(int index) {
          return (data[index >> 6] & (1L << index)) != 0;
        }
    
        /** Number of bits */
        int bitSize() {
          return data.length * Long.SIZE;
        }
    
        /** Number of set bits (1s) */
        int bitCount() {
          return bitCount;
        }
    
        BitArray copy() {
          return new BitArray(data.clone());
        }
    
        /** Combines the two BitArrays using bitwise OR. */
        void putAll(BitArray array) {
          checkArgument(data.length == array.data.length,
              "BitArrays must be of equal length (%s != %s)", data.length, array.data.length);
          bitCount = 0;
          for (int i = 0; i < data.length; i++) {
            data[i] |= array.data[i];
            bitCount += Long.bitCount(data[i]);
          }
        }
    
        @Override public boolean equals(Object o) {
          if (o instanceof BitArray) {
            BitArray bitArray = (BitArray) o;
            return Arrays.equals(data, bitArray.data);
          }
          return false;
        }
    
        @Override public int hashCode() {
          return Arrays.hashCode(data);
        }
      }
    }
    

    哈希与取模

    由于位运算非常高效,通常都会用在哈希与取模运算中。比如 HashMap 的哈希值映射:

    hash = (h = key.hashCode()) ^ (h >>> 16);   // 高位与低位异或,充分使用到高位和低位
    index = (n - 1) & hash;  // n = 2^m
    

    异或运算用在 FNV 哈希算法里。可以在 GitHub 里搜索 FNV 得到各种语言的实现。 FNV 的一个 Java 实现可参见: “fnv-java”

    去重与交换

    异或操作符合交换律、结合律和自反性质,可用于去重和交换。 a^a = 0, a^0 = a , abc = a(bc) = a(cb)。 这使得异或有一些妙用。

    一个经典的面试题是:假如你有一个用 1001 个整数组成的数组,这些整数是任意排列的,但是你知道所有的整数都在 1 到 1000 之间(包括 1000 )、此外,除了一个数字出现两次外,其他的数字只出现了一次。使用异或的解法是:先将所有数异或一遍,再依次异或 1-1000 的数即可。 用个简单的例子,假设含有 [1,2,3,2] ,那么方法是: 1232123 = 2 重复数就是 2.

    一个变形的面试题是:一个数组存放若干整数,一个数出现奇数次,其余数均出现偶数次,找出这个出现奇数次的数?实际上道出了上一题的本质。

    异或可以用来交换两个变量。

    a=a^b;  b=a^b;  a=a^b;
    

    科学计算

    补码

    使用位运算实现科学计算,即是从位的角度来分析数值计算。绝大多数机器上,整数是通过补码来表示的。补码的定义如下:


    加法

    异或可以实现无进位加法。对于有进位的加法,可以分解为两部分:

    • 无进位的部分:使用异或运算 a^b = c ;
    • 有进位的部分:使用 (a & b) << 1 可以得到进位的和 d ;
    • 相加:将 c 和 d 相加,这时候会递归调用加法运算。

    如下代码所示:

    
    // 位运算实现加法
    int bitAdd(int a,int b)
    {
        if(b==0)
            return a;
        int sum = a^b;
        int carry =(a&b)<<1;
        return bitAdd(sum,carry);
    }
    

    对于平均值的计算,思路类似。只是进位 d 除以 2 正好等于 a & b。

    
        // 平均值
        public static int avg(int a, int b) {
            return ((a ^ b) >> 1) + (a & b);
        }
    
    

    其它使用位运算进行一些计算的例子如下(由于字长是有限的,计算机表示的整数范围是有限的,因此计算要谨防溢出。在根据加减运算来判断大小时,也要谨防溢出导致的错误):

    (x ^ y) >= 0;  // 判断两个数的符号是否相同(实际上是高位异或,如果相同就是 0 ,不同就是 1)
    x & 1 == 0 ?  false : true;  // 判断奇偶,即判断最低位是 0 还是 1 
    x & (x-1) ;  // 可以去掉最右边的 1 ,计算某个数有多少个二进制位 1 
    
        // 绝对值,当取最小负整数时没有对应绝对值。
        public static int abs(int n) {
            return (n ^ (n >> 31)) - (n >> 31);
        }
    
        public static int max(int a, int b) {
            if ((a^b) > 0) {
                // 同符号时 a-b 不会溢出,避免错误
                return b & ((a-b) >> 31) | a & (~(a-b) >> 31);
            }
            return ((a ^ (1 << 31)) < 0) ? a : b;   // 不同符号取非负即可
        }
    
        public static int min(int a, int b) {
            if ((a^b) > 0) {
                // 同符号时 a-b 不会溢出,避免错误
                return a & ((a-b) >> 31) | b & (~(a-b) >> 31);
            }
            return ((a ^ (1 << 31)) >= 0) ? a : b;    // 不同符号取负即可
        }
    
        // 判断正负,即判断最高位是 0 还是 1
        public static boolean isNotNegative(int x) {
            return (x ^ (1 << 31)) >= 0 ? false : true;
        }
    
    

    小结

    位是一切程序的起点。位运算的使用主要体现在位向量和位运算的高效上,广泛应用于科学计算、哈希计算、存在性检测、状态位组合标识等场景。掌握位运算,就像携带了一把匕首,能够有效解决不少问题。


    参考资料


  • 相关阅读:
    Java 从入门到进阶之路(五)
    Java 从入门到进阶之路(四)
    Java 从入门到进阶之路(三)
    Java 从入门到进阶之路(二)
    Java 从入门到进阶之路(一)
    调用百度翻译 API 来翻译网站信息
    jquery.i18n 网站呈现各国语言
    VUE+Element UI实现简单的表格行内编辑效果
    Python 爬虫从入门到进阶之路(十八)
    Python 爬虫从入门到进阶之路(十七)
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/14026127.html
Copyright © 2020-2023  润新知