计算机,顾名思义,是用于高速计算的电子计算机器。它可以进行数值计算,又可以进行逻辑计算,还具有存储记忆功能。是能够按照程序运行,自动、高速处理海量数据的现代化智能电子设备。[1]但是,计算机的功能毕竟是有限的,再强大的计算机在面对一些问题时也有它的局限性,本系列文章就来具体阐述其中的一种问题:高精度运算。
先从一个问题说起:编程计算并输出1~40之间的所有数的阶乘。
乍一看题目,似乎很简单:在初学循环语句的时候,累乘问题是一个较基本的问题,实现其功能的代码如下:
#include <stdio.h>
int main(void)
{
int i;
long p = 1;
for (i = 1; i <= 40; i++)
{
p = p * i;
printf("%d! = %ld\n",i,p);//输出1~40之间的所有数的阶乘值
}
return 0;
}
运行一下程序,我们就会发现情况和我们想象的不太一样:
啊这……似乎从十几的阶乘开始,就出现了一些不可描述的错误。为什么会这样呢?
其实这正是因为long类型变量有他存储范围的上限,当超过这个范围时,数值便会溢出,导致发生错误。那么有没有方法可以解决这个问题呢?我们就需要用到数组:用一个包含50个元素的数组存储一个大数,每个数组元素存储大数中的一位数字。
而两个数据的乘法则需要模拟乘法竖式的运算,忘记了竖式运算方法的朋友们可以自行翻阅《人民教育出版社数学四年级上册》第四章:三位数乘两位数[2]哦~(不会吧不会吧不会真的有人不如小学生吧(狗头))
先来复习一下:乘法的竖式运算分为三步:(1)位数多的数的每一位和位数少的数的个位相乘,位数多的数的每一位和位数少的数的十位相乘,……,位数多的数的每一位和位数少的数的最高位相乘;(2)按由乘低位到乘高位的以此往前一位写每一次乘得的结果;(3)所有结果相加,得到最终结果。其中,后位向前进位的步骤蕴含在第一步和第三步中。
乍一听是不是很复杂?其实原理大家都懂,只是描述的方式不同。但其实在乘法的竖式运算的步骤是经过人为把乘法运算简化的,其中第二步就是一个简化的结果,涉及到一些运算技巧。那我们如果要让计算机实现第二步,就显得比较困难。如果位数少的数只有一两位还好办,但如果位数少的数的位数也比较多,这时第二步的处理就会显得尤为复杂。
这时候,我们就要回到多位数乘法的本质:多位数乘法实质上是一个多位数的逐位与另一个多位数相乘,并使后位向前进位,最终得到一个新的多位数。而进位的原则就是让每一位的数都处于0~9之间。
我们先把问题拆解:首先我们要有一个存储大数的数组,于是我们定义一个常量SIZE为51,并定义数组int data[SIZE] = {0};。之所以要定义一个长度为51的数组,是为了在后续判断大数位数是否大于50位,若大于50,数组溢出,则提示错误信息,这就需要另外定义一个变量int index = 1;来记录数组元素个数,表示阶乘值的位数;而把数组初始化为0,是为了避免数组未初始化而产生随机值导致错误。而此后应让data[1] = 1;,准备计算阶乘。
接下来我们要定义一个变量int n;,n的值由用户输入,含义为准备计算的阶乘中的最大数。为避免用户输入非法数据,我们需要检测用户输入数据的合法性:
printf("Input n:");
int ret = scanf("%d", &n);
char c = getchar();
while ((ret!=1)||(n<=0)||(c!='\n'))
{
printf("Input int n > 0:");
fflush(stdin);
ret = scanf("%d", &n);
c = getchar();
}
具体原理可参见我之前的文章C语言丨检测用户键盘输入数据的合法性,这里不再赘述。
得到了准备计算的阶乘中的最大数后,我们就可以开始使用循环,计算1~n之间所有数的阶乘值了:
for (i=1; i<=n; i++)//计算阶乘i
{
for (j=1; j<=index; j++)
{
data[j] = data[j] * i;//每一位数字都乘以i
}
}
让第二重循环的执行条件为j<=index而非j<SIZE的原因是index记录的是数组元素的个数,也即该储存的大数的位数,当j>index时,data[j]全部为0,而0乘任何数都为0,就没必要进行多余的计算。
那么使用了这样一个循环条件,必然要在每一次计算中改变index的值。根据我们为index赋值的含义,只要数组中的最高位元素大于等于10,即data[index] >= 10,则意味着该数的位数要大于index,此时则需要执行index++;操作。通过如下代码可实现:
//单独处理最高位,若计算之后的最高位大于或等于10,则位数index加1
while (data[index] >= 10 && index <= SIZE-1)
{
data[index+1] = data[index] / 10;//向最高位进位
data[index] = data[index] % 10;//进位之后的值
index++;//位数index加1
}
其实在处理最高位时,我们已经用到了进位的方法,也即while循环中的前两行,这是进位的核心代码。把index换为k,让k遍历1~index-1的所有值,即可将除最高位之前所有位进位。此段代码应放在处理最高位的前面,因为只有先处理了最高位之前的所有位,才有所谓的“最高位”。
因此我们可以整理得计算阶乘的代码如下:
for (i=1; i<=n; i++)//计算阶乘i
{
for (j=1; j<=index; j++)
{
data[j] = data[j] * i;//每一位数字都乘以i
}
for (k=1; k<index; k++)
{
if (data[k] >= 10)//若阶乘值的每位数字大于或等于10,则进位
{
data[k+1] = data[k+1] + data[k]/10;//当前位向前进位
data[k] = data[k] % 10;//进位之后的值
}
//单独处理最高位,若计算之后的最高位大于或等于10,则位数index加1
while (data[index] >= 10 && index <= SIZE-1)
{
data[index+1] = data[index] / 10;//向最高位进位
data[index] = data[index] % 10;//进位之后的值
index++;//位数index加1
}
}
到此为止,我们已经完成了阶乘计算的操作了,剩下要做的的只有打印阶乘值。这时我们之前定义的SIZE=51以及index就起作用了:若index>50,则表示数组溢出,提示错误信息。
if (index <= SIZE-1)//检验数组是否溢出,若未溢出,则打印阶乘值
{
printf("%d! = ", i);
for (j=index; j>0; j--)//从最高位开始打印每一位阶乘值
{
printf("%d", data[j]);
}
printf("\n");
}
else//若大于50,数组溢出,则提示错误信息
{
printf("Overflow! \n");
exit(1);
}
此处,index的作用再一次体现了出来:我们无需处理最高位前的所有0元素,只需从最高位开始打印阶乘值的每一位数即可。
到此,我们就完成了整个题目的代码编写了。让我们来回顾一下整个历程:逐位相乘->后位向前进位->处理最高位->输出阶乘值。下面给出完整的代码:
#include <stdio.h>
#include <stdlib.h>
#define SIZE 51
int main(void)
{
int data[SIZE]={0};//存储50位数,元素全部初始化为0,不使用data[0]
int index = 1;//数组元素个数,表示阶乘值的位数
int n;//准备计算的阶乘中的最大数
int i, j, k;
data[1] = 1;//初始化,令1!=1
printf("Input n:");
int ret = scanf("%d", &n);
char c = getchar();
while ((ret!=1)||(n<=0)||(c!='\n'))
{
printf("Input int n > 0:");
fflush(stdin);
ret = scanf("%d", &n);
c = getchar();
}
for (i=1; i<=n; i++)//计算阶乘i
{
for (j=1; j<=index; j++)
{
data[j] = data[j] * i;//每一位数字都乘以i
}
for (k=1; k<index; k++)
{
if (data[k] >= 10)//若阶乘值的每位数字大于或等于10,则进位
{
data[k+1] = data[k+1] + data[k]/10;//当前位向前进位
data[k] = data[k] % 10;//进位之后的值
}
//单独处理最高位,若计算之后的最高位大于或等于10,则位数index加1
while (data[index] >= 10 && index <= SIZE-1)
{
data[index+1] = data[index] / 10;//向最高位进位
data[index] = data[index] % 10;//进位之后的值
index++;//位数index加1
}
if (index <= SIZE-1)//检验数组是否溢出,若未溢出,则打印阶乘值
{
printf("%d! = ", i);
for (j=index; j>0; j--)//从最高位开始打印每一位阶乘值
{
printf("%d", data[j]);
}
printf("\n");
}
else//若大于50,数组溢出,则提示错误信息
{
printf("Overflow! \n");
exit(1);
}
}
return 0;
}
[3]
和高精度加法一样,代码的核心就是人工模拟计算的过程,其中包括加/乘法和进位,只要能解决这两个问题,一切高精度算法都可以迎刃而解。
参考文献:
[1]百度百科.
[2]数学四年级上册,人民教育出版社,P48-56.
[3]苏小红 王甜甜 赵玲玲 范江波 车万翔 等编著 王宇颖 主审,C语言程序设计学习指导(第4版),高等教育出版社,P126-127.