代码改变世界

面试题:位操作实现四则运算

2013-09-01 23:05  youxin  阅读(6274)  评论(0编辑  收藏

首先看一个面试题:

题目:写一个函数,求两个整数的之和,要求在函数体内不得使用+、-、×、÷。

分析:这又是一道考察发散思维的很有意思的题目。当我们习以为常的东西被限制使用的时候,如何突破常规去思考,就是解决这个问题的关键所在。

看到的这个题目,我的第一反应是傻眼了,四则运算都不能用,那还能用什么啊?可是问题总是要解决的,只能打开思路去思考各种可能性。首先我们可以分析人们是如何做十进制的加法的,比如是如何得出5+17=22这个结果的。实际上,我们可以分成三步的:第一步只做各位相加不进位,此时相加的结果是12(个位数57相加不要进位是2,十位数01相加结果是1);第二步做进位,5+7中有进位,进位的值是10;第三步把前面两个结果加起来,12+10的结果是22,刚好5+17=22

前面我们就在想,求两数之和四则运算都不能用,那还能用什么啊?对呀,还能用什么呢?对数字做运算,除了四则运算之外,也就只剩下位运算了。位运算是针对二进制的,我们也就以二进制再来分析一下前面的三步走策略对二进制是不是也管用

5的二进制是10117的二进制10001。还是试着把计算分成三步:第一步各位相加但不计进位,得到的结果是10100(最后一位两个数都是1,相加的结果是二进制的10。这一步不计进位,因此结果仍然是0);第二步记下进位。在这个例子中只在最后一位相加时产生一个进位,结果是二进制的10;第三步把前两步的结果相加,得到的结果是10110,正好是22。由此可见三步走的策略对二进制也是管用的。

11+11 (第1步结果为00,第二步进位为110.第三步相加110正确)

接下来我们试着把二进制上的加法用位运算来替代。第一步不考虑进位,对每一位相加。0011的结果都00110的结果都是1。我们可以注意到,这和异或的结果是一样的。对异或而言,0011异或的结果是0,而0110的异或结果是1。接着考虑第二步进位,对000110而言,都不会产生进位,只有11时,会向前产生一个进位。此时我们可以想象成是两个数先做位与运算,然后再向左移动一位。只有两个数都是1的时候,位与得到的结果是1,其余都是0。第三步把前两个步骤的结果相加。如果我们定义一个函数AddWithoutArithmetic第三步就相当于输入前两步骤的结果来递归调用自己

有了这些分析之后,就不难写出如下的代码了:

int AddWithoutArithmetic(int num1, int num2)
{
        if(num2 == 0)
                return num1;
 
        int sum = num1 ^ num2;
        int carry = (num1 & num2) << 1;
 
        return AddWithoutArithmetic(sum, carry);
}

 上面的代码和机器内的加法器的原理是一样的。可以看计算机组成原理(白中英)P31.

之前我的系列博客中有这么一道题,求1+2+…+n,要求不能使用乘除法、forwhileifelseswitchcase等关键字以及条件判断语句(A?B:C)。刚兴趣的读者,可以到http://zhedahht.blog.163.com/blog/static/2541117420072915131422/看看。

怎么判断递归确实是可以结束的?

补充一下,最后应加入一个递归有限步结束的证明。
简单证明如下:
因为每步的和都可分解为两个部分,一个异或值(记为A),一个与值(记为B)。
第n+1步与第n步相比:
B的二进制表示中1的个数是不增的(由与的性质可知)。countTrueBits(B(n+1)) <= countTrueBits(B(n))
另外,B(n)一直在左移,并且 B的bit数有限,故二进制1的个数不可能一直保持不变,因此只能越来越少。最后终止在有限步
回复
 
楼主能证明这个算法是可结束的吗?
也就是说如何证明不存在这样两个数a,b,经过若干次函数的迭代,又变回a和b~
上面的程序写成非递归:
int add (int a,int b)
{
     int ans;
     while(b)
     {
         //直到没有进位
        ans=a^b;
        b=(a&b)<<1;
        a=ans;
     }
     return a;
}

还有一种巧妙的做法;

利用数组下标隐式做加法;

//利用数组下标影式做加法
int add2(int a,int b)
{
    char *c;
    c=(char *)a;
    return (int)&c[b];
}

 

 

求减法:

我们首先看看补码减法

[x-y]补=[x]补-[y]补=[x]补+【-y]补

