【分治法】【C 语言】大整数加减乘

目录

大整数加法

回想一下小学计算多位数加法的做法,靠右对齐,个位对个位,十位对十位,从右往左逐位相加,逢十进一,这里也是用同样的方法。

设两个大整数分别为 A 和 B,相加结果为 Res,进位为 carry,有

Res[i]=(A[i]+B[i]+carry) % 10 R e s [ i ] = ( A [ i ] + B [ i ] + c a r r y )   %   10

但在 C 的实现上还需要考虑几个问题。

  1. 大整数是用字符串并且以大端形式存储的(低地址放高位),位数不一致怎么对齐?
  2. 最高位产生进位怎么处理?例如 23 + 80 = 103,23 + 77 = 100
  3. 操作数中有负数时的处理办法

第一个问题很好解决,补 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,减法计算公式为

Res[i]=(A[i]B[i]borrow2+borrow110) % 10 R e s [ i ] = ( A [ i ] − B [ i ] − b o r r o w 2 + b o r r o w 1 ∗ 10 )   %   10

在 C 语言实现上还需考虑几个问题:

  1. 计算 A-B 时,A < B 时如何处理?
  2. 需要去除结果前面的 0。例如 1111-1110 = 0001,需要把 1 前面的 0 去掉后输出。
  3. 两操作数中有负数的处理办法。

解决方法如下:

  • 如果 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。最终结果为:

12345678=700 66 52 1234 ∗ 5678 = 700 ⏟ 高 位 66 ⏟ 中 位 52 ⏟ 低 位

下面给出递归算法:
这里为了简化,就不递归到两位数相乘了,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 ),但上面公式

ABCD=AC(AD+BC)BD A B ∗ C D = A ∗ C ( A ∗ D + B ∗ C ) B ∗ D
可以继续优化,中间部分:
AD+BC=(AB)(DC)+AC+BD A ∗ D + B ∗ C = ( A − B ) ( D − C ) + A C + B D
因为 AC 和 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;
}

参考

大整数乘法
大整数加减法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值