本節概要
高精度運算涉及高精度加法,高精度減法,高精度乘法,高精度除低精度,高精度除高精度 5類。以下的講解中,不考慮負數的情況;除法運算中,我們規定除數小於被除數;規定高精度數字的位數不超過200。
- 本節內容
- 高精度數字的輸入和存儲方法
- 高精度加法
- 高精度減法
- 高精度乘法
- 高精度除低精度
- 高精度除高精度
- 總結
1. 高精度數字的輸入和存儲方法
輸入
單個高精度數字可能長達200位,可以通過兩種方式讀入:1. 單字符讀取、判斷並存儲,直到遇到“\n”(換行符),可以使用scanf("%c", &c)
,或c=getchar()
這種方法麻煩;2. 將數字作為字符串讀入,存入字符數組中,這種方法最簡單、高效:scanf("%s", temp);
存儲
字符到數字:使用字符數組存儲容易,但計算比較麻煩,因數字在字符數組中是以字符0~9的形式存在(ASCII碼范圍:48~57),將所有數位以數字0~9的形式轉存在一個int數組中會方便運算。N=='N'-'0'
可以實現字符N到數字N的轉換。
反序存放:使用豎式做加法運算時,規則是最低位和最低位對齊,在int數組中如果將數字正序存放,對應計算位的下標就不一樣,不便於計算,因此我們將數字反序存放在數組中,就可以使得低位和低位一一對應(下標相同),方便運算,如12345+9987:
//array index 1 2 3 4 5 6 7 8 9
5 4 3 2 1
+ 7 8 9 9
-------------
2 3 3 2 2
//jw 1 1 1 1
//array index是數組下標,jw代表進位
存儲位數: 上面的例子並沒有使用數組的0號單元,這主要是為了方便運算的緣故,例如n位數字,我們從1循環至n即可,這樣思路更加清晰。而留下的0號單元剛好可以用來存儲數組的長度,長度Array[0]=strlen(temp);
代碼示例
void getNum(int num[]){ //數組初始化和高精度數字構建
char temp[500];
scanf("%s", temp); //數字作為字符串讀入
memset(num, 0, sizeof(int)*500);//將數組的所有字節置0
num[0]=strlen(temp); //將位數存於num[0]中
for(int i=1; i<=num[0]; i++) //逆序將每一位存儲於num中
num[i]=temp[num[0]-i]-'0';
return;
}
//使用方法
int main(){
int num1[500];
int num2[500];
getNum(num1);
getNum(num2);
......
return 0;
}
上面的代碼中用到了
memset()
,該函數位於cstring頭文件中,用於內存塊的賦值,在例子中起到了初始化數組的作用
參數1是內存塊的起始地址, num是數組的名字,即起始地址
參數2是初始化值,范圍是0~255,因memset是按字節為單位進行的
參數3是初始化的字節數,sizeof(int)計算出int的字節數,乘以數組大小500
這里若是沒有用
memset
進行內存塊初始化會導致運算出錯:如果參與計算的數字A和數字B位數不同,高位相加時,小數字的高位空間的值具有不確定性,如31415+12:
//array index 1 2 3 4 5 6 7 8 9 10 ...
A 5 1 4 1 3
B 2 1 ? ? ?
------------------------
C 7 2 ? ? ?
//jw 0 0 ? ? ?
可見,數組在使用前全部置0可以避免這個問題,你也可以用循環來解決,當然使用memset更加方便。
2. 高精度加法
思路
按位相加:從兩個數A、B的低位開始按位相加,一邊加一邊進位,C[i]=A[i]+B[i]+jw
,進位是上一次相加產生的。
代碼示例
void jiafa(int A[], int B[], int C[]){ //C=A+B
int i, jw; //i從1循環至max(A[0], B[0])
for(i=1, jw=0; i<=A[0] || i<=B[0]; i++){
C[i]=A[i]+B[i]+jw; //按位相加
jw=C[i]/10; //求進位
C[i]%=10; //與10求余
}
C[i]=jw; //存放最后一個進位
C[0]=C[i] ? i : i-1; //確定C的位數
return;
}
//使用方法
int main(){
int num1[500];
int num2[500];
int result[500];
getNum(num1);
getNum(num2);
memset(result, 0, sizeof(result)); //在當前函數中才可以如此計算大小
jiafa(num1, num2, result);
printNum(result); //后面提供函數代碼
return 0;
}
3. 高精度減法
思路
大小判斷:相減之前要判斷兩個數A、B的大小關系,如果A>B,則計算A-B,否則計算B-A,需要編程實現大小判斷。
按位相減:從兩個數A、B的低位開始按位相減,如果L[i]<R[i]
,就需要向高位借位,因為我們已經確定了左數字L大於右數字R,一定能借到。 if(L[i]<R[i]){ C[i]=L[i]+10-R[i]; L[i+1]--;}
確定有效位:按位減完后,高位有可能變為0,如1000-990后等於10,這時就要重新計算確定結果的有效位數C[0]
是多少。可以從位置max(L[0], R[0]
開始倒推,直至遇到非0值停止,這時的位置就是有效位數。
代碼示例
bool compare(int L[], int R[]){ //實現兩個高精度數字的大小比較
if(L[0]>R[0]) return true; //位數高的大
if(L[0]<R[0]) return false; //位數低的小
int i;
for(i=L[0]; i>=1; i--){ //位數相同逐位比較
if(L[i]>R[i]) return true; //數字大的大
if(L[i]<R[i]) return false; //數字小的小
}
return true; //能執行到這里說明相等,返回true
}
void jianfa(int L[], int R[], int C[]){ //C=L-R
for(int i=1; i<=L[0]; i++)
C[i]=L[i]; //把L的所有數位復制到C
C[0]=L[0]; //C的位數暫等於L的位數
int i;
for(i=1; i<=R[0]; i++){ //右值有多少位,就減多少次
if(C[i]<R[i]){
C[i+1]--; //向高位借1
C[i]+=10; //當前位+10
}
C[i]-=B[i]; //按位減
}
if(i<C[0]) return; //計算有效位數:L[0]-R[0]>=2
else{ //計算有效位數:L[0]-R[0]==1 or 0
while(C[i]==0 && i>=1) i--;
A[0]=(i==0) ? 1 : i; //例如10000-9999
}
return;
}
//使用方法
int main(){
int num1[500];
int num2[500];
int result[500];
getNum(num1);
getNum(num2);
memset(result, 0, sizeof(result));
if(compare(num1, num2)) jianfa(num1, num2, result);
else jianfa(num2, num1, result);
printNum(result);
return 0;
}
4. 高精度乘法
思路
類似加法,可以用豎式求乘法,同樣也有進位,對每一位進行乘法運算時,必須進行錯位相加:如856×25,見如下規律:
//array index 1 2 3 4 5 6 7 8 9 ...
A 6 5 8
B 5 2
----------------------------------
0 8 2 4
2 1 7 1
----------------------------------
0 0 4 1 2
//array index 1 2 3 4 5 6 7 8 9 ...
A a1 a2 a3 a4
B b1 b2
----------------------------------
C c1 c2 c3 c4
c2 c3 c4 c5
//本例中未寫出進位
綜上分析,乘法要進行B[0]
輪,每輪要進行A[0]
次乘法和進位運算,這就找到了循環的規律。結合錯位相加,可知C[i+j-1]=C[i+j-1]+A[i]*B[i]+jw
,進位jw=c[i+j-1]/10
有效位數:計算完成后,需要確定結果的有效位數,使用和減法類似的方法從可能的最大位數往后推,直到遇到非0的數位,當前位置就是有效位數。對於M位的數字A和N位的數字B向乘,最大位數為M+N
。
代碼示例
void chengfa(int A[], int B[], int C[]){ //C=A*B
int i,j;
for(i=1; i<=B[0]; i++){ //B[0]輪乘法
int jw; //進位
for(j=1, jw=0; j<=A[0]; j++){ //每輪按位乘A[0]次
C[i+j-1]=C[i+j-1]+A[j]*B[i]+jw; //錯位相加
jw=C[i+j-1]/10; //求進位
C[i+j-1]%=10; //進位后,與10求余
}
C[i+A[0]]=jw; //存儲最后一個進位
}
int len=A[0]+B[0]; //最大可能位數
while(C[len]==0 && len>1) len--; //確定有效位數
C[0]=len;
return;
}
//使用方法
int main(){
...
chengfa(num1, num2, result);
printNum(result);
return 0;
}
5. 高精度除低精度
思路
高精度初低精度采用豎式思想,由於除數是低精度,最后的商可能是高精度,余數一定為低精度。
商有效位數:M位的數字A除以N位的數字B,商的位數最大為M-N+1
,因為B是低精度數字,計算長度N比較麻煩,簡單起見,也可以認為商的最大位數為M
, 從最大數位往后推,直到遇到非0的數位,當前位置就是有效位數。
以下是一個模擬計算過程的例子,113056/23:
//
被除數數位 當前位數字 過程被除數 過程除結果
i=6 1 1/23 商:0
i=5 1 11/23 商:0
i=4 3 113/23 商:4,余:21
i=3 0 210/23 商:9,余:3
i=2 5 35/23 商:1,余:12
i=1 6 126/23 商:5,余:11
//計算結果 商:4915, 余數:11
代碼示例
void chuDi(int A[], int B, int C[], int &yushu){ //A/B=C 余數:yushu
int i, t; //t為過程被除數
for(i=A[0], t=0; i>=1; i--){ //從被除數高位循環到低位
t=t*10+A[i]; //更新t
C[i]=t/B; //計算C[i]
t%=B; //更新t
}
yushu=t; //計算完后t就是余數
i=A[0]; //計算C有效位數
while(C[i]==0 && i>1) i--;
C[0]=i;
return;
}
//使用方法
int main(){
int num1[500];
int num2;
int shang[500];
int yushu;
getNum(num1);
scanf("%d", num2);
chuDi(num1, num2, shang, yushu);
printNum(shang);
count<<" "<<yushu;
return 0;
}
6. 高精度除高精度
思路
高精度除高精度是這幾種運算中最難的一種。仍然可以模仿豎式的方式進行,過程有點兒麻煩。另一種辦法是利用減法:被除數A-除數B,看看能減多少個,直到A<B
,這時剪去的個數就是商,剩下的A就是余數。如果利用前面編寫好的高精度減法jianfa()
和compare()
的話,實現起來豈不是很容易?可惜不是這樣,假設被除數A的位數M與除數B位數N之差M-N
很大,這個范圍有可能從0~200,按照最大的情況200考慮,這意味着商值也是高精度數字,而你要減10^200次才能減完,這是一個天文數字。
所以,明白否?解決這個問題也很簡單,我們不一個一個減,而是按照最大可能的數量級減,例如:12345/45:
//商的最大位數i=M-N+1,即4,設計一個 臨時減數,減數后面補齊i-1個0,再進行減法
i=4 12345 < 45000 可以減0個 shang[4]=0 減后A:12345
i=3 12345 < 4500 可以減2個 shang[3]=2 減后A:3345
i=2 3345 < 450 可以減7個 shang[2]=7 減后A:195
i=1 195 < 45 可以減4個 shang[1]=4 減后A:15
//因shang[4]=0,故商的有效位數shang[0]--,為3
//結果 商為274,余數15
看明白了嗎,這樣的計算過程,僅僅減了2+7+4=13
次,而非274
次,可見一個一個減是絕對不靠譜的。下面提供了代碼,注意:減法函數jianfa()
被修改了,使得直接把結果存入A中,而不需要存在另一個數組C中,這是為了簡化后面運算的緣故。
代碼示例
void jianfa(int A[], int B[]){ //修改了的減法,使得直接在A數組上減
int i; //不再需要存在數組C,簡化后面的運算
for(i=1; i<=B[0]; i++){
if(A[i]<B[i]){
A[i+1]--; //向高位借1
A[i]+=10; //當前位+10
}
A[i]-=B[i]; //按位減
}
if(i<A[0]) return; //計算有效位數
else{
while(A[i]==0 && i>=1) i--;
A[0]=(i==0)? 1 : i;
}
return;
}
//shang=A/B, yushu為余數
bool chuGao(int A[], int B[], int shang[], int yushu[]){
memset(shang, 0, sizeof(int)*300); //初始化shang
memset(yushu, 0, sizeof(int)*300); //初始化yushu
shang[0]=A[0]-B[0]+1; //商的最大數位
for(int i=shang[0]; i>=1; i--){ //從高位到低位開始計算商
int jianshu[300]; //構造要減的減數
memset(jianshu, 0, sizeof(jianshu));
//這個函數下面有解釋
memcpy(jianshu+i, B+1, sizeof(int)*B[0]);
jianshu[0]=B[0]+i-1; //確定減數的位數
while(compare(A, jianshu)){ //通過循環減
jian(A, jianshu);
shang[i]++; //減去一個商的對應為+1
}
}
if(shang[shang[0]]==0) shang[0]--; //判斷商的最高位是否有效
memcpy(yushu, A, sizeof(int)*300); //A就是余數,把它完全復制給yushu
}
//使用方法
int main(){
int num1[300]; //數組A存儲第1個數字信息
int num2[300]; //數組B存儲第2個數字信息
int shang[300];
int yushu[300];
init(num1);
init(num2);
chuGao(num1, num2, shang, yushu);
print(shang);
printf(" ");
print(yushu);
return 0;
}
上面的代碼中用到了memcpy(),該函數位於cstring頭文件中,用於內存塊之間的復制
參數1是destination,即復制的目的地,為一地址
參數2是source,即復制數據的來源,為一地址
參數3是要復制的字節數,sizeof(int)計算出int的字節數,乘以相應的元素個數N例程中的
memcpy(jianshu+i, B+1, sizeof(int)*B[0])
是從B數組的1號位置開始(跳過B[0]),復制B[0]
個數位,目的地為jianshu
數組的第jianshu+1+i-1
個位置,+1
是為了跳過jianshu[0]
,+i-1
是把這幾個位置空成0,以構造最大數量級的減數。暈了么有?別怕,其實可以把代碼寫的長一些,但是更簡單一些的,這里是偷懶了;-)
7. 總結
用途
高精度運算在信息學奧賽計算中常常出現,因此必須掌握。例如計算大型斐波那契數列、計算高階次冪、計算N!
等,如果不使用高精度進行計算,內存溢出是必然結果。
最后提供print()函數,十分簡單,你應該可以自己寫出來:-)
代碼示例
void print(int X[]){ //輸出高精度數字
for(int i=X[0]; i>=1; i--) printf("%d", X[i]);
return;
}