前兩天突發奇想,寫一個乘法的實現,但不用乘號*。並測試一下性能如何。因此就有了下面的代碼:(本文主要目的是為了玩遞歸和位移,因此僅限自然數)
首先,標准乘法:
1 int commonMultiplication(int a, int b) => a * b;
第二,從數學的角度,乘法其實就是加法,只是加法的簡寫而已,因此 a * b 可以理解為 b 個 a 相加;故得出用加法代替的乘法。為了減少加法的次數,取 a, b 兩數的最小值進行循環:
1 int plusMultiplication(int a, int b) { 2 var t = a > b ? b : a, r = 0; 3 if (t == a) { 4 a = b; 5 b = t; 6 } 7 8 while (b-- > 0) r += a; 9 return r; 10 }
最后,位移乘法。計算機進行位移操作的速度是非常快的,因此,如果能把乘法換算成位移操作的話,速度會不會更快?但是根據計算機二進制的特點,每次向左位移,相當於是乘了2的n次方,怎么辦?我們看看例子:
31 * 18
怎么用位移來實現呢?首先我們取小的數:18,進行拆解。拆解的算法是,任何一個自然數,在計算機中都可以用二進制表示,那么,任何一個自然數,也可以拆解為2的冪的和。比如 18 的二進制表示 為 10010,拆解成十進制,即為 18 = 16 + 2;那么上面可以拆解為:
31 * 18 = 31 * (16 + 2) = 31 * 16 + 31 * 2
此時即可換成位移的算法:
31 * 18 = 31 * (16 + 2) = 31 * 16 + 31 * 2 = 31 * 2^4 + 31 * 2^1 = (31 << 4) + (31 << 1)
是不是轉成了位移?這里要注意對乘數 18 的處理,位移的關鍵是找到乘數 b 如何拆解為 2 的冪,這里要遞歸拆解,以18為例,第一次的關鍵是找到16這個2的冪,然后用18 - 16,結果等於 2,繼續找2等於多少2的冪,直到差小於2為止。同時,考慮到a*b,在實際計算中即使小的乘數也很大的情況,采用類似二分搜索的方式,給定階數,從大的階數向小的階數遞歸。階數的設定可以參考計算機的位數,即32位還是64位。因此,計算一個數可以拆分出來的最大2的冪的代碼如下:
1 int _bitShift(int d, int order) { 2 if (order == 0) return 0; 3 var i = 0; 4 for (var t = d; (t >>= order) > 0; d = t, t = d) i += order; 5 return i + _bitShift(d, order >> 1); 6 }
位移乘法實現的代碼
int bitMultiplication(int a, int b, int order) { var t = a > b ? b : a, r = 0; if (t == a) { a = b; b = t; } for (var i = _bitShift(b, order); i > 0; i = _bitShift(b, order)) { r += a << i; b -= 1 << i; } return b == 1 ? r + a : r; }
經過采用隨機大數進行多次循環測試,結果顯示,用加法實現的乘法速度最慢,最快的仍然是 普通乘法,即用乘號的乘法;位移乘法居中。看來底層的計算機指令還是對*乘法做了優化。
最后說明一下,本文純屬原創,沒有使用dart 自帶math包,尤其是在求一個數可以拆出來的最大的2的冪的數字時。否則直接用math包中的log函數即可得出結果,但這樣就起不到自己練習位運算和遞歸的目的了。