关于阶乘的两个常见算法及一个相关面试题

阶乘的定义

阶乘是数学中的一个术语。对于一个非负整数 n,n的阶乘指的是所有小于等于n的正整数的乘积,记为n!。例如,

c018852104089c3b86ad5bba9e9223f9

4870ed458b29bbc1925d3466e81e4b0d

符号n!是由Christian Kramp(1760 – 1826)于1808年引入的。

阶乘的严格定义为:

f06eb9403ca1f410055f763de0b6bd9f

并且de99b4b53fe479345eef7a1bafcc0504 ,因为阶乘是针对所有的非负整数。

后者基于一个事实:0个数的乘积为1。这个是很有用的:

  • 递归关系 04f0de9cd29fc21e0bb3bf57a31a760b 适用于n = 0的情况;
  • 这个定义使得组合学(combinatorics )中许多包 含0的计算能够有效。

阶乘的概念相当简单、直接,但它的应用很广泛。在排列、组合、微积分(如泰勒级数)、概率论中都有它的身影。

但我这里最想说的是(与本文主题相关),在计算机科学的教学中,阶乘与斐波那契数列一道经常被选为递归算法的素材,因为阶乘满足下面的递归关系(如 果n ≥ 1):

3843d65ea89dde4c2c1c27f23d2b2fc7

言归正传

下面来考虑如何在程序中计算阶乘。根据阶乘的定义和它满足的递归关系,我们很容易得到这样的算法:

public
 static
 long
 Calculate(int
 n)
{
    if
 (n < 0) { throw
 new
 ArgumentOutOfRangeException("n必须为非负数。"
); }
    if
 (n == 0) { return
 1; }
 
    return
 n * Calculate(n - 1);
}
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

随着n的增大,n!会迅速增大,其速度可能会超出你的想象。如果n不大,这种算法还可以,但对long类型来说,很快就会溢出。对计算器来说,大多 数可以计算到69!,因为70! > 10100

上面这种累积相乘的算法的主要问题在于普通类型所容纳的数值太小,即使是double类型,它的最大值不过是 1.79769313486232e308,即一个309位的数字。

我们来考虑另外一种方案,将乘积的每一位数字都存放在数组中,这样的话一个长度为10000的数组可以存放任何一个10000位以内的数字。

假设数组为uint[] array = new uint[10000],因为1! = 1,所以首先置a[0] = 1,分别乘以2、3,得到3! = 6,此时仍只需要一个元素a[0];然后乘以4得到24,我们把个位数4放在a[0],十位数2放在a[1],这样存放结果就需要两个元素;乘以5的时 候,我们可以这样进行:用5与各元素由低到高逐一相乘,先计算个位数(a[0])4 × 5,结果为20,这样将a[0]置为0,注意要将2进到十位数,然后计算原来的十位数(a[1])2 × 5,结果为10加上刚才进的2 为12,这样十位数是2,而1则进到百位,这样就得到5! = 120;以此类推……

下面给出上述方案的一个实现:

public
 static
 uint
[] CalculateLargeNumber(int
 n)
{
    if
 (n < 0) { throw
 new
 ArgumentOutOfRangeException("n必须为非负数。"
); }
    if
 (n == 0 || n == 1) { return
 new
 uint
[] { 1 }; }
 
    // 数组的最大长度

    const
 int
 MaxLength = 100000;
    uint
[] array = new
 uint
[MaxLength];
    // 1! = 1

    array[0] = 1;
 
    int
 i = 0;
    int
 j = 0;
    // valid为当前阶乘值的位数(如5! = 120,此时valid = 3)

    int
 valid = 1;
    for
 (i = 2; i <= n; i++)
    {
        long
 carry = 0;
        for
 (j = 0; j < valid; j++)
        {
            long
 multipleResult = array[j] * i + carry;
            // 计算当前位的数值

            array[j] = (uint
)(multipleResult % 10);
            // 计算进到高位的数值

            carry = multipleResult / 10;
        }
        // 为更高位赋值

        while
 (carry != 0)
        {
            array[valid++] = (uint
)(carry % 10);
            carry /= 10;
        }
    }
 
    // 截取有效元素

    uint
[] result = new
 uint
[valid];
    Array.Copy(array, result, valid);
 
    return
 result;
} 

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }用这个方法可以得出70!是101位(1.1979 × 10100 ),450!是1001位,而1000!有2568位。需要注意的是,结果数的最高位存放在数组的索引最大的元素中,所 以打印结果时要按正确的顺序。

