大数阶乘 模拟法实现

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 总结

大数处理思路:

  1. 64位数据类型表示范围内的,可以通过sprintf函数转换成字符串简化代码
  2. 64位数据类型表示范围外的,尝试模拟法等特别的算法

参考链接

https://blog.csdn.net/yu121380/article/details/792640191
https://blog.csdn.net/vaeloverforever/article/details/79263203

posted @ 2020-10-11 00:26  LanceHansen  阅读(63)  评论(0编辑  收藏  举报