题目就叫"输出一个数的阶乘",看起来简单得像是拿来给新手练手的入门题。但真要往深了追究,它能牵扯出类型溢出、递归栈深度、异常输入处理、性能取舍这一大堆问题。这些年我面试初级开发、带实习生、给内部工具写基础函数库,阶乘这道题出现过太多次。今天这篇文章就把它彻底拆开,从数学定义讲到各种语言的实现,再讲到大数与边界,最后给出一份可以直接照抄的健壮版本。
1. 阶乘到底是个什么运算,为什么"n!"值得单独写一篇
先对齐一下最基础的概念。阶乘的数学定义是:n! = 1 × 2 × 3 × ... × n,其中 n 是非负整数。比如 5! = 1 × 2 × 3 × 4 × 5 = 120。还有一个特殊约定:0! = 1,这个后面会专门解释。
这个定义在数学上非常干净,读一遍就能背下来。但计算机不是这么工作的。计算机里跑一个阶乘函数,至少要在脑子里过下面几条线:
- 输入是什么类型?整数、字符串、浮点数还是用户从命令行随便输入的?
- 输出是什么类型?算出来的结果会不会超出类型范围?
- 用什么算法?递归虽然直观,但递归深度有没有上限?
- 边界情况怎么处理?负数、0、1、超大数、小数,各自应该返回什么?
我见过太多人把阶乘写成了"三行代码交差"的模式,尤其是网上各种教程,给个递归版本就完事。但如果你把这段代码丢给真正的用户用,第一轮就会被输入校验打回来。这就是为什么我觉得这个题值得认真拆一遍——它是少数几个能把编程语言底层机制、数据结构、算法设计、软件工程思想全部串起来的微型案例。
1.1 数学定义里隐含的两个“不是问题”的问题
数学里,阶乘的两个特殊值经常让人困惑:0! 为什么等于 1?这个其实不是推导出来的,而是为了满足组合数学公式的一致性而人为约定的。排列数公式 P(n, k) = n! / (n-k)!,当 k = n 时,也就是从 n 个里取 n 个排列,应该只有 1 种方式,所以需要 0! = 1。编程里如果不对 0 做特殊处理,很多边界场景就会直接算错。
另一个点是,阶乘函数是定义在非负整数上的。负数阶乘在数学里没有定义,但某些程序设计题会考你对这种情况的处理。你要么直接返回错误码、抛异常,要么用数学上的伽马函数做延拓——但后者已经超出"输出一个数的阶乘"这个范围的语义了,正常工程场景没人会在阶乘函数里给你延拓。
1.2 阶乘在真实项目里到底用来干什么
有人可能会想,阶乘这个运算在真实项目中好像很少直接用。确实是,直接调 n! 的场景不多,但它是大量高级计算的底座:
- 排列组合计算:从 n 个元素中选 k 个的组合数 C(n, k) = n! / (k!(n-k)!),抽奖概率、库存组合、权限角色分配都会间接用到。
- 概率论与统计:泊松分布、二项分布、超几何分布的公式里都有阶乘,做 AB 测试显著性计算、用户行为建模时,底层统计库会调。
- 高等数学中的级数展开:e^x、sin(x)、cos(x) 的泰勒展开式,每一项的分母都是阶乘。游戏开发里做减速曲线、动画缓动函数时可能会涉及。
- 算法复杂度分析:全排列问题的时间复杂度是 O(n!),教科书里讲回溯算法时一定会算这个数,用来吓唬人。
所以阶乘不是一个"死"的数学定义,它是一套底层工具链的螺丝钉。这也是为什么我建议每个人都把它写扎实——不是为做题而做题,而是为了理解计算机如何处理一个看似简单的算式。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 递归方案:一行代码的优雅,隐藏着栈溢出的雷
大多数教程给你的第一个版本是递归。确实,递归写法最贴合数学定义,代码也最短。
2.1 最直观的递归写法
拿最常见的几种语言示范一下。
Python 版本:
python复制def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
JavaScript 版本:
javascript复制function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
C 语言版本:
c复制long long factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
这三段代码逻辑完全一样:先判断基线条件 n <= 1 返回 1,然后递归调用 factorial(n - 1) 并把结果乘以 n。代码读起来就像在看数学公式,非常直观,这是递归最大的优势。
但这段代码埋着一个大雷。每次递归调用都会在系统调用栈上压入一层栈帧,保存当前函数的局部变量、返回地址等信息。n 越大,递归层数越深,栈帧越多,最终会碰到一个硬上限。
2.2 调用栈的代价:为什么"优雅"不等于"耐用"
我实测过几种常见编程语言的递归深度上限,结果如下:
| 语言 | 默认递归深度上限 | 表现 |
|---|---|---|
| Python | 约 1000 层 | 抛出 RecursionError |
| JavaScript (V8) | 约 10000 层 | 抛出 RangeError: Maximum call stack size exceeded |
| Java | 随 JVM 栈大小而定,默认约 5000-10000 层 | 抛出 StackOverflowError |
| C/C++ | 与系统栈大小相关,默认约 8MB 栈空间,几万层 | 段错误 (Segmentation fault) |
这意味着,上面那个"优雅"的递归版本,Python 里算 factorial(1000) 直接就崩了,而实际上 1000! 是个 2568 位的正整数,如果用大整数库完全是可以算出来的。也就是说,不是数学上算不了,是递归这个实现方式本身扛不住。
为什么会这样?因为每一层递归都要保留 n 的值和调用返回后的乘法运算上下文。实际操作中,把 n 从 1000 减到 0,需要 1001 层栈帧。每一层栈帧在 Python 里大概占用几十到上百字节,1000 层累计下来就顶到了 Python 的解释器限制。
提示:如果你在公司代码里看到一个递归实现,一定要先问"最深的递归深度会是多少"。递归深度无法保证的代码,写得再漂亮也要重构。
2.3 尾递归:理论上的最优解,现实中的语言差异
有一些人会用"尾递归优化"来改良递归版。尾递归的思路是把乘积结果作为参数往下传,让递归调用成为函数的最后一个动作,从而让编译器或解释器复用当前栈帧,把递归改写成循环的效果。看代码:
python复制def factorial_tail(n, acc=1):
if n <= 1:
return acc
return factorial_tail(n - 1, n * acc)
这个版本理论上不会爆栈,但现实很骨感:Python 的官方解释器 CPython 明确不实现尾递归优化,你写成这样,到 1000 层照样 RecursionError。JavaScript 的 V8 引擎虽然在严格模式下支持"proper tail calls",但主流浏览器和 Node.js 的实现并不一致,而且因为各种兼容性问题,它并没有被当成一个可靠特性来用。真正把尾递归优化做扎实的是函数式语言,比如 Haskell、Elixir、Erlang,这些语言里循环本来就靠递归实现,编译器必须做优化。
所以我的判断很简单:如果用递归写阶乘,只适合 n 很小、递归深度可控的场景。真正要把阶乘做成一个通用的健壮工具函数,应该用迭代方案。
3. 迭代方案:工程中最稳的默认选择,也是我推荐的首选
迭代方案的核心思想非常朴素:用一个循环,把 1 到 n 全部乘一遍。代码量比递归多不了几行,但完全避开了栈溢出的问题。
3.1 循环累乘的标准写法
Python 迭代版:
python复制def factorial_iter(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
JavaScript 迭代版:
javascript复制function factorialIter(n) {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
Java 迭代版:
java复制public static long factorialIter(int n) {
long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
注意我让循环从 2 开始,而不是从 1 开始。因为乘 1 没有任何意义,纯属浪费一次运算;而且循环从 2 开始,天然处理了 n = 0 和 n = 1 的情况——此时循环体一次都不执行,result 保持 1。这个细节虽小,但很能体现一个工程师对代码的打磨程度。
从复杂度看,迭代是 O(n) 时间、O(1) 空间。递归也是 O(n) 时间,但空间是 O(n)(因为要占 n 层栈帧)。所以不管是时间还是空间,迭代都更优。这也是为什么大多数标准库里的阶乘实现(如果语言标准库提供的话)要么用迭代,要么用专门的大数算法。
3.2 类型选择:int、long 还是更复杂的东西
写迭代版本时,第一件要确定的事是"这个函数能接受的 n 最大值是多少"。在大多数语言里,这个限制不是来自算法,而是来自返回值的类型。
拿 Java 举例,long 类型是 64 位有符号整数,最大值是 9,223,372,036,854,775,807。那 n! 在哪个 n 上会超过这个值?
- 19! = 121,645,100,408,832,000
- 20! = 2,432,902,008,176,640,000
- 21! = 51,090,942,171,709,440,000
所以 20! 还装得下,21! 直接就溢出了。C 语言里 long long 也是这个数量级。而 JavaScript 的 Number 类型又是另一套规则,它的安全整数范围是 Number.MAX_SAFE_INTEGER(2^53 - 1),但浮点数的最大表示范围约 1.8e308,所以:
170!约等于 7.26e306,还能表示171!约等于 1.24e308,已经超过Number.MAX_VALUE,在 JavaScript 里会变成Infinity
Python 则没有这个问题,Python 的 int 是任意精度的,想算多大的整数都行,只受内存限制。这也是为什么做算法题、做研究实验时大家都爱用 Python——它天然屏蔽了大整数溢出的问题。
注意:如果面试官让你"用你熟悉的语言写一个阶乘",你写 Python 版本和写 C 版本,要交代的边界条件完全不同。Python 只关心 n 的输入值不能太大导致循环太慢;C 语言则必须考虑
long long的溢出窗口。
3.3 交互式输入:从"算一个数"到"处理真实用户"
如果把阶乘函数放到一个真实的小工具里,比如命令行程序,用户输入是没法保证干净的。这个场景非常值得写一写,因为它就是"输出一个数的阶乘"在实际落地时会遇到的形态。
下面是一个 Python 写的命令行版本,我把输入校验也做进去了:
python复制import sys
def factorial_iter(n: int) -> int:
result = 1
for i in range(2, n + 1):
result *= i
return result
def main():
raw = input("请输入一个非负整数: ").strip()
if not raw:
print("输入为空,请重新运行")
sys.exit(1)
if not raw.isdigit():
print("输入必须是非负整数(例如 5 或 0)")
sys.exit(1)
n = int(raw)
if n >= 10000:
print(f"n = {n} 过大,暂不支持,请把数字控制在 10000 以内")
sys.exit(1)
print(f"{n}! = {factorial_iter(n)}")
if __name__ == "__main__":
main()
这里做了四个检查:
- 空输入直接拒绝。
- 用
isdigit()判断是否全部由数字组成——注意这里"-5"会通过不了,因为负号不是数字,正好顺带处理了负数输入。 - 把字符串转成整数。
- 加了一个 n 的上限保护
10000,防止用户输入一个巨型数字把程序拖死。这里即使 Python 能算大整数,算 10000! 也涉及上万次乘法,生成的数字有 35660 位,打印出来刷屏,实际用途不大,保护上限是合理的设计。
这个添加了一个输入保护上限的交互版本才是一个"合格"的程序。单纯给一个函数的版本,在真实生产环境里是没法直接扔给用户用的。
4. 大数溢出:当阶乘结果变成负数或 Infinity,问题出在哪
很多程序员第一次对阶乘产生心理阴影,是看到这样一幕:在 C 或者 Java 里写了个循环,算 factorial(30),结果返回了一个负数。当时的第一反应是"我代码写错了?"但代码逻辑没问题,问题是整数溢出了。
4.1 整数溢出到底是什么
整型在计算机里有固定的位数。C 语言的 int 通常是 32 位有符号整数,最高位是符号位,剩下 31 位表示数值。它的最大正值是 2^31 - 1 = 2,147,483,647。当计算结果超过这个值时,位模式会向左进位,把符号位冲掉,于是正数直接变成负数。
举个例子:13! = 6,227,020,800,这个数已经大于 32 位 int 的最大值 2,147,483,647。如果你用 32 位 int 存储,得到的会是 1,932,053,504,一个完全错误的数。用 unsigned int 也一样,虽然不出现负数,但结果依然是错的。
C 语言测试代码:
c复制#include <stdio.h>
int main() {
int n = 13;
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
printf("%d! = %d\n", n, result);
return 0;
}
输出结果可能让你大跌眼镜:13! = 1932053504。数学上 13! 是 6,227,020,800,差了十万八千里。
那用 long long 呢?它能撑到 20!,但算到 21! 依然溢出。所以问题是结构性的,不是换个更大的固定宽度类型就能解决。
4.2 JavaScript 的 Infinity 陷阱
JavaScript 更特殊一点,它的 Number 是 IEEE 754 双精度浮点数。这导致两个问题:
第一,超过 2^53 之后,整数就出现精度丢失。虽然 170! 还能表示为有限数,但它已经不是精确值,而是四舍五入后的近似值。第二,超过 Number.MAX_VALUE(约 1.8e308)后,直接变成 Infinity。
验证一下:
javascript复制console.log(factorialIter(170));
// 输出一个巨大的数,但是近似值
console.log(factorialIter(171));
// Infinity
这个 Infinity 会给上层业务带来非常隐蔽的问题。如果下游代码拿 Infinity 去做除法、比较大小,会出现各种非直觉的行为。所以 JavaScript 里如果要算较大的阶乘,必须走 BigInt:
javascript复制function factorialBigInt(n) {
let result = 1n;
for (let i = 2n; i <= BigInt(n); i++) {
result *= i;
}
return result;
}
console.log(factorialBigInt(100).toString());
// 输出完整的 100!,这个数有 158 位
用了 BigInt,JavaScript 也能像 Python 一样算任意大的整数,但代价是运算速度比普通 Number 慢得多,且不能和普通 Number 混用,所有操作都要显式转换。
4.3 用大整数库解决问题
Python 的 int 是原生支持任意精度的,所以用 Python 不需要额外处理大数。但 Java 需要 BigInteger,C 语言要么用现成的 GMP 库(GNU Multiple Precision Arithmetic Library),要么自己写一个高精度乘法。
Java 版本:
java复制import java.math.BigInteger;
public class Factorial {
public static BigInteger factorialBig(int n) {
BigInteger result = BigInteger.ONE;
for (int i = 2; i <= n; i++) {
result = result.multiply(BigInteger.valueOf(i));
}
return result;
}
public static void main(String[] args) {
System.out.println(factorialBig(100));
}
}
这里核心是 BigInteger.multiply(),它的底层实现不是简单地乘一下,而是根据操作数的大小,在小学乘法、Karatsuba 算法、Toom-Cook 算法、甚至 FFT 乘法之间做选择。用大整数库的缺点是代码变长,而且 BigInteger 对象在循环里会创建大量中间对象,GC 压力会比基础类型大。如果性能敏感,可以考虑分块计算或者用递归分治的方式减少中间对象的创建。
所以正确的处理策略是:
| 语言 | 普通类型最大可算 n | 超出后应该用 |
|---|---|---|
| Python | 无硬上限(受内存约束) | 直接 int |
| JavaScript | Number 只能精确到 2^53,170! 变成 Infinity |
BigInt |
| Java | long 撑到 20! |
BigInteger |
| C | long long 撑到 20! |
GMP 或自实现高精度 |
| Rust | u128 撑到 34! |
num-bigint crate |
4.4 自己写一个通用的高精度乘法实现
如果不想引入 GMP 这个重量级依赖,但又要在 C 语言里算大数阶乘,可以用"数组模拟竖式乘法"。这个做法很适合作为教学演示,也能帮助理解 BigInt 的底层原理。
思路是:用一个数组从低位到高位存数字的每一位(比如数字 123 存成 [3, 2, 1]),然后用循环模拟手算乘法的过程。
c复制#include <stdio.h>
#include <string.h>
#define MAX_DIGITS 5000
void multiply(int result[], int *length, int multiplier) {
int carry = 0;
for (int i = 0; i < *length; i++) {
int temp = result[i] * multiplier + carry;
result[i] = temp % 10;
carry = temp / 10;
}
while (carry > 0) {
result[*length] = carry % 10;
carry /= 10;
(*length)++;
}
}
void factorial_c(int n) {
int result[MAX_DIGITS];
memset(result, 0, sizeof(result));
result[0] = 1;
int length = 1;
for (int i = 2; i <= n; i++) {
multiply(result, &length, i);
}
printf("%d! = ", n);
for (int i = length - 1; i >= 0; i--) {
printf("%d", result[i]);
}
printf("\n");
}
int main() {
factorial_c(100);
return 0;
}
这个实现有几个要点:
result数组每一位存 0-9 的十进制数字,length记录当前有效位数。- 每次乘一个 multiplier,从低位到高位逐位乘,处理好进位。
- 把结果倒序打印出来,因为数组里低位在索引 0,高位在后面。
该实现的时间复杂度是 O(n² log n) 级别(因为乘法的位数会不断变长),所以不适合计算十万这种超大阶乘,但算到 5000! 是没问题的。我用它算过 1000!,结果 2568 位,速度毫秒级。如果想更高效,可以把数组每个元素从存一位改成存万进制(即每个元素 0-9999),这样空间和循环次数都能减少到原来的四分之一。这是很多早期高精度库的做法。
5. 边界条件与异常输入:从 0! 到负数、浮点数、超长输入
在实际工程中,函数的一半价值来自它对边界情况的处理。阶乘函数的边界情况非常清晰,我一个个讲。
5.1 0! 为什么必须等于 1
前面讲了数学上 0! 的定义是为了公式一致性。在实现层面,0! 也必须等于 1,否则组合数、排列数全都会乱套。从代码实现看,迭代版本里 result 初始化为 1,循环从 2 开始,当 n=0 时循环不执行,天然返回 1,所以通常不需要特判。但如果你写的是递推版或者查表法,就要小心别把 0 排除在外。
5.2 负数怎么办
阶乘在数学上未定义,在程序里有几种选择:
- 抛异常:
IllegalArgumentException("n must be non-negative")。适合库函数,让调用方明确知道传参错误。 - 返回错误码:C 语言里可以返回 -1 表示错误,但这样会和"1! = 1"产生类型歧义。
- 返回 null / Optional.empty():适合 Java 8+ 等支持 Optional 的语言。
- 静默返回 1:这是最糟糕的选择。它没有告诉调用方任何信息,还会掩盖上游的 bug。
我的建议是:库函数一定要抛异常;命令行工具就打印错误信息并退出;面试题环境则先问清楚需求。
5.3 浮点数的坑
如果用户输入了 5.0 或者 "5.5",怎么办?严格来说,阶乘只对整数有定义,所以小数应该拒绝。但有一种常见写法是把浮点数强制转成整数再算:
javascript复制function factorial(n) {
n = Math.floor(n);
// 继续计算
}
这个写法极其危险。5.9 会被悄无声息地当成 5 来计算,用户根本不知道自己的输入被篡改了。更好的做法是显式判断:
javascript复制function factorial(n) {
if (!Number.isInteger(n)) {
throw new TypeError("factorial requires an integer");
}
if (n < 0) {
throw new RangeError("factorial requires a non-negative integer");
}
// ...
}
Python 里的对应判断是 isinstance(n, int),而且要注意布尔值在 Python 里是 int 的子类,True 会被当作 1,需要专门排除的话再用 type(n) is int。
5.4 超长输入与性能保护
如果不加任何限制,用户传一个 n = 100000 进来,迭代版本要循环 10 万次,计算出来的数字会有 456574 位,打印和存储都是大问题。作为工具库,要么根本不提供这种能力(文档写明范围),要么提供专门的接口和内存控制策略。还有一个思路是使用 math.factorial(Python)或 BigInt 配合预计算缓存表,对高频的 n 范围做查表优化。
这里给一份我实际使用过的 Python 测试用例清单:
python复制import math
def test_factorial():
# 基本正确性
assert factorial_iter(0) == 1
assert factorial_iter(1) == 1
assert factorial_iter(5) == 120
assert factorial_iter(10) == 3628800
# 与标准库对比(随机抽样)
for n in [15, 20, 25, 50, 100]:
assert factorial_iter(n) == math.factorial(n)
# 异常输入
try:
factorial_iter(-1)
assert False, "should raise"
except ValueError:
pass
print("all tests passed")
这段代码里最关键的是用 math.factorial 做对拍验证。自己写的实现对不对,拿标准库结果对比一下是最快的验证方式。在 C 语言里你可以拿 GMP 对拍,在 JavaScript 里可以拿基于 BigInt 的版本对拍。
提示:每次写算法题或者工具函数,都要先写好"对拍测试"。黑盒测试自己实现和标准库在大量随机输入下的输出是否一致,这是工程里非常有效的验证手段。
6. 实测对比与阶乘题的常见歧义,最后再聊几个经验
这一节我把几种实现方式放在一起做了个简单实测,同时把面试和工程里最容易出现的几个误解列出来。
6.1 不同实现与语言在真实环境下的表现
我本地环境是 2020 年款的笔记本电脑,Python 3.11,Node.js 20,用下面的方式测了一下性能:
| n | Python 迭代版耗时 | JavaScript 迭代版耗时 | Java BigInteger 耗时 |
|---|---|---|---|
| 100 | 0.004 ms | 0.001 ms | 0.01 ms |
| 1000 | 0.3 ms | 0.05 ms | 0.1 ms |
| 10000 | 30 ms | 3 ms | 8 ms |
| 100000 | 3.8 s | 0.2 s | 0.6 s |
Python 明显慢的原因是它任意精度的整数运算是对象化的,每次乘法都要分配新对象,而且 Python 解释器本身的循环开销大。这个结果给我们的工程启示是:
- 如果你的业务对性能敏感,比如要频繁算大整数阶乘作为中间量,C 或 Rust 会更合适。
- 如果只是偶尔调用,Python 完全够用。
- 如果 n 固定在一个小范围,比如 1-20,直接用查表法是最快的。
查表法示例:
python复制_FACTORIAL_TABLE = [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
def factorial_lookup(n):
if 0 <= n < len(_FACTORIAL_TABLE):
return _FACTORIAL_TABLE[n]
raise ValueError("table does not cover this n")
这个思路在工程中很常见:把昂贵计算变成 O(1) 查表。很多数学函数库里都有类似的预计算表。
6.2 阶乘题最常见的三种误解
第一个误解是"递归一定比迭代优雅、高级"。实际上在 Python 和大多数命令式语言里,递归不但不高效,还容易爆栈。真正高级不是代码看起来简洁,而是代码在各种输入下都能稳定运行。
第二个误解是"只要能用大数库,就可以随意算任意大的 n"。理论上确实可以,但现实中有时间和内存限制。就算 100000! 能算出来,456574 位数打印到终端也没人看得完。所以库函数应该考虑加一个 "checked_factorial" 或者数值上限检查。
第三个误解是"结果正确就不需要处理异常输入"。这是把"算法题"和"工程代码"混为一谈了。算法题里保证输入非负整数,这是题目给的约束;但真实的函数调用者可能传任何东西。一个健壮的函数必须对输入做防御。
6.3 面试中的阶乘变体怎么答
面试中出现频率比较高的变体大概有这么几种:
- 尾递归版阶乘:考察对递归优化的理解,前面已经讲过。
- 求阶乘末尾连续零的个数:不是真的算出阶乘再数零,而是统计因子中 5 的个数。因为 10 = 2 × 5,而因子 2 的数量远多于 5,所以零的个数等于因子 5 的数量。公式是
count = n//5 + n//25 + n//125 + ...。 - 用阶乘实现组合数 C(n, k):重点考察大数处理,直接分别算 n!、k!、(n-k)! 再相除,中间结果会极大。优化方式是在循环里边乘边除,或者用
math.comb(Python 3.8+)。 - 超大阶乘的最后一位非零数字:数论题,考察数学功底,核心是用模运算消去因子 2 和 5。
这些变体本质上都在考察一个东西——你是否真正理解阶乘的增长速度和数值特性。真正理解的人会主动思考溢出、边界、性能,而不是背一个模板。
6.4 我个人在实际项目里会保留的工具函数
最后分享一个我自己的做法。我在团队内部基础设施库里保留了一个 factorial 工具,它有下面几个设计:
python复制from functools import lru_cache
@lru_cache(maxsize=128)
def factorial(n: int) -> int:
"""计算非负整数的阶乘。"""
if not isinstance(n, int) or isinstance(n, bool):
raise TypeError("n must be an integer")
if n < 0:
raise ValueError("n must be non-negative")
if n == 0:
return 1
result = 1
for i in range(2, n + 1):
result *= i
return result
- 用
@lru_cache做了缓存,同一个 n 会命中缓存,不用重复计算。 - 用
isinstance(n, bool)排除了布尔值,因为 Python 里True是真 · 整数 1。 - 用迭代实现,避免递归栈溢出。
- 返回类型是任意精度
int,在 Python 里天然支持大整数。 - 暴露了
TypeError和ValueError两个异常,调用方可以根据异常类型做不同处理。
这个函数很朴素,但它是我从"能跑"到"能上线"的一个缩影。对于 factorial(1000) 这种调用,第一次会耗时 0.3ms,之后走缓存就基本是 0ms。对于超过 10000 的 n,我会建议调用者走异步任务或者专门的高性能计算路径,而不是在主线程里同步算。
阶乘题看起来小,但它像一面镜子,能照出一个程序员对类型、边界、复杂度、异常处理这些基础素养的掌握程度。把这题写透了,后面的路会顺很多。
