大数阶乘 模拟法实现
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

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号