跳至正文
View Categories

6 min read

1. 概述 #

高精度数的概念 #

整数和小数有计算精度的限制。比如:

  • unsigned long long 的最大值: 1844 6744 0737 0955 1615 (一千八百多万亿)
  • long double 的范围: 1.18973e+4932 ~ 3.3621e-4932

太大的数,或者小数点太多的数,在计算上有限制。就需要使用特殊的处理方法。这就称为高精度计算。在一般的科学计算中,会经常算到小数点后几百位或者更多,当然也可能是几千亿几百亿的大数字。一般这类数字我们统称为高精度数,高精度算法是用计算机对于超大数据的一种模拟加,减,乘,除,乘方,阶乘,开方等运算。对于非常庞大的数字无法在计算机中正常存储,于是,将这个数字拆开,拆成一位一位的,或者是四位四位的存储到一个数组中, 用一个数组去表示一个数字,这样这个数字就被称为是高精度数。

数据的接收和存贮 #

  • 当输入的数很长时,可采用字符串方式输入。

这样可以输入位数很长的数,利用字符串函数和操作运算,将每一位数取出,存入数组中。
因为数组存放的元素顺序与我们计算的顺序是相反的,在竖式计算中我们是将其右对齐(个位对个位,十位对十位,以此类推),而读取数字后的两个数组是左对齐的,因此我们要将里面的元素逆置

请读入下面的数:
123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789

示例程序:
void init()
{
    string s;
    cin >> s; // 读入字符串
    len = s.length();
    for(i=0; i <= len; ++i) //逆置元素
        a[i]=s[len-1] - '0';   // 将字符转化成数字
}

高精度数位数的确定 #

  • 方法:接收时往往是用字符串,所以它的位数就等于字符串的长度。

进位,错位处理 #

下面例程中的 a b c 均为整形数组。

  • 加法进位:
c[i] = a[i]+b[i];
if(c[i] >= 10)
{
    c[i] %= 10;
    ++c[i+1];
}
  • 减法借位:
if(a[i] < b[i])
{
    --a[i+1];
    a[i]+=10;
}
c[i] = a[i]-b[i];

2. 高精度加法 #

请输入两个数,计算它们的和。

【算法分析】
竖式方法举例:962+93

【例程】

#include < iostream >
#include < string >
using namespace std;
//高精度加法函数
void add(int a[], int a_len, int b[], int b_len, int* result, int& result_len)
{
    int i = 0, x = 0;
    result_len = 0;
    int max_a_b = a_len > b_len ? a_len : b_len;
    while (i< max_a_b)
    {
        int sub_a = i < a_len ? a[i] : 0; //判断i是否超过数组a的长度,若超过该位为0
        int sub_b = i < b_len ? b[i] : 0; //判断i是否超过数组b的长度,若超过该位为0

        result[i] = sub_a + sub_b + x;//逐位相加
        x = result[i] / 10;
        result[i] %= 10;
        result_len++;
        i++;
    }
    if (x != 0)
    {
        result[i] = x;
        ++result_len;
    }
}

int main()
{
    string astr, bstr;
    cin >> astr;
    cin >> bstr;

    int lena = astr.size(), lenb = bstr.size();
    int a[100], b[100];
    for (int i = 0; i< lena; ++i)
        a[lena - i - 1] = astr[i] - '0'; // 将数字倒过来,并转化成数字

    for (int i = 0; i< lenb; ++i)
        b[lenb - i - 1] = bstr[i] - '0';

    int c[100] = { 0 }, lenc=0;
    add(a, lena, b, lenb, c, lenc); // 计算

    for (int i = 0; i < lenc; ++i)
    {
        cout << c[lenc - i - 1]; // 再将数字倒过来输出
    }
    cout << endl;
    return 0;
}

3. 高精度乘法 #

进位和错位处理:高精度✖️单精度 #

