最近在做OJ题时遇到了一个求阶乘的题,看到数据范围时惊了我一跳,题目要求输入一个整数N求N!,其中N的范围为0=<N<=10000。我们要知道21!已经超过64位整数的范围了,那10000!将会是多么大的数据。于是我就想到了,大数乘法!
大数运算有很多方式。
一种方式是利用字符串数组存放数据,即每一个字节存放一位10进制数,然后模拟我们手算方式进行计算。
大数加法(10进制法)
char a[100]="111111111111111111111111111";
char b[100]="2222222222222222222";
char c[101]={0};//存放a+b结果
一个n位数加上一个m位数,其结果最多为 max(n,m)+1 位。
我们手算加法时,一般从低位开始算起,这里我们也一样,从字符串末尾开始处理,将结果从C数组的末尾向开头存放,等到运算完成之后,去掉前导的0即可。
for(ic=101-1; ia>=0 && ib>=0; --ib,--ic)
{
c[ic] += a[ia]-48 + b[ib]-48;
c[ic-1] = c[ic]/10; //向高位进1
c[ic] = c[ic]%10 + 48; //如果当前位超过10,则取于是即可,然后+48转换成字符。
}
将a多出来的部分放到c中。
while(ia>=0)
{
c[ic] = c[ic]-48 + a[ia]+48;
c[ic-1] = c[ic]/10; //向高位进1
c[ic] = c[ic]%10 + 48; //如果当前位超过10,则取于是即可,然后+48转换成字符。
++ic;
++ia;
}
同理,将b多出来的部分放到C中。
while(ib>=0)
{
...同a
}
判断最高位是否有进位
if(c[ic] != 0)
{
c[ic]+=48;//转换成字符
}
else
{
++ic; //ic指向的位置为第一个有效字符的位置
}
如果这是一个大数加法函数的话,可以将结果字符数组作为返回值返回,则可以有个技巧可用。
即:return c+ic;
也可以将后面的数据挪到c数组开头。
以上就是大整数加法的大致过程。最需要关注的就是进位部分,即:
c[ic-1] = c[ic]/10; //向高位进1
c[ic] = c[ic]%10 + 48; //如果当前位超过10,则取于是即可,然后+48转换成字符。
大整数加法(10亿进制法)
当然还有一种使用使用整数数组来存放数据,进行运算的。已知一个int类型变量最大有符号值是21亿,10位数,我们可以将超过9位数的部分放到另一个int类型变量中去。也就是说我们的整数变成了10亿进制数了,即逢10亿进1。
假设有两个大整数:
22222123456789
1111987654321
我们用整数数组来存放它们。
int a[10]={123456789,22222};//即a[0]=123456789,a[1]=22222
int b[10]={987654321,1111};//即b[0]=987654321,b[1]=1111
int c[11]={0};
这样存贮,运算起来将变得更放便了。
设maxLen是a,b数组中,使用到的int型变量个数。
for(i=0;i<maxLen;++i)
{
c[i] += a[i]+b[i];
c[i+1]=c[i]/10 000 000 000;
c[i] %= 10 000 000 000;
}
判断最高位是否有进位
if(c[maxLen] != 0) ++maxLen;
这样,很方便就计算完事了。如果要输出结果,我们只需要将最高位数(即 maxLen-1 下标对应的那个数)按%d格式输出(可以忽略前导的0),其余各位转换成9位的字符串输出,就OK了。
例如:
printf("%d",c[maxLen-1]); //先将高位数按整数格式输出,以忽略前导0
for(i=maxLen-2;i>=0;--i)//将后面的数依次转换为5位字符串输出
{
printf("%s",itoa(c[i],9,temp));
}
itoa是我自己写的整数到字符串的转换函数,具体实现,可以参看我后面写的求10000!的程序。
这种做法很简洁,而且计算效率会很高,难点就是如何将输入的数截断,即转换成10亿进制数。这里又将涉及到字符串处理了。
设char str[]="223456789123456789";
我们需要从整数的低位,即字符串末尾,开始分离,这样才能先分离出低位10亿进制数。
int data=0;
int t=0;
int j=0;
for i:=n-1 to 0
do
data += (str[i]-48)*10^(t);
++t;
if(t == 10)
a[j++]=data;
data=0;
t=0;
大整数乘法
我们同样可以模拟手算乘法来计算大整数的乘法。
设int la[],int lb[],分别存放被乘数和乘数,int lc[]存放相乘结果,与上面加法类似,我们需要从数字的低位向高位运算,对于数组来说,就需要从低下标向高下标计算(因为我们的la[0]存放的是最低位。)。
至于每一个int变量应该存放多大的数字最合适,我们需要考虑到两个数相乘结果不能超过int范围,即不能超过10位十进制数。已知n位数与m位数相乘结果最多为 (n+1)+(m+1)-1 = n+m+1位。我们的乘数和被乘数将采用同一个进制,即整数的位数将相同,所以有 n+n+1<10(注意,不能取到10位,因为int最大有符号数为21亿) ,得n<4.5,所以每个int值取4位十进制数最合适,即我们要将数变换成了10000进制数,逢10000进1。
假设有两个大整数:
22222123456789
1111987654321
我们用整数数组来存放它们。
int la[10]={1234,5678,9222,22};//即la[0]=1234,la[1]=5678,la[2]=9222,la[3]=22
int lb[10]={9876,5432,1111,1};//即lb[0]=9876,lb[1]=5432,lb[2]=1111,lb[3]=1
int lc[21]={0};
例:模拟手算乘
for(ib=0; ib<lenb; ++ib)
{
for(ia=0; ia<lena; ++ia)
{
ic=ib+ia;
lc[ic] += la[ia]*lb[ib];
lc[ic+1] += lc[ic]/10000;//注意这里是+=
lc[ic] %= 10000;
}
}
其余的处理,与上面的大整数加法类似。
大整数减法与加法类似,而除法与减法是同一个原理,可以使用减法来实现除法。如果要得到高效的算法,可以参考计算机组成原理,了解2进制的算术运算算法,同理就可以应用到任意进制的运算中。
以上部分,可能有很多漏洞,欢迎大家指点。
求10000以内阶乘的源代码gcc:
#include <stdio.h>
#include <string.h>
#define MAX_DATA_LENGTH 10000 //数据最大长度
//#define TEST
#ifdef TEST
#define MAX_NJ1 20
__int64 nj1[100];
#endif
int njnow[MAX_DATA_LENGTH+1];//记录当前阶乘值
int njlast[MAX_DATA_LENGTH+1];//记录上次阶乘值
/*整数转换成字符串
**a:待转换整数
**bits:要转换的十进制位数
**destStr:目标字符串
*/
char* itoa(int a,int bits,char *destStr)
{
int i;
for(i=bits-1;i>=0;--i)
{
destStr[i]=a%10+48;
a/=10;
}
return destStr;
}
/* 大整数乘法
** now:存放结果
** last:存贮被乘数数组
** len:使用到的数组位数
** n:乘数
** 返回值:使用到的数组位数
*/
int mul(int now[],int last[],int len,int n)
{
/*
设la、lc是大整数,b是32位整数
将大整数看成100000进制数,即逢100000进1,这样可以使用
1
la=la0*10^0+la1*10^5+la2*10^10+...。
lc=la*b=lc0*10^0+lc1*10^5+lc2*10^10...
lc0=la0*b%10^5
lc1=la1*b%10^5+la0*b/10^5
lc2=la2*b%10^5+la1*b/10^5
*/
int i;
for(i=0;i<len;++i)
{
now[i] += last[i]*n;
now[i+1] = now[i]/100000;
now[i] %= 100000;
}
if(now[i] !=0 )++len;
return len;
}
/* 获得n的阶乘值
** pLength:存放用到的数组位数
** 返回值:结果数组指针
*/
int* getNJ(int n,int *pLenth)
{
int i;
int *pNow=njnow;
int *pLast=njlast;
int *pTemp=NULL;
int len=1;
pNow[0]=1;
for(i=1;i<=n;++i)
{
pTemp=pNow;
pNow=pLast;
pLast=pTemp;
memset(pNow,0,sizeof(int)*len);
len=mul(pNow,pLast,len,i);
}
*pLenth=len;
return pNow;
}
#ifdef TEST
/*使用64位整数来计算阶乘
*/
void setNJ()
{
int i;
nj1[0]=1;
for(i=1;i<=MAX_NJ1;++i)
{
nj1[i]=i*nj1[i-1];
}
}
#endif
int main()
{
int n,i;
char temp[10];
int *pNJ=NULL;
int len=0;
#ifdef TEST
setNJ();
#endif
memset(temp,0,sizeof(temp));
while(scanf("%d",&n) == 1)
{
#ifdef TEST
if(n<=MAX_NJ1)
printf("%I64d\n",nj1[n]); //用于比较大数运算结果的正确性
#endif
pNJ=getNJ(n,&len);
printf("%d",pNJ[len-1]); //先将高位数按整数格式输出,以忽略前导0
for(i=len-2;i>=0;--i)//将后面的数依次转换为5位字符串输出
{
printf("%s",itoa(pNJ[i],5,temp));
}
printf("\n");
}
return 0;
}
大整数阶乘2
最近又做了一道大整数阶乘题,浏览量一下高手的代码,发现一些技巧。
输出格式:不需要将整数转换成字符串输出,直接使用整数格式化输出%n.nd(例如:四位整数,%4.4d)。如果整数不足n位,则在前面补0,总共补齐n位。
技巧性的代码,果然很简洁哦!
#include <stdio.h>
#include <stdlib.h>
int a[100];
int factorial(int n)
{
int len=1;
int i,j;
int c;//进位
div_t temp;
a[0]=1;
for(i=1;i<=n;++i)
{
c=0;
for(j=0;j<len;++j)
{
temp=div(a[j]*i+c,10000);//div为求商、余函数
a[j]=temp.rem;//获得商
c=temp.quot;//获得余数
}
if(c>0)//进位
{
a[len++]=c;
}
}
return len;
}
int main()
{
int n;
int len,i;
while(1)
{
scanf("%d",&n);
if(n<0)break;
len=factorial(n);
printf("%d",a[--len]);//输出最高位
for(i=len-1;i>=0;--i)
{
printf("%4.4d",a[i]);
}
printf("\n");
}
return 0;
}
不过,如果是多组测试数据,显然,上面的代码没有太大的优势。所以我们可以,牺牲空间,换时间。
#include <stdio.h>
#include <stdlib.h>
int a[101][100];
int length[101];
int fact(int n)
{
int len=length[n-1];
int i;
int c=0;//进位
div_t temp;
for(i=0;i<len;++i)
{
temp=div(a[n-1][i]*n+c,10000);
a[n][i]=temp.rem;
c=temp.quot;
}
if(c>0)
{
a[n][len++]=c;
}
length[n]=len;
return len;
}
int factorial(int n)
{
if(length[n]==0)
{
factorial(n-1);//先计算n-1的阶乘
length[n]=fact(n);//计算n的阶乘
}
return length[n];
}
int main()
{
int n;
int len,i;
length[0]=1;
a[0][0]=1;
while(1)
{
scanf("%d",&n);
if(n<0)break;
len=factorial(n);
printf("%d",a[n][--len]);
for(i=len-1;i>=0;--i)
{
printf("%4.4d",a[n][i]);
}
printf("\n");
}
return 0;
}