大数阶乘 模拟法实现
0x0 一道PTA试题
7-7 阶乘的非零尾数 (20分)
“求 N 阶乘末尾的第一个非零数字”是一道常见的企业笔试题。这里我们略微做个变化,求 N 阶乘末尾的第一个非零 K 位数,同时输出末尾有多少个零。
输入格式:
输入给出一个不超过107的正整数 N 和要求输出的位数 0<K<10。
输出格式:
在一行中输出 N 阶乘末尾的第一个非零 K 位数(注意前导零也要输出)、以及末尾 0 的个数,其间以 1 个空格分隔。
输入样例:
18 5
输出样例:
05728 3
0x1 前置知识
https://baike.baidu.com/item/%E9%98%B6%E4%B9%98/4437932?fr=aladdin#7
求n!末尾0的个数的思路:
一个数 n 的阶乘末尾有多少个 0 取决于从 1 到 n 的各个数的因子中 2 和 5 的个数,而 2 的个数是远远多于 5 的个数的,因此求出 5 的个数即可。
用 n 不断除以5,直到结果为0,然后把中间得到的结果累加。 例如100 / 5 = 20, 20 / 5 = 4, 4 / 5 = 0,则 1 到 100 中因子 5 的个数为 ( 20 + 4 + 0 ) = 24 个。即 100 的阶乘末尾有 24 个 0。
不断除以 5 是因为每间隔 5 个数有一个数可以被 5整除,然后在这些可被 5 整除的数中 每间隔 5 个数又有一个可以被 25 整除,故要再除一次… 直到结果为 0,表示没有能继续被 5 整除的数了。
0x2 long long
纯数值计算版本
#include <stdio.h>
#include <math.h>
long long
fact( int n );
long long
count_mask( long long result );
int
calc_zero( int n );
int
main( int argc, char *argv[] )
{
int n, k;
int zero = 0;
long long result;
long long mask = 1;
scanf("%d %d", &n, &k );
result = fact( n );
zero = calc_zero( n );
result /= pow( 10, zero );
mask = count_mask( result );
for( mask; mask ; mask /= 10 ){
if( mask <= pow( 10, k - 1) ){
printf("%d", result / mask );
}
result %= mask;
}
printf(" %d", zero);
return 0;
}
long long
fact( int n )
{
long long fact = 1;
for( n; n > 1; n-- ){
fact *= n;
}
return fact;
}
long long
count_mask( long long result )
{
long long mask = 1;
while( result >= 9 ){
mask *= 10;
result /= 10;
}
return mask;
}
int
calc_zero( int n )
{
int zero = 0;
while( n > 0 ){
zero += n / 5;
n /= 5;
}
return zero;
}
字符串简化版本
#include <stdio.h>
#include <string.h>
#define LENGTH strlen( result )
long long
fact( int n );
int
main( int argc, char *argv[] )
{
int i;
int n, k;
int zero = 0;
char result[20];
scanf("%d %d", &n, &k );
sprintf( result, "lld", fact( n ) );
while( n > 0 ){
zero += n / 5;
n /= 5;
}
for( i = LENGTH - k - zero; i < SIZE - zero; i++ ){
printf("%c", rsult[i] );
}
printf(" %d", zero);
return 0;
}
long long
fact( int n )
{
long long fact = 1;
for( n; n > 1; n-- ){
fact *= n;
}
return fact;
}
运行结果:当n = 21时,result溢出。
原因:long long最大能够表示的数是263 - 1 = 9223372036854775807, 而21! = 51090942171709440000。
9223372036854775807
51090942171709440000
0x3 模拟法
64位的数据类型只能表示到20!,我们需要一种新的方法来计算这样大规模的数据,模拟法便是一种解决思路
0x4 模拟法I
注:个人能力所限,下文的所有代码均没有通过PTA的OJ,主要讨论模拟法的实现过程,敬请谅解
利用数组可以模拟大数阶乘的过程
/*
**改写自https://blog.csdn.net/yu121380/article/details/79264019
*/
#include <stdio.h>
int
main( int argc, char **argv )
{
int i, j;
int n, k;
int temp, digit = 1;
int zero = 0;
int a[3000];
int num;
a[0] = 1;
scanf("%d %d", &n, &k);
for( i = 2; i <= n; i++ ){
num = 0;
for( j = 0; j < digit; j++ ){
temp = a[j] * i + num;
a[j] = temp % 10;
num = temp / 10;
}
while( num ){
/*
**判断退出循环后,num的值是否为0
*/
a[digit++] = num % 10;
num /= 10;
}
}
while( n > 0 ){
zero += n / 5;
n /= 5;
}
for( i = zero + k - 1; i >= zero; i-- ){
printf("%d", a[i]);
}
printf(" %d", zero);
return 0;
}
0x05 realloc
realloc函数可以用于修改一个已分配的内存空间,但realloc使用起来十分危险,有内存溢出和野指针的风险,决定不采纳
0x06 模拟法II 链表
这个版本使用了单链表的数据结构来存储阶乘数
#include <stdio.h>
#include <stdlib.h>
typedef struct _node{
int value;
struct _node *next;
}Node;
#define DEBUG 0
int
main( int argc, char **argv )
{
int i;
int n, k;
int offset = 0;
int fact = 1;
int result[10];
/*
**由于单链表不能从中间开始访问,故采用数组来存储零之前的10位,逆序输出即得到答案
*/
int remain;
Node *p, *tp, *q;
p = ( Node* )malloc( sizeof( Node ) );
p->next = NULL;
p->value = 1;
scanf("%d %d", &n, &k );
while( fact++ < n ){
for( tp = p ; tp != NULL; tp = tp->next ){
tp->value *= fact;
}
for( tp = p, remain = 0; tp != NULL; tp = tp->next ){
/*
**头插法建表
*/
tp->value += remain;
remain = tp->value / 10;
tp->value %= 10;
if( tp->next == NULL && remain != 0 ){
q = ( Node* )malloc( sizeof( Node ) );
q->value = 0;
q->next = tp->next;
tp->next = q;
}
}
#if DEBUG
for( tp = p; tp != NULL; tp = tp->next ){
printf("%d", tp->value );
if( tp->next == NULL ){
printf("\n");
}
}
#endif
}
while( n > 0 ){
offset += n / 5;
n /= 5;
}
for( i = 0; i < offset; i++ ){
/*
**指针移动到第一个非零数,同时把末尾的节点释放
*/
tp = p->next;
free(p);
p = tp;
}
for( i = 0, tp = p; i < 10; i++ ){
result[i] = tp->value;
tp = tp->next;
}
for( i = k - 1; i >= 0; i-- ){
printf("%d", result[i] );
}
for( tp = p; tp != NULL; tp = tp->next ){
tp = p->next;
free(p);
p = tp;
}
free(p);
printf(" %d", offset);
return 0;
}
0x07 总结
大数处理思路:
- 64位数据类型表示范围内的,可以通过sprintf函数转换成字符串简化代码
- 64位数据类型表示范围外的,尝试模拟法等特别的算法
参考链接
https://blog.csdn.net/yu121380/article/details/792640191
https://blog.csdn.net/vaeloverforever/article/details/79263203