位运算之美--用 + 、- 和位运算实现正整数除法和取模(1)

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

今天看了一位师兄去年的笔经总结,其中有一题是“不许用%和/来实现求任意数除以3的余数”,我想考官的目的应该是想考察学生对位运算的熟悉程度吧,于是我把题目扩展成“只能用+,-和位运算实现正整数除法(/)和取模(%)”,注意:这里不能使用其它的库例程来辅助计算,如log,log10等。在思考这道题目的过程中,我又涉及到了许多二进制相关的题目,如:


    判断给定的整数是不是2的整数次幂
    判断给定的整数是不是4的整数次幂
    求给定整数的二进制表示中1的个数
    求给定整数的二进制表示中0的个数
    求给定整数的二进制表示中最高位1的位置
    求大于等于给定整数的最小的2的整数次幂
    求给定整数的二进制表示的有效位数
    ...
    9月21日补充:这里只考虑值为正整数的情况。
    这些题目都是经典老题,频繁出现于各类笔试面试题中,除了能考察位运算外,还能考察应聘者能否给出创新的算法来更好地解决问题。可以说这些题目都不难,如果使用32位的int来表示整数的话,蛮力法都可以比较好地完成任务,但是如果想尽可能地提高效率,那就需要动一番脑经了。下面给出我对这些问题的整理和C++实现,并在下次的文章中给出只用+,-和位运算实现的正整数除法和取模。
    从某种意义上讲,特别是从充分利用底层硬件的计算能力(利用特殊的cpu指令)来看,这些解法肯定不是最优的,所以还希望大侠们多多指点。
    
    判断给定的整数是不是2的整数次幂
    这应该是最简单的,利用最高位是1,其后所有位为0的特性,常数时间解决问题:

1 //判断n是否是2的正整数冪
2 inline bool is_2exp(unsigned int n)
3 {
4     return !(n&(n-1));
5 }

求给定整数的二进制表示中1的个数

考虑到n-1会把n的二进制表示中最低位的1置0并把其后的所有0置1,同时不改变此位置前的所有位,那么n&(n-1)即可消除这个最低位的1。

这样便有了比顺序枚举所有位更快的算法:循环消除最低位的1,循环次数即所求1的个数。此算法的时间复杂度为O(n的二进制表示中的1的个数),最坏情况下的复杂度O(n的二进制表示的总位数)。

 1//计算n的二进制表示中1的个数
 2inline int count1(unsigned int n)
 3{
 4    int r = 0;
 5    while(n)
 6    {
 7        n &= n-1;
 8        r++;
 9    }

10    return r;
11}


既然有了求给定整数的二进制表示中1的个数的办法,那么想要求给定整数的二进制表示中0的个数就很简单了。事实上,在二进制中,完全可以把0和1看作是对称的两个对象,取反操作(~)可以任意的切换这两个对象,只要先对n进行一次取反,然后再用上述算法即能得到二进制表示中0的个数。首先看下面的代码:

 1//计算n的二进制表示中0的个数
 2inline int count0_wrong(unsigned int n)
 3{
 4    int r = 0;
 5    n &= ~n;
 6    while(n)
 7    {
 8        n &= n-1;
 9        r++;
10    }

11    return r;
12}


不知大家有没有看出问题来?是的,~操作符会把所有高位的都取反,而不是只把有效位取反,所以我们需要一个能保持高位不变的位取反操作,下面是我的实现,时间复杂度和求二进制表示中1的个数的算法相同,都与二进制表示中1的个数有关:

 1//保持高位取反
 2inline unsigned int negate_bits(unsigned int n)
 3{
 4    if(n==0) return 1;
 5    unsigned int r=0, m=~n;
 6    while(n)
 7    {
 8        r |= (n^(n-1))&m;
 9        n &= n-1;
10    }

11
12    return r;
13}


有了这个特殊的取反操作,求给定整数的二进制表示中0的个数的办法就简单了:

 1//计算n的二进制表示中0的个数
 2inline int count0( unsigned int n)
 3{
 4    int r = 0;
 5    n = negate_bits(n);
 6    while(n)
 7    {
 8        n &= n-1;
 9        r++;
10    }

11    return r;
12}