曾经的一道面试题

我去年在某公司面试的时候曾经遇到这样的一个面试题:100!的后面会带多少个0?

这个问题该怎么分析呢?先找简单的情况来看,5! = 120,后面带着一个0,这个0是怎么产生的?1×2×3×4×5,应该是4×5产生的,而4 = 2×2,我们应该看到如果乘积的因子中包含2和5,就会产生在结尾的0。根据数论知识,我们知道任何大于1的整数都可以分解为若干个素数的乘积,那么如果 我们把一个阶乘按此分解,其形式必然是2a ×5b ×p1 a1 ...pn an , 这样可以得到0的个数为Min(a, b)。这样我们就可以知道面试题的答案了。不过我们再深入看一下。

根据上面的分析,问题可以转化为阶乘分解后包含多少个2和5的因子。直觉告诉我,5的个数一定会少于2的个数,如果能证明这个,那么结论是:0的个 数就是因子5的个数。

假设函数F2(n!)表示n!所包含的因子2的个数,可以证明F2((2n)!) = F2(n!) + n,比如当n = 2时,F2(2!) = 1,F2(4!) = 1 + 2 = 3。令n = 2t ,可以得到F2(2t+1 !) = F2(2t !) + 2t ,再根据数学归纳法,可以得到结论:F2(2n !) = 2n - 1。

类似地,假设函数F5(n!)表示n!所包含的因子5的个数,可以证明F5(5n !) = (5n - 1)/(5 - 1)。有了这两个结论,我们可以进一步确定:F5(n!) <= F2(n!)。(证明过程略,仍使用数学归纳法)

那么结论便是:0的个数就是因子5的个数。F5(5!) = 1,所以5!带1个0,即120;F5(10!) = 2,所以10!带2个0,即3,628,800。

好了,就到这里,在网页上写这些上下标好麻烦!

小结

本文首先给出了阶乘的定义,然后描述了它的两种简单算法,最后讲述了一个与阶乘相关的题目的思路。

关于阶乘还有很多很多理论和算法,还有待我们去学习!

参考资源:

http://en.wikipedia.org/wiki/Factorial

 评论

2008-05-19 12:47 | 张中健      
怎么把一个很大的数全部打印出来?
2008-05-19 12:54 | 张中健      
如:1000000000000!(假定超过double.maxvalue)怎么把 1-1000000000000!全部遍历一次?
2008-05-19 12:57 | Phinecos(洞庭散人)      
@张中健
可以用大数类,可以用字符数组来表示大数,进行加减乘除等运算,
2008-05-19 12:59 | Anders Cui      
@张中健
同意Phinecos(洞庭散人)所说
数组的用法见文中的算法2

2008-05-19 13:13 | CoderZh      
有的时候看到数学公式就头疼啊
算法和数据结构也是我的弱项
2008-05-19 16:46 | 农夫三拳      
常见的与阶乘的算法我知道的有:
1. n!末尾的0的个数 (数5)
2. n!左边第2位数字 (double 加除法)
3. n!用阶乘的和表示 (贪心)
4. n!位数(第二类 Strling数 + log)
etc...
2008-05-19 17:32 | Anders Cui      
@农夫三拳
你说的都不太知道,愿闻其详啊 :)
2008-05-19 18:25 | 怪怪      
好文~!
2008-05-19 18:34 | 张中健      
@Anders Cui
@CoderZh
1000000000000!的结果是 可以计算出来的,重要的是要遍历,打印所有所有的值(1-1000000000000!)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值