課程設(shè)計題目:BigInteger(億進制高精度)類設(shè)計實驗
一株茶、問題描述
C/C++ 語言中的 int 類型能表示的整數(shù)范圍是~,unsigned int 類型能表示的整數(shù)范圍是 ~滞诺,即 ~吨悍,所以胚想,int 和 unsigned int 類型都不能存儲超過10位的整數(shù)尼啡。有些問題需要處理的整數(shù)遠遠不止10位暂衡,這種大整數(shù)用C/C++語言的基本數(shù)據(jù)類型無法直接表示。請編寫算法完成兩個大整數(shù)的加崖瞭、減狂巢、乘和除等基本的代數(shù)運算。
二书聚、基本要求
大整數(shù)的長度在100位以下隧膘;- 設(shè)計存儲結(jié)構(gòu)表示大整數(shù);
- 設(shè)計算法實現(xiàn)兩個大整數(shù)的加寺惫、減、乘和除等基本的代數(shù)運算蹦疑;
- 分析算法的時間復雜度和空間復雜度西雀。
三、概要設(shè)計
1. 數(shù)據(jù)結(jié)構(gòu)的設(shè)計
采取符號和數(shù)位分離歉摧,以便模擬手算艇肴。
存儲結(jié)構(gòu)我們使用 C++ 自帶的 vector
腔呜,這樣既在能隨機存取的情況下,又能實現(xiàn)按需分配空間再悼。
存儲數(shù)位時核畴,我們每8位存放在一個 int 里(億進制高精度)。較之每1位存一個 int 的寫法(十進制高精度)冲九,雖然不改變時間復雜度和空間復雜度谤草,但通過壓位能降低常數(shù)(相當于 n 變成 n/8,n 為 BigInteger 的位數(shù))莺奸,實際運行表現(xiàn)有較大改善丑孩。
2. 算法的設(shè)計
關(guān)于 BigInteger 類實現(xiàn)的各種算法,多為模擬手算過程灭贷,在此就以較復雜的除法(BigInteger 除 BigInteger)為例温学,其余的算法就只給出時間復雜度和空間復雜度,實現(xiàn)方法便不一一贅述了甚疟。
偽代碼如下
BigInteger operator/(BigInteger a, BigInteger b) {
if (b == 0) throw "被零除";
c.sign = a.sign * b.sign;
a.sign = b.sign = 1; // 這樣只用考慮整數(shù)除整數(shù)
for (int i = a.s.size() - b.s.size(); i >= 0; i--) {
// 模擬手算的豎式除法仗岖,這里用二分求要填的數(shù)
int l = 0, r = BigInteger::BASE - 1;
int mid = (l + r + 1) >> 1;
for (; l < r; mid = (l + r + 1) >> 1) {
d = b * mid 再移位(乘以10^?);
// 更新 l 或 r
if (a > d) l = mid;
else r = mid - 1;
}
a = a - b * l 再移位(乘以10^?);
c.s[i] = l; // 豎式除法中的填上數(shù)步驟
}
return c;
}
記為大整數(shù)的位數(shù)(十進制),為進制览妖,為 BigInteger 中一位對應(yīng)十進制的寬度轧拄。在億進制高精度的情況下,這里黄痪。
我們分析整個除法計算次數(shù)最多的步驟紧帕。類似手算除法的豎式除法,商的位數(shù)最多是除數(shù)位數(shù)與被除數(shù)的位數(shù)之差桅打,又因為采用億進制是嗜,故商的位數(shù)與同階。每一位我們都要試商挺尾,若用二分法試商鹅搪,則試商次數(shù)為。商試過程中遭铺,我們需要到道高精度乘低精度和高精度比較大小丽柿,時間復雜度為。故時間復雜度為魂挂,忽略常數(shù)得甫题。
整個算法中,需要幾個臨時的 BigInteger 變量涂召,故空間復雜度為坠非。
時間復雜度 | 空間復雜度 |
---|---|
其余的基本代數(shù)運算:
加、減
時間復雜度 | 空間復雜度 |
---|---|
乘果正、取模
時間復雜度 | 空間復雜度 |
---|---|
比較運算符
時間復雜度 | 空間復雜度 |
---|---|
3. 抽象數(shù)據(jù)類型的設(shè)計
ADT BigInteger
Operation
構(gòu)造函數(shù)
前置條件:BigInteger 不存在
輸入:一個 long long 或正確的 string
功能:構(gòu)造相應(yīng)的 BigInteger
輸出:無
后置條件:一個相應(yīng)的 BigInteger
析構(gòu)函數(shù)
前置條件:一個已經(jīng)存在的 BigInteger
輸入:無
功能:銷毀該 BigInteger
輸出:無
后置條件:釋放該 BigInteger 所占用的存儲空間
+ - *
前置條件:無
輸入:兩個已經(jīng)存在的 BigInteger
功能:實現(xiàn)兩個 BigInteger 的加炎码、減或乘運算
輸出:返回運算結(jié)果(BigInteger)
后置條件:兩個 BigInteger 不變
/ %
前置條件:無
輸入:兩個已經(jīng)存在的 BigInteger
功能:實現(xiàn)兩個 BigInteger 的整除或取模運算
輸出:若除數(shù)非零盟迟,返回運算結(jié)果(BigInteger),否則拋出異常
后置條件:兩個 BigInteger 不變
< > == <= >=
前置條件:無
輸入:兩個已經(jīng)存在的 BigInteger
功能:實現(xiàn)兩個 BigInteger 的比較大小
輸出:返回運算結(jié)果(bool)
后置條件:兩個 BigInteger 不變
+= -= *= /= %=
前置條件:一個已經(jīng)存在的 BigInteger
輸入:另一個已經(jīng)存在的 BigInteger
功能:實現(xiàn)類似于 C++ int 的五個賦值運算符
輸出:返回運算結(jié)果(BigInteger)
后置條件:類似于 C++ int 的結(jié)果
<<
前置條件:無
輸入:一個已經(jīng)存在的 BigInteger
功能:輸出該 BigInteger
輸出:返回運算結(jié)果(ostream)
后置條件:該 BigInteger 不變
>>
前置條件:有輸入流
輸入:一個已經(jīng)存在的 BigInteger
功能:從輸入流中提取大整數(shù)到該 BigInteger 中
輸出:返回運算結(jié)果(istream)
后置條件:該 BigInteger 讀入了大整數(shù)
endADT
四潦闲、詳細設(shè)計
1. 設(shè)計抽象數(shù)據(jù)類型對應(yīng)的C++類定義
為避免文章過于冗雜攒菠,請參見后文五、運行與測試 -> 4. 程序清單與運行結(jié)果歉闰。
2. 設(shè)計每個成員函數(shù)
成員函數(shù)除了前文三辖众、概要設(shè)計 -> 3. 抽象數(shù)據(jù)類型的設(shè)計中所列出的借口,還有部分輔助函數(shù)新娜,例如void __defragment()
用于實現(xiàn)整理 BigInteger赵辕,BigInteger __BigInteger_shift(const BigInteger &b, const int &x)
用于實現(xiàn)除法過程中需要的移位。它們的實現(xiàn)并不復雜概龄,為避免文章過于冗雜还惠,請參見后文五、運行與測試 -> 4. 程序清單與運行結(jié)果私杜。
3. 設(shè)計主函數(shù)
主函數(shù)主要包括調(diào)用構(gòu)造函數(shù)蚕键,加減乘除取模等運算進行測試。
五衰粹、運行與測試
1. 測試環(huán)境
運行環(huán)境:Windows 20H2, i7-9750H @ 2.60GHz, 16GB RAM
編譯器:gcc version 8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project)
編譯命令:-g
運行終端:cmd
2. 在調(diào)試程序的過程中遇到的問題與解決方案
問題1:執(zhí)行完sprintf
后變量i
莫名其妙改變了
解決方案:經(jīng)過仔細檢查锣光,我排除了自己編程出錯的情況,于是判斷為編譯器異常铝耻。后重裝編譯器誊爹,現(xiàn)問題已解決。
問題2:對拍時與 Python 計算結(jié)果有差異
解決方案:出現(xiàn)問題的原因是瓢捉,Python 中的整除是向下取整频丘,而 C++ 一般是向零取整,所以當遇到負數(shù)的情況下泡态,兩種取整方式計算出的商和余數(shù)可能不同搂漠。后在和 Python 的對拍中,只考慮自然數(shù)大整數(shù)運算某弦,現(xiàn)問題已解決桐汤。
問題3:對拍中 Python 運行速度遠遠快于 C++
解決方案:原來是對拍程序里的有個計時器忘記累加時間了,改正后現(xiàn)問題已解決靶壮。
3. 設(shè)計的測試數(shù)據(jù)與測試結(jié)果
測試1為設(shè)計部分極端樣例怔毛,觀察程序是否符合預期。
輸入樣例1
12
-5
輸出樣例1
7
17
-60
-2
2
輸入樣例2
-12
5
輸出樣例2
-7
-17
-60
-2
-2
輸入樣例3
-12
-5
輸出樣例3
-17
-7
60
2
2
-2
輸入樣例4
12345678901234567890
0
輸出樣例4
12345678901234567890
12345678901234567890
0
然后在除零時拋出異常腾降。
有先導0的情況會在測試2中測試馆截。
測試2為用隨機數(shù)生成100位左右的大整數(shù),和標程對拍,對拍10000次蜡娶。
4. 程序清單及運行結(jié)果
程序清單如下
// main.cpp
#include <iostream>
#include "BigInteger.h"
using namespace std;
int main()
{
// BigInteger y;
// BigInteger x = y;
// BigInteger z = -12356789012345678ll;
// cout << z;
BigInteger a, b;
cin >> a >> b;
cout << a + b << "\n";
cout << a - b << "\n";
cout << a * b << "\n";
cout << a / b << "\n";
cout << a % b << "\n";
return 0;
}
// BigInteger.h
#ifndef BIGINTEGER_H
#define BIGINTEGER_H 1
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class BigInteger
{
public:
BigInteger(long long num = 0);
BigInteger(const string &str);
~BigInteger();
BigInteger operator=(const long long num);
BigInteger operator=(const string &str);
BigInteger operator+() const;
BigInteger operator-() const;
friend BigInteger operator+(const BigInteger &a, const BigInteger &b);
friend BigInteger operator-(const BigInteger &a, const BigInteger &b);
friend BigInteger operator*(const BigInteger &a, const BigInteger &b);
friend BigInteger operator/(const BigInteger &a, const BigInteger &b);
friend BigInteger operator%(const BigInteger &a, const BigInteger &b);
BigInteger operator+=(const BigInteger&b);
BigInteger operator-=(const BigInteger&b);
BigInteger operator*=(const BigInteger&b);
BigInteger operator/=(const BigInteger&b);
BigInteger operator%=(const BigInteger&b);
friend bool operator<(const BigInteger &a, const BigInteger &b);
friend bool operator>(const BigInteger &a, const BigInteger &b);
friend bool operator==(const BigInteger &a, const BigInteger &b);
friend bool operator<=(const BigInteger &a, const BigInteger &b);
friend bool operator>=(const BigInteger &a, const BigInteger &b);
friend ostream &operator<<(ostream &out, const BigInteger &x);
friend istream &operator>>(istream &in, BigInteger &x);
private:
vector<int> s;
int sign = 1;
static const int BASE = 100000000;
static const int WIDTH = 8;
void __defragment();
friend BigInteger __BigInteger_shift(const BigInteger &b, const int &x);
};
#endif /* BIGINTEGER_H */
#ifndef BIGINTEGER_H
#define BIGINTEGER_H 1
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class BigInteger
{
public:
BigInteger(long long num = 0);
BigInteger(const string &str);
~BigInteger();
BigInteger operator=(const long long num);
BigInteger operator=(const string &str);
BigInteger operator+() const;
BigInteger operator-() const;
friend BigInteger operator+(const BigInteger &a, const BigInteger &b);
friend BigInteger operator-(const BigInteger &a, const BigInteger &b);
friend BigInteger operator*(const BigInteger &a, const BigInteger &b);
friend BigInteger operator/(const BigInteger &a, const BigInteger &b);
friend BigInteger operator%(const BigInteger &a, const BigInteger &b);
BigInteger operator+=(const BigInteger&b);
BigInteger operator-=(const BigInteger&b);
BigInteger operator*=(const BigInteger&b);
BigInteger operator/=(const BigInteger&b);
BigInteger operator%=(const BigInteger&b);
friend bool operator<(const BigInteger &a, const BigInteger &b);
friend bool operator>(const BigInteger &a, const BigInteger &b);
friend bool operator==(const BigInteger &a, const BigInteger &b);
friend bool operator<=(const BigInteger &a, const BigInteger &b);
friend bool operator>=(const BigInteger &a, const BigInteger &b);
friend ostream &operator<<(ostream &out, const BigInteger &x);
friend istream &operator>>(istream &in, BigInteger &x);
private:
vector<int> s;
int sign = 1;
static const int BASE = 100000000;
static const int WIDTH = 8;
void __defragment();
friend BigInteger __BigInteger_shift(const BigInteger &b, const int &x);
};
#endif /* BIGINTEGER_H */
測試1
樣例1(符合預期)
D:\OneDrive - mail2.sysu.edu.cn\MyDocuments\code\DSA\week04\BigInteger>BigInteger.exe
12
-5
7
17
-60
-2
2
樣例2(符合預期)
D:\OneDrive - mail2.sysu.edu.cn\MyDocuments\code\DSA\week04\BigInteger>BigInteger.exe
-12
5
-7
-17
-60
-2
-2
樣例3(符合預期)
D:\OneDrive - mail2.sysu.edu.cn\MyDocuments\code\DSA\week04\BigInteger>BigInteger.exe
-12
-5
-17
-7
60
2
-2
樣例4(符合預期)
D:\OneDrive - mail2.sysu.edu.cn\MyDocuments\code\DSA\week04\BigInteger>BigInteger.exe
12345678901234567890
0
12345678901234567890
12345678901234567890
0
terminate called after throwing an instance of 'char const*'
測試2(符合預期)
和 Python 對拍
Python 代碼
# BigInteger.py
a = int(input())
b = int(input())
print(a + b)
print(a - b)
print(a * b)
print(a // b)
print(a % b)
對拍程序(.cpp)
// 對拍.cpp
#include <ctime>
#include <iostream>
using namespace std;
const int _T = 1e4;
int main()
{
clock_t mycpp_start_time, mycpp_end_time;
double mycpp_total_time;
clock_t ans_py_start_time, ans_py_end_time;
double ans_py_total_time;
int TTT = _T;
while (TTT--)
{
cout << "Case: " << _T - TTT << "\n";
system("rand.exe > BigInteger.in");
ans_py_start_time = clock();
system("python BigInteger.py < BigInteger.in > BigInteger.ans");
ans_py_end_time = clock();
ans_py_total_time += (double)(ans_py_end_time - ans_py_start_time) / CLOCKS_PER_SEC;
mycpp_start_time = clock();
system("BigInteger.exe < BigInteger.in > BigInteger.out");
mycpp_end_time = clock();
mycpp_total_time += (double)(mycpp_end_time - mycpp_start_time) / CLOCKS_PER_SEC;
if (system("fc BigInteger.ans BigInteger.out"))
{
system("pause");
break;
}
}
if (TTT == -1)
{
cout << "Complete.\n";
cout << "Run times: " << _T << "\n";
cout << "Ans py total time: " << ans_py_total_time << "s\n";
cout << "My cpp total time: " << mycpp_total_time << "s\n";
}
else
cout << "error" << endl;
system("pause");
return 0;
}
隨機數(shù)據(jù)生成程序(.cpp)(不考慮負數(shù),原因見五映穗、運行與測試 -> 2. 在調(diào)試程序的過程中遇到的問題與解決方案)
// rand.cpp
#include <cstdio>
#include <cstdlib>
#include <ctime>
using namespace std;
inline void work(int x)
{
// if (rand() & 1)
// printf("-");
while (rand() & 15)
printf("0");
while (x--)
printf("%d", (rand() << 15) + rand());
printf("\n");
}
int main()
{
srand(time(NULL));
rand(), rand(), rand();
work(12);
work(10);
return 0;
}
對拍結(jié)果如下
D:\OneDrive - mail2.sysu.edu.cn\MyDocuments\code\DSA\week04\BigInteger>對拍.exe
(中間輸出過長已省略)
Case: 9999
Comparing files BigInteger.ans and BIGINTEGER.OUT
FC: no differences encounteredCase: 10000
Comparing files BigInteger.ans and BIGINTEGER.OUT
FC: no differences encounteredComplete.
Run times: 10000
Ans py total time: 357.602s
My cpp total time: 173.728s
Press any key to continue . . .
像這類的程序窖张,C++ 比 Python 運行速度更快但代碼實現(xiàn)更難,這是符合預期的蚁滋。
和 cpp 標程對拍
cpp標程來自技能書的博客高精度壓位(億進制)模板宿接,網(wǎng)址https://blog.csdn.net/long_hen/article/details/105023965。不過他取模的兩個數(shù)均為負的情況符號寫錯了辕录,我?guī)退倪^來了睦霎。
代碼過長,在此便不展示了走诞。
對拍程序大體同上副女,只修改了部分名稱。
隨機數(shù)據(jù)生成程序同上蚣旱,但刪去注釋碑幅,即考慮有負數(shù)的情況。
對拍結(jié)果如下
D:\OneDrive - mail2.sysu.edu.cn\MyDocuments\code\DSA\week04\BigInteger>對拍.exe
(中間輸出過長已省略)
Case: 9999
Comparing files BigInteger.ans and BIGINTEGER.OUT
FC: no differences encounteredCase: 10000
Comparing files BigInteger.ans and BIGINTEGER.OUT
FC: no differences encounteredComplete.
Run times: 10000
Ans cpp total time: 136.167s
My cpp total time: 173.101s
Press any key to continue . . .
考慮到我用的是vector
塞绿,而標程用的是數(shù)組沟涨,這個結(jié)果可以接受。
六异吻、總結(jié)與心得
C++ 相較于其他編程語言的高性能在本例中體現(xiàn)得淋漓盡致裹赴。
高精度不愧為最能鍛煉代碼能力的模板之一,原理簡單但實現(xiàn)難诀浪,同時還得考慮很多特殊情況棋返。寫一遍高精度,受益匪淺笋妥。
實際上懊昨,上述部分寫法有些部分為了代碼實現(xiàn)方便而小幅度犧牲了性能,例如 BigInteger 的構(gòu)造函數(shù)可以直接寫而不是調(diào)用
operator =
春宣。如需獲得更好的性能表現(xiàn)酵颁,這些部分可以改善,但會使代碼更加冗雜月帝。如果采用FFT(快速傅里葉變換)和NTT(快速數(shù)論變換)算法躏惋,可以將乘法的時間復雜度優(yōu)化到。再利用牛頓迭代嚷辅,可以將除法的時間復雜度優(yōu)化到簿姨,進而優(yōu)化取模。不過可惜的是,本人實力有限扁位。
筆者以前沒有敲過高精度除高精度准潭,也沒敲過億進制高精度。以前總想著代碼實現(xiàn)很難很恐怖域仇,但敲完這次后感覺也沒有那么繁瑣刑然。人有時候真的只是被自己嚇倒了。不禁讓人想起一句曾經(jīng)的網(wǎng)絡(luò)流行語:
消除恐懼的最好辦法就是面對恐懼暇务。加油泼掠,奧利給!
七垦细、參考資料
- 劉汝佳. 算法競賽入門經(jīng)典. 清華大學出版社, 2009.