看到这里,聪明的读者肯定看出问题来了,其实我干了一件很蠢的事情。看看上述算法的时间复杂度,negate_bits花了O(n的二进制表示中1的个数),while循环计算取反后的n的二进制表示中1的个数,事实上就是O(n的二进制表示中0的个数),两部分加起来其实就是二进制表示总的有效位数,换句话说,这个算法是线性的,而事实上,我们完全可以先线性地求出这个总的有效位数,然后减去1的位数,即得到0的位数,根本不用费那么大劲去整个保持高位的取反操作,两者的时间复杂度在渐进意义上也是相同的。所以,我犯傻了,但是这里又引出另一个问题:


    求给定整数的二进制表示的有效位数
    
上面提到了线性地求这个位数(下文记为m),即“循环右移1位,记录右移次数”,时间复杂度O(m)。但是我想,一看到这个题目,所有人的第一反应应该是floor(log2(n))+1吧,但是请注意,本文在一开始就规定了“不能使用库例程”,那么在这个限制下该怎么做呢?有没有比线性时间更好的算法呢?其实到目前为止我也没有什么特别好的算法,希望谁有什么精妙的算法能指点一下,不要打我。。。
 1//求给定整数的二进制表示的位数,线性算法
 2int count_bit(unsigned int n)
 3{
 4    int r = 0;
 5    while(n)
 6    {
 7        n>>=1;
 8        r++;
 9    }

10    return r;
11}

    求大于等于给定整数的最小的2的整数次幂
    首先是最简单的思路:求出n的二进制表示的总位数m,于是1<<m即为所求值,当然这里要排除n自身就是2的整数次幂的情况,复杂度O(m),实现如下:
 1//求大于等于n的最小的2的正整数冪,方法1
 2//时间复杂度O(n的二进制位长度)
 3unsigned int high_2exp_1(unsigned int n)
 4{
 5    if(n<=1) return 1;
 6    if(is_2exp(n)) return n;
 7
 8    unsigned int r = 1;
 9    while(n)
10    {
11        n >>= 1;
12        r <<= 1;
13    }

14
15    return r;
16}

    

事实上这就涉及到上面求二进制表示位数的问题,所以目前为止在此基础上的算法都是线性时间的。    

那有没有不用计算位数m,从而效率更好的算法呢,能不能像在计算二进制表示中1的个数时那样根据1的个数来设计算法呢?回到那一题中,“n-1会把n的二进制表示中最低位的1置0并把其后的所有0置1”,那么n|=n-1就把n的二进制表示中最低位1后的所有0置1,再加上1,那么就把最低位1左移了一位。

于是,便有了更好的算法:循环左移最低位的1,直到n是2的整数次幂。该算法跟二进制表示中的1的个数和位置有关,最坏时间复杂度还是O(二进制表示位数),但是比起上一个实现,这个算法在多数情况下都比上一个算法快。实现如下:

 1//求大于等于n的最小的2的正整数冪,方法2
 2//计算时间与n的二进制表示中1的个数和位置有关,比方法1效率高
 3//最坏情况下的时间复杂度与方法1相同
 4unsigned int high_2exp_2(unsigned int n)
 5{
 6    if(n<=1) return 1;
 7
 8    while(!is_2exp(n))
 9    {
10        n |= n-1;
11        n++;
12    }

13
14    return n;
15}
    
    最后来一个简单的扩充题目:
    判断给定的整数是不是4的整数次幂
    观察4的整数次幂的特征,容易发现除了满足n&(n-1)==0外,唯一的1位后的0的个数是偶数,这从4x=22k也能简单地得到。这就很直观地衍生出一个简单的算法:
 1//判断n是否是4的整数次幂
 2bool is_4exp(unsigned int n)
 3{
 4    if(!is_2exp(n)) return false;
 5
 6    int bit_len = count_bit(n)-1;//线性时间求二进制位数
 7    if((bit_len&0x1)!=1)
 8        return true;
 9    else
10        return false;
11}


算法很直观,但是比起is_2exp的常数时间is_4exp的线性时间总让我觉得不能接受,期待有人更好的改进。

    
  


原文出处:http://www.cppblog.com/xiaoyisnail/archive/2010/11/24/96707.html#134462

