1. 程式人生 > 其它 >shell 做加法運算_劍指面試題[65]——不用加減乘除做加法(位運算)

shell 做加法運算_劍指面試題[65]——不用加減乘除做加法(位運算)

技術標籤:shell 做加法運算

題目描述:不用加減乘除做加法_牛客網

寫一個函式,求兩個整數之和,要求在函式體內不得使用+、-、*、/四則運算子號。

解法(1):空間O(1),時間O(n)。

不能用四則運算,只能用二進位制位運算模擬。二進位制中,異或是不進位的加法,進位可以用相與並左移1得到,繼續令二者相加直到進位為0。

舉例,4+6=10。

①用‘與運算+右移1’模擬進位位置,用異或模擬不進位的相加結果。

②得到8和2。重複①,直至沒有進位。

ccd0f6524febfa90d055e4fdfb96916e.png
# 死迴圈版
# -*- coding:utf-8 -*-
class Solution:
    def Add(self, s, carry):
        # write code here
        while carry:
            s, carry = s^carry, (carry & s)<<1
        return s 

死迴圈原因:當進位結果導致原有儲存位數擴增時,異或結果為負數時位數擴增體現為數值位左邊補1,則兩者的進位將永遠不為0,陷入死迴圈。陷入死迴圈的場景是 負數+正數,且正數的絕對值比負數大。

分析:因為正數的絕對值大,所以用多少位儲存取決於正數,設數值位用n-1位儲存,第n位是符號位,正數的數值位最高位即第n-1位是1。由於負數絕對值較小,因此補碼的數值位值大於正數數值位,因此負數的數值位最高位即第n-1位也是1,兩個加數相與後得到最高數值位即第n-1位是1的正數。兩個加數異或後得到一個n位負數。相與結果左移一位得到進位結果,進位結果的數值位是n位,因此異或結果的負數也必須用n位數值位表示,則負數由n+1位表示,第n位和第n+1位符號位均為1。新的加數為該負數和左移結果。繼續重複上面的操作,由於進位結果和負數的數值最高位都為1,相與後又會導致數位擴增,進位結果永遠不為0,陷入死迴圈。舉例如下圖。

示例分析:-1+2得到的不進位和、進位數(兩個加數)的二進位制形式為11{0...}1、01{0...}0,其中{}內表示迴圈過程中增加的1或0的個數。可以看出,當進位只有最高數值位為1時,繼續操作將不斷左移最高位1,不進位和將不斷增加數值位中的0,而正確和可以表示為{0...}1,因此只要取出{0...}1值,即去掉符號位、數值位最高位剩下的二進位制值,亦即異或結果的低n-2位返回即可。

f1b189c360a52dded582ebdde828b6a3.png
不限制位數時,-1 +2陷入死迴圈

實現1:限制最高儲存位數為32(因為測試用例不超過32位)。當進位1不斷左移到達2^31時,繼續左移將溢位,此時丟棄進位,返回異或結果的低30位數。

# -*- coding:utf-8 -*-
class Solution:
    def Add(self, num1, num2):
        # write code here       
        if num1&num2:
            # 此時num1跟num2用32位儲存,數值位佔31位,最高數值位為1,如果相與結果左移將溢位
            if num1&num2 == 0x3fffffff+1:
                # 返回低30位的結果
                return (num1^num2)&0x3fffffff
            return self.Add(num1^num2, (num1&num2)<<1)
        return num1^num2

實現2:其實,當進位數的二進位制位中只有一個1且1在最高的數值位上時,最終和的所有資訊已經儲存在不進位加和數當中,此時就可以丟棄進位了。該方法適用於事先不知道儲存位數限制的情景。

總結:當兩個加數一正一負,且正數的絕對值≥負數的絕對值,且正數的二進位制位中只有一個1時,屬於可能陷入死迴圈的特殊情況,提前結束,返回負數的n-2位二進位制數。

# -*- coding:utf-8 -*-
class Solution:
    def Add(self, num1, num2):
        # write code here
        tmp_carry = num1&num2    
        tmp_sum = num1^num2
        if not tmp_carry:
            return tmp_sum
        if tmp_carry>0 and tmp_sum<0 and tmp_carry<<1 >= abs(tmp_sum)
            and not tmp_carry&(tmp_carry-1):
            return tmp_sum & (tmp_carry-1)
        
        return self.Add(tmp_sum, tmp_carry<<1)

其他解法

  1. 新增越界檢查才能避免負數死迴圈。跟0xFFFFFFFF(邊界數)相與,相當於將負數的二進位制補碼視為某個正數的原碼,有(2^32)-原負數=該正數,即二者對於2^32同模。符號參與的進位1將一直往左移直到超過邊界數時退出迴圈。最後要再將該“正數”轉化為對應補碼的負數。關於補碼https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html
  2. 正負數判斷及還原:對於負數來說,~((n&0xFFFFFFFF)^0xFFFFFFFF)=n。

以-15為例,~(-15)=241^0xFF=((-15)&0xff)^0xff。

a60b4dc0fc9671062930f3edcb0a6524.png
241(與-15對256同模,即對(2**8)同模) 對應的二進位制數為: 11110001

11110001^0xFF -----> 00001110(14),相當於00001111-1,即15-1。

~(00001110) -----> 11110001(補碼) ,對應的十進位制數是 -15

e5c25440cacf8b1efed581d15d8d2739.png

此處有一個規律:~n = -(n+1)。取反改變符號!!

總結:對於負數a,

  • 令b=a&0xFFFFFFFF,相當於將a往前撥2**32步得到正數b,即b=a+2**32;
  • 令c=b^0xFFFFFFFF,相當於求(2**8-1)減去b得到正數c,即c=(2**8-1)-b;
  • 由上面二式可得,a=-c-1,即c比a的絕對值小1;
  • 令d=~c,相當於將c往後撥2**8-1步得到負數d,由公式得d=~c=-(c+1)=a。

*設定成32位應該是考慮到其他語言的特點,測試樣例中不會出現超過32位整型的數,實際上,把邊界調大的話,不會影響最終結果。參考https://blog.csdn.net/lrs1353281004/article/details/87192205

# -*- coding:utf-8 -*-
class Solution: 
    def Add(self, s, carry): 
        while(carry): 
           s, carry = (s^carry) & 0xFFFFFFFF,((s & carry)<<1) & 0xFFFFFFFF
        # [-2^31,(2^31)-1]
        return s if s<=0x7FFFFFFF else ~(s^0xFFFFFFFF)