目录
大整数加法
回想一下小学计算多位数加法的做法,靠右对齐,个位对个位,十位对十位,从右往左逐位相加,逢十进一,这里也是用同样的方法。
设两个大整数分别为 A 和 B,相加结果为 Res,进位为 carry,有
但在 C 的实现上还需要考虑几个问题。
- 大整数是用字符串并且以大端形式存储的(低地址放高位),位数不一致怎么对齐?
- 最高位产生进位怎么处理?例如 23 + 80 = 103,23 + 77 = 100
- 操作数中有负数时的处理办法
第一个问题很好解决,补 0 即可。例如计算 12345 + 1 对 1 补 0,转化为 12345 + 00001。
第二个问题,设置一个进位位,把 Res[0] 作为进位位,从右往左开始逐位计算,到最高位时,判断如果产生了进位,Res[0] 置为 1,否则将 Res 整体往左移一位。
第三个问题,这里用了最简单的分类讨论的办法:
例如计算 a + b
- 如果 a 为正数,b 为负数,调用减法,计算 a - |b|,反之计算 b-|a|;
- 如果 a,b 都为负数,计算 | a|+|b|,最后在结果前添加符号;
(这里绝对值的实现实际上就是去除负号,把大整数整体左移一位即可)
实现源码
//取出负号
void deleteSign(char *a) {
int len = strlen(a);
for(int i = 0;i < len;i++) {
a[i] = a[i + 1];
}
}
//添加负号
void addSign(char *a) {
int len = strlen(a);
for(int i = len;i > 0;i--) {
a[i] = a[i - 1];
}
a[len + 1] = '\0';
a[0] = '-';
}
//操作数对齐
void formatNumber(char *a, char *b) {
int a_len = strlen(a);
int b_len = strlen(b);
if(a_len == b_len) return;
int Is_a_longer = a_len > b_len ? 1 : 0;
char *shortNum = Is_a_longer ? b : a;
int difflen = Is_a_longer ? (a_len - b_len) : (b_len - a_len);
char zero[SIZE];
for(int i = 0;i < difflen;i++) zero[i] = '0';
*(zero + difflen) = '\0';
strcat(zero, shortNum);
strcpy(shortNum, zero);
}
//加法
void add(char *a, char *b,char *res) {
int flag = 0;
// 处理符号位
if(a[0] == '-' && b[0] != '-') {
// 去除符号位
deleteSign(a);
// 让ab保持对齐
formatNumber(a, b);
sub(b, a, res);
return;
}
else if(a[0] != '-' && b[0] == '-') {
deleteSign(b);
formatNumber(a, b);
sub(a, b, res);
return;
}
else if(a[0] == '-' && b[0] == '-') {
deleteSign(a);
deleteSign(b);
formatNumber(a, b);
flag = 1;
}
else {
formatNumber(a, b);
}
int len = strlen(a);
res += 1;
int carry = 0;
int i = len-1;
res[len] = '\0';
for(; i >= 0; i--) {
int digitSum = a[i] - '0' + b[i] - '0' + carry;
res[i] = digitSum % 10 + '0';
carry = (digitSum >= 10) ? 1 : 0;
}
res -= 1;
// 如果最高位相加产生进位,res[0]置为1
if(carry == 1) {
res[0] = '1';
}
else {
for(int j = 0;j <= len ;j++) res[j] = res[j+1];
}
// 如果两操作数都为负数,在结果前添加负号
if(flag) addSign(res);
}
大整数减法
与加法中进位不同的是,减法中需要考虑的是进位。
设两大整数为 A 和 B,计算 A - B,来自高位的借位为 borrow1,从低位的借位的为 borrow2,减法计算公式为
在 C 语言实现上还需考虑几个问题:
- 计算 A-B 时,A < B 时如何处理?
- 需要去除结果前面的 0。例如 1111-1110 = 0001,需要把 1 前面的 0 去掉后输出。
- 两操作数中有负数的处理办法。
解决方法如下:
- 如果 A <B 时,将 Res[i] 置为符号‘-’,从 Res[1] 开始存储 B-A 的结果。
- 用指针扫描结果第一个不为 0 的位置,从这个位置开始作为最终输出的结果
- 同样采用了分类讨论的办法:
例如计算 a-b
如果 a > 0,b <0,计算 a + |b|,反之计算 -(|a|+|b|);
如果 a < 0,b < 0,计算 | b| - |a|;
实现源码
void sub(char *a, char *b, char *res) {
// 符号处理
if(a[0] == '-' && b[0] != '-') {
deleteSign(a);
formatNumber(a, b);
add(a, b, res);
addSign(res);
return;
}
else if(a[0] != '-' && b[0] == '-') {
deleteSign(b);
formatNumber(a, b);
add(a, b, res);
return;
}
else if(a[0] == '-' && b[0] == '-') {
deleteSign(a);
deleteSign(b);
formatNumber(a, b);
}
else {
formatNumber(a, b);
}
// a > b 返回1,a = b 返回0,a < b 返回-1
int cmpVal = strcmp(a, b);
if(!cmpVal) {
res[0] = '0';
res[1] = '\0';
return;
}
int len = strlen(a);
int is_a_larger = 0;
if(cmpVal > 0) is_a_larger = 1;
// 被减数置为较大的数
char *minuend = is_a_larger ? a : b;
char *subtrahend = is_a_larger ? b : a;
// 给结果添加符号
if(!is_a_larger) {
res[0] = '-';
res += 1;
}
int borrow = 0;
res[len] = '\0';
for(int i = len-1; i >= 0; i--)
{
int digitDif = minuend[i] - subtrahend[i] - borrow;
borrow = (digitDif < 0) ? 1 : 0;
res[i] = digitDif + borrow*10 + '0';
}
// 去掉结果前的0,例如将0078换为78
int Src = 0, Dst = 0;
while(res[Src] =='0') {// 扫描到第一个不为0的位置
Src++;
}
while(res[Src] != '\0') {// 从Src的位置开始拷贝
res[Dst++] = res[Src++];
}
res[Dst] = '\0';
}
大整数乘法
- 操作数对齐
- 乘法计算公式(分治法的关键)
操作数对齐就是之前提到的补 0,使两操作数位数相同并向右靠齐。
乘法计算公式当然不是指 99 乘法表或者逐位相乘,而是方便使用分治法的公式。这里直接给结论:AB * CD = A*C(A*D + B*C)B*D
例如计算 1234 * 5678,取一个最小规模 2,当相乘的两个数如果位数小于等于 2,直接调用系统乘法,返回结果。(递归出口)
将两操作数折半,1234 分为 12 和 34,5678 分为 56 和 78,结果由以下三个部分拼接而成:
高位:A*C = 12 * 56 = 672
中位:(A*D + B*C) = 12*78 + 34*56 = 2840
低位:B*D = 34* 78 = 2652
每一部分从右往左算起超过折半长度大小的部分作为进位,例如低位向中位的进位为 26,中位部分实际为 2840 + 26 = 2866,向高位的进位为 28,所以高位为 700。最终结果为:
下面给出递归算法:
这里为了简化,就不递归到两位数相乘了,4 位数相乘,计算机还是能够得到精确值的):
1. 如果两个整数 M 和 N 的长度都小于等于 4 位数,则直接返回 M*N 的结果的字符串形式。
2. 如果如果 M、N 长度不一致,补齐 M 高位 0(不妨设 N>M),使都为 N 位整数。
3. N/2 取整,得到整数的分割位数。将 M、N 拆分成 m1、m2,n1,n2。
4. 将 m1、n1;m2、n1;m1、n2;m2、n2 递归调用第 1 步,分别得到结果 AC(高位)、BC(中位)、AD(中位)、BD(低位)。
5. 判断 BD 位是否有进位 bd,并截取 bd 得到保留位 BD’;判断 BC+AD+bd 是否有进位 abcd,并截取进位得到保留位 ABCD’;判断 AC+abcd 是否有进位 ac,并截取进位得到保留位 AC’。
6. 返回最终大整数相乘的结果:ac AC’ABCD’ BD’。
时间复杂度优化
到上面为止,其实时间复杂度仍为 O( n2 n 2 ),但上面公式
实现源码
// 返回子串
char* substring(char *a, int begin ,int end){
int len = end - begin + 1;
char *str = (char *)malloc(sizeof(char) * (len + 1));
int i = 0;
for(int j = begin;j <= end;i++, j++) str[i] = a[j];
str[i] = '\0';
return str;
}
// 返回进位部分
char* getcarry(char* a, int len1) {
int len = strlen(a);
if(len > len1) {
int difflen = len - len1;
char *carry = (char*)malloc(sizeof(char) * (SIZE));
int i = 0;
for(;i < difflen;i++) {
carry[i] = a[i];
}
carry[i] = '\0';
i = 0;
for(int j = difflen; i < len1;j++, i++) a[i] = a[j];
a[i] = '\0';
return carry;
}
else{
char zero[SIZE];
int difflen = len1 - len;
for(int i = 0;i < difflen;i++) zero[i] = '0';
zero[difflen] = '\0';
strcat(zero, a);
strcpy(a, zero);
return NULL;
}
}
// 乘法,符号位处理
char* mul(char *a, char *b) {
char *mulRes;
if((a[0] != '-' && b[0] != '-') || (a[0] == '-' && b[0] == '-')) {
if(a[0] == '-') {
deleteSign(a);
deleteSign(b);
}
formatNumber(a, b);
mulRes = Multi(a, b);
}
else {
a[0] == '-'? deleteSign(a):deleteSign(b);
formatNumber(a, b);
mulRes = Multi(a, b);
addSign(mulRes);
}
return mulRes;
}
char* Multi(char *a, char *b) {
char *res = (char *)malloc(sizeof(char) * SIZE);
int len = strlen(a);
if(len <= minMUL) {
itoa(atoi(a)*atoi(b), res, 10);
return res;
}
int len1 = len / 2;
int len2 = len - len1;
char *A = substring(a, 0, len1-1);
char *B = substring(a, len1, len-1);
char *C = substring(b, 0, len1-1);
char *D = substring(b, len1, len-1);
int lenM = len1 > len2 ? len1 : len2;
char *AC = mul(A, C);
char *BD = mul(B, D);
char ADBC[SIZE], AB[SIZE], DC[SIZE];
sub(A, B, AB);
sub(D, C, DC);
char *ABDC = mul(AB, DC);
add(AC, BD, ADBC);
add(ADBC, ABDC, ADBC);
char *cBD = getcarry(BD, len2);
if(cBD) {
add(cBD, ADBC, ADBC);
}
char *cADBC = getcarry(ADBC, lenM);
if(cADBC) {
add(AC, cADBC, AC);
}
strcat(ADBC, BD);
strcat(AC, ADBC);
strcpy(res, AC);
//去掉结果前的0,例如将0078换为78
int Src = 0, Dst = 0;
while(res[Src] =='0') {//扫描到第一个不为0的位置
Src++;
}
while(res[Src] != '\0') {//从Src的位置开始拷贝
res[Dst++] = res[Src++];
}
res[Dst] = '\0';
return res;
}