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

简介: http://www.cnblogs.com/anderslly/archive/2008/05/19/factorial-algorithms.html 阶乘的定义 阶乘是数学中的一个术语。对于一个非负整数n,n的阶乘指的是所有小于等于n的正整数的乘积,记为n!。

http://www.cnblogs.com/anderslly/archive/2008/05/19/factorial-algorithms.html

阶乘的定义

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

c018852104089c3b86ad5bba9e9223f9

4870ed458b29bbc1925d3466e81e4b0d

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

阶乘的严格定义为:

f06eb9403ca1f410055f763de0b6bd9f

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

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

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

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

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

3843d65ea89dde4c2c1c27f23d2b2fc7

言归正传

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

1 public static long Calculate(int n)
2 {
3     if (n < 0) { throw new ArgumentOutOfRangeException("n必须为非负数。"); }
4     if (n == 0) { return 1; }
5  
6     return n * Calculate(n - 1);
7 }

随着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;以此类推……

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

 1 public static uint[] CalculateLargeNumber(int n)
 2 {
 3     if (n < 0) { throw new ArgumentOutOfRangeException("n必须为非负数。"); }
 4     if (n == 0 || n == 1) { return new uint[] { 1 }; }
 5  
 6     // 数组的最大长度
 7     const int MaxLength = 100000;
 8     uint[] array = new uint[MaxLength];
 9     // 1! = 1
10     array[0] = 1;
11  
12     int i = 0;
13     int j = 0;
14     // valid为当前阶乘值的位数(如5! = 120,此时valid = 3)
15     int valid = 1;
16     for (i = 2; i <= n; i++)
17     {
18         long carry = 0;
19         for (j = 0; j < valid; j++)
20         {
21             long multipleResult = array[j] * i + carry;
22             // 计算当前位的数值
23             array[j] = (uint)(multipleResult % 10);
24             // 计算进到高位的数值
25             carry = multipleResult / 10;
26         }
27         // 为更高位赋值
28         while (carry != 0)
29         {
30             array[valid++] = (uint)(carry % 10);
31             carry /= 10;
32         }
33     }
34  
35     // 截取有效元素
36     uint[] result = new uint[valid];
37     Array.Copy(array, result, valid);
38  
39     return result;
40 } 

用这个方法可以得出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×p1a1...pnan,这样可以得到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

作者: Anders Cui
出处: http://anderslly.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
 

 

 

 

 

 

 

 

 

 

相关文章
|
5月前
|
负载均衡 NoSQL 算法
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
这篇文章是关于Java面试中Redis相关问题的笔记,包括Redis事务实现、集群方案、主从复制原理、CAP和BASE理论以及负载均衡算法和类型。
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
|
3月前
|
算法 Java 数据库
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩分享分库分表的基因算法设计,涵盖分片键选择、水平拆分策略及基因法优化查询效率等内容,助力面试者应对大厂技术面试,提高架构设计能力。
美团面试:百亿级分片,如何设计基因算法?
|
3月前
|
算法 前端开发 Java
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
这篇文章总结了单链表的常见面试题,并提供了详细的问题分析、思路分析以及Java代码实现,包括求单链表中有效节点的个数、查找单链表中的倒数第k个节点、单链表的反转以及从尾到头打印单链表等题目。
47 1
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
|
3月前
|
机器学习/深度学习 算法 Java
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
|
3月前
|
算法 Java 数据库
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩在读者群中分享了关于分库分表的基因算法设计,旨在帮助大家应对一线互联网企业的面试题。文章详细介绍了分库分表的背景、分片键的设计目标和建议,以及基因法的具体应用和优缺点。通过系统化的梳理,帮助读者提升架构、设计和开发水平,顺利通过面试。
美团面试:百亿级分片,如何设计基因算法?
|
3月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
125 2
|
3月前
|
算法 定位技术
数据结构与算法学习九:学习递归。递归的经典实例:打印问题、阶乘问题、递归-迷宫问题、八皇后问题
本文详细介绍了递归的概念、重要规则、形式,并展示了递归在解决打印问题、阶乘问题、迷宫问题和八皇后问题等经典实例中的应用。
66 0
|
4月前
|
机器学习/深度学习 JavaScript 算法
面试中的网红虚拟DOM,你知多少呢?深入解读diff算法
该文章深入探讨了虚拟DOM的概念及其diff算法,解释了虚拟DOM如何最小化实际DOM的更新,以此提升web应用的性能,并详细分析了diff算法的实现机制。
|
5月前
|
消息中间件 存储 算法
这些年背过的面试题——实战算法篇
本文是技术人面试系列实战算法篇,面试中关于实战算法都需要了解哪些内容?一文带你详细了解,欢迎收藏!
|
5月前
|
JavaScript 算法 索引
【Vue面试题二十三】、你了解vue的diff算法吗?说说看
这篇文章深入分析了Vue中的diff算法,解释了其在新旧虚拟DOM节点比较中的工作机制,包括同层节点比较、循环向中间收拢的策略,并通过实例演示了diff算法的执行过程,同时提供了源码层面的解析,说明了当数据变化时,如何通过Watcher触发patch函数来更新DOM。
【Vue面试题二十三】、你了解vue的diff算法吗?说说看

热门文章

最新文章