zoukankan      html  css  js  c++  java
  • 编程之美求二进制中1的个数

    求二进制中1的个数。对于一个字节(8bit)无符整形变量,求其二进制表示中"1"的个数,要求算法的执行效率尽可能的高。

    我们首先看看无符号字节类型,c中char中默认是signed的,写一段代码:

    int main()
    {
         char a='';
        printf("%d",a+0);
    }
    €的代码是128(ascii表:http://www.weste.net/tools/ASCII.asp),超出了char的范围-128-127;结果输出:
    -128.
    加上unsigned后,unsigned char a='€';结果输出:
    128.
    根据题目的要求,我们要用unsigned char,我们可以用typeddef重定义:
    typedef unsigned char Byte;

    解法1:

    可以举一个八位的二进制例子来进行分析。对于二进制操作,我们知道,除以一个2,原来的数字将会减少一个0。如果除的过程中有余,那么就表示当前位置有一个1。

    以10 100 010为例;

    第一次除以2时,商为1 010 001,余为0。

    第二次除以2时,商为101 000,余为1。

    因此,可以考虑利用整型数据除法的特点,通过相除和判断余数的值来进行分析。于是有了如下的代码。

    int count(Byte v)
    {
        int num=0;
        while(v)
        {
            if(v%2==1)
            {
                num++;
            }
           
            v/=2;
        }
        return num;
    }

    刚开始,main函数代码如下:

    int main()
    {
         
        Byte a;
        scanf("%c",&a);
        int num=count(a);
        printf("%d
    ",num);
         
    }

    结果总是错误,我输入a输出3。调试时发现vmod2,v/=2是以ascii码计算的,除后v=48,这么得3的。规律;

    字符与数学相运算总是以ascii形式。

    发现审题错误,

    一个字节(8bit)无符号整形变量。定义Byte定义错了。

    应该定义成

     typedef unsigned short

    c语言无符号整数怎么定义

    整型变量的分类: 基本整型:int 短整型:short int 长整型:long int对以上三类加上修饰符unsigned以指定是“无符号数”。如果加上修饰符singed,则指定的是“有符号数”,如果既不指定为signed也不指定是unsigned,则隐含为有符号(signed)。

    无符号整数的陷阱

    请问以下代码的输出?
    unsigned int a=1,
    int b=-2; int c=-2;
    cout<<b<<endl; if(a+c>0)
    cout<<a+b<<endl;
    运行结果:
    4294967294
    4294967295
    Press any key to continue
    首先,程序的第一行,变量b和a一样,都是无符号整型,这是一个陷阱
    其次,在32位系统中,int的范围是-2147483648~+2147483647,而unsigned int的范围是0~4294967295。负数在无符号整型中用补码表示,所以b是4294967294。
    第三,int默认为signed int,它与unsigned int运算时,结果被转换为unsigned int,所以a是4294967295。
    short int 和 long int可以缩写为short 和 long。

     现在遇到的难题是:如何定义一个字节的无符号整形。

    在C中是无法定义一个字节的整形的。没办法,只能定义short类型了.

    typedef unsigned short int Byte;

    int main()
    {
         
        Byte a;
        scanf("%u",&a); //改为%hu就正确了。
        int num=count(a);
        printf("%d
    ",num);
          
    }

    %u Unsigned decimal integer

    % i或d Signed decimal integer。
    我们的是无符号的short,用%u,但是%u是int类型的,有问题吗?
    运行,输入10后能正确得出结果,但会出现runtime error:
    stack around the variable 'a' was corrupted.
     
    显然是控制符的问题。
    转载的一篇文章:

    符号属性     长度属性     基本型     所占位数     取值范围       输入符举例      输出符举例

    --            --          char         8         -2^7 ~ 2^7-1        %c          %c、%d、%u

    signed        --          char         8         -2^7 ~ 2^7-1        %c          %c、%d、%u

    unsigned      --          char         8         0 ~ 2^8-1           %c          %c、%d、%u

    [signed]      short       [int]        16        -2^15 ~ 2^15-1              %hd

    unsigned      short       [int]        16        0 ~ 2^16-1             %hu 、%ho、%hx

    [signed]      --           int         32        -2^31 ~ 2^31-1              %d

    unsigned      --          [int]        32        0 ~ 2^32-1              %u 、%o、%x

    [signed]      long        [int]        32        -2^31 ~ 2^31-1              %ld

    unsigned      long        [int]        32        0 ~ 2^32-1             %lu 、%lo、%lx

    [signed]      long long   [int]        64        -2^63 ~ 2^63-1             %I64d

    unsigned      long long   [int]        64        0 ~ 2^64-1          %I64u、%I64o、%I64x

    --            --          float        32       +/- 3.40282e+038         %f、%e、%g

    --            --          double       64       +/- 1.79769e+308 %lf 、%le、%lg   %f、%e、%g

    --            long        double       96       +/- 1.79769e+308        %Lf 、%Le、%Lg

    上面表有些有错误,正确表看cplusplus。

    从上表可以看出,对于unsigned short int,应该使用%hu,对于

    unsigned long int,应该使用 %lu;

    只写unsigned 表示unsigned int;

    几点说明:

    1. 注意! 表中的每一行,代表一种基本类型。“[]”代表可省略。

       例如:char、signed char、unsigned char是三种互不相同的类型;

       int、short、long也是三种互不相同的类型。

       可以使用C++的函数重载特性进行验证,如:

       void Func(char ch) {}

       void Func(signed char ch) {}

       void Func(unsigned char ch) {}

       是三个不同的函数。

    2. char/signed char/unsigned char型数据长度为1字节;

       char为有符号型,但与signed char是不同的类型。

       注意! 并不是所有编译器都这样处理,char型数据长度不一定为1字节,char也不一定为有符号型

    3. 将char/signed char转换为int时,会对最高符号位1进行扩展,从而造成运算问题。

       所以,如果要处理的数据中存在字节值大于127的情况,使用unsigned char较为妥当。

       程序中若涉及位运算,也应该使用unsigned型变量。

    4. char/signed char/unsigned char输出时,使用格式符%c(按字符方式);

       或使用%d、%u、%x/%X、%o,按整数方式输出;

       输入时,应使用%c,若使用整数方式,Dev-C++会给出警告,不建议这样使用。

    5. int的长度,是16位还是32位,与编译器字长有关。

       16位编译器(如TC使用的编译器)下,int为16位;32位编译器(如VC使用的编译器cl.exe)下,int为32

    位。

    6. 整型数据可以使用%d(有符号10进制)、%o(无符号8进制)或%x/%X(无符号16进制)方式输入输出。

       而格式符%u,表示unsigned,即无符号10进制方式。

    7. 整型前缀h表示short,l表示long。

       输入输出short/unsigned short时,不建议直接使用int的格式符%d/%u等,要加前缀h。

       这个习惯性错误,来源于TC。TC下,int的长度和默认符号属性,都与short一致,

       于是就把这两种类型当成是相同的,都用int方式进行输入输出。

    8. 关于long long类型的输入输出:

       "%lld"和"%llu"是linux下gcc/g++用于long long int类型(64 bits)输入输出的格式符。

       而"%I64d"和"%I64u"则是Microsoft VC++库里用于输入输出__int64类型的格式说明。

       Dev-C++使用的编译器是Mingw32,Mingw32是x86-win32 gcc子项目之一,编译器核心还是linux下的gcc。

       进行函数参数类型检查的是在编译阶段,gcc编译器对格式字符串进行检查,显然它不认得"%I64d",

       所以将给出警告“unknown conversion type character `I' in format”。对于"%lld"和"%llu",gcc理

    所当然地接受了。

       Mingw32在编译期间使用gcc的规则检查语法,在连接和运行时使用的却是Microsoft库。

       这个库里的printf和scanf函数当然不认识linux gcc下"%lld"和"%llu",但对"%I64d"和"%I64u",它则是

    乐意接受,并能正常工作的。

    9. 浮点型数据输入时可使用%f、%e/%E或%g/%G,scanf会根据输入数据形式,自动处理。

       输出时可使用%f(普通方式)、%e/%E(指数方式)或%g/%G(自动选择)。

    10. 浮点参数压栈的规则:float(4 字节)类型扩展成double(8 字节)入栈。

        所以在输入时,需要区分float(%f)与double(%lf),而在输出时,用%f即可。

        printf函数将按照double型的规则对压入堆栈的float(已扩展成double)和double型数据进行输出。

        如果在输出时指定%lf格式符,gcc/mingw32编译器将给出一个警告。

    11. Dev-C++(gcc/mingw32)可以选择float的长度,是否与double一致。

    12. 前缀L表示long(double)。

        虽然long double比double长4个字节,但是表示的数值范围却是一样的。

        long double类型的长度、精度及表示范围与所使用的编译器、操作系统等有关。

    转自:http://hi.baidu.com/dhh1216_cgcg/blog/item/3c6b3a79679ddfe12e73b3c9.html

    【解法二】使用位操作

    前面的代码看起来比较复杂。我们知道,向右移位操作同样也可以达到相除的目的。唯一不同之处在于,移位之后如何来判断是否有1存在。对于这个问题,再来看看一个八位的数字:10 100 001。

    在向右移位的过程中,我们会把最后一位直接丢弃。因此,需要判断最后一位是否为1,而"与"操作可以达到目的。可以把这个八位的数字与00000001进行"与"操作。如果结果为1,则表示当前八位数的最后一位为1,否则为0。代码如下:

    int count(Byte v)
    {
        int num=0;
        while(v)
        {
             
            num+=v& 0x01;
             
            v/=2;
        }
        return num;
    }

    【解法三】

      位操作比除、余操作的效率高了很多。但是,即使采用位操作,时间复杂度仍为O(log2v),log2v为二进制数的位数。那么,还能不能再降低一些复杂度呢?如果有办法让算法的复杂度只与"1"的个数有关,复杂度不就能进一步降低了吗?

    同样用10 100 001来举例。如果只考虑和1的个数相关,那么,我们是否能够在每次判断中,仅与1来进行判断呢?

    为了简化这个问题,我们考虑只有一个1的情况。例如:01 000 000。

    如何判断给定的二进制数里面有且仅有一个1呢?可以通过判断这个数是否是2的整数次幂来实现。另外,如果只和这一个"1"进行判断,如何设计操作呢?我们知道的是,如果进行这个操作,结果为0或为1,就可以得到结论

    如果希望操作后的结果为0,01 000 000可以和00 111 111进行"与"操作。

    这样,要进行的操作就是 01 000 000 &(01 000 000 - 00 000 001)= 01 000 000 &

    00 111 111 = 0。

    因此就有了解法三的代码:

    int count(Byte v)
    {
        int num=0;
        while(v)
        {
            v &= (v-1);
            num++;
        }
        return num;
    }

    如110,

    110&(101)=100  每次与都会减去最后一个1.

    100&(011)=000

    num=2;正确。

    【解法四】使用分支操作

    解法三的复杂度降低到O(M),其中M是v中1的个数,可能会有人已经很满足了,只用计算1的位数,这样应该够快了吧。然而我们说既然只有八位数据,索性直接把0~255的情况都罗列出来,并使用分支操作,可以得到答案,代码如下:

    代码清单2-4

    int Count(int v)  
    {  
        int num = 0;  
        switch (v)  
        {  
            case 0x0:  
                num = 0;  
                break;  
            case 0x1:  
            case 0x2:  
            case 0x4:  
            case 0x8:  
            case 0x10:  
            case 0x20:  
            case 0x40:  
            case 0x80:  
                num = 1;  
                break;  
            case 0x3:  
            case 0x6:  
            case 0xc:  
            case 0x18:  
            case 0x30:  
            case 0x60:  
            case 0xc0:  
                num = 2;  
                break;  
                //...  
        }  
        return num;  
    } 
    View Code

    解法四看似很直接,但实际执行效率可能会低于解法二和解法三,因为分支语句的执行情况要看具体字节的值,如果a =0,那自然在第1个case就得出了答案,但是如果a =255,则要在最后一个case才得出答案,即在进行了255次比较操作之后!

    看来,解法四不可取!但是解法四提供了一个思路,就是采用空间换时间的方法,罗列并直接给出值。如果需要快速地得到结果,可以利用空间或利用已知结论。这就好比已经知道计算1+2+ … +N的公式,在程序实现中就可以利用公式得到结论。

    最后,得到解法五:算法中不需要进行任何的比较便可直接返回答案,这个解法在时间复杂度上应该能够让人高山仰止了。

    【解法五】查表法

    代码清单2-5

    这是个典型的空间换时间的算法,把0~255中"1"的个数直接存储在数组中,v作为数组的下标,countTable[v]就是v中"1"的个数。算法的时间复杂度仅为O(1)。

    在一个需要频繁使用这个算法的应用中,通过"空间换时间"来获取高的时间效率是一个常用的方法,具体的算法还应针对不同应用进行优化。

    扩展问题

    1. 如果变量是32位的DWORD,你会使用上述的哪一个算法,或者改进哪一个算法?

    2. 另一个相关的问题,给定两个正整数(二进制形式表示)A和B,问把A变为B需要改变多少位(bit)?也就是说,整数A 和B 的二进制表示中有多少位是不同的?

    (首先A异或B,得到C,再求C中1的个数)。

    对于32位的DWORD,可以动态建表或静态建表
    1)动态建表
    由于表示在程序运行时动态创建的,所以速度上肯定会慢一些,把这个版本放在这里,有两个原因
            1.填表的方法,这个方法的确很巧妙。

            2.类型转换,这里不能使用传统的强制转换,而是先取地址再转换成对应的指针类型。

    代码如下:
           int BitCount(unsigned int n) 

           { 

        // 建表
        unsigned char BitsSetTable256[256] = {0} ; 
        // 初始化表 
        for (int i = 0; i < 256; i++) 
        { 
            BitsSetTable256[i] = (i & 1) + BitsSetTable256[i / 2]; 
        } 
        unsigned int c = 0 ; 

        // 查表

        unsigned char * p = (unsigned char *) &n ; 
        c = BitsSetTable256[p[0]] + BitsSetTable256[p[1]] + 
                BitsSetTable256[p[2]] + 
    BitsSetTable256[p[3]]; 
        return c ; 
    }

    先说一下填表的原理,根据奇偶性来分析,对于任意一个正整数n
    1.如果它是偶数,那么n的二进制中1的个数与n/2中1的个数是相同的,比如4和2的二进制中都有一个1,6和3的二进制中都有两个1。
    为啥?因为n是由n/2左移一位而来,而移位并不会增加1的个数。
    2.如果n是奇数,那么n的二进制中1的个数是n/2中1的个数+1,比如7的二进制中有三个1,7/2 = 3的二进制中有两个1。
    为啥?因为当n是奇数时,n相当于n/2左移一位再加1。
    再说一下查表的原理
    对于任意一个32位无符号整数,将其分割为4部分,每部分8bit,对于这四个部分分别求出1的个数,再累加起来即可。而8bit对应2^8 = 256种01组合方式,这也是为什么表的大小为256的原因。
     
    注意类型转换的时候,先取到n的地址,然后转换为unsigned char*,这样一个unsigned int(4 bytes)对应四个unsigned char(1 bytes),分别取出来计算即可。举个例子吧,以87654321(十六进制)为例,先写成二进制形式-8bit一组,共四组,以不同颜色区分,这四组中1的个数分别为4,4,3,2,所以一共是13个1,如下面所示。
    10000111 01100101 01000011 00100001 = 4 + 4 + 3 + 2 = 13
    2)静态建表
    首先构造一个包含256个元素的表table,table[i]即i中1的个数,这里的i是[0-255]之间任意一个值。然后对于任意一个32bit无符号整数n,我们将其拆分成四个8bit,然后分别求出每个8bit中1的个数,再累加求和即可,这里用移位的方法,每次右移8位,并与0xff相与,取得最低位的8bit,累加后继续移位,如此往复,直到n为0。所以对于任意一个32位整数,需要查表4次。以十进制数2882400018为例,其对应的二进制数为10101011110011011110111100010010,对应的四次查表过程如下:红色表示当前8bit,绿色表示右移后高位补零。
    第一次(n & 0xff) 10101011110011011110111100010010
    第二次((n >> 8) & 0xff) 00000000101010111100110111101111
    第三次((n >> 16) & 0xff)00000000000000001010101111001101
    第四次((n >> 24) & 0xff)00000000000000000000000010101011
    代码如下:
    int BitCount7(unsigned int n)

            unsigned int table[256] = 
            { 
                0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 
                1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
                1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
                2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
                1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
                2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
                2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
                3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
                1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
                2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
                2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
                3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
                2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
                3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
                3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
                4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8 
        }; 
        return      table[n & 0xff] +
                        table[(n >> 8) & 0xff] +
                        table[(n >> 16) & 0xff] +
                        table[(n >> 24) & 0xff] ;
    }
     更多很巧解法:
    参考:
    http://blog.csdn.net/wangjun_1218/article/details/4464129
    http://www.cnblogs.com/graphics/archive/2010/06/21/1752421.html
    http://blog.csdn.net/jiqiren007/article/details/6403133
    http://blog.csdn.net/justpub/article/details/2292823
  • 相关阅读:
    CodeForces 347B Fixed Points (水题)
    CodeForces 347A Difference Row (水题)
    CodeForces 346A Alice and Bob (数学最大公约数)
    CodeForces 474C Captain Marmot (数学,旋转,暴力)
    CodeForces 474B Worms (水题,二分)
    CodeForces 474A Keyboard (水题)
    压力测试学习(一)
    算法学习(一)五个常用算法概念了解
    C#语言规范
    异常System.Threading.Thread.AbortInternal
  • 原文地址:https://www.cnblogs.com/youxin/p/3233954.html
Copyright © 2011-2022 走看看