《算法笔记》——第五章 大整数运算 学习记录

大整数运算

对一道A+B的题目,如果A和B的范围在int范围内,那么相信大家很快就能写出程序。但是如果A和B是有着1000个数位的整数,恐怕就没有办法用已有的数据类型来表示了,这时就只能老实去模拟加减乘除的过程。怎么样?听起来像是小学生学的东西吧?实际上原理就是小学的,所以不要去害怕这个看上去很高深的东西。此外,大整数又称为高精度整数,其含义就是用基本数据类型无法存储其精度的整数。

大整数存储

很简单,使用数组即可。例如定义int型数组d[1000],那么这个数组中的每一位就代表了存放的整数的每一位。如将整数235813存储到数组中,则有d[0]= 3, d[1]= 1, d[2]= 8, d[3]
=5, d[4]=3, d[5]=2,即整数的高位存储在数组的高位,整数的低位存储在数组的低位。

不反过来存储的原因是,在进行运算的时候都是从整数的低位到高位进行枚举,顺位存储和这种思维相合。但是也会由此产生一个需要注意的问题:把整数按字符串%s读入的时候,实际上是逆位存储的,即str[0] = '2', str[1]= '3',..., str[5]= '3',因此在读入之后需要在另存为至d[]数组的时候反转一下。

而为了方便随时获取大整数的长度,一般都会定义一个int 型变量len来记录其长度,并和d数组组合成结构体:

struct bignum
{
    int d[1000];
    int len;
};

显然,在定义结构体变量之后,需要马上初始化结构体。为了减少在实际输入代码时总是忘记初始化的问题,读者最好使用“构造函数”,即在结构体内部加上以下代码:

struct bignum
{
    int d[1000];
    int len;
    bignum()
    {
        memset(d,0,sizeof d);
        len=0;
    }
};

“构造函数”是用来初始化结构体的函数,函数名和结构体名相同、无返回值,因此非常好写。

这样在每次定义结构体变量时,都会自动对该变量进行初始化。
而输入大整数时,一般都是先用字符串读入,然后再把字符串另存为至bignum结构体。由于使用char数组进行读入时,整数的高位会变成数组的低位,而整数的低位会变成数组的高位,因此为了让整数在bignum中是顺位存储,需要让字符串倒着赋给d[]数组:

bignum change(char str[])
{
    bignum a;
    a.len=strlen(str);
    for(int i=0;i<a.len;i++)
        a.d[i]=str[a.len-i-1]-'0';
    return a;
}

如果要比较两个bignum变量的大小,规则也很简单:先判断两者的len大小,如果不相等,则以长的为大;如果相等,则从高位到低位进行比较,直到出现某一位不等,就可以判断两个数的大小。下面的代码直接依照了这个规则:

int compare(bignum a,bignum b)
{
    if(a.len > b.len) return 1;
    else if(a.len < b.len) return -1;
    else
    {
        for(int i=a.len-1;i>=0;i--)
            if(a.d[i] > b.d[i]) return 1;
            else if(a.d[i] < b.d[i]) return -1;
    }
    return 0;
}

接下来主要介绍四个运算:

  1. 高精度加法
  2. 高精度减法
  3. 高精度与低精度的乘法
  4. 高精度与低精度的除法。

至于高精度与高精度的乘法和除法,考试一般不会涉及,因此留
给有兴趣的读者自行了解。

搞精度加法

以147+65为例,下面来回顾一下小学的时候是怎么学习两个整数相加的:

  1. 7+5=12,取个位数2作为该位的结果,取十位数1进位。
  2. 4+6,加上进位1为11,取个位数1作为该位的结果,取十位数1进位。
  3. 1+0,加上进位1为2,取个位数2作为该位的结果,由于十位数位0,因此不进位。

可以因此归纳出对其中一位进行加法的步骤:将该位上的两个数字和进位相加,得到的结果取个位数作为该位结果,取十位数作为新的进位。

高精度加法的做法与此完全相同,可以直接来看实现的代码:

bignum add(bignum a,bignum b)
{
    bignum c;
    c.len=max(a.len,b.len);

    int carry=0;
    for(int i=0;i<c.len;i++)
    {
        int t=a.d[i]+b.d[i]+carry;
        c.d[i]=t%10;
        carry=t/10;
    }
    if(carry) c.d[c.len++]=carry;
    return c;
}

最后指出,这样写法的条件是两个对象都是非负整数。如果有一方是负的,可以在转换到数组这一步时去掉其负号,然后采用高精度减法;如果两个都是负的,就都去掉负号后用高精度加法,最后再把负号加回去即可。

高精度减法

