Python LintCode:A + B問題,最詳原理

Python LintCode:A + B問題,最詳原理

概述

該題中要求不使用加法運算符,那麼我們就需要思考"+"運算符是如何實現的,我們先看在數電中加法是如何實現的,如下圖所示(半加器結構)。
加法器,由一個異或門和與門組成

可以看出,加法器由一個異或門(上) 和一個與門(下) 組成,S爲計算和,C爲計算中產生的進位。那麼這個時候我們就需要考慮使用位運算來完成上述題目。
我們舉一個實際的例子來說明(假設用一個byte存儲整數):2 + 9

2 + 9(對應於A + B)
2對應的二進制可以表示爲0000 0010
9對應的二進制可以表示爲0000 1001

正常情況下,我們在做加法的時候,對應位相加,如果有進位則加上進位,計算出最終結果時進位爲0。
①對於2 + 9,我們先考慮無進位加法,個位相加和2 + 9 = 1,所以S = 1, 只考慮進位加法:2 + 9 個位產生了進位,所以進位1在十位,個位爲0,即C = 10。以此方法可以推廣到更高位。
②此時,我們讓A = S,B = C,再次計算,個位相加爲1 + 0爲1,十位相加爲0 + 1爲1,所以最終結果爲11,因爲個位和十位均無進位產生,所以C = 0。

我們再舉一個實際的例子來說明(十進制數):29 + 73

29 + 73(對應於A + B)
29對應的二進制可以表示爲0001 1101
73對應的二進制可以表示爲0100 1001

①對於29 + 73,我們先考慮無進位加法,個位相加和9 + 3 = 2,十位相加爲2 + 7 = 9,所以S = 92, 只考慮進位加法:9 + 3 個位產生了進位,十位2 + 7並無進位產生,所以C = 10。
②此時,我們讓A = S,B = C,再次計算,個位相加爲2 + 0爲2,十位相加爲9 + 1爲0,所以最終結果爲2,十位產生了進位,所以C = 100。
③再次令A = S,B = C,再次計算,個位相加爲2 + 0,十位爲0,百位爲1,最終結果S = 102,各個位均無進位產生。所以C = 0,此時結束計算,最終結果即爲102。
通過上面兩個例子可以知道,只要C不爲0時,循環執行①②步驟,最終結果即爲S。那麼既然要用位運算,不可避免的要使用二進制數,那麼適用於十進制的方法是否也適合二進制,我們繼續採樣上述例子(2 + 9):
0000 0010 + 0000 1001在不考慮進位的情況下,其結果爲0000 1011,那麼這個結果即爲二進制下無進位加法,也等價於A ^ B(A與B的異或)
只考慮進位的情況下:觀察上述二進制碼,我們發現各個位都沒有進位,因此C = 0,此時加法的結果位S = 0b1011 = 11

對於29 + 73這個例子
首先考慮無進位加法,0001 1101 + 0100 1001結果爲S = 0101 0100(即A ^ B),只考慮進位加法C = 0001 0010,我們發現這個C的值恰好爲(a & b) << 1,即A與B相與後,左移一位。
接下來執行A = S,B = C,繼續,那麼無進位加法S = 0100 0110, C = 0010 0000,繼續執行A = S,B = C,無進位加法S = 0110 0110,C = 0000 0000,此時進位爲0,那麼S即爲最終結果S = 0110 0110 = 102。
以上四個例子(2個十進制和2個二進制)主要說明了一個算法的流程,在十進制下如何進行,二進制下如何進行,總結上述方法的過程如下:

對於給定的A B整數值,通過位運算進行加法運算
步驟①:S = A ^ B
步驟②:C = (A & B) << 1
步驟③:如果C不爲0,執行A = S,B = C,然後重複執行步驟①和步驟②

如何用高級語言實現

通過概述一欄中的描述,我們已經知道了位運算的這樣一個過程,但是請注意,我在舉例子的時候提到過我們使用一個byte來存儲,我們可以很快的計算出最終的結果(因爲C中的左移操作可以很快完成),在C或者C++、Java等高級語言中,int類型值得存儲使用4個字節,一共32位,這裏用C語言完成上述步驟,非遞歸方式,遞歸得話寫起來會更簡單:

