最左侧1问题

来源:互联网 发布:天下三捏脸数据男贴吧 编辑:程序博客网 时间:2024/05/17 08:38
今天给大家介绍一个有趣的位运算题目,叫最左侧1问题,英文名叫count leading zeros或most significant bits problem。问题比较好理解,就是给定一个整数,然后看最左侧的1出现在什么位置,等价于寻找整数的最高有效位或者前置零的个数。比如,0x128只有一个1,从右侧遍历1出现在第7位(从0开始计数)。我们给出几种不同的解法。以下解法返回的均是从右侧遍历的位置。

针对该问题,一个很容易想到的解法就是移位操作,右移原数直到0为止。这种方法很容易想到,实现也比较简单:

int left_most_one_1(int n){int pos=-1;while (n){n>>=1;pos++;}return pos;}

这种方法的运行时间和1的位置相关,整体而言复杂度较高。我们对其进行优化。一个整数的各个位可以看成是排好序的,寻找最左侧1就类似于在有序数组中查找特定元素,所以第二种方法即是二分法:

int left_most_one_2(int n){if (n==0){return -1;}int exp=4;int pos=(1<<exp);while (exp>0){exp--;if (n>>pos){pos+=(1<<exp);} else{pos-=(1<<exp);}}return n>>pos!=0?pos:pos-1;}

二分的标准是通过右移判断结果是否为0,如果为0说明1在低位部分,否则就是在高位部分,然后通过不停地二分即可找到所在位置。事实上,由于循环次数较少,我们还可以循环展开,使其性能更高。这个任务就交给大家了。

       后面两种方法就不属于常规方法了,有些投机取巧的成分。第三种方法用到了我们之前介绍的《类型强转和地址强转》。我们知道,在计算机中一个浮点数是按照IEEE标准实现的。给定一个浮点数,其结构如下:

上述格式的符号部分为1位,阶码部分为8位,尾数部分为23位;如果是双精度(double)类型,符号部分为1位,阶码部分为11位,尾数部分为52位。

       可以看出,阶码部分即隐含着该浮点数的最高有效位。所以我们可以通过将整数强转成浮点数然后解析阶码部分获得最左侧1问题的解。要实现对阶码部分的解析就用到地址强转。代码如下:

int left_most_one_3(int n){float b=n;return (*((int*)&b)>>23&255)-127;}

上述代码首先将整数提升成浮点数,然后获得浮点数的地址,对该地址进行强转,转成int型地址。获得int型地址之后再获得该地址对应的int值。最后根据浮点数的结构解析阶码减去127即是最左侧1的位置。是不是有些匪夷所思,计算机就是这么奇妙。

(注:上述方法有缺陷,在极少数例子下会返回错误的结果,原因出在浮点数的精度上。给定一个32位整数,将其转换成32位浮点数的时候会存在精度缺失问题。这是因为32位浮点数的尾数只有23位,加上隐含的1也只有24位的精度。当原始整数的有效位超过24位时,整数转浮点数就会丢弃末尾的一个或多个1,从而产生误差。误差是不可避免的,另一个严重问题是在丢弃末尾1的同时,可能会产生进位以减少误差,这是这个原因导致上述方法不正确。举一个简单的例子,上述方法第一个报错的整数33554431,它等于2^25-1,二进制表示是25个1。由于浮点数的尾数最大只能包含24个有效位,所以将它转换成浮点数就会存在精度缺失的问题。编译器在处理这种情况的时候可能会直接丢弃末尾的1也可能在末尾加1丢弃0。在我的电脑上是加1丢0,原始整数就被转换成了2^25=33554432.0,进而产生一个进位,从而返回错误结果。这个例子可能还不直观,给大家换个数2147483584,这个数在被转换成浮点数之后变为2147483648.0,和原始整数相差了64!大家能明白一些了吗?最大的整数2^31-1包含31个1,在转换成浮点数的时候最多可能会丢弃7位有效数字。7位有效数字的上限是128,根据丢弃值的大小是否大于等于64,编译器会选择对结果加1,从而产生进位和误差。有什么解决方案吗?有!float不行,那就换double吧。double的尾数有52位,总以承载整个int整数。但是对64位整数,它也无能为力了。换用double之后,代码可以修改为:

int left_most_one_3(int n){if (n==0){return -1;}double b=n;return ((*(long long*)&b)>>52&2047)-1023;}

感谢王博让我对浮点数的认识更近一步!)

       第四种方法更加偷懒,直接调用编译器的内置函数。针对GCC编译器,其内置了很多有用函数,比如__builtin_popcount可以用来统计一个二进制数中的1个数。再比如,__builtin_clz可以用来获得前置零的个数,这个函数就是我们需要的。在VS下,对应的两个函数是:__popcnt和__lzcnt。所以代码就很简单咯:

#include <intrin.h>int left_most_one_4(int n){return 31-__lzcnt(n); //gcc has __builtin_clz}
针对该问题还有很多其他解法,但是上述四种方法足以让你打开眼界。我决定了,下次面试就问这个题目,写出二分法者即通过面试~
1 0