从[y]补求[-y]补的法则是:对[y]补包括符号位“求反且最末位加1”,即可得到[-y]补。

数在计算机内部表示就是补码形式的。

//减法:这个和加法一样了,首先取减数的补码,然后相加。 
int negative(int a)//取补码
{
    return add(~a,1);
}


int sub(int a,int b)
{
    return add(a,negative(b));
}

 

模拟乘法:
先看一个式子:
   111        7
   101        5
=====
       111
    0 0 0
 1 1 1
=====
100011     (35)
我们求乘法时,首先b的第一位跟a相乘,然后第二位为0,第三位相乘。
我们现在不从乘法的角度考虑,从加法考虑。从b的第0位开始,如果第0位为1,则加上a(111),接着第2位,第二位为0,不会加0000,第3次为1,加上11100.可以看到,b每向右移一位,a的值就要向左移动。
 
//正数乘法运算  
int Pos_Multiply(int a,int b)  
{  
    int ans = 0;  
    while(b)  
    {  
        if(b&1)  //b最后一位是否为1
            ans = Add(ans, a);  
        a = (a<<1);  
        b = (b>>1);  
    }  
    return ans;  
} 

 

整数除法(正整数):

除法就是由乘法的过程逆推,依次减掉(如果x够减的)y^(2^31),y^(2^30),...y^8,y^4,y^2,y^1。减掉相应数量的y就在结果加上相应的数量。

//除法就是由乘法的过程逆推,依次减掉(如果x够减的)y^(2^31),y^(2^30),...y^8,y^4,y^2,y^1。减掉相应数量的y就在结果加上相应的数量。  
int Pos_div(int x,int y)
{
    int ans=0;
    for(int i=31;i>=0;i--)
    {
        // //比较x是否大于y的(1<<i)次方,避免将x与(y<<i)比较,因为不确定y的(1<<i)次方是否溢出  
        if( (x>>i) >=y )
        {
            ans+=(1<<i);
            x-= (y<<i);
        }
    }
    return ans;
}

例:

5/2

101/10

i=1时,ans+=(1<<1)=2;

x=101-100=1;

i=0;   1>>0  <     y 退出

结果i=2;

完整的实现:

// 加减乘除位运算 
// 程序中实现了比较大小、加减乘除运算。所有运算都用位操作实现 
// 在实现除法运算时,用了从高位到低位的减法 
// 具体如下,算法也比较简单,所以没有作注释
#include<iostream>
#include<cstdio>
using namespace std;

int Add(int a, int b)
{
    int ans;
    while(b)
    {  //直到没有进位
        ans = a^b;        //不带进位加法
        b = ((a&b)<<1);   //进位
        a = ans;
    }
    return a;
}

//这个和加法一样了,首先取减数的补码,然后相加。
int negtive(int a)   //取补码
{
    return Add(~a, 1);
}
int Sub(int a, int b)
{
    return Add(a, negtive(b));
}

// 判断正负 
int ispos( int a ) 
{ //
    return (a&0xFFFF) && !(a&0x8000);
}
int isneg( int a ) 应该改为bool类型
{ //负
    return a&0x8000;
}
bool iszero( int a )
{ //0
    return !(a&0xFFFF);
}

//正数乘法运算
int Pos_Multiply(int a,int b)
{
    int ans = 0;
    while(b)
    {
        if(b&1)
            ans = Add(ans, a);
        a = (a<<1);
        b = (b>>1);
    }
    return ans;
}

//乘法运算
int Multiply(int a,int b)
{
    if( iszero(a) || iszero(b) )
        return 0;
    if( ispos(a) && ispos(b) )
        return Pos_Multiply(a, b);
    if( isneg(a) )
    {
        if( isneg(b) )
        {
            return Pos_Multiply( negtive(a), negtive(b) );
        }
        return negtive( Pos_Multiply( negtive(a), b ) );
    }
    return negtive( Pos_Multiply(a, negtive(b)) );
}