先考虑一个简单的情况:乘数中的一个是普通的 int 类型。有没有简单的处理方法呢?
一个直观的思路是直接将a每一位上的数字乘以b。从数值上来说,这个方法是正确的,但它并不符合十进制表示法,因此需要将它重新整理成正常的样子。
重整的方式,也是从个位开始逐位向上处理进位。但是这里的进位可能非常大,甚至远大于9,因为每一位被乘上之后都可能达到9b的数量级。所以这里的进位不能再简单地进行-10运算,而是要通过除以10的商以及余数计算。详见代码注释,也可以参考下图展示的一个计算高精度数1337乘以单精度数42的过程。

当然,也是出于这个原因,这个方法需要特别关注乘数b的范围。若它和 10^9(或相应整型的取值上界)属于同一数量级,那么需要慎用高精度—单精度乘法。

void mul_short(int a[], int b, int c[]) {
  clear(c);

  for (int i = 0; i < LEN - 1; ++i) {
    // 直接把 a 的第 i 位数码乘以乘数,加入结果
    c[i] += a[i] * b;

    if (c[i] >= 10) {
      // 处理进位
      // c[i] / 10 即除法的商数成为进位的增量值
      c[i + 1] += c[i] / 10;
      // 而 c[i] % 10 即除法的余数成为在当前位留下的值
      c[i] %= 10;
    }
  }
}

进位和错位处理:高精度✖️高精度 #

如果两个乘数都是高精度,那么竖式乘法又可以大显身手。
于是可以将b分解为它的所有数码,其中每个数码都是单精度数,将它们分别与a相乘,再向左移动到各自的位置上相加即得答案。当然,最后也需要用与上例相同的方式处理进位。

注意这个过程与竖式乘法不尽相同,我们的算法在每一步乘的过程中并不进位,而是将所有的结果保留在对应的位置上,到最后再统一处理进位,但这不会影响结果。

void mul(int a[], int b[], int c[]) {
  clear(c);

  for (int i = 0; i < LEN - 1; ++i) {
    // 这里直接计算结果中的从低到高第 i 位,且一并处理了进位
    // 第 i 次循环为 c[i] 加上了所有满足 p + q = i 的 a[p] 与 b[q] 的乘积之和
    // 这样做的效果和直接进行上图的运算最后求和是一样的,只是更加简短的一种实现方式
    for (int j = 0; j <= i; ++j) c[i] += a[j] * b[i - j];

    if (c[i] >= 10) {
      c[i + 1] += c[i] / 10;
      c[i] %= 10;
    }
  }
}

高精度乘法实现 #

请输入两个数,计算它们的积。

【算法分析】
竖式方法举例:

  • 25*836
int b[] = {2,5};
int a[] = {8,3,6};

            b1 b0
___x_____a2_a1_a0__
         c2 c1 c0
      d2 d1 d0     
___e2_e1_e0________
f5 f4 f3 f2 f1 f0


我们不妨改一下数据的下标,以便于更好的理解下面的代码。
            b1 b0
___x_____a2_a1_a0__
         c2 c1 c0
      d3 d2 d1     
___e4_e3_e2________
f5 f4 f3 f2 f1 f0

## 1 
fi 的计算过程推导,是代码实现的关键。
1. f的序号和ab有什么关系?为什么是【f序号 = a序号 + b序号】
2. fi的值,是一个累加过程。

## 2 
两数乘积的位数,如何确定?
f5 可能没有吗?

【例程】

#include < iostream >
#include < string >
using namespace std;


void multiply(int a[], int b[], int a_len, int b_len, int* f, int& f_len)
{
    int i = 0, x = 0;
    f_len = 0;

    for (int i = 0; i < a_len; ++i)
    {
        int x = 0; // x 表示进位
        for (int j = 0; j < b_len; ++j)
        {
            // 乘法核心代码
            f[i+j] += a[i] * b[j] + x;
            x = f[i+j] / 10;
            f[i+j] %= 10;
        }
        f[i + b_len] = x;
    }

    // 根据最后一位是否为 0 ,判断计算结果的位数
    if (f[a_len + b_len - 1] == 0)
        f_len = a_len + b_len - 1;
    else
        f_len = a_len + b_len;
}

