• 前导0计数Integer.numberOfLeadingZeros,后缀0计数Integer.numberOfTailingzeros


    前导0计数

    问题

    1的前导0个数为31,对应16进制数为0x0000 0001。
    2的前导0个数位30,对应16进制数为0x0000 0002。

    如何计算一个int型数对应二进制数前导0的位数呢?

    解决思路

    轮询bit位算法

    开始想到的最简单的办法是从左边最高位开始,向低位轮训,一直到遇见位1。

    // 轮询bit位
    int numberOfLeadingZerors(int x) {
          int n = 0;
    
          for (int i = 31; i >= 0; i--) {
                if ( x & (1 << i) != 0) {
                      break;
                }
                else n++;
          }
    
          return n;
    }
    

    这样,最多需要循环32次,每个循环执行4个指令:判断、按位与、移位、递增。这样可能需要执行32 x 4条指令。

    二分搜索算法

    有没有更快的算法?
    可以使用二分搜索技术计算前导0。这里二分搜索,没有用索引指示器,而是用到了进制数的特性,直接利用二进制数 + 移位操作,来作为0个数的判断点。比如0x0000FFFF,x <= 0x0000FFFF,意味着x至少有16个前导0。

    从下面的代码可以看出,二分搜索法所需要的指令最多为20个(6个if,10个赋值4个移位)指令操作。

    // 二分搜索计算前导0
    int numberOfLeadingZeros(int x) {
          if (x == 0) return 32;
          int n = 0;
          if (x <= 0x0000FFFF) { n = n + 16; x = x << 16; } // 判断高16位是否为0:先通过高16位,判断该数是否位于较小的一半,如果是,说明至少会有16个0,再把该数左移16位,相当于增大模的一半
          if (x <= 0x00FFFFFF) { n = n + 8; x = x << 8; }   // 判断高8位
          if (x <= 0x0FFFFFFF) { n = n + 4; x = x << 4; } // 判断高4位
          if (x <= 0x3FFFFFFF) { n = n + 2; x = x << 2; } // 判断高2位
          if (x <= 0x7FFFFFFF) { n = n + 1; }  // 判断高1位
          return n;
    }
    

    按位与版本,二分搜索计算前导0

    // 二分搜索计算前导0
    int numberOfLeadingZeros(int x) {
          if (x == 0) return 32;
          int n = 0;
          if ((x & 0xFFFF0000) == 0) { n = n + 16; x = x << 16; } // 判断高16位是否为0:先通过高16位,判断该数是否位于较小的一半,如果是,说明至少会有16个0,再把该数左移16位,相当于增大模的一半
          if ((x & 0xFF000000) == 0) { n = n + 8; x = x << 8; }   // 判断高8位
          if ((x & 0xF0000000) == 0) { n = n + 4; x = x << 4; } // 判断高4位
          if ((x & 0xC0000000) == 0) { n = n + 2; x = x << 2; } // 判断高2位
          if ((x & 0x80000000) == 0) { n = n + 1; }  // 判断高1位
          return n;
    }
    

    最后一个if语句,可以改写成 : n = n + 1 - (x >> 31)
    如果将n初值由0设为1,那么最后一个if语句可以改成成:n = n - (x >> 31),其他语句保持不变。

    右移位按位与版本,二分搜索计算前导0

    // 二分搜索计算前导0
    int numberOfLeadingZeros(int x) {
          if (x == 0) return 32;
          int n = 1;
          if ((x >> 16) == 0) { n = n + 16; x = x << 16; }
          if ((x >> 24) == 0) { n = n + 8; x = x << 8; } 
          if ((x >> 28) == 0) { n = n + 4; x = x << 4; }
          if ((x >> 30) == 0) { n = n + 2; x = x << 2; }
          n = n - (x >> 31);
          return n;
    }
    

    如果将n初值设为32,通过减法,上面的算法可以改写成:

    // 二分搜索计算前导0,做减法
    int numberOfLeadingZeros(int x) {
          if (x == 0) return 32;
          int n = 32;
          
          int y;
          y = x >>> 16; if (y != 0) { n -= 16; x = y; } // 验证高16位(bit31 ~ 16)是否为0,如果是,就不用从总计数n - 16;如果不是,就需要 n - 16,因为该区域之前已经算作是0 了
          y = x >>>  8; if (y != 0) { n -= 8; x = y; } // 验证bit31 ~ 8 是否为0,此时bit31 ~ 16 必为0
          y = x >>>  4; if (y != 0) { n -= 4; x = y; } // 验证bit31 ~ 4 是否为0,此时bit31 ~ 8 必为0
          y = x >>>  2; if (y != 0) { n -= 2; x = y; }  // 验证bit31 ~ 2 是否为0,此时bit31 ~ 2必为0
          y = x >>>  1; if (y != 0) return n - 2; // 验证bit31 ~ 1是否为0,当y = x >>> 1且y ≠ 0 时,说明bit1非0,也就是说n多计了bit1和bit0,需要减去
          return n - x;
    }
    

    改写成循环形式

    int nunberOfLeadingZeros(int x) {
          int y;
          int n = 32;
          int c = 16;
          do {
                y = x >>> c; if ( y != 0) { n -= c; x = y; }
                c = c >> 1;
          }while( c != 0);
    
          return n - x;
    }
    

    应用 & JDK源码

    在解析Java源码Integer.toHexString过程中, 碰到了这个函数Integer.numberOfLeadingZeros(int),用于计算前导0的个数。
    Java源码解析:Integer.toHexString

    查看其源码,

        public static int numberOfLeadingZeros(int i) {
            // HD, Figure 5-6
            if (i == 0)
                return 32;
            int n = 1;
            if (i >>> 16 == 0) { n += 16; i <<= 16; }
            if (i >>> 24 == 0) { n +=  8; i <<=  8; }
            if (i >>> 28 == 0) { n +=  4; i <<=  4; }
            if (i >>> 30 == 0) { n +=  2; i <<=  2; }
            n -= i >>> 31;
            return n;
        }
    

    后缀0计数

    二分搜索算法

    同前导0计数,可以通过构造后缀0数目的掩码,写出后缀0计数的二分搜索算法。

    核心思想: 如果右半部分为0,则在左半部继续查找,累计后缀0个数 + 右半部位数;否则,继续在右半部的右半部继续查找,累计后缀0个数不变。

    int numberOfTailingZeros(int x) {
          if (x == 0) return 32;
          int n = 1;
          if ((x & 0x0000FFFF) == 0) { n += 16; x = x >>> 16; }
          if ((x & 0x000000FF) == 0) { n += 8; x = x >>> 8; }
          if ((x & 0x0000000F) == 0) { n += 4; x = x >>> 4; }
          if ((x & 0x00000003) == 0) { n += 2; x = x >>> 2; }
           
          /* 到这里x最多无符号右移了30位,也就是说,只有原始的2个bit位,还没有判断,不过此时作为最新x的bit1、0。
          x = x1 x0 (二进制表示形式,x0, x1 = 0, 1)
          例如,如果此时x = 0b01,那么最终后缀0的个数是 n - 1, 因为n初值是1,所以需要减去1。
          如果此时x = 0b10,那么最终后缀0的个数是 n。
          综上,后缀0个数最终值为 n - (x & 1)
          */
          return n - (x & 1); // 也可以 if (x & 0x00000001) != 0) { n = n-1; } return n;
    }
    

    应用 & JDK源码

    位于Integer.java中的Integer.numberOfTrailingZeros(int),用于计算后缀0个数,其源码:

        public static int numberOfTrailingZeros(int i) {
            // HD, Figure 5-14
            int y;
            if (i == 0) return 32;
            int n = 31;
            y = i <<16; if (y != 0) { n = n -16; i = y; }
            y = i << 8; if (y != 0) { n = n - 8; i = y; }
            y = i << 4; if (y != 0) { n = n - 4; i = y; }
            y = i << 2; if (y != 0) { n = n - 2; i = y; }
            return n - ((i << 1) >>> 31);
        }
    
    

    参考

    1. 《算法心得 高效算法的奥秘(第二版》
  • 相关阅读:
    多态性的理解
    类(三)——继承与多态
    类(二)——拷贝控制(浅拷贝,深拷贝,浅赋值,深赋值)
    类 (一) ——基本概念
    STL容器底层数据结构的实现
    异常处理
    C++实现单例模式
    类的成员函数的连续调用与返回值问题
    拷贝构造函数的参数为什么必须使用引用类型?拷贝赋值运算符的参数为什么也是引用类型?
    U盘装机教程
  • 原文地址:https://www.cnblogs.com/fortunely/p/14274342.html
Copyright © 2020-2023  润新知