ARTS 挑战打卡的第9天 --- 如何知道一个数是否为2的若干次幂(Algorithm)

简介: ARTS 挑战打卡的第9天 --- 如何知道一个数是否为2的若干次幂(Algorithm)

前言

(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))


目录
相关文章
|
3月前
|
算法 C语言 C++
【practise】数组中出现次数超过一半的数字
【practise】数组中出现次数超过一半的数字
|
3月前
|
算法
【算法】模拟算法——数青蛙(medium)
【算法】模拟算法——数青蛙(medium)
|
C语言
ARTS 挑战打卡的第2天 --- leetcode消失的数字(Algorithm)
ARTS 挑战打卡的第2天 --- leetcode消失的数字(Algorithm)
62 0
|
C语言 C++
PAT (Basic Level) Practice (中文)1099 性感素数(20分)
“性感素数”是指形如 (p, p+6) 这样的一对素数。之所以叫这个名字,是因为拉丁语管“六”叫“sex”(即英语的“性感”)。(原文摘自 http://mathworld.wolfram.com/SexyPrimes.html) 现给定一个整数,请你判断其是否为一个性感素数。
149 0
|
存储 测试技术
牛客hot100--BM50---两数之和(简单难度)
牛客hot100--BM50---两数之和(简单难度)
133 0
牛客hot100--BM50---两数之和(简单难度)
|
人工智能 测试技术
PAT (Basic Level) Practice (中文) B1008 数组元素循环右移问题 (20 分)
PAT (Basic Level) Practice (中文) B1008 数组元素循环右移问题 (20 分)
104 0
PAT (Basic Level) Practice (中文) B1008 数组元素循环右移问题 (20 分)
|
索引
PAT甲级 1007. Maximum Subsequence Sum(25分) 复杂度优化到O(n)
PAT甲级 1007. Maximum Subsequence Sum(25分) 复杂度优化到O(n)
82 0
|
Java
HDOJ 1018 Big Number(大数位数公式)
HDOJ 1018 Big Number(大数位数公式)
113 0
HDOJ 2054 A == B ?(精确大数相等)
HDOJ 2054 A == B ?(精确大数相等)
125 0