php计算分数的阶乘,一道面试题 - PHP 实现阶乘的若干方法

背景

前一阵子在面试腾讯 WXG 的高级 PHP 开发岗,其中一次面试留了道算法题,要求用尽量多的方法实现 PHP 的阶乘,并对比各类方法的优劣。php

最近全部面试都结束了,正好抽点时间写写博客,因而打算分享一下个人解题过程,后面抽空再分享 WXG 的七面面经。laravel

解题

据本人了解,阶乘的实现方法通常能够分为三种,一般意义下的递归和循环各算一种,还有一大类经过一些巧妙的数学方法减小运算次数(尤为是乘法运算次数),进而优化计算效率。面试

若是要考虑到高精度、大整数的阶乘,对于 PHP 语言而言,状况会更复杂一些,好比使用 BCMath 扩展提供的一些方法时,显式的数字与字符串转换操做比较频繁。算法

本文在只考虑 n 为整数的状况下,分别尝试实现上述的几种状况,每种状况给出可用的代码示例,并在文末附上几种方法的综合对比状况。sql

普通递归实现

首先是普通递归实现,根据递归的通用公式 fact(n) = n * fact(n-1) 很容易写出阶乘的计算代码。普通递归实现的优势在于代码比较简洁,和通用公式同样的过程使得代码容易理解。缺点则在于因为须要频繁地调用自身,须要大量的入栈出栈操做,总体的计算效率不高(见文末表格)。shell

function fact(int $n): int

{

if ($n == 0) {

return 1;

}

return $n * fact($n - 1);

}

普通循环实现

普通循环实现有些「动态规划」的味道,但因为中间态变量使用频率低,不须要额外存储空间,因此要比通常的动态规划算法简单。普通递归方法是自顶向下(由 n 到 1)的计算过程,而普通循环是自底向上进行计算。数组

所以相对而言,代码没有上述方法直观,但因为少了频繁的入栈出栈过程,计算效率会高一些(见文末表格)。服务器

function fact(int $n): int

{

$result = 1;

$num = 1;

while ($num <= $n) {

$result = $result * $num;

$num = $num + 1;

}

return $result;

}

自行实现的大整数阶乘

因为 PHP 中 int 类型的范围限制,上述两种方法最多只能精确计算到 20 的阶乘。若是只是考虑到 20 的阶乘的状况,那么用查表法实现会更快:事先计算好 0-20 的阶乘并存储到一个数组中,须要用时查询一次即可。架构

为了可以适应大数的阶乘,获得精确的计算结果,本文基于「普通循环方法」进行改进,使用数组存储计算结果中的每一位(由低到高位),经过相乘进位的方式依次计算每一位的结果。并发

不言而喻,本方法的优势在于能够适用于高精度的大数阶乘场合,缺点就是对于小数阶乘而言,计算过程复杂且速度慢。

function fact(int $n): array

{

$result = [1];

$num = 1;

while ($num <= $n) {

$carry = 0;

for ($index = 0; $index < count($result); $index++) {

$tmp = $result[$index] * $num + $carry;

$result[$index] = $tmp % 10;

$carry = floor($tmp / 10);

}

while ($carry > 0) {

$result[] = $carry % 10;

$carry = floor($carry / 10);

}

$num = $num + 1;

}

return $result;

}

BCMath 扩展方法

BCMath 是 PHP 的一个数学扩展,用于处理字符串表示的数字(任意大小和精度)的数值计算。因为是使用 C 语言实现的扩展,计算速度会比上述自行实现的快。

在本人的笔记本上,一样是计算 2000 的阶乘,自行实现的须要平均 0.5-0.6 秒,使用 BCMath 耗时 0.18-0.19 秒。该方法的缺点主要在于须要安装相应的扩展,属于非代码层面的改动,对于环境管理升级不便的应用而言,可实践性有待商榷。

function fact(int $n): string

{

$result = '1';

$num = '1';

while ($num <= $n) {

$result = bcmul($result, $num);

$num = bcadd($num, '1');

}

return $result;

}

优化算法

在本文开头有提到,优化算法尝试尽量地减小运算次数(尤为是乘法的运算次数)来实现快速阶乘。考虑到对于小整数阶乘而言,最快的算法应该是查表法,时间复杂度为 O(1),因此本小节主要针对大整数的精确阶乘进行讨论和测试。

据了解,目前阶乘优化比较常见的是经过 n! = C(n, n/2) * (n/2)! * (n/2)! 式子进行复杂度优化,而该式子中的亮点主要在于 C(n, n/2) 的优化。考虑到大整数状况下,PHP 语言实现 C(n, n/2) 的效率不高,并且实现的代码可读性比较差(频繁的数字与字符串的显式转换),因此本文用的是另一种比较巧妙的方法。

乘法的计算速度一般要低于加减法运算,经过减小乘法的运算次数能够提升总体运算速度。经过数学概括能够发现,对于 n 的阶乘,能够依次求出比 (n/2)^2 小 一、1+三、1+3+5... 的数值,再依次相乘获得目标值。

该算法的优势在于计算速度较快,而缺点就是实现过程不直观、不易理解。经测试,如下代码计算 2000 的阶乘平均时间为 0.11 秒,大约是普通循环方法的一半耗时。

除了这种方法优化,也有看到其它的相似的思路,好比对 1...n 中的数反复检验是否被 2 整除,记录下被 2 整除的次数 x,并尝试概括出共同的奇数相乘式,最后乘以 2^x 获得结果。

function fact(int $n): string

{

$middleSquare = pow(floor($n / 2), 2);

$result = $n & 1 == 1 ? 2 * $middleSquare * $n : 2 * $middleSquare;

$result = (string)$result;

for ($num = 1; $num < $n - 2; $num = $num + 2) {

$middleSquare = $middleSquare - $num;

$result = bcmul($result, (string)$middleSquare);

}

return $result;

}

综合对比

本文中提到的方法是按照由劣到优的顺序,所以,下列表格中每一行中提到优劣势,主要是和其上一两种方法对比。

表格中「测试耗时」一列的测试环境为我的笔记本,硬件配置为 Dell/i5-8250U/16GB RAM/256GB SSD Disk,软件配置为 Win 10/PHP 7.2.15。

bd1219b89ec6537a0705c7c151c16aef.png

总结

虽然本文将实现方法分为三大类,但其实也能够分为循环和递归两大类,在这两类中分别使用相应的算法优化计算效率。But,整体而言,循环的效率要优于递归。

讲道理,本文中使用的优化算法并非最优解,只是用 PHP 相对好实现,代码易读性也比较高。有兴趣的读者能够谷歌了解更多的骚操做。

点关注,不迷路

好了各位,以上就是这篇文章的所有内容了,能看到这里的人呀,都是人才。以前说过,PHP方面的技术点不少,也是由于太多了,实在是写不过来,写过来了你们也不会看的太多,因此我这里把它整理成了PDF和文档,若是有须要的能够

8c28daabbe63567f5ced2ff794d488c0.png

48a17ead5379c55ea9911c77a81809fb.png

以上内容但愿帮助到你们,不少PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提高,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货须要的能够免费分享给你们,须要的能够加入个人

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值