int main()
{
    // 从屏幕获得输入
    string astr, bstr;
    cin >> astr;
    cin >> bstr;

    int lena = astr.size(), lenb = bstr.size();
    int a[100] = { 0 }, b[100] = { 0 }; // 假设数字的位数不超过 100
    for (int i = 0; i < lena; ++i)
        a[lena - i - 1] = astr[i] - '0'; // 将数字倒过来

    for (int i = 0; i < lenb; ++i)
        b[lenb - i - 1] = bstr[i] - '0';


    // 计算高精度乘法
    int f[100] = { 0 }, lenf(0);
    multiply(a, b, lena, lenb, f, lenf); // 必须是两个非负数,且 a 大于 b


    // 输出结果
    for (int i = 0; i < lenf; ++i)
    {
        cout << f[lenf - i - 1];
    }
    cout << endl;
    return 0;
}

4. 高精度除法 #

高精度除以低精度 #

假设 a 是一个高精度整数,b是一个普通的整数。
【算法分析】
在高精度除以单精度时,从高位到低位逐位相除。最需要注意的问题是,后一位继承前一位的余数问题。
设高精度的数位数字为a[i],单精度数为b,第i+1位除b后的余数位r,把r加到i位时,应乘以进制X,即s=X*r+a[i].

  • 注意位移处理即可
    int b = 5;
    int a[] = {8,3,6};

(1)
     _1___
    5)836
     _5___
      3

(2)
     _16__
    5)836
     _5___
      33
     _30__
       3

(3)
     _167__
    5)836
     _5___
      33
     _30__
       36
     __35_
        1

【例程】

#include < iostream >
#include < string >
using namespace std;

void assign(int a[], int a_len, int* dst, int& dst_len)
{
    for (int i = 0; i < a_len; ++i)
        dst[i] = a[i];
    dst_len = a_len;
}

void divide(int a[], int a_len, int b, int* result, int& result_len)
{
    int c[100] = { 0 }, lenc = 0; // c 用于存放临时的计算结果

    if (a[0] == 0)
    {
        result_len = 1;
        assign(c, 1, result, result_len);
        return; // 0是被除数,则商为0
    }

    int i = 0, x = 0;
    result_len = 0;

    for (int i = 0; i < a_len; ++i)
    {
        c[i] = (x * 10 + a[i]) / b;
        x = (x * 10 + a[i]) % b;
        ++lenc;
    }

    // 去除前部可能出现的 0
    int c_startID = 0;
    for (; c_startID < 100; ++c_startID)
        if (c[c_startID] != 0)
            break;

    for (int i = c_startID; i < lenc; ++i)
    {
        result[i - c_startID] = c[i];
        ++result_len;
    }
}

int main()
{
    // 从屏幕获得输入 被除数a和除数b均以字符串的形式输入
    string astr, bstr;
    cin >> astr;
    cin >> bstr;
    //获得字符串a和b的长度也就是,a和b的位数
    int lena = astr.size(), lenb = bstr.size();
   //设置数组a[100]高精度数字a
    int a[100] = { 0 }, b = 0; 
    for (int i = 0; i < lena; ++i)
        a[i] = astr[i] - '0'; // 除法不需要将数字倒过来
    for (int i = 0; i < lenb; ++i)
        b += (bstr[i] - '0') * pow(10, lenb - i - 1); // 字符串转 10 进制数
    //不能除以0
    if (b == 0)
        return -1; 

    // 设置数组c[100]存储计算结果,c[100]中存储的也是高精度的数字
    int c[100] = { 0 }, lenc(0);
    divide(a, lena, b, c, lenc); // a 是被除数,是高精度值。b是低精度值,用 int 即可存储

    // 输出
    for (int i = 0; i < lenc; ++i)
    {
        cout << c[i]; // 正向输出
    }
    cout << endl;
    return 0;
}

高精度除以高精度 #

假设 a 是一个高精度整数,b 也是一个高精度整数。

【算法分析】

  • 方法:使用减法模拟除法

将被除数,不断减去除数,直到余数小于除数。

  • 1. 需要用到高精度减法
  • 2. 需要用到高精度比较大小
  • 3. 需要用到高精度加法

【例程】

#include < iostream >
#include < string >
using namespace std;