int aplusb(int a, int b) {
        // a b 可以爲任意整數
        int carry = 0;
        int sum_ = 0;
        
        while(b != 0){
            carry = (a & b) << 1;
            sum_ = (a ^ b);
            a = sum_;
            b = carry;
        }
        return a;
    }

在Python中,同樣代碼只能進行正整數與正整數的加和計算,如果A與B存在負數,那麼程序會進入死循環,這是由Python對int的儲存機製造成,那麼我先上正確的代碼,然後解釋爲什麼要這麼寫

def aplusb(a, b):
	while b != 0:
	    sum_ = a ^ b
	    carry = (a & b) << 1
	    a = sum_ % 0x100000000
	    b = carry % 0x100000000
	return a if a <= 0x7FFFFFFF else ~(a ^ 0xFFFFFFFF)

在python中,int類型並不是4 bytes存儲,而是24 bytes,可以認爲python中的int支持無限長的整型值,這樣的話會導致進位在左移時永遠不會溢出,也就是永遠無法獲得最終的結果,所以我們需要手動去模擬邊界,32bit的最大值爲 23212^{32} - 1,也就是說32bit的支持的整數長度爲 2322^{32},寫成二進制爲0x100000000,這樣的話通過取模運算,就可以將A與B的值限制在32bit可以表示的集合中,那麼我們將正整數集合劃分爲[-0x10000000, 0x7FFFFFFF]這樣兩部分,可以類似於8bit的範圍爲[-128, 127],最大值爲255,一共可以表示256個數,這樣構造結束之後,當C = 0時,只要A的值小於等於最大值0x7FFFFFFF,那麼就可以直接返回,而對於大於0x7FFFFFFF,可知其最高位必爲1,我們需要對其進行處理,將其符號位提取出來。

再舉一個簡介明瞭的例子,比如我們用4 bit來存儲一個int值,假如這個值爲-2,其二進制爲1110,如果我想把這個值轉換爲8 bit類型的值,應該如何做呢?這裏我們需要知道的是,負數的二進制表示爲正數的補碼形式。
**我們先看-2在8 bit中如何表示:
2的原碼:0000 0010
2的反碼:1111 1101
2的補碼:1111 1110 (8bit中,-2的二進制表示) **

而0000 1110在8bit中對應的值是14,我們需要將其轉換,首先將 0000 1110 與 0xF做異或操作,結果爲0000 0001,然後取反1111 1110,這樣的話,就將4bit中的-2轉換爲8bit中的-2了,總結下來就兩步:
① 將4bit中的結果與0xF異或
② 將異或的結果取反

可以將其擴展至32位,只要將結果與0xFFFFFFFF做異或運算,然後取反即可。如果說這個你沒看懂,那我再寫一個推理,如下:

1111 1110(8bit,-2的二進制表示) 8bit轉16bit

原碼:0000 0000 0000 0010 (2的原碼)
反碼:1111 1111 11111 1101
補碼:1111 1111 1111 1110 (16bit的 -2的表示)

0000 0000 1111 1110 (16bit的 -2的表示)
0000 0000 1111 1111 (0xFF)
異或:0000 0000 0000 0001
取反:1111 1111 1111 1110 (8bit轉16bit的結果)

1111 1111 1111 1110 (16位 -2的表示) 轉8bit的-2 (1111 1110)

1111 1111 1111 1110 (16位 -2的表示)
0000 0000 1111 1111 (0xFF)

異或:1111 1111 0000 0001
取反:0000 0000 1111 1110
結果:1111 1110 與上述結果一致

Python的int存儲機制,使得我們要從高位轉低位,所以當我們要轉換位32bit的表示時,需要與0xFFFFFFFF做異或然後取反即爲最終結果。

簡單說一下

這道題以前是用C++寫的,感覺不是特別難理解,最近用python重新寫的時候才發現以前竟然邁過了好多坑,這個題用python來做真的能有很多收穫,如果大家有什麼問題,歡迎指出來,我一個一個字敲的,難免會有錯誤。

參考鏈接

[1] 位運算詳解以及在 Python 中需要的特殊處理
[2] Python 位運算一些坑
[3] 位運算實現加、減、乘、除運算

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章