1. 程式人生 > >階乘之計算從入門到精通-任意階乘計算

階乘之計算從入門到精通-任意階乘計算

摘要:本文討論如何使用一個簡單的演算法計算一個大整數n的階乘,大數採用char陣列儲存,一個元素表示1位10進位制數。本中給出一個完整的計算大數階乘的程式,該程式在迅馳1.7G筆記本上計算10000的階乘大約2.7秒。
 
    在《大數階乘之計算從入門到精通-大數的表示》中,我們學習瞭如何表示和儲存一個大數。在這篇文章中,我們將討論如何對大數做乘法運算,並給出一個可以求出一個整數n的階乘的所有有效數字的程式。大整數的儲存和表示已經在上一篇文章做了詳細的介紹。其中最簡單的表示法是:大數用一個字元型陣列來表示,陣列的每一個元素表示一位十進位制數字,高位在前,低位在後。那麼,用這種表示法,如何做乘法運算呢?其實這個問題並不困難,可以使用模擬手算的方法來實現。回憶一下我們在小學時,是如何做一位數乘以多位數的乘法運算的。例如:2234*8。
  
 
我們將被乘數表示為一個數組A[], a[1],a[2],a[3],a[4]分別為2,2,3,4,a[0]置為0。
Step1: 從被乘數的個位a[4]起,取出一個數字4.
Step2: 與乘數8相乘,其積是兩位數32,其個位數2作為結果的個位,存入a[4], 十位數3存入進位c。
Step3: 取被乘數的上一位數字a[3]與乘數相乘,並加上上一步計算過程的進位C,得到27,將這個數的個位7作為結果的倒數第二位,存入a[3],十位數2存入進位c。
Step4:重複Step3,取a[i](i依次為4,3,2,1)與乘數相乘並加上c,其個位仍存入a[i], 十位數字存入c,直到i等於1為止。
Step5:將最後一步的進位c作為積的最高位a[0]。
 
這一過程其實和小學學過的多位數乘以一位數的珠算乘法一模一樣,學過珠算乘法的朋友還有印象嗎?
 
在計算大數階乘的過程中,乘數一般不是一位數字,那麼如何計算呢?我們可以稍作變通,將上次的進位加上本次的積得到數P, 將P除以10的餘數做為結果的本位,將P除以10的商作為進位。當被乘數的所有數字都和乘數相乘完畢後,將進位C放在積的最前面即可。下面給出C語言程式碼。
一個m位數乘以n位數,其結果為m+n-1,或者m+n位,所以需首先定義一個至少m+n個元素的陣列,並置前n位為0。
 
計算一個m位的被乘數乘以一個n位的整數k,積仍儲存於陣列a

 

 

void mul(unsigned char a[],unsigned long k,int m,int n)
{
	int i;
	unsigned long p;
	unsigned long c=0;
	
	for ( i=m+n-1; i>=n;i--)
	{
		p= a[i] * k +c;
		a[i]=(unsigned char)( p % 10);
		c= p / 10;
	}
	
	while (c>0)
	{
		a[i]=(unsigned char)( c % 10);
		i--;
		c /=10;
	}
}
 
int main(int argc, char* argv[])
{
	int i;
	unsigned char a[]={0,0,0,2,3,4,5};
	mul(a,678,4,3);
	i=0;
	while ( a[i]==0)
		i++;
	for (;i<4+3;i++)
		printf("%c",a[i]+’0’); //由於數a[i](0<=a[i] <=9)對應的可列印字任符為’0’到’9’,所以顯示為i+’0’ 
	return 0;
}


  

從上面的例子可知,在做乘法之前,必須為陣列保留足夠的空間。具體到計算n!的階乘時,必須準備一個能容納的n!的所有位數的陣列或者記憶體塊。即陣列採有靜態分配或者動態分配。前者程式碼簡潔,但只適應於n小於一個固定的值,後者靈活性強,只要有足夠的記憶體,可計算任意n的階乘,我們這裡討論後一種情況,如何分配一塊大小合適的記憶體。

n!有多少位數呢?我們給出一個近似的上限值:n!<(n+12)nn!<(n+12)n,下面是推導過程。
Caes 1: n是奇數,則中間的那個數m=(n+1)/2, 除了這個數外,我們可以將1到n之間的數分成n/2組,每組的兩個數為 m-i 和m+i (i=1到m-1),如1,2,3,4,5,6,7 可以分為數4,和3對數,它們是(3,5),(2,6)和(1,7),容易知道,每對數的積都於小 m2m2,故 n!<mn=(n+12)nn!<mn=(n+12)n的次方。
 
 
     Case 2: n 是個偶數,則中間的兩個數(n/2)和(n/2+1),其它的幾對兒數是(n/2-1,n/2+2),(n/2-2,n/2+3)等等。容易證明,這幾對兒數中,中間的那對數n/2和n/2+1的乘積最大,因為共有n/2對數,故
    n!<(n2(n2+1))n/2=(n2+2n4)n2<((n+12)2)n2=(n+12)nn!<(n2(n2+1))n/2=(n2+2n4)n2<((n+12)2)n2=(n+12)n

     由以上兩種情況可知,對於任意大於1的正整數n, n!<(n+12)nn!<(n+12)n。如果想求出n!更準確的上限,可以使用司特林公式,參見該系列文章《階乘之計算從入門到精通-近似計算之二》。
 
到此,我們已經解決大數階乘之計算的主要難題,到了該寫出一個完整程式的時候了,下面給出一個完整的程式碼。

程式執行

 

 

新建一個VS工程

選擇空專案

新增一個cpp檔案

把程式碼貼進去

遇到 scanf()函式報錯問題

右鍵專案-屬性

圖示選否

 

#include "stdio.h" 
#include "stdlib.h" 
#include "memory.h" 
#include "math.h" 
#include "malloc.h" 

void calcFac(unsigned long n)
{
	unsigned long i, j, head, tail;
	int blkLen = (int)(n*log10(double(n + 1) / 2)); //計算n!有數數字的個數 
	blkLen += 4;  //保險起見,多加4位 

	if (n <= 1)
	{
		printf("%d!=0\n", n);     return;
	}

	char *arr = (char *)malloc(blkLen);
	if (arr == NULL)
	{
		printf("alloc memory fail\n"); return;
	}

	memset(arr, 0, sizeof(char)*blkLen);
	head = tail = blkLen - 1;
	arr[tail] = 1;

	for (i = 2; i <= n; i++)
	{
		unsigned long c = 0;
		for (j = tail; j >= head; j--)
		{
			unsigned long prod = arr[j] * i + c;
			arr[j] = (char)(prod % 10);
			c = prod / 10;
		}
		while (c>0)
		{
			head--;
			arr[head] = (char)(c % 10);
			c /= 10;
		}
	}
	printf("%d!=\n", n);
	for (i = head; i <= tail; i++)
		printf("%c", arr[i] + '0');
	printf("\n");

	free(arr);
}

void testCalcFac()
{
	int n;
	while (1)
	{
		printf("n=?\n");
		scanf("%ld", &n);
		if (n == 0)
			break;
		calcFac(n);
	}
}

int main(int argc, char* argv[])
{
	testCalcFac();
	return 0;
}