//除法就是由乘法的过程逆推,依次减掉(如果x够减的)y^(2^31),y^(2^30),...y^8,y^4,y^2,y^1。减掉相应数量的y就在结果加上相应的数量。
int Pos_Div(int x,int y)
{
    int ans=0;
    for(int i=31;i>=0;i--)
    {
        //比较x是否大于y的(1<<i)次方,避免将x与(y<<i)比较,因为不确定y的(1<<i)次方是否溢出
        if((x>>i)>=y)
        {
            ans+=(1<<i);
            x-=(y<<i);
        }
    }
    return ans;
}

//除法运算
int MyDiv( int a, int b )
{
    if( iszero(b) )
    {
        cout << "Error" << endl;
        exit(1);
    }
    if( iszero(a) )
        return 0;
    if( ispos(a) )
    {
        if( ispos(b) )
            return Pos_Div(a, b);
        return negtive( Pos_Div( a, negtive(b)) );
    }
    if( ispos(b) )
        return negtive( Pos_Div( negtive(a), b ) );
    return Pos_Div( negtive(a), negtive(b) );
} 


// 比较两个正数的大小(非负也可) 
int isbig_pos( int a, int b ) 
{  //a>b>0
    int c = 1;
    b = (a^b);
    if( iszero(b) )
        return 0;
    while( b >>= 1 )
    {
        c <<= 1;
    }
    return (c&a);
} 

// 比较两个数的大小 
int isbig( int a, int b ) 
{ //a>b
    if( isneg(a) )
    {
        if( isneg(b) )
        {
            return isbig_pos( negtive(b), negtive(a) );
        }
        return 0;
    }
    if( isneg(b) )
        return 1;
    return isbig_pos(a, b);
}

判断负:

int isneg( int a ) 
{ //负
    return a&0x8000;
}
如果a为负,前面为1111则& 1000变成1,为true,为0111,&1000为0,非负。
如果是0,则还是返回true。
// 判断正负 
int ispos( int a ) 
{ //
    return (a&0xFFFF) && !(a&0x8000);
}
前面a&0xffff是为了防止0,

--------------------------------------------------

加减乘除位运算

// 程序中实现了比较大小、加减乘除运算。所有运算都用位操作实现

 

#include <iostream>
using namespace std;
 
// 加法
int add( int a, int b )
{
int c;
while( c = (a&b) )
{
a = (a^b);
b = (c<<1);
}
return (a^b);
}
 
// 求补码
int rev( int a )
{
return add((~a), 1);
}
 
// 判断正负
int ispos( int a )
{ //
return (a&0xFFFF) && !(a&0x8000);
}
int isneg( int a )
{ //
return a&0x8000;
}
int iszero( int a )
{ // 0
return !(a&0xFFFF);
}
 
// 比较两个正数的大小(非负也可)
int isbig_pos( int a, int b )
{ // a>b>0
int c = 1;
b = (a^b);
if( iszero(b) ) return 0;
while( b >>= 1 )
{
c <<= 1;
}
return (c&a);
}
 
// 比较两个数的大小
int isbig( int a, int b )
{ // a>b
if( isneg(a) )
{
if( isneg(b) )
{
return isbig_pos( rev(b), rev(a) );
}
return 0;
}
if( isneg(b) ) return 1;
return isbig_pos(a, b);
}
 
// 减法
int sub( int a, int b )
{
return add(a, rev(b));
}
 
// 正数乘法
int pos_mul( int a, int b )
{
int c = 0x8000;
int re = a;
while( (c>>=1) && (!(b&c)) );
while( c >>= 1 )
{
re <<= 1;
if( c&b )
re = add(re, a);
}
return re;
}
 
// 乘法
int mul( int a, int b )
{
if( iszero(a) || iszero(b) ) return 0;
if( ispos(a) && ispos(b) )
return pos_mul(a, b);
if( isneg(a) )
{
if( isneg(b) )
{
return pos_mul( rev(a), rev(b) );
}
return rev( pos_mul( rev(a), b ) );
}
return rev( pos_mul(a, rev(b)) );
}
 
// 正数除法
int pos_div( int a, int b)
{
int re = 0, temp = b;
if( isbig_pos(b, a)) return 0;
do{
temp <<= 1;
}while( !isbig_pos(temp, a) );
while( isbig_pos(temp, b) )
{
re <<= 1;
temp >>= 1;
if( !isbig_pos(temp, a))
{
a = sub(a, temp);
re = add(re, 1);
}
}
return re;
}
 
