前言
(1)今天看到一个有意思的问题,如何判断一个数字是否为2的若干次幂。这个问题并不难,但是对于我们的C语言功底还是有一点点的考验的。
(2)希望各位可以先自行思考,实在想不出来再看后面的讲解。提示,C语言的位运算是一个好东西。
解析
2的若干次幂数所存在的特征点
(1)首先,我们需要知道2的若干次幂所存在的特征点。当我们知道了这个特征点之后,就可以将这个特征点与其他数进行分离了。
(2)我们都知道,计算机是2进制系统。如果让一个数字乘以2,我们是不是可理解为,让这个二进制数右移动一位呢?
(3)既然我们知道了,在计算机中乘以2就是进行一次右移操作。那么2的n次幂,是不是就是二进制数1进行右移n次呢?例如,数字2换成二进制是10,而十进制的数字2恰好就是2的1次幂,因而二进制的1进行一次右移操作。
(4)好,我们再进行扩展,既然2的n次幂就是数字1进行右移n次。那么2的n次幂在二进制中是不是有一个特点,那就是只有一个位为1!(不理解的同学可以看一下下面这几个例子,我们发现2的n次幂的数8和4只有一个位为1)
如何提取这个特征点
(1)现在我们知道了2的若干次幂在二进制中,只会有一个位是1,其他位是数字0。因此,我们是不是得想个办法,判断一个二进制数只存在一个1。
(2)于是,我们可以想到"&"运算。他的特点在于,有0出0。假设我们的二进制数是100,那么让100减去1变成011。这样原来那个存放唯一数字1的地方就会变成0了。
(3)然后100&011,就会变成0。最后逻辑取反即可。
!((x)&(x-1))
(4)但是肯定有朋友会想举其他例子尝试反驳,很不幸的是你找不到的。
(5)为什么这么说呢?因为,我们要抓住这个方法的巧妙之处,我们是将唯一的存放1的位变成0。
(6)但是,我们假设有一个可以反驳的数1010。(注意,这里是假设)1010-1=1001,有没有发现一个问题,这里的最高位的数字1永远无法被消除。因此,进行如上操作之后,最终还是可以分辨出这不是2的n次幂。
上述代码存在bug?
bug1 — 不承认1为2的若干次幂
(1)看到上述代码,有一些东西肯定想到了一个刁钻的角度。数字1经过上述代码,最终输出的也是1啊。所以你这个代码有问题!这个时候我建议还是重新学一下小学数学啊(苦笑)。1就是2的0次幂呀。
(2)但是有一些朋友就是不承认1怎么办呢?也很简单,判断这个数是不是1呗。
x == 1 ? 0 : !((x)&(x-1))
bug2 — 数字0所导致的问题
(1)数字1导致的问题解决了,我们角度再刁钻一点,假设这个数字是0呢?真的可以吗?
(2)我们在Linux中执行如下代码。
#include <stdio.h> #include <stdlib.h> #define if_2_equation(x) !((x)&(x-1)) int main(int argc,char** argv) { char i; //如果输入参数小于2个,打印本程序使用方法 if(argc < 2) { printf("Usage: \r\n"); printf("%s <string> \r\n",argv[0]); return -1; } i = strtol(argv[1], NULL, 0); printf("number = %d \r\n",i); if(if_2_equation(i)) { printf("yes\r\n"); } else { printf("no\r\n"); } return 0; }
(3)执行发现,数字0也被当成了2的若干次幂,这个从数学的角度上来看,怎么也说不清楚啊。为什么会发生这种情况呢?很简单,0&所有的数,都是0。因此,这里就存在bug。
(4)怎么处理呢?很简单,进行一次判断呗。
#define if_2_equation(x) x == 0 ? 0 : !((x)&(x-1))
(5)但是有些骚年会想,我1和0这两个数都不打算当成2的若干次幂来判断,这个怎么处理呢?也很简单,进行两次判断呗。
#define if_2_equation(x) x == 0 ? 0 : (x == 1 ? 0 : !((x)&(x-1)))
bug3 — 负数可能带来的问题
-128结果和推测的不一样
(1)我们现在知道了,进行我们就是要判断是否只有最高位有1,其他位为0。
(2)那么,我就有一个疑惑了。像-128转换为补码就是0x80,那么-128是不是会被上面这个宏给判断成2的若干次幂呢?
(3)于是,带着疑惑,我敲下了如下代码。
#include <stdio.h> #include <stdlib.h> #define if_2_equation(x) x == 0 ? 0 : !((x)&(x-1)) int main(int argc,char** argv) { char i; if(argc < 2) { printf("Usage: \r\n"); printf("%s <string> \r\n",argv[0]); return -1; } i = strtol(argv[1], NULL, 0); printf("number = %d \r\n",(int)i); printf("number = 0x%x \r\n",(int)i & 0xff); printf("number = 0x%x \r\n",(int)(i-1) & 0xff); printf("!(0x80 & 0x7f) = 0x%x \r\n",!(0x80 & 0x7f)); printf("if_2_equation(i) = 0x%x \r\n",if_2_equation(i)); if(if_2_equation(i)) { printf("yes\r\n"); } else { printf("no\r\n"); } return 0; }
(4)这个结果很有意思,!(0x80 & 0x7f) = 0x1 。但是if_2_equation(i) = 0x0 ,这两个居然不一样?!
(5)这个问题纠结了我许久,后面突然想到,我是不是可以看看汇编指令呢?
(6)查看汇编代码可知,我们这里虽然是按照1字节的char型数据来存储,但是在汇编代码中,他依旧是会按照4字节的方式来进行计算,最终的if_2_equation()返回的数据其实是一个4字节的数据。
(7)于是,最终的结果和预期的不同:
<1>预期结果:-128补码是0x80,-128-1补码是0x7F。最终0x80 & 0x7F= 0x00,逻辑取反之后 !0x00 = 1。
<2>实际结果:-128补码是0xFF80,-128-1补码是0xFF7F。最终0xFF80 & 0xFF7F = 0xFF00,逻辑取反之后 !0xFF00 = 0。
-2147483648结果和推测的不一样
(1)既然这个宏会将数据扩展成为4字节来进行计算。那么我就找到int型数据的存储最小值来进行计算呗。
(2)但是实操之后发现, -2147483648居然输出的也是0。
<1>预期结果:-2147483648补码是0x8000 0000,-2147483648-1补码是0x7FFF FFFF。最终0x8000 0000 & 0x7FFF FFFF= 0x0000 0000,逻辑取反之后 !0x0000 0000= 1。
<2>推测实际结果:-2147483648补码是0xFFFF FFFF 8000 0000,-128-1补码是0xFFFF FFFF 7FFF FFFF。最终0xFFFF FFFF 8000 & 0xFFFF FFFF 7FFF FFFF = 0xFFFF FFFF 0000 0000,逻辑取反之后 !0x 0xFFFF FFFF 0000 0000 = 0。
(3)依旧是直接看汇编代码,我们发现-2147483648虽然可以被4字节存放,但是他居然被扩展称为了8字节数据。
(1)以上都是空想,那么实测一下。与猜测的实际结果一致。
#include <stdio.h> #include <stdlib.h> #define if_2_equation(x) x == 0 ? 0 : !((x)&(x-1)) int main(int argc,char** argv) { long int i; //如果输入参数小于2个,打印本程序使用方法 if(argc < 2) { printf("Usage: \r\n"); printf("%s <string> \r\n",argv[0]); return -1; } i = strtol(argv[1], NULL, 0); printf("sizeof(-2147483647) = 0x%ld \r\n",sizeof(-2147483647)); printf("sizeof(-2147483648) = 0x%ld \r\n",sizeof(-2147483648)); printf("number = %ld \r\n",i); printf("number = 0x%lx \r\n",i); printf("number-1 = 0x%lx \r\n",i-1); printf("number = 0x%lx \r\n",i&0xff); printf("number-1 = 0x%lx \r\n",(i-1)&0xff); printf("if_2_equation(-2147483648) = 0x%x \r\n",if_2_equation(-2147483648)); printf("!(0x80000000&0x7fffffff) = 0x%x \r\n",!(0x80000000&0x7fffffff)); if(if_2_equation(i)) { printf("yes\r\n"); } else { printf("no\r\n"); } return 0; }
结果如何保证代码的百分百可行
(1)虽然说,下面这个宏可以判断一个数是否为2的若干次幂。但是经过上面的分析发现。似乎还是存在不确定性,因为我们不知道编译器是否一定会进行扩展。虽然我查看了几种不同的汇编代码发现-2147483648都会被扩展为8字节,但是为了百分百的可靠性,我们还是做好一个完全准备。
#define if_2_equation(x) x == 0 ? 0 : !((x)&(x-1))
(2)我们先使用is_Positive_number()这个宏判断这数是否为负数,如果为负数,那么直接返回0。为正数再进行判断。
#define is_Positive_number(A) A>=0 #define if_2_equation(x) is_Positive_number(x) == 0 ? 0 : !((x)&(x-1))