以147-65为例,再来回顾一下小学的时候是怎么学习两个整数相减的:

  1. 5-7<0,不够减,因此从高位4借1,于是4减1变成3,该位结果为15-7=8。
  2. 3-6<0,不够减,因此从高位1借1,于是1减1变成1,该位结果为13-6=7。
  3. 上面和下面均为0,结束计算。

同样可以得到一个很简练的步骤:对某一步,比较被减位和减位,如果不够减,则令被减位的高位减1、被减位加10再进行减法;如果够减,则直接减。

最后一步要注意减法后高位可能有多余的0,要忽视它们,但也要保证结果至少有一位数。

高精度减法的完整代码即为把上面的sub函数替代高精度加法中add函数的位置即可,记得调用的时候也是用sub函数,这里就不再重复给出代码。

最后需要指出,使用sub函数前要比较两个数的大小,如果被减数小于减数,需要交换两个变量,然后输出负号,再使用sub函数。

bignum sub(bignum a,bignum b)
{
    if(compare(a,b) == -1)
    {
        cout<<'-';
        return sub(b,a);
    }

    bignum c;
    c.len=a.len;
    for(int i=0;i<c.len;i++)
    {
        if(a.d[i] < b.d[i])
        {
            a.d[i+1]--;
            a.d[i]+=10;
        }
        c.d[i]=a.d[i]-b.d[i];
    }
    while(c.len > 1 && c.d[c.len-1] == 0)
        c.len--;
    return c;
}

高精度与低精度的乘法

所谓的低精度就是可以用基本数据类型存储的数据,例如int 型。这里讲述的就是bignum类型与int类型的乘法,其做法和小学学的有一点不一一样。以147x35为例,这里把147视为高精度bignum类型,而35视为int类型,并且在下面的过程中,始终将35作为一个整体看待。

  1. 7x35=245,取个位数5作为该位结果,高位部分24作为进位。
  2. 4x35= 140,加上进位24,得164,取个位数4为该位结果,高位部分16作为进位。
  3. 1x35=35,加上进位16,得51,取个位数1为该位结果,高位部分5作为进位。
  4. 没的乘了,此时进位还不为0,就把进位5直接作为结果的高位。

对某一步来说是这么一个步骤:取bignum的某位与int型整体相乘,再与进位相加,所得结果的个位数作为该位结果,高位部分作为新的进位

完整的AxB的代码只需要把高精度加法里的add函数改成这里的mul函数,并注意输入的时候b是作为int型输入即可。

bignum mul(bignum a,int b)
{
    bignum c;
    c.len=a.len;

    int carry=0;
    for(int i=0;i<c.len;i++)
    {
        int t=a.d[i]*b+carry;
        c.d[i]=t%10;
        carry=t/10;
    }
    while(carry)//乘法的进位可能不止一位
    {
        c.d[c.len++]=carry%10;
        carry/=10;
    }
    
    while(c.len > 1 && c.d[c.len-1] == 0) c.len--;//b == 0
    
    return c;
}

另外,如果a和b中存在负数,需要先记录下其负号,然后取它们的绝对值代入函数。

高精度与低精度的除法

除法的计算方法和小学所学是相同的。以1234/7为例:

  1. 1与7比较,不够除,因此该位商为0,余数为1。
  2. 余数1与新位2组合成12,12与7比较,够除,商为1,余数为5。
  3. 余数5与新位3组合成53,53与7比较,够除,商为7,余数为4。
  4. 余数4与新位4组合成44,44与7比较,够除,商为6,余数为2。

归纳其中某一步的步骤:上一步的余数乘以10加上该步的位,得到该步临时的被除数,将其与除数比较:

  • 如果不够除,则该位的商为0;
  • 如果够除,则商即为对应的商,余数即为对应的余数。

最后一步要注意减法后高位可能有多余的0,要忽视它们,但也要保证结果至少有一位数。

bignum divide(bignum a,int b)
{
    bignum c;
    c.len=a.len;

    for(int i=c.len-1;i>=0;i--)
    {
        r=r*10+a.d[i];
        c.d[i]=r/b;
        r%=b;
    }
    while(c.len > 1 && c.d[c.len-1] == 0) c.len--;
    return c;
}

在上述代码中,考虑到函数每次只能返回一个数据,而很多题目里面会经常要求得到余数,因此把余数写成“引用”的形式直接作为参数传入,或是把r设成全局变量。引用在2.7.5节已有讲述,其作用是在函数中可以视作直接对原变量进行修改,而不像普通函数参数那样,在函数中的修改不影响原变量的值。这样当函数结束时,r的值就是最终的余数。

posted @ 2021-02-14 23:27  Dazzling!  阅读(23)  评论(0编辑  收藏  举报