面試題:位操作實現四則運算


首先看一個面試題:

題目:寫一個函數,求兩個整數的之和,要求在函數體內不得使用+、-、×、÷。

分析:這又是一道考察發散思維的很有意思的題目。當我們習以為常的東西被限制使用的時候,如何突破常規去思考,就是解決這個問題的關鍵所在。

看到的這個題目,我的第一反應是傻眼了,四則運算都不能用,那還能用什么啊?可是問題總是要解決的,只能打開思路去思考各種可能性。首先我們可以分析人們是如何做十進制的加法的,比如是如何得出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;
}

原創文章,轉載請注明: 轉載自新書《程序員面試筆試寶典》官網

本文鏈接地址: 位操作實現加減乘除四則運算

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM