欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

大数算法之大数加减法

程序员文章站 2022-04-16 16:24:39
...

大数算法之大数加减法

大数算法之大数加减法

当我们第一次编程,成功地在黑洞洞的控制台窗口中打印出“Hello World”的字符后,准备编写的第一个有实用性的程序是什么?我想,对于大多数人而言,这一问题的答案会是:四则运算器。C语言本身提供了四则运算的运算符+ - * /。因此写一个简单的加减乘除法程序可谓轻而易举。

Calculate A + B.

还有比这个更简单的C语言题目吗?

#include <stdio.h>
int main(){
    int a, b;
    scanf("%d %d",&a ,&b);
    printf("%d\n",a+b);
    return 0;
}

现在让我们提升一下要求,让这个程序能计算两个大于2^64的数相加的和。该怎么办?

仍然想只用一行printf解决吗?遗憾的是,即使是int64的变量类型也无法容纳如此巨大的数字,为了实现我们的要求,必须另辟蹊径。解决这样的超大数字的计算的算法称之为大数算法。大数算法其实非常简单,其实质就是竖式,一个二年级的小学生也能轻松在纸上完成大数加法,然而用C语言实现大数加法却成了许多初识算法竞赛的大学生的梦魇。本文详细讨论如何实现大数算法中的加减法的运算。

114514.1919 + 12450 = ?

首先,请计算上述的算术题。但要求必须列竖式运算,就像小学二年级时的你所做的那样。

第一步,我们需要把数字进行按小数点对齐,即完成如图所示的变换:

大数算法之大数加减法

大数算法之大数加减法

第二步,每一位相加,超过10则进1,结果为:

大数算法之大数加减法

除此以外,还要考虑到输入的是负数的可能性。实际上,加上一个负数相当于减去一个负数的绝对值,因此为了实现一个能计算负数的大数加法,就必须同时考虑大数减法。

现在考虑用什么方式把数据输入到程序中才能让程序把两个数按位相加,很容易想到利用数组来实现。即把数字的每一位存到一个单独的变量中。

尽管利用字符串功能能够极为方便地把数据存入数组,但是这样做难以处理输入中可能存在的负号和小数点,因此在这里采用getchar函数模拟字符串输入。

void input(){
    point_a = 0; point_b = 0; len_a = 0; len_b = 0;
    bool input_flag = 0;
    sign_a = 1; sign_b = 1;
    //输入数据
    while((tmp = getchar()) != EOF)
    {
        if(!input_flag &&(tmp != 32 && tmp != '\n')){
            if(tmp == '-')
                sign_a = 0; //标记负号
            else
                a[len_a++] = tmp;
            if(a[len_a-1] == '.')
                point_a = len_a - 1; //记录小数点位置
        }
        if(input_flag && (tmp != 32 && tmp != '\n')){
            if(tmp == '-')
                sign_b = 0; //标记负号
            else
                b[len_b++] = tmp;
            if(b[len_b-1] == '.')
                point_b = len_b - 1; //记录小数点位置
        }
        if(input_flag && (tmp == 32 || tmp == '\n'))
            break; //结束输入
        if(tmp == 32 || tmp == '\n')
            input_flag = 1; //a输入结束,转为输入b
    }
    if(tmp == EOF)
        return;
    //输入为整数时,把小数点挪到个位数之后
    if(!point_a)
        point_a = len_a;
    if(!point_b)
        point_b = len_b;
    return;
}

该段代码是一个输入函数,其中tmp为一个char型的全局变量,在函数之外声明。它能将输入的每一位的数字和小数点都存入数组a[]和数组b[](同样在函数外声明)中并标记小数点的位置,但它不会储存正负号,被存储的其实是输入数的绝对值。这是因为负号实际上是让加法变成减法,只需要让输入函数在检测到负号时更改一个全局变量(sign_a和sign_b)的值即可,在后续的函数中将会使用到这个全局变量。
现在,我们已经将要进行加法运算的两个数分别存入了两个数组当中,但它们还未按小数点的位置对齐。接下来我们将写一个函数完成对齐的操作。

因为小数点在数组的中间位置,所以直接按小数点对齐会十分麻烦且容易出错,方便起见,我们先把两个数的小数部分移除,在对齐整数之后添加小数点,然后再把小数部分放到小数点之后。

void align(){
    int i, j,
        len_da = 0, len_db = 0;
    //比较绝对值大小
    if(point_a > point_b || (point_a == point_b && a[0] >= b[0]))
        compare = 1;
    else
        compare = 0;
    //将小数位暂存至另一个数组中
    for(i = point_a + 1; i < len_a; ++i)
        d_a[len_da++] = a[i];
    for(i = point_b + 1; i < len_b; ++i)
        d_b[len_db++] = b[i];
    //对齐小数位
    int d_large = len_da > len_db ? len_da : len_db;
    int d_short = len_da + len_db - d_large;
    for(i = d_short; i < d_large; ++i){
        if(len_da == d_short)
            d_a[i] = '0';
        else
            d_b[i] = '0';
    }
    //对齐小数点
    int int_large, int_short;
    int_large = point_a > point_b ? point_a : point_b;
    int_short = point_a + point_b - int_large;
    for(i = 1; len_short - i >= 0; ++i){
        if(len_short == len_a)
            a[len_large - i] = a[len_short - i];
        else
            b[len_large - i] = b[len_short - i];
    }
    for(i = 0; i < int_large - int_short; ++i){
        if(point_a == int_short)
            a[i] = '0';
        else
            b[i] = '0';
    }
    a[int_large] = '.';
    b[int_large] = '.';
    //将小数位放在小数点之后
    for(i = int_large + 1, j=0; j < d_large; ++i, ++j){
        a[i] = d_a[j];
        b[i] = d_b[j];
    }
    len = i;
    //将数据由数字的ASCII码转换为数字
    for(i=0; i < len; ++i){
        if(a[i] != '.'){
            a[i] -= char2d;
            b[i] -= char2d;
        }
    }
    a[i] = '\0';
    b[i] = '\0';
    return;
}

