我们知道一般编译器的int类型最大值约为21亿,至多能存储10位整数,大多数情况下足够我们使用了。但有时我们会遇到需要存储和运算更大整数的情况,例如斐波那契数列。我们通常把这类存储和运算超大整数问题称为大数运算或超长整数运算。
目录
一、从斐波那契数列说起
1.引例
题目: 开始,有一对小兔子。一个月后,小兔子变成大兔子,并且开始怀孕。两个月后,生出一对小兔子,这时共有两对兔子(一对大兔子,一对小兔子),同时大兔子又开始怀孕。三个月后,以前出生的小兔子变成大兔子,以前怀孕的大兔子又生出一对小兔子,这时共有三对兔子(两对大兔子,一对小兔子),同时大兔子又全部怀孕。…… 假设6年内都没有兔子死亡。
编写程序,输入n,计算n个月后,有多少对兔子。要求至少能计算6年内的兔子对数。
分析: 由题目可知,小兔子成长为大兔子需要一个月,一对大兔子从怀孕到生下一对小兔子需要一个月。开始,有一对小兔子;设n-1个月后,共有s对小兔子,b对大兔子(大兔子全部怀孕);则n个月后,就有b对小兔子,b+s对大兔子(上个月的小兔子全部变成大兔子了)。列表分析:
n个月后 | 0 | 1 | 2 | 3 | 4 | 5 | … |
小兔子(对) | 1 | 0 | 1 | 1 | 2 | 3 | … |
大兔子(对) | 0 | 1 | 1 | 2 | 3 | 5 | … |
总对数 | 1 | 1 | 2 | 3 | 5 | 8 | … |
很多同学的代码会这么写:
#include <stdio.h>
int main() {
int n=0,i=0;
int b=0,s=1,t=0; //b为大兔子对数,s为小兔子对数,t为中间变量
scanf("%d",&n); //n个月后
for(i=1;i<=n;i++){
t=b; //记录上个月大兔子对数
b=b+s; //本月大兔子对数=上月大兔子对数+上月小兔子对数
s=t; //本月小兔子对数=上月大兔子对数
}
printf("%d\n",b+s);
return 0;
}
注意,题目要求能计算6年内的兔子对数,但是n取46时,变量就已经溢出了。
2.解决方法一(long long与_int64)
造成上述问题的主要原因是int类型变量溢出了,简单来说就是存不下这么大的值,一般int占4个字节,最大值约为21亿,而n=72时,兔子的对数远大于21亿。要解决上述问题,比较简单的方法就是用long long变量来存储这些更长的整数。
CodeBlocks编译器下
//算法一(CodeBlocks编译器)
#include <stdio.h>
int main() {
int n=0,i=0;
long long b=0,s=1,t=0;
scanf("%d",&n); //n个月后
for(i=1;i<=n;i++){
t=b; //记录上个月大兔子对数
b=b+s; //本月大兔子对数=上月大兔子对数+上月小兔子对数
s=t; //本月小兔子对数=上月大兔子对数
}
b=b+s; //n个月后兔子总对数
printf("%lld\n",b);
return 0;
}
VC++6.0编译器下
//算法一(VC++6.0编译器)
//由于VC++6.0未定义long long类型,所以我们使用一个相似的类型叫_int64
#include <stdio.h>
int main() {
int n=0,i=0;
_int64 b=0,s=1,t=0; //b为大兔子对数,s为小兔子对数,t为中间变量
scanf("%d",&n); //n个月后
for(i=1;i<=n;i++){
t=b; //记录上个月大兔子对数
b=b+s; //本月大兔子对数=上月大兔子对数+上月小兔子对数
s=t; //本月小兔子对数=上月大兔子对数
}
b=b+s; //n个月后兔子总对数
printf("%I64d\n",b);//I64的I为字母i的大写
return 0;
}
3.斐波那契数列
经研究,我们发现兔子的对数实际上构成了一个斐波那契数列——n个月后兔子的对数等于前两个月兔子对数之和。于是有了第二种算法。
斐波那契数列:
//算法二(VC++6.0编译器)
//注意本代码中的n不是斐波那契数列中的n
//题目等价于: 输入一个非负整数n,求斐波那契数列的第n+1项
#include <stdio.h>
int main() {
int n=0,i=0;
_int64 fn=0,f1=0,f2=0;
scanf("%d",&n); //n个月后
f1=0; //f(0)
f2=1; //f(1)
fn=1; //n=0时,对应f(1)
for(i=1;i<=n;i++){
fn=f1+f2;
f1=f2; //f1为前一项
f2=fn; //f2为后一项
}//计算f(n+1)
printf("%I64d\n",fn);//I64的I为字母i的大写
return 0;
}
4.更好的解决方法
不管是算法一还是算法二,都未从根本上解决如何存储超长整数的问题,只是因为看问题的角度不同所以算法有一些差别。并且,我们从解决方法一中可以看出,long long类型和_int64类型的移植性不是很好,codeblocks支持long long但不支持_int 64,vc支持_int64但不支持long long。
为了使程序有更好的可移植性,我们考虑使用多个int类型变量,来存储一个较长的整数,每个int变量保存较长整数的一部分。
例如我们用两个int变量a1、a2,来存储较长整数123450123456789。其中a1存储较长整数低位,a2存储较长整数的高位,即a2=123450,a1=123456789。输出时先判断高位数a2是否大于0,如果大于0则打印a2,再打印低位数a1。
#include <stdio.h>
#define M 1000000000//M进制
/*
我们可以把这种存储超长整数的方法视为存储一个M进制整数。
虽然我们在存储和运算中用M进制来表示超长整数,但最后的输出却是按10进制表示的。由于int类型最大值约为21亿,如果M取值为20亿,那么这个超长整数的每个分量的最高位只能取0/1/2,这样转化成10进制时会比较麻烦。
为了表示方便,M的值不应大于10亿,最好取10、100、1000、...、1亿、10亿这样的值。
*/
int main() {
int n=0,i=0;
int over_flag=0;//进位标志
int fn1=0,fn2=0,a1=0,a2=0,b1=0,b2=0;//1为低位,2为高位
scanf("%d",&n);
a1=0; //f(0)
b1=1; //f(1)
fn1=1; //n=0时,对应f(1)
for(i=1;i<=n;i++){
//f(i+1)=a+b
over_flag=0;
//计算低位
fn1=a1+b1;
if(fn1>=M){
fn1=fn1-M;
over_flag=1;//进位
}else{
over_flag=0;
}
//计算高位
if(over_flag==1)
fn2=a2+b2+1;
else
fn2=a2+b2;
/*--------溢出检测-------*/
if(fn2<0||fn2>=M){
printf("overflow!\n");
return 1;
}
/*--------溢出检测end----*/
//a=b
a1=b1;
a2=b2;
//b=f(i+1)
b1=fn1;
b2=fn2;
}
//输出fn
if(fn2>0)
printf("%d",fn2);
printf("%d\n",fn1);
return 0;
}
二、无符号超长整数运算
如果你学习了数组,可以考虑使用int数组或字符数组来存储超长整数。
在下面的程序中,我们使用长度为N的int数组来保存无符号整数,其中数组下标为0的元素存储整数的最低4位。M为数组元素的上限,为了方便乘法的实现(int类型最多能表示10位有效数字),同时使数组的每个元素保存尽可能多的整数,所以M取10000。
#include <stdio.h>
#define M 10000//涉及乘法,M应<=10000
#define N 10//数组长度
//函数声明
void output(int num[]);
void input(int num[]);
int add(int num1[],int num2[],int sum[]);
int cmp(int num1[],int num2[]);
int sub(int num1[],int num2[],int difference[]);
int mul(int num1[],int num2[],int product[]);
int div(int num1[],int num2,int quotient[]);
int div2(int n1[],int n2[],int quotient[]);
/*===无符号超长整数 输出函数===*/
void output(int num[]){
int i=0,j=0,flag=0;
for(i=N-1;i>=0;i--){
for(j=M/10;j>=10&&(num[i]<j)&&flag;j=j/10)
printf("0");
if(num[i]>0)
flag=1;//num>0
else if(num[i]<0){
printf("output():Negative numbers are forbidden!\n");
return;
}
if(num[i]>=0&&flag)
printf("%d",num[i]);
}
if(flag==0)
printf("0");//num=0
return;
}
/*===无符号超长整数 输入函数===*/
void input(int num[]){
int i=0,j=0,sum=0,length=0,counts=0;
char s[10*N]={0},ch;
if(scanf("%s",s)!=EOF){
while((ch=getchar())!='\n'&&ch!=' '&&ch!=EOF);
}//清除缓冲区的回车/空格
for(length=0;s[length]!='\0'&&length<10*N;length++);
for(i=0;i<N;i++)
num[i]=0;//初始化
j=1;
for(i=length-1;i>=0;i--){
sum=sum+(s[i]-'0')*j;
j=j*10;
if(j==M){
if(counts>=N){
printf("input():Array overflow!\n");
return;
}
num[counts]=sum;
counts++;
sum=0;
j=1;
}
}
if(j>1){
if(counts>=N){
printf("input():Array overflow1!\n");
return;
}
num[counts]=sum;
counts++;
}
return;
}
/*
===无符号超长整数 加法===
sum=num1+num2
*/
int add(int num1[],int num2[],int sum[]){
int i=0,carry=0;//carry:进位
for(i=0;i<N;i++){
if(num1[i]<0||num2[i]<0){
printf("add():Negative numbers are forbidden!\n");
return -2;
}
sum[i]=num1[i]+num2[i]+carry;
if(sum[i]>=M){
sum[i]=sum[i]-M;
carry=1;//进位
}else{
carry=0;
}
}
//溢出检测:
if(carry==1){
printf("Add Positive Overflow!\n");
return -1;//错误
}
return 1;
}
/*
==整型数组 大小比较==
如果num1>num2返回1,<返回-1,=返回0
*/
int cmp(int num1[],int num2[]){//
int i=0;
for(i=N-1;i>=0;i--){
if(num1[i]<num2[i]){
return -1;
}else if(num1[i]>num2[i])
return 1;
}
return 0;
}
/*
===无符号超长整数 减法===
difference=num1-num2
*/
int sub(int num1[],int num2[],int difference[]){//difference=num1-num2
int i=0,borrow=0;//borrow:借位
if(cmp(num1,num2)==-1){
printf("sub():num1 < num2 is error!\n");
return 0;
}
for(i=0;i<N;i++){
if(num1[i]<0||num2[i]<0){
printf("sub():Negative numbers are forbidden!\n");
return -2;
}
difference[i]=num1[i]-num2[i]+borrow;
if(difference[i]<0){
difference[i]=difference[i]+M;
borrow=-1;//借位
}
else{
borrow=0;
}
}
//溢出检测:
if(borrow==-1){
printf("Sub Negative Overflow!\n");
return -1;//错误
}
return 1;
}
/*
===无符号超长整数 乘法===
product=num1*num2
因为是高精度整数*高精度整数,需要考虑数组长度N是否足够长
*/
int mul(int num1[],int num2[],int product[]){
int i=0,j=0,k=0,t=0,carry=0;
int m[N][N]={0},product0[N]={0};
for(i=0;i<N;i++){
if(num1[i]<0||num2[i]<0){
printf("mul():Negative numbers are forbidden!\n");
return -2;
}
}
for(i=0;i<N;i++){
t=0;
carry=0;
for(j=0;j<N;j++){
t=num2[i]*num1[j]+carry;
m[i][j]=t%M;
carry=t/M;
}
}
for(i=0;i<N-1;i++){
if(i>0&&m[i][N-1]>0){
printf("Mul Positive Overflow!\n");
return -1;
}
//m[i]左移i次:
for(j=0;j<i;j++){
//左移
for(k=N-1;k>0;k--)
m[i][k]=m[i][k-1];
m[i][0]=0;
}
}
for(i=0;i<N;i++){
if(add(product0,m[i],product0)!=1){
printf("Mul Positive Overflow!\n");
return -1;
}
}
for(i=0;i<N;i++)
product[i]=product0[i];
return 1;
}
/*
===无符号超长整数 除法===
quotient=num1/num2
高精度整数/低精度整数
*/
int div(int num1[],int num2,int quotient[]){
int i=0,t=0,remain=0;
if(num2<=0|num2>M){
printf("div():num2 is illegal!\n");
return -2;
}
for(i=0;i<N;i++){
if(num1[i]<0){
printf("div():Negative numbers are forbidden!\n");
return -2;
}
}
for(i=N-1;i>=0;i--){
t=num1[i]+remain;
quotient[i]=t/num2;
remain=(t%num2)*M;
}
return 1;
}
/*
===无符号超长整数 除法===
quotient=n1/n2
高精度整数/高精度整数
*/
int div2(int n1[],int n2[],int quotient[]){
int i=0,flag=1,counts=0,times=0;
int t[N]={0},m[N]={0},num1[N]={0},num2[N]={0};
for(i=0;i<N;i++){
if(n1[i]<0||n2[i]<0){
printf("div2():Negative numbers are forbidden!\n");
return -2;
}
num1[i]=n1[i];
num2[i]=n2[i];
quotient[i]=0;
}
//检查除数是否为0
for(i=N-1;i>=0&&(num2[i]==0);i--);
if(i<0){
printf("除数不能为0\n");
return -1;
}
//被除数小于除数
if((flag=cmp(num1,num2))==-1){
quotient[0]=0;
return 1;
}
//被除数等于除数
else if(flag==0){
quotient[0]=1;
return 1;
}
/*---计算除数最大移位---*/
if(num2[N-1]>0)
flag=0;//flag=0表示除数不能移位
for(counts=0;counts<N&&(flag>0);){
//num2左移4位:
for(i=N-1;i>0;i--)
num2[i]=num2[i-1];
num2[0]=0;
counts++;//左移次数+4,counts+1
flag=cmp(num1,num2);
}
counts=counts*4;
for(i=0;i<counts&&(cmp(num1,num2)<0);i++){
if(div(num2,10,num2)!=1) return -1;//如果除数num2大于被除数num1,右移1位
}
counts=counts-i;//此时counts为除数左移位数
/*--计算除数最大移位end--*/
for(;counts>=0;counts--){
if(cmp(num1,num2)>=0){
for(times=0;times<M&&(cmp(num1,num2)>=0);times++){
sub(num1,num2,num1);
}
for(i=0;i<N;i++)
{t[i]=0;m[i]=0;}
t[0]=times;
m[0]=10;
for(i=0;i<counts;i++){
if(mul(t,m,t)!=1)
return -1;
}
if(add(quotient,t,quotient)!=1) return -1;
}
if(div(num2,10,num2)!=1) return -1;//除数右移1位
}
return 1;
}
int main(){
char ch[2];
int f1[N]={0},f2[N]={0},fn[N]={0};
//输入:
input(f1);
scanf("%s",ch);
input(f2);
if(ch[1]!='\0'){
printf("Operator is wrong!\n");
return 1;
}
//运算:
switch(ch[0]){
case '+':if(add(f1,f2,fn)!=1)return 1;break;
case '-':if(sub(f1,f2,fn)!=1)return 1;break;
case '*':if(mul(f1,f2,fn)!=1)return 1;break;
case '/':if(div2(f1,f2,fn)!=1)return 1;break;
default:printf("Operator is wrong!\n");return 1;
}
//输出:
output(f1);
printf("%c",ch[0]);
output(f2);
printf("=");
output(fn);
return 0;
}
关于除法部分的代码,如果觉得不好理解可以看我的另一篇文章:大数除法(超长整数运算除法器)详解
三、拓展
1.有符号超长整数
我们可以在整型数组中存储符号位,比如int a[N]数组,用a[0]存储符号位,a[0] == -1时表示负数、a[0] == 1时表示正数、a[0] == 0时表示0。同时也要注意,与无符号超长整数相比,有符号超长整数在运算时需要确定它的符号。
有符号超长整数运算与无符号超长整数运算转化的例子 :
设a, b(a>b>=0)为无符号超长整数。对于有符号超长整数运算,-a+b可以转变为-(a-b),其中(a-b)为无符号超长整数运算;-b-a可以转变为-(a+b),其中(a+b)为无符号超长整数运算;(-a)*(-b)可以转变为a*b等等。
2.超精度浮点数
可以考虑把超精度浮点数转换成超长整数进行运算。对于乘法除法,直接将浮点数整个转化为整数,同时计算积或商的小数点位置。对于加法减法,由于小数点位置固定,只需将浮点数的整数部分和小数部分分开计算,注意小数位数较少的可能要补0,还要考虑小数部分进位的问题。