你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

C编程面试题的多种解法

[复制链接]
gaosmile 发布时间:2020-4-27 16:50
问题描述
任意给定一个32位无符号整数n,求n的二进制表示中1的个数,比如n = 5(0101)时,返回2,n = 15(1111)时,返回4
这也是一道比较经典的题目了,相信不少人面试的时候可能遇到过这道题吧,下面介绍了几种方法来实现这道题,相信很多人可能见过下面的算法,但我相信很少有人见到本文中所有的算法。如果您上头上有更好的算法,或者本文没有提到的算法,请不要吝惜您的代码,分享的时候,也是学习和交流的时候。
普通法
我总是习惯叫普通法,因为我实在找不到一个合适的名字来描述它,其实就是最简单的方法,有点程序基础的人都能想得到,那就是移位+计数,很简单,不多说了,直接上代码,这种方法的运算次数与输入n最高位1的位置有关,最多循环32次。
int BitCount(unsigned int n)( j* U# L1 I& g- Y
{
! ~+ b5 a/ m' i    unsigned int c =0 ; // 计数器
+ t9 E" W8 ^: l; d# _. I    while (n >0)
* N- k% g" L8 e/ L$ N. _5 M9 j    {
5 X/ J  j7 Z/ I' x        if((n &1) ==1) // 当前位是1* O! P' @0 y9 i
            ++c ; // 计数器加10 `, _: {0 o4 P+ Q5 N
        n >>=1 ; // 移位
: \8 V3 D$ S$ L' r6 k" W' @  Q    }$ S. Y1 \( H! y' K2 R3 t# Q$ I
    return c ;! s* U9 a; C. a: T( _) z
}

( s" `% p& Y" \% {
一个更精简的版本如下
int BitCount1(unsigned int n)
  Z! J" }% [% O- s. _! K3 r$ @{+ B9 D- J, T5 o' j4 `7 M
    unsigned int c =0 ; // 计数器
' e8 ]/ R) _9 s" f    for (c =0; n; n >>=1) // 循环移位9 X& c$ X! C5 x; o, }
        c += n &1 ; // 如果当前位是1,则计数器加14 U5 e' V) N% W
    return c ;) S9 X( `! G  U: _1 ^+ }: W
}
1 U5 y1 G1 n! n, |* [! j
问:如果输入参数是int,这种方法还能奏效吗?
如何修改?看看下面的方法:
int BitCount(int n)) [3 a) [' Z. a9 k% x
{. x4 `9 C4 l; q# Z/ V3 _
    int num = 0;
3 M7 Q3 ]8 G/ x1 K  f" E# T    unsigned int flag = 1;
1 v$ T, M2 I; P; L* @0 m" C    while(0 != flag)
3 v  ?. w/ n$ z, d0 C$ M    {
! K! u: j7 w6 \7 R1 _, d2 v2 c+ l        if(n & flag)8 ~, J7 B( z0 O& j/ s5 K2 K
            num++;
. a* j5 g  z9 R- v        flag = flag << 1;
6 T' @+ R" A/ E    }9 N/ }& i9 E$ G( i. D; s) j
    return num;( N6 `& y4 }% ~- i$ ^/ {
}

2 B* Q" v1 l: ]' n8 D
0 F2 J! v. ?& D: h
快速法
这种方法速度比较快,其运算次数与输入n的大小无关,只与n中1的个数有关。如果n的二进制表示中有k个1,那么这个方法只需要循环k次即可。其原理是不断清除n的二进制表示中最右边的1,同时累加计数器,直至n为0,代码如下
int BitCount2(unsigned int n)  \, ?/ U0 H* }3 l/ T& B& Q
{
7 c3 {4 r- e+ R    unsigned int c =0 ;
# x! j# c0 l6 o# g3 p" J    for (c =0; n; ++c)$ t4 X; I, I1 t$ B+ Z
    {
/ A/ K! w9 e6 w2 l, J+ C        n &= (n -1) ; // 清除最低位的14 M8 r0 O4 y. m5 F" V8 c/ {
    }0 V" b. `! u- T
    return c ;+ S, ]' t% A6 @- I' V- |
}

! v4 N4 H2 {! @  c" |2 |4 ^
为什么n &= (n – 1)能清除最右边的1呢?因为从二进制的角度讲,n相当于在n - 1的最低位加上1。举个例子,8(1000)= 7(0111)+ 1(0001),所以8 & 7 = (1000)&(0111)= 0(0000),清除了8最右边的1(其实就是最高位的1,因为8的二进制中只有一个1)。再比如7(0111)= 6(0110)+ 1(0001),所以7 & 6 = (0111)&(0110)= 6(0110),清除了7的二进制表示中最右边的1(也就是最低位的1)。
查表法动态建表
由于表示在程序运行时动态创建的,所以速度上肯定会慢一些,把这个版本放在这里,有两个原因
  • 介绍填表的方法,因为这个方法的确很巧妙。
  • 类型转换,这里不能使用传统的强制转换,而是先取地址再转换成对应的指针类型。也是常用的类型转换方法。

    ) U- k: v# \% N0 c8 u6 i
int BitCount3(unsigned int n)
& v) l3 |0 m* j% h5 g/ e6 S{ ; v+ @& ?. x0 M% e; P
    // 建表# R" a. ?- G6 K/ U
    unsigned char BitsSetTable256[256] = {0} ; $ [/ B/ `& M9 D
! @, b" Y: `$ w, L
    // 初始化表
: \& A. F8 p' \    for (int i =0; i <256; i++) 5 n* `! I" R. J, I5 {* m7 X7 z5 X" P
    {
/ l4 j" U* C3 L6 A, x        BitsSetTable256 = (i &1) + BitsSetTable256[i /2];
/ b' N9 k. X6 j    }
6 f$ d& t: z* M2 g  j; ~! k' a6 s  @4 L# c! b
    unsigned int c =0 ;
; r1 b+ z) J3 C5 R+ m: B" {
1 n/ K9 K; z6 o7 M* K9 W    // 查表. D0 A0 Q6 H: w- N4 p2 e
    unsigned char* p = (unsigned char*) &n ;
! O: E( H6 J$ W& t4 {; D! u8 O- d6 x. V# G
    c = BitsSetTable256[p[0]] + 7 A  L! J+ q. t, I! K
        BitsSetTable256[p[1]] + ; [! ]* |+ A) d, Q7 e
        BitsSetTable256[p[2]] +
% |; A/ s/ r& N        BitsSetTable256[p[3]]; & o4 X, y. F$ \7 n

! P# p6 G# N- d8 h% p) Z( k, @    return c ;
4 g" x9 b9 e6 M; A7 b* }}
4 r* J) y5 `5 t+ _" g5 P2 w
先说一下填表的原理,根据奇偶性来分析,对于任意一个正整数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
静态表-4bit
原理和8-bit表相同,详见8-bit表的解释
int BitCount4(unsigned int n)
  r6 I* `8 }8 k9 T0 U+ I{
  q  b3 A# e" x' ^9 L7 v- T  u0 \    unsigned int table[16] = 6 c! X  j/ k2 j5 r1 i
    {$ s! ?0 R0 |9 y
        0, 1, 1, 2, 6 E6 n6 i/ C$ m1 H0 T
        1, 2, 2, 3, ( S6 v0 o+ |9 B: t, j- j6 }
        1, 2, 2, 3, 7 S8 }1 l5 f* Z. @. q
        2, 3, 3, 4' l7 G  N, k8 W* O7 m; h+ j
    } ;$ `& p$ @. q- W3 H' n1 J+ n) q
( q/ ]# c' f: J
    unsigned int count =0 ;
" s' u1 R* ^5 }    while (n)6 \# Q& ?, [) x% s( e% `" i7 b
    {
7 r1 T# K& ]5 Y. i8 J- f        count += table[n &0xf] ;% h9 \! N% x- Z; g: a" Q0 R
        n >>=4 ;
0 E$ ~$ v% W9 K) D/ t/ w$ _" [    }& N& k: R  Q/ z8 B: t2 e
    return count ;
  M3 o; G, O2 }}

( P6 I) D$ _( S3 y静态表-8bit
首先构造一个包含256个元素的表table,table即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)
) r+ b% I% \7 ~$ l7 b& Y. p{
* ]: C: y3 N& F( M    unsigned int table[256] = 0 s# ]6 ^( d! d
    { 0 `; W) @1 T6 c: a* @+ R/ T
        0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 4 O1 ~$ H5 W& a  R) a
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, % H$ S' h- V' }' S% `2 a, m" H
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
' [7 Q: i2 L2 f# s        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3 h: o  h0 |* t. J4 \" F2 g9 [        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
/ x8 S/ s' P4 _3 U$ v& L        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, # q& U% O" I+ r) K8 S( m" q
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, / p9 g* Y+ p/ l
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
4 G) W% A0 P' k. ?        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
- J: ^# Q+ @# @8 G0 _2 L        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
# z6 z- j  E* n: A$ e2 @2 M        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 9 F8 k# e0 I- n
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 0 ^' l+ t& S, [# p/ }
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
% z  S6 F9 ]" M. V/ g, n        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, ) r0 P1 o+ F& G: P
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
8 a  C; Y' C+ ~0 V* J* X: Z        4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8,
4 _8 {! g( Z2 e9 x3 u. l' P    };
  d& G4 a- }2 Q, b: H+ z
2 J4 @& [8 Z/ L    return table[n &0xff] +
3 d% }( t7 `# G        table[(n >>8) &0xff] +8 t. v, P, y- V' x
        table[(n >>16) &0xff] +* h, c5 U  ^; y. b, H
        table[(n >>24) &0xff] ;9 q* g* ]. Q" l, h$ `; d4 @  Y
}

8 a7 k  Y3 l: z% f9 [5 c
当然也可以搞一个16bit的表,或者更极端一点32bit的表,速度将会更快。
平行算法
网上都这么叫,我也这么叫吧,不过话说回来,的确有平行的意味在里面,先看代码,稍后解释
int BitCount4(unsigned int n) 0 M4 E# M$ Z: L9 v9 p6 {9 T1 R
{
/ g- R& Q" u. V4 z- P9 d7 w    n = (n &0x55555555) + ((n >>1) &0x55555555) ; ; F( W; ^6 s- s$ p
    n = (n &0x33333333) + ((n >>2) &0x33333333) ;
0 z7 y: ?3 V; O' {2 F    n = (n &0x0f0f0f0f) + ((n >>4) &0x0f0f0f0f) ;
. w  K* q: t+ B- R    n = (n &0x00ff00ff) + ((n >>8) &0x00ff00ff) ; ! N' D% Y2 k  w/ B7 h0 }/ I+ [
    n = (n &0x0000ffff) + ((n >>16) &0x0000ffff) ; 7 i( C4 h6 Y2 D+ I
5 Z8 D) X# J9 u- g. h& z8 `7 w/ M
    return n ; : o: A7 z+ z% [3 Y! }- p7 g) K
}
5 J+ H# W  Z+ @0 p* J
速度不一定最快,但是想法绝对巧妙。说一下其中奥妙,其实很简单,先将n写成二进制形式,然后相邻位相加,重复这个过程,直到只剩下一位。
以217(11011001)为例,有图有真相,下面的图足以说明一切了。217的二进制表示中有5个1
完美法int BitCount5(unsigned int n) 9 H3 m3 |& a- d' N+ r
{8 {( o) ?3 w# b% t
    unsigned int tmp = n - ((n >>1) &033333333333) - ((n >>2) &011111111111);; ]  Z- W2 a: Y3 T. _
    return ((tmp + (tmp >>3)) &030707070707) %63;
1 b; _2 e6 V. r4 [6 H}
2 i6 l/ q9 g$ x  b' ]
最喜欢这个,代码太简洁啦,只是有个取模运算,可能速度上慢一些。区区两行代码,就能计算出1的个数,到底有何奥妙呢?为了解释的清楚一点,我尽量多说几句。
第一行代码的作用
先说明一点,以0开头的是8进制数,以0x开头的是十六进制数,上面代码中使用了三个8进制数。
将n的二进制表示写出来,然后每3bit分成一组,求出每一组中1的个数,再表示成二进制的形式。比如n = 50,其二进制表示为110010,分组后是110和010,这两组中1的个数本别是2和3。2对应010,3对应011,所以第一行代码结束后,tmp = 010011,具体是怎么实现的呢?由于每组3bit,所以这3bit对应的十进制数都能表示为2^2 * a + 2^1 * b + c的形式,也就是4a + 2b + c的形式,这里a,b,c的值为0或1,如果为0表示对应的二进制位上是0,如果为1表示对应的二进制位上是1,所以a + b + c的值也就是4a + 2b + c的二进制数中1的个数了。举个例子,十进制数6(0110)= 4 * 1 + 2 * 1 + 0,这里a = 1, b = 1, c = 0, a + b + c = 2,所以6的二进制表示中有两个1。现在的问题是,如何得到a + b + c呢?注意位运算中,右移一位相当于除2,就利用这个性质!
4a + 2b + c 右移一位等于2a + b
4a + 2b + c 右移量位等于a
然后做减法
4a + 2b + c –(2a + b) – a = a + b + c,这就是第一行代码所作的事,明白了吧。
第二行代码的作用
在第一行的基础上,将tmp中相邻的两组中1的个数累加,由于累加到过程中有些组被重复加了一次,所以要舍弃这些多加的部分,这就是&030707070707的作用,又由于最终结果可能大于63,所以要取模。
需要注意的是,经过第一行代码后,从右侧起,每相邻的3bit只有四种可能,即000, 001, 010, 011,为啥呢?因为每3bit中1的个数最多为3。所以下面的加法中不存在进位的问题,因为3 + 3 = 6,不足8,不会产生进位。
tmp + (tmp >> 3)-这句就是是相邻组相加,注意会产生重复相加的部分,比如tmp = 659 = 001 010 010 011时,tmp >> 3 = 000 001 010 010,相加得
001 010 010 011
000 001 010 010

001 011 100 101
011 + 101 = 3 + 5 = 8。(感谢网友Di哈指正。)注意,659只是个中间变量,这个结果不代表659这个数的二进制形式中有8个1。
注意我们想要的只是第二组和最后一组(绿色部分),而第一组和第三组(红色部分)属于重复相加的部分,要消除掉,这就是&030707070707所完成的任务(每隔三位删除三位),最后为什么还要%63呢?因为上面相当于每次计算相连的6bit中1的个数,最多是111111 = 77(八进制)= 63(十进制),所以最后要对63取模。
位标志法
感谢网友 @gussing提供
struct _byte
. u$ c  ~* E9 T3 y{ + T; h- g$ M/ q1 A  l& d1 a
    unsigned a:1; 0 h2 G5 s/ y6 V( ^$ T
    unsigned b:1;
! L; w3 W+ h% Y# H- \- w" `! I- ~    unsigned c:1;
/ x( q- h4 i2 p  V( _    unsigned d:1; - e( j2 ^$ H0 H/ v( c. z
    unsigned e:1;
# B  R/ ~6 C7 H( i# R0 G8 Q    unsigned f:1; 2 _" Y0 d1 \5 s8 S& ~2 ^
    unsigned g:1;
; j3 L" j1 J: [) @    unsigned h:1; " h2 r2 F, y6 t4 k
}; ' e8 K6 e3 |. e1 v

& l- f; n, T3 z  W: Zlong get_bit_count( unsigned char b ) ) W4 o+ \/ F4 f6 t
{
& {( B; i: {  ^$ z    struct _byte *by = (struct _byte*)&b; 9 g' R& ?& ]) j/ N
    return (by->a+by->b+by->c+by->d+by->e+by->f+by->g+by->h); : Q5 }: L. E& D6 I; `! Z
}
赞 收藏 评论3 发布时间:2020-4-27 16:50

举报

3个回答
goyhuan 回答时间:2020-4-27 18:09:14
强  
李康1202 回答时间:2020-4-27 18:48:20
强!
a316363723 回答时间:2020-4-29 15:36:25
这个字体看的眼睛很难受,感觉眼睛跟着字体歪了一样。

所属标签

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版