// 除法
int idiv( int a, int b )
{
if( iszero(b) )
{
cout << "error" << endl;
exit(1);
}
if( iszero(a) ) return 0;
if( ispos(a) )
{
if( ispos(b) )
return pos_div(a, b);
return rev( pos_div( a, rev(b)) );
}
if( ispos(b) )
return rev( pos_div( rev(a), b ) );
return pos_div( rev(a), rev(b) );
}
 
int main ()
{
int a, b;
while( cin >> a >> b)
{
if(isbig(a,b)&&(a<=b)) cout << "big error" << endl;
if(add(a,b) != (a+b)) cout << "add error" << endl;
if(sub(a,b) != (a-b)) cout << "sub error" << endl;
if(mul(a,b) != (a*b)) cout << "mul error" << endl;
if(idiv(a,b) != (a/b)) cout << "div error" << endl;
}
return 0;
}
--

更多:http://blog.sina.com.cn/s/blog_6375752c0100vh0s.html

 

1. 题目描述

如何使用位操作分别实现整数的加减乘除四种运算?

2. 解决方案

需要熟练掌握一些常见功能的位操作实现,具体为:

<1> 常用的等式:-n = ~(n-1) = ~n+1

<2> 获取整数n的二进制中最后一个1:n&(-n) 或者 n&~(n-1),如:n=010100,则-n=101100,n&(-n)=000100

<3> 去掉整数n的二进制中最后一个1:n&(n-1),如:n=010100,n-1=010011,n&(n-1)=010000

(1) 加法实现

可以很容易地用“异或”和“或”操作实现整数加法运算:对应位数的“异或操作”可得到该位的数值,对应位的“与操作”可得到该位产生的高位进位,如:a=010010,b=100111,计算步骤如下:

第一轮:a^b=110101,(a&b)<<1=000100, 由于进位(000100)大于0,则进入下一轮计算,a=110101,b=000100,a^b=110001,(a&b)<<1=001000,由于进位大于0,则进入下一轮计算:a=110001,b=001000,a^b=111001,(a&b)<<1=0,进位为0,终止,计算结果为:111001。

代码如下:

 

 
1
2
3
4
5
6
7
8
9
10
int add(int a, int b) {
  int carry, add;
  do {
    add = a ^ b;
    carry = (a & b) << 1;
    a = add;
    b = carry;
  } while(carry != 0);
  return add;
}

(2) 减法实现

减法可很容易地转化为加法:a - b = a + (-b) = a + (~b + 1 )

代码如下:

 
1
2
3
int subtract(int a, int b) {
  return add(a, add(~b, 1));
}

(3) 乘法实现

先看一个实例:1011*1010:

 
1
2
3
4
5
6
7
1011
   * 1010
 ----------
    10110 < 左移一位,乘以0010
+ 1011000 < 左移3位,乘以1000
----------
  1101110
因而乘法可以通过系列移位和加法完成。最后一个1可通过b&~(b-1)求得,可通过b& (b-1)去掉,为了高效地得到左移的位数,可提前计算一个map,代码如下:

 

 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int multiply(int a, int b) {
  bool neg = (b < 0);
  if(b < 0)
    b = -b;
  int sum = 0;
  map<int, int> bit_map;
  for(int i = 0; i < 32; i++)
    bit_map.insert(pair<int, int>(1 << i, i));
    while(b > 0) {
      int last_bit = bit_map[b & ~(b - 1)];
      sum += (a << last_bit);
      b &= b - 1;
    }
  if(neg)
    sum = -sum;
  return sum;
}

(4) 除法实现

乘法可很容易转化为减法操作,主要思想与乘法实现类似,代码如下:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int divide(int a, int b) {
  bool neg = (a > 0) ^ (b > 0);
  if(a < 0)
    a = -a;
  if(b < 0)
    b = -b;
  if(a < b)
    return 0;
  int msb = 0;
  for(msb = 0; msb < 32; msb++) {
    if((b << msb) >= a)
      break;
  }
  int q = 0;
  for(int i = msb; i >= 0; i--) {
    if((b << i) > a)
      continue;
    q |= (1 << i);
    a -= (b << i);
  }
  if(neg)
    return -q;
  return q;
}

原创文章,转载请注明: 转载自新书《程序员面试笔试宝典》官网

本文链接地址: 位操作实现加减乘除四则运算