读者会注意到,在该函数最开始的地方有一段判断两个数绝对值大小的代码,这似乎与我们目前的目标无关。但是在涉及到负数时,加法运算会变为两个正数的减法运算,减法存在计算出负数的可能性。让大数减法的模拟竖式模块具有处理负数结果的能力较为困难,一种简单的方法是先计算出两数相减的绝对值,再视情况添加负号。而计算绝对值需要我们事先知道两数的绝对值孰大孰小。所以这段代码是为了后面的计算做准备。

易错提醒:在对齐整数部分时,需要将较小的数在数组中的位置前挪,如1、2、4、5、0本来分别存储在a[0], a[1], a[2], a[3], a[4]中,现在要将其变更为a[1], a[2], a[3], a[4], a[5],并将a[0]补零。这样的挪动只能从后向前,不能从前向后,否则可能会出现部分数据被覆盖的问题。

现在,通过对齐小数点和补零,存储在a[]和b[]中的不同大小的两个数占据的数组空间大小是相同的,接下来的运算部分就非常简便了。

//加法运算
void bigadd(){
    int i;
    for(i=0; i < len; ++i){
        if(a[len - 1 - i] == '.'){ //遇到小数点,把小数点存入result数组中,并跳到下一位
            result[i] = '.';
            continue;
        }
        result[i] = a[len - 1 - i] + b[len - 1 - i]; //把两个数按位相加
    }
    for(i=0; i < len; ++i){
        if(result[i] == '.')
            continue;
        if(result[i] > 9) //进位
        {
            result[i] %= 10;
            if(result[i+1] != '.') //进位时要跳过小数点
                result[i+1] += 1;
            else
                result[i+2] += 1;
        }
    }
    return;
}

//减法运算
void bigsubtract(char *a, char *b){
    int i;
    for(i=0; i < len; ++i){
        if(a[len - 1 - i] == '.'){
            result[i] = '.';
            continue;
        }
        result[i] = a[len - 1 - i] - b[len - 1 - i];
    }
    for(i=0; i < len; ++i){
        if(result[i] == '.')
            continue;
        if(result[i] < 0)
        {
            result[i] += 10;
            if(result[i+1] != '.')
                result[i+1] -= 1;
            else
                result[i+2] -= 1;
        }
    }
    return;
}
注意到减法运算函数bigsubtract是有两个指针参数的,这是因为该函数不具备处理负数结果的功能,因此必须保证被减数大于减数,此处a为被减数,b为减数。

但是我们还需要一个函数来判断应该怎么正确调用这两个运算函数以计算出结果。

void calculate(){
    sign_result = 1;
    if(sign_a && sign_b) //a和b都是正数,直接绝对值相加
    {
        bigadd();
    }
    if(!sign_a && !sign_b) //a和b都是负数,绝对值相加并补上负号
    {
        bigadd();
        sign_result = 0;
    }
    if(sign_a && !sign_b)//a是正数,b是负数
    {
        if(compare) //a的绝对值大于等于b的绝对值,直接绝对值相减
        {
            bigsubtract(a,b);
        }
        else //a的绝对值比b的绝对值小,用b的绝对值减去a的绝对值并补上负号
        {
            bigsubtract(b,a);
            sign_result = 0;
        }
    }
    if(!sign_a && sign_b)//同上
    {
        if(compare)
        {
            bigsubtract(a,b);
            sign_result = 0;
        }
        else
        {
            bigsubtract(b,a);
        }
    }
    return;
}

现在大部分工作已经完成,可以进入收尾环节了,但是切不可以掉以轻心,需要将结果进行加工处理才能让其被漂亮地打印在控制台窗口上。

我们要解决的问题有:(1)去除运算产生的前导零和后导零;(2)小数部分为零时只显示整数部分;(3)让结果的最高位存储在数组的首位,次高位存储在第二位,并以此类推之;(4)把数字转换成对应的ASCII码。

void output(){
    //将数字转换为对应的ASCII码
    int i, j;
    for(j = len; result[j] == 0 && result[j-1] != '.'; --j)
        continue;
    for(i=0; i <= j; ++i){
        if(result[j-i] != '.')
            final[i] = result[j-i] + char2d;
        else
            final[i] = '.';
    }
    //去除小数位多余的零
    final[i] = '\0';
    for(i = strlen(final) - 1; (point_a != len_b) && (point_a != len_a); --i){
        if(final[i] == '0')
            final[i] = '\0';
        else
            break;
    }
    //没有小数位时去除小数点
    i = strlen(final) - 1;
    if(final[i] == '.')
        final[i] = '\0';
    //输出
    if(!sign_result)
        printf("-");
    puts(final);
    return;
}

最后在主函数中调用这几个函数即可。

int main()
{
    while(1)
    {
        memset(result,0,sizeof(result));
        memset(final,0,sizeof(final));
        input();
        if(tmp == EOF)
            break;
        align();
        calculate();
        output();
    }
    return 0;
}

以上代码中调用的全局变量的声明如下:

char a[BUFSIZ], b[BUFSIZ],
     d_a[BUFSIZ], d_b[BUFSIZ],
     len_a, len_b,
     tmp,
     result[BUFSIZ], final[BUFSIZ],
     char2d = '0';
int len,
    point_a, point_b;
bool sign_a, sign_b, sign_result,
     compare;