前导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. 《算法心得 高效算法的奥秘(第二版》
posted @ 2021-01-14 12:19  明明1109  阅读(1140)  评论(0编辑  收藏  举报