使用位运算(&)实现运算(%) 最近做一道ctf题的时候发现一个有趣的现象,汇编如下: 反汇编之后看的更懵了, 这右移这么多是想干什么??? 后来我发现这么几行汇编, 很相似啊,而这个经过反汇编之后是这样的: 我好像突然发现点什么,这是在啊,仔细的看了一下这个这个汇编,v6其实一直都是0了,所以这几句就是在用v4加前一个s[i]。 接下来说一说为什么位运算(&)可以实现运算(%)。 有公式: X % 2^ n = X & (2^ n – 1) 2^ n表示2的n次方,也就是说,一个数对2^ n == 一个 阅读详情

相关推荐

位运算——用+,-位运算实现正整数除法()

<br />作者:翼帆@cppblog  <br />原文地址:http://www.cppblog.com/xiaoyisnail/archive/2009/09/19/96707.html<br />本文版权归作者cppblog共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 <br /><br />    今天看了一位师兄去年的笔经总结,其中有一题是“不许用%/来实现求任意数除以3的余数”,我想考官的目的应该是想考察学生对位运算的熟悉程

博学之,审问之,慎思之,明辨之,笃行之 639

位运算

(1) 判断int型变量a是奇数还是偶数 a&1 = 0 偶数 a&1 = 1 奇数 (2) int型变量a的第k位 (k=0,1,2……sizeof(int)),即a>>k&1 (3) 将int型变量a的第k位清0,即a=a&~(1<<k) (4) 将int型变量a的第k位置1, 即a...

hao1183716597的专栏 3483

为什么可以使用位运算(&)实现运算(%)呢?

实现的原理如下: indexFor()方法其实主要是将hash生成的整型转换成链表数组中的下标。那么return h & (length-1);是什么意思呢?其实,他就是。Java之所有使用位运算(&)来代替运算(%),最主要的考虑就是效率。位运算(&)效率要比代替运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常...

xindaxinda123的博客 7056

位运算--+- 位运算实现正整数除法(2)

在上一篇文章(下文中称为“文())中,我提出了一个具体的问题“只能用+,-位运算实现正整数除法(/)(%)”,并整理了一些位运算相关的题目算法,本文将给出上述问题的一个完整的解答思路实现。 首先思考最简单的除法实现,即循环减除数,减到不能再减为止,所减次数即所求的商,事实上这就是我们初学四则运算时对除法的定义,实现:  1int integer_div_0(int d

tkp2014的专栏 720

位运算——用+,-位运算实现整数除法()

位运算——用+,-位运算实现整数除法()  今天看了一位师兄去年的笔经总结,其中有一题是“不许用%/来实现求任意数除以3的余数”,我想考官的目的应该是想考察学生对位运算的熟悉程度吧,于是我把题目扩展成“只能用+,-位运算实现整数除法(/)(%)”,注意:这里不能使用其它的库例程来辅助计算,如log,log10等。在思考这道题目的过程中,我又涉及到了许多二进制相关的题

zht_304的专栏 4228

位运算——用+,-位运算实现正整数除法()

作者:翼帆@cppblog       原文地址:http://www.cppblog.com/xiaoyisnail/archive/2009/09/21/96883.html     终于有时间写本文的第二部分了。在上一篇文章(下文中称为“文())中,我提出了一个具体的问题“只能用+,-位运算实现正整数除法(/)(%)”,并整理了一些位运算相关的题目算法,本文将给出

遨游网络huster的专栏 1146

位运算实现除法

移位实现除法运算 1、基础知识 二进制数:K = b0 * 2^0 + b1 * 2^1 + b2 * 2^2 ++ bn * 2^n + … 故乘二有:K = b0 * 2^1 + b1 * 2^2 + b2 * 2^3 ++ bn * 2^(n+1) + … 即左移一:K = 00 * 2^0 + b0 * 2^1 + b1 * 2^2 ++ bn * 2^(n+1) + … 同理除二:K = b0 *2^-1 + b1 * 2^0 + b2 * 2^1 ++ bn * 2^(

qq_41896736的博客 7257

十进制转二进制,短除法位运算两种方法

除法:比如十进制整数19 19/2=9……1 9/2=4……1 4/2=2……0 2/2=1……0 所以最后计算的结果就是10011除法代码: #include using namespace std; int main() {     int n;     while(cin>>n)     {         int cnt=0;         while(n!=1) 

Li_zhonglei的博客 1万+
上一篇: 位运算之美--用 +、- 和位运算实现正整数除法和取模(2)
下一篇: 位运算之 -- 按位与(&)操作 -- 快速取模算法
tkp2014
博客等级 码龄12年 137粉丝 241原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值