大整數相乘問題總結以及Java實現
最近在跟coursera上斯坦福大學的算法專項課,其中開篇提到了兩個整數相乘的問題,其中最簡單的方法就是模擬我們小學的整數乘法,可想而知這不是比較好的算法,這門課可以說非常棒,帶領我們不斷探索更優的算法,然後介紹可以通過使用分而治之的思想來解決這個問題。下面對該問題的方法以及實現進行介紹。
問題定義
輸入:2個n位的整數x和y
輸出:x * y
如求: 1234567891011121314151617181*2019181716151413121110987654 的結果
求解該問題要註意的是由於整數的位數可能超過基本類型的表示範圍,所以一種方式是將其轉化為字符串進行表示,另一種方式是可以使用一些語言自帶的大整數類型(如Java的BigInteger)。參考一些資料才發現,該問題的解法其實有很多種,主要列舉以下:
1.模擬小學乘法: 豎式乘法累加。
2.分治乘法: 最簡單的是Karatsuba乘法,一般化以後有Toom-Cook乘法.
3.快速傅裏葉變換
4.中國剩余定理
我們主要介紹模擬乘法累加以及使用分治思想的Karatsuba乘法,最後使用Java進行實現。
模擬小學乘法
7 8 9 6 5 2 × 3 2 1 1 ----------------- 7 8 9 6 5 2 <---- 第1趟 7 8 9 6 5 2 <---- 第2趟 .......... <---- 第n趟 ----------------- ? ? ? ? ? ? ? ? <---- 最後的值用另一個數組表示
如上所示,需要將乘數與被乘數逐位相乘,最後再進行累加,時間復雜度為{O(n^2)}.模擬乘法累加還有一個改進版,上述方法在實現時,每次計算乘法都需要考慮進位,最後在加法時也需要進位,比較麻煩。一種改進的版本如下:
9 8
× 2 1
-------------
(9)(8) <---- 第1趟: 98×1的每一位結果
(18)(16) <---- 第2趟: 98×2的每一位結果
-------------
(18)(25)(8) <---- 這裏就是相對位的和,還沒有累加進位
改進的方法先不算任何的進位,也就是說,將每一位相乘,相加的結果保存到同一個位置,到最後才計算進位。我們可以先將結果保存到一個數組中(不考慮進位),最後對數組從右向左進行遍歷,大於10進行進位。Java實現如下:
public class BigNumMul {
//simple method 模擬乘法累加
public static String bigNumberMul(String num1, String num2) {
// 分配一個空間,用來存儲運算的結果,num1長的數 * num2長的數,結果不會超過num1+num2長
int[] res = new int[num1.length() + num2.length()];
// 先不考慮進位問題,根據豎式的乘法運算,num1的第i位與num2的第j位相乘,結果應該存放在結果的第i+j位上
for (int i=0; i<num1.length(); i++) {
int a = num1.charAt(i) - ‘0‘;
for (int j=0; j<num2.length(); j++) {
int b = num2.charAt(j) - ‘0‘;
res[i+j] += a * b; //max: num1.length()+num2.length()-2
}
}
StringBuilder sb = new StringBuilder();
//單獨處理進位
int carry = 0;
//最多就到res.length-2, 最後一個元素沒有被占用,還是初始值0
for (int k=res.length-2; k >= 0; k--) {
int digit = (res[k] + carry) % 10;
carry = (res[k] + carry) / 10;
sb.insert(0, digit);
}
if (carry > 0) {
sb.insert(0, carry);
}
String str = sb.toString().replaceFirst("^0*", "");
return str.substring(0,str.length()-1);
}
}
顯然使用O(n^2)的算法是不夠好的,我們應該想一下有沒有更好的算法,就像這門課上經常說的一句:Can we do better ?
分治:Karatsuba算法
分治算法的主要思想是能將問題分解為輸入規模更小的子問題,然後遞歸求解子問題,最後將子問題的結果合並得到原問題的結果,最典型的如歸並排序算法。為了得到規模更小的子問題,就要將較大的整數拆分為位數較少的兩個整數,參考coursera上的算法專項課,主要計算過程如下:
如上圖所示,將每個數分別拆分為兩部分,分別計算ac, bd, 以及(a+b)(c+d),最後再減去前面兩個,將其組合成最終的結果。我們采用更一般的方式將其表達出來,相應計算方法如下:
上述給出了更通用的寫法,將x和y同分解後的更小的整數進行表示,最後通過遞歸的計算ac, ad, bc, bd就可以得到x*y的結果。上述是沒有優化過的分治算法,每次遞歸需要4次乘法,合並結果需要O(n)時間復雜度,所以可以得到時間復雜度的表示:
{T(n) = 4T(n/2) + O(n)}
通過主方法,可以求得上述時間復雜度為O(n^2),並沒有得到好的改善。
Karatsuba算法將上述的4次乘法優化為3次從而減少了時間復雜度。具體過程如下:
可以看到上述利用(a+b)(c+d)的結果減去ac和bd得到ad+bc的結果,從而只需要計算三次乘法,其時間復雜度可以表示為:
T(n)=3T(n/2)+6n=O(n^{log_{2}3})
根據上述算法,使用Java進行實現代碼如下:
//Karatsuba乘法
//此種情況使用long時,數過大可能出現越界,應考慮使用BigInteger
public static long karatsuba(long num1, long num2) {
//遞歸終止條件
if (num1 < 10 || num2 < 10) {
return num1 * num2;
}
// 計算拆分長度
int size1 = String.valueOf(num1).length();
int size2 = String.valueOf(num2).length();
int halfN = Math.max(size1, size2) / 2;
/* 拆分為a, b, c, d */
long a = Long.valueOf(String.valueOf(num1).substring(0, size1-halfN));
long b = Long.valueOf(String.valueOf(num1).substring(size1-halfN));
long c = Long.valueOf(String.valueOf(num2).substring(0, size2-halfN));
long d = Long.valueOf(String.valueOf(num2).substring(size2-halfN));
// 計算z2, z0, z1, 此處的乘法使用遞歸
long z1 = karatsuba(a, c);
long z2 = karatsuba(b, d);
long z3 = karatsuba((a + b), (c + d)) - z1 - z2;
return (long)(z1 * Math.pow(10, 2*halfN) + z2 + z3 * Math.pow(10, halfN));
}
註意上述遞歸的終止條件以及如何表示a, b, c, d. 上述實現使用的是Java中的long類型,但是當整數變大時,使用long類型可能會發生溢出,這裏可以使用String來模擬整數的加法及乘法,或者使用Java的BigInteger類型,其實BigInteger內部也是使用的String進行存儲,我使用的是BigInteger類型,實現代碼如下:
//使用BigInteger的karatsuba算法
//註意BigInteger的運算沒有操作符重載
//參考: coursera算法專項1
public static BigInteger karatsuba(BigInteger num1, BigInteger num2) {
if (num1.compareTo(BigInteger.valueOf(10)) < 0 || num2.compareTo(BigInteger.valueOf(10)) < 0) {
return num1.multiply(num2);
}
int n = Math.max(num1.toString().length(), num2.toString().length());
int halfN = n / 2 + n % 2; //另一種劃分方法
//返回num1 / halfN 和 num1 % halfN
BigInteger[] a_b = num1.divideAndRemainder(BigInteger.valueOf(10).pow(halfN));
BigInteger a = a_b[0];
BigInteger b = a_b[1];
BigInteger[] c_d = num2.divideAndRemainder(BigInteger.valueOf(10).pow(halfN));
BigInteger c = c_d[0];
BigInteger d = c_d[1];
BigInteger step1 = karatsuba(a, c);
BigInteger step2 = karatsuba(b, d);
BigInteger step3 = karatsuba(a.add(b), c.add(d));
BigInteger step4 = step3.subtract(step2).subtract(step1); //step3-step2-step1
BigInteger res = step1.multiply(BigInteger.valueOf(10).pow(2*halfN)).add(step2)
.add(step4.multiply(BigInteger.valueOf(10).pow(halfN)));
return res;
}
最後的測試代碼如下:
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String a = sc.next();
String b = sc.next();
// 開始計算
//String str = BigNumMul.bigNumberMul(a, b);
//long res = BigNumMul.karatsuba(Long.valueOf(a), Long.valueOf(b));
//String str = Long.toString(res);
BigInteger res = BigNumMul.karatsuba(new BigInteger(a), new BigInteger(b));
String str = res.toString();
System.out.println(a + " * " + b + " = " + str);
}
總結與感想
(1)對一個問題要深入調研和分析,多嘗試不同的解決方法。
(2)可以多分析一些諸如此類的經典問題,還是比較有意思的。
參考資料:
1.coursera算法專項
2.大數乘法問題及其高效算法
3.https://stackoverflow.com/questions/17531042/karatsuba-algorithm-without-biginteger-usage
4.https://chenyvehtung.github.io/2017/03/02/about-multiplication.html
大整數相乘問題總結以及Java實現