int zero[] = { 0 };
int one[] = { 1 };
//赋值过程,将数组a[]中的数据赋值给b[],并将a的长度a_len赋值给b_len
void assign(int a[], int a_len, int* b, int& b_len)
{
    for (int i=0; i < a_len; ++i)
        b[i] = a[i];
    b_len = a_len;
}
//高精度加法计算函数
void add(int a[], int a_len, int b[], int b_len, int* result, int& result_len)
{
    int i = 0, x = 0;
    result_len = 0;
    int max_a_b = a_len > b_len ? a_len : b_len;
    while (i< max_a_b)
    {
        int sub_a = i < a_len ? a[i] : 0;
        int sub_b = i < b_len ? b[i] : 0;
        result[i] = sub_a + sub_b + x;
        x = result[i] / 10;
        result[i] %= 10;
        result_len++;
        i++;
    }
    if (x != 0)
    {
        result[i] = x;
        ++result_len;
    }
}
//高精度减法计算函数
void jianfa(int a[], int a_len, int b[], int b_len, int* result, int& result_len)
{
    int i = 0, x = 0;
    result_len = 0;
    int max_a_b = a_len > b_len ? a_len : b_len;
    while (i < max_a_b)
    {
        int sub_a = i < a_len ? a[i] : 0;
        int sub_b = i < b_len ? b[i] : 0;
        // 减法
        if (sub_a < sub_b)
        {
            --a[i + 1];
            sub_a += 10;
        }
        result[i] = sub_a - sub_b;
        result_len++;
        i++;
    }
    if (x != 0)
    {
        result[i] = x;
        ++result_len;
    }

    // 假如结果全都是0,输出一个0
    int sum(0);
    for (int i = 0; i < result_len; ++i)
    {
        sum += result[i];
        if (sum != 0)
            break;
    }

    if (sum == 0)
        result_len = 1;

}

// 如果 a==b 返回 0; 如果 a > b, 返回 1; 如果 a < b, 返回 -1;
int compare_a_b(int a[], int a_len, int b[], int b_len)
{
    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[i] > b[i])
                return 1;
            else if (a[i] < b[i])
                return -1;
            else
                continue;
        }
    }

    return 0; // 相等
}

void divide(int a[], int a_len, int b[], int b_len, int* shang, int& shang_len, int* yushu, int& yushu_len)
{
    if (0 == compare_a_b(zero, 1, a, a_len))
    {
        shang_len = 1;
        yushu_len = 1;
        return; // 0是被除数,则商为0
    }

    for (;;)
    {
        jianfa(a, a_len, b, b_len, a, a_len); // 相当于 a -= b;

        add(shang, shang_len, one, 1, shang, shang_len);
        assign(a, a_len, yushu, yushu_len);// 将最终减法剩余的结果,赋值给余数

        if (-1 == compare_a_b(a, a_len, b, b_len))  // 
            break; // 当余数a小于除数b的时候,退出循环。
    }
}

int main()
{
    // 从屏幕获得输入
    string astr, bstr;
    cin >> astr;
    cin >> bstr;
    int lena = astr.size(), lenb = bstr.size();
    int a[100] = { 0 }, b[100] = { 0 };

    for (int i = 0; i< lena; ++i)
        a[lena - i - 1] = astr[i] - '0'; // 将数字倒过来

    for (int i = 0; i< lenb; ++i)
        b[lenb - i - 1] = bstr[i] - '0';

    if (0 == compare_a_b(zero, 1, b, lenb))
        return -1; // 被除数不能是 0


    // 计算除法
    int shang[100] = { 0 }, yushu[100] = { 0 }, shang_len(0), yushu_len(0);
    divide(a, lena, b, lenb, shang, shang_len, yushu, yushu_len); // a 是被除数,是高精度值。b是低精度值,用 int 即可存储


    // 输出
    cout << "商:";
    for (int i=0; i < shang_len; ++i)
        cout << shang[shang_len - i - 1]; // 再将数字倒过来输出
    cout << endl;

    cout << "余数:";
    for (int i = 0; i < yushu_len; ++i)
        cout << yushu[yushu_len - i - 1]; // 再将数字倒过来输出
    cout << endl;
    
    return 0;
}