前一陣子在面試騰訊 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 是 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。
雖然本文將實現方法分爲三大類,但其實也能夠分爲循環和遞歸兩大類,在這兩類中分別使用相應的算法優化計算效率。But,整體而言,循環的效率要優於遞歸。
講道理,本文中使用的優化算法並非最優解,只是用 PHP 相對好實現,代碼易讀性也比較高。有興趣的讀者能夠谷歌瞭解更多的騷操做。
好了各位,以上就是這篇文章的所有內容了,能看到這裏的人呀,都是人才。以前說過,PHP方面的技術點不少,也是由於太多了,實在是寫不過來,寫過來了你們也不會看的太多,因此我這裏把它整理成了PDF和文檔,若是有須要的能夠
更多學習內容能夠訪問【對標大廠】精品PHP架構師教程目錄大全,只要你能看完保證薪資上升一個臺階(持續更新)
以上內容但願幫助到你們,不少PHPer在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那裏入手去提高,對此我整理了一些資料,包括但不限於:分佈式架構、高可擴展、高性能、高併發、服務器性能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點高級進階乾貨須要的能夠免費分享給你們,須要的能夠加入個人 PHP技術交流羣