大数算法之大数加减法

大数算法之大数加减法


当我们第一次编程,成功地在黑洞洞的控制台窗口中打印出“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;


  • 23
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: RSA是一种非对称算法,使用公钥和私钥进行密和解密。而GMP(GNU Multiple Precision Arithmetic Library)是一个C/C++库,用于实现对大整数的高精度计算。通过结合RSA和GMP,可以实现RSA解密算法大数的处理。 在RSA算法中,首先需要生成一对密钥,包括公钥和私钥。公钥用于密数据,私钥用于解密数据。生成密钥对的过程可以通过GMP库实现高精度的计算和随机数生成。 接下来,使用公钥对要密的数据进行密,这时候需要使用GMP库来处理大数运算,包括幂运算和取模运算。将明文转换为对应的大整数后,通过公钥进行密计算,并将结果转换为密文。 当需要解密密文时,使用私钥进行解密操作。同样需要使用GMP库进行大数运算,包括求幂和取模运算。将密文转换为对应的大整数后,通过私钥进行解密计算,并将结果转换为明文。 在实际应用中,一般会选择合适的大素数作为RSA算法的参数,这时候GMP库可以帮助我们进行高精度的素数运算和随机数生成。同时,GMP库还可以辅助处理大整数的运算,提高RSA算法的效率和安全性。 综上所述,RSA解密算法可以通过GMP大数实现。通过GMP库的高精度计算和随机数生成功能,可以辅助进行RSA密钥的生成、大整数的解密计算。这种组合可以提高RSA算法大数的处理能力,保证了密和解密的安全性和效率。 ### 回答2: RSA解密算法是一种非对称算法,用于保护网络通信中的数据安全。GMP(GNU Multiple Precision Arithmetic Library)是一个开源的大数运算库,提供了高精度的数值计算函数,可以用于实现RSA解密算法。 在RSA解密算法中,首先需要选择两个不同的大素数p和q,并计算它们的乘积n。然后选择一个整数e作为公钥,使得e与(p-1)(q-1)互质。接下来,计算一个整数d作为私钥,满足ed ≡ 1 (mod (p-1)(q-1))。公钥为(n, e),私钥为(n, d)。 密时,将明文m转换为整数M,然后使用公式C ≡ M^e mod n进行密。解密时,将密文C使用公式M ≡ C^d mod n进行解密,得到明文m。 GMP大数库可以提供高精度的整数计算函数,使得在实现RSA算法中能够处理超过机器原生整数表示范围的大数。它提供了高效的大数运算方法,包括大数加法减法、乘法和取模运算,以及快速幂运算等。通过使用GMP大数库,可以保证RSA解密算法在计算大数时的准确性和高效性。 综上所述,RSA解密算法可以通过使用GMP大数库来实现,其中GMP大数库提供了高精度的整数计算函数,可以保证RSA算法在处理大数时的准确性和效率。 ### 回答3: RSA(Rivest-Shamir-Adleman)解密算法是一种非对称算法,常用于保护数据的机密性和完整性。RSA算法涉及大数运算,在实现过程中,可以使用GMP(GNU Multiple Precision Arithmetic Library)大数库来处理大整数运算。 GMP是一个开源的大数库,它提供了高精度的整数和浮点数运算功能。使用GMP库可以有效地处理超过CPU位数范围的整数运算,满足RSA算法所需的大整数需求。 在实现RSA解密算法时,首先需要生成一对公私钥。GMP库可以提供生成大素数、随机数和快速幂模运算等功能。通过这些功能,可以生成RSA算法所需的大素数p和q,并计算出公钥e和私钥d。 在密时,可以使用GMP库中的幂模运算函数,将明文m密为密文c,使用公钥e进行运算:c = m^e mod n。其中,n是p和q的乘积。 在解密时,同样可以使用GMP库中的幂模运算函数,将密文c解密为明文m,使用私钥d进行解密运算:m = c^d mod n。同样,n是p和q的乘积。 通过使用GMP大数库来实现RSA解密算法,可以保证对大整数的高效处理和准确计算,确保RSA算法的安全性和正确性。同时,GMP库的开源特性也使得算法实现灵活和可扩展。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值