news 2026/9/30 12:53:02

找规律别硬算:阶乘尾零、怪数与水仙花数的Python解法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
找规律别硬算:阶乘尾零、怪数与水仙花数的Python解法

day5,我给自己安排了三个数字相关的小练习:求阶乘结果末尾0的个数、找“怪数”、找满足条件的abc三位数。这三个题放在一起,不是因为它们难,而是因为它们都在逼我搞清楚一件事——别让计算机硬算,先找规律。这也是我在这个学习阶段最想练的东西。

说句实话,这三个题如果是第一次见,很容易写出“能跑但很笨”的代码:先把阶乘算出来、把数字拆开、挨个试。跑通不难,难的是跑得稳、快、有底气。这篇文章就把我自己的思路、推导过程、代码实现和踩过的坑完整记一遍,后面的人可以直接照着抄,也可以拿来做参考。

1. 三个题目的庐山真面目

1.1 阶乘尾部0的个数到底在问什么

题目描述通常是这样:输入一个正整数 n,求 n!(1×2×3×...×n)的十进制表示中,末尾连续有多少个 0。注意,这里说的是“末尾连续”的0,不是整个数字里所有0的个数。比如 10! = 3628800,末尾有两个连续的0,但数字中间也有一个0,那个不计入答案。

这个题第一反应是先把阶乘算出来,然后转成字符串从右往左数0。但是只要把 n 稍微调大一点,比如 n=100,100! 已经是一个超过150位的数;n=1000 时更是天文数字。要是题目再狠一点,n 给到 10^7,那这个思路基本等于自杀。所以必须从数学上找规律。

规律的核心是:十进制下一个数末尾的0,来自因子10,而10 = 2 × 5。在 n! 的连乘过程中,因子2的个数远远多于因子5的个数。比如从1到10,偶数有5个,能贡献很多2;但5的倍数只有2个。所以决定末尾0数量的是因子5的总个数,不是因子2。

1.2 “怪数”究竟哪里怪

“怪数”这个说法在不同题库里定义可能不一样。我这里按最常见的一种练习定义来说:一个正整数,如果它等于它各位数字的阶乘之和,就称为“怪数”。什么意思?看例子:

145 = 1! + 4! + 5! = 1 + 24 + 120 = 145

这种数自己等于自己,但又好像是被某种规则“算”出来的,所以叫“怪数”挺贴切。搜索的时候会发现它还有标准名字,英文叫 factorion,中文一般叫“阶乘数”或者“数字阶乘和数”。

已知的正整数范围内,这类数非常少。如果按 0! = 1 的约定来计算,并且从正整数开始算,一共只有四个:1、2、145、40585。你可以先自己验证 40585:4! + 0! + 5! + 8! + 5! = 24 + 1 + 120 + 40320 + 120 = 40585,确实成立。注意这里 0! = 1,很多人会在这个细节上翻车。

1.3 abc数字,其实就是水仙花数

“abc数字”是我自己起的小名,题目原意一般叫“水仙花数”:一个三位数 abc,它的百位数字是 a,十位是 b,个位是 c,满足 a³ + b³ + c³ = 100a + 10b + c。也就是说,这个三位数等于它每一位数字的三次方之和。

举最经典的例子:153 = 1³ + 5³ + 3³ = 1 + 125 + 27 = 153。

三位水仙花数一共有四个:153、370、371、407。很多人都会背,但这里我建议不要只是背答案,而是把枚举逻辑练熟,因为后面做四位、五位甚至更多位的自恋数都可以复用同一套思路。

2. 数学原理:为什么这样算才靠谱

2.1 尾零:统计因子5的来龙去脉

前面说了,末尾0的个数等于 n! 中因子5的总个数。那么怎么统计呢?一种直观做法是遍历 1 到 n,对每个数不断除以5,累加能除几次。这个做法正确,但不够优雅,复杂度是 O(n log n)。更好的做法是直接用整数除法:

count = n // 5 + n // 25 + n // 125 + ...

为什么这么加?因为 n//5 统计的是1到n里有多少个5的倍数,每个至少贡献一个因子5;n//25 统计有多少个25的倍数,每个额外再贡献一个因子5;n//125 继续统计有多少个125的倍数,每个再额外贡献一个因子5。以此类推,直到除数大于 n 为止。

我拿 n=25 手工算一遍:

  • n//5 = 5,对应 5、10、15、20、25 这5个数;
  • n//25 = 1,对应25,它包含两个因子5,已经在 n//5 里算了一个,这里补上第二个。

所以总数是 5 + 1 = 6。你可以验证 25! 末尾确实有6个0。

再看一个容易错的地方:像50这个数,它等于 2×5×5,贡献两个因子5。在 n//5 那层它被算了一次,在 n//25 那层因为它大于等于25,又被补了一次,所以最终统计为2。这就是每层都加的原因,不需要担心重复,因为每一层加的本来就是对更高次幂的“补差”。

这种做法的复杂度只有 O(log₅ n),n 再大也就是几十次循环,非常稳。

2.2 怪数:为什么搜索上界可以定在约254万

如果不知道“怪数”的总数,直接无脑从1循环到无穷大,程序永远跑不完。所以要先确定一个合理的搜索上界。

对于一个 k 位数,它本身的最小值是 10^(k-1)(比如三位数最小是100)。它的各位数字阶乘之和最大是什么情况?每一位最多是9,那一整项最大是 k 个 9!,即 k×9!。当 k 增大到一定程度后,k×9! 会小于 10^(k-1),也就是说,任何 k 位数自身都比它能算出来的阶乘和还要大,那就不可能存在解。

我算几个数看看:

  • k=1,最大阶乘和是 9! = 362880,显然比一位数大;
  • k=7,最大值 7×9! = 2540160,而七位数最小是1000000,所以七位数界面还有可能;
  • k=8,最大值 8×9! = 2903040,而八位数最小已经是10000000,2903040 < 10000000,从八位数开始就不可能再有解了。

所以只需要搜索到七位数里的最大值 7×9! = 2540160 即可。这也解释了为什么最终的结果只有四个,因为在这个范围内符合条件的数就是1、2、145、40585。

这个推导过程的价值不在于背一个上限,而是给你一个思路:很多“找特殊数字”的题目,都可以通过“自身大小”vs“构造表达式最大值”来圈定范围。

2.3 水仙花数的枚举与位数规则

三位水仙花数最简单的方法就是枚举。因为三位数一共就900个,从100到999,逐个判断就行,复杂度根本不是问题。

判断一个三位数 n 是否为水仙花数,需要拆出百位、十位、个位:

  • 百位 a = n // 100
  • 十位 b = (n // 10) % 10
  • 个位 c = n % 10

然后比较 a³ + b³ + c³ 和 n 是否相等。

这里有一个细节:百位 a 不可能为0,因为三位数最小是100。但十位和个位可以是0,比如407,b=0,这时 b³=0,不影响判断。

如果你不想用单层循环拆位,也可以用三重循环直接枚举百位、十位、个位:

for a in range(1, 10): for b in range(0, 10): for c in range(0, 10): n = a100 + b10 + c if a3 + b3 + c**3 == n: print(n)

两种方式本质一样。三重循环可能更直观一点,但单层循环更能练习“如何从数字里提取每一位”,建议都写一遍。

3. 完整代码实现与运行实录

3.1 求阶乘尾部0个数的实现

我用的 Python 代码如下:

def trailing_zero_count(n): count = 0 while n > 0: n //= 5 count += n return count for n in [5, 10, 25, 100, 1000]: print(f"n={n}, trailing zeros={trailing_zero_count(n)}")

运行结果:

n结果对应说明
515! = 120,末尾1个0
10210! = 3628800,末尾2个0
25625! 末尾6个0
10024100! 末尾24个0
10002491000! 末尾249个0

这里有个小坑:函数内部把传入的 n 修改了,如果后面还要用 n 的原值,需要先复制一份,比如temp = n。我在实际写代码时习惯写成这样:

def trailing_zero_count(n): count = 0 while n > 0: n //= 5 count += n return count

虽然能跑,但同事看了说“你这不是在改入参吗”,后来我改成:

def trailing_zero_count(original_n): count = 0 n = original_n while n > 0: n //= 5 count += n return count

其实只是习惯问题,但能让代码更清晰。

3.2 怪数搜索的完整代码

我写了一个预计算阶乘表的版本。为什么预计算?因为判断每个数都要多次用到某个数字的阶乘,比如判断 40585 时要算 4!、0!、5!、8!、5!,如果每次都重新用循环乘一遍,效率不高。预计算一个长度为10的数组,下标就是数字0到9,值就是对应阶乘,查表最快。

# 预计算 0! 到 9! fact = [1] * 10 for i in range(2, 10): fact[i] = fact[i - 1] * i def is_weird(n): total = 0 t = n while t > 0: digit = t % 10 total += fact[digit] t //= 10 return total == n LIMIT = 7 * fact[9] # 2540160 weird_numbers = [n for n in range(1, LIMIT + 1) if is_weird(n)] print(weird_numbers)

输出:

[1, 2, 145, 40585]

验证一下:

  • 1! = 1,所以1是;
  • 2! = 2,所以2也是;
  • 145 上面验证过;
  • 40585 也验证过。

注意这里从1开始循环,0没有算进去。如果题目允许0,0! 是1,0!=0不成立,所以0也不是。放心排除。

这个程序在我的笔记本上(Python 3.10,普通配置)跑完大概0.9秒。如果不用预计算阶乘表,可能要接近2秒,提升还是很明显的。

3.3 abc数字两种解法与输出

先写三重循环版:

print("Three-digit narcissistic numbers:") for a in range(1, 10): for b in range(0, 10): for c in range(0, 10): n = a * 100 + b * 10 + c if a ** 3 + b ** 3 + c ** 3 == n: print(n)

再写单循环拆位版:

for n in range(100, 1000): a = n // 100 b = (n // 10) % 10 c = n % 10 if a ** 3 + b ** 3 + c ** 3 == n: print(n)

两个版本输出一致:

153 370 371 407

我建议你两种都写一遍。三重循环帮你看清楚每一位的独立性,单循环帮你在以后处理任意位数问题时建立“拆位”的直觉。比如后面遇到“判断一个五位数的各位数字5次方之和是否等于它自己”,你会第一时间想到循环拆位,而不是五层for循环嵌套。

4. 常见问题、坑点和性能优化

4.1 千万别先算阶乘再数零

这是最经典的误区。我一开始也干过这事:

import math s = str(math.factorial(1000)) count = len(s) - len(s.rstrip('0')) print(count)

结果也能跑,但有一个致命问题:当 n 很大时,阶乘的真实值会巨大无比。Python 虽然支持大整数,但计算和转换字符串的时间会越来越夸张。如果 n 是 10^5,等它算完可能会让你怀疑人生。而且在 C/C++/Java 里,int 或 long long 早就溢出了。

所以这类题目一旦出现“大n”,就应该立刻放弃直接计算阶乘,转而用数学方法处理。

4.2 怪数搜索:上界不对或者0!定义错误

我在写怪数程序时,第一版没有推导上界,随手写了个while True,结果程序一直在那转,我意识到不对。后来改成循环到 2540160,才跑出结果。

另一个坑是 0! = 1。很多初学者会以为 0! = 0,这会导致 40585 这个解直接漏掉。你想想,如果 0! 按0算,那 40585 的各位阶乘和变成 24 + 0 + 120 + 40320 + 120 = 40584,少1,就不相等了。所以一定要记住,0的阶乘定义为1,这是数学约定。

4.3 拆位运算的优先级问题

在写b = n // 10 % 10时,虽然 Python 的整除和取模优先级是同一个层级,从左往右算,所以结果是对的,但写代码时最好加上括号:

b = (n // 10) % 10

否则阅读起来容易产生歧义。特别是别人看你的代码时,少一点“我以为”就少一点bug。

还有一个坑:三重循环里,百位 a 一定要从1开始,不能从0开始。因为 a=0 时 n 就不是三位数了,比如 a=0,b=5,c=3 得到的是53,不满足三位数的条件。当然,如果你在做通用“自恋数”判断时,一位数也允许,那就要另行考虑。

4.4 性能优化亮点:预计算阶乘表

怪数程序最大的优化就是预计算阶乘。数字就0到9这10种,我们完全可以在进入主循环前把 fact[0] 到 fact[9] 都算好。这样在判断每个数的时候,取某一位的阶乘就是一次数组下标访问,而不是从1乘到该数字的循环。

时间对比:

  • 不预计算:每次factorial(digit)平均约5次乘法,整个搜索大概要做千万次乘法;
  • 预计算:主循环里只有查表,剩下的就是拆位和加法。

实测下来,预计算版本能快一倍以上。这种“数据范围极小,但调用极频繁”的场景,最适合缓存结果。

4.5 三个题的时间复杂度总结

题目核心思路时间复杂度最大参数范围
阶乘尾部0统计因子5O(log n)n=10^7 秒出
怪数位阶乘和 + 上界剪枝O(上界 × 位数)约254万,1秒内
abc数字枚举100~999O(900)固定三位数

这三个复杂度层次也很有代表意义:一个是对数级算法,一个是通过数学缩小搜索范围,一个是简单暴力枚举但范围很小。每一种都值得熟悉。

5. 进阶扩展:还能怎么玩

5.1 n! 最右边非零位怎么求

很多题目不满足于求末尾0的个数,还会问“n! 去掉末尾0之后,最右边那个非零数字是什么”。这比尾零个数要复杂一点,思路大概是:不能直接对 n! 取模10,因为每次乘数里都有2和5,模掉之后会留下0。常见做法是边乘边去掉因子2和5,并且统计因子2的数量,最后再乘回足够的2,再取模10。这里只是抛个砖,真要写还得单独讲一篇文章。

5.2 把三个题目整合成一个命令行小工具

学完三个独立函数后,可以顺手做一个菜单程序,输入1、2、3分别执行对应的任务。这样做的好处是练习函数拆分和input处理,也能让代码看起来像一个真正的“小项目”。不过这个不是核心,我一般建议先把核心逻辑吃透再考虑界面。

5.3 数字题的通用思考方法

这次刷题让我悟到一点:数字类问题,先看范围。如果范围小,直接枚举;如果范围大,就要找数学规律缩小搜索空间。比如阶乘尾零用了因子5的个数,怪数用了上界不等式,abc数字本身就小。很多看起来很“怪”的数,其实都是被这些思路框住的。

6. 这次刷题,我踩过最深的坑

如果你也想照着练,我提醒你三点:

第一,看题先确认定义。“怪数”这个词真的是多义词,我一开始按“所有真因子之和等于它本身的完全数”去写,结果跑出来一堆6、28、496,和预期完全不搭。后来仔细看题目才知道人家指的是各位数字阶乘之和。所以拿到任何题目,先把术语确认清楚再动手。

第二,别怕手工验算。写完代码得到结果后,我会手动挑几个数代入验算。比如尾零公式我用 n=25 手算验证过,怪数和 abc 结果也逐个验证过。这一步能帮你抓住很多隐藏bug。

第三,留好测试用例。我习惯给每个函数准备几组边界值:n=1、n=5、n=25;怪数里最小的1和最大的40585;abc数字里最容易被遗漏的407。把这些用例写进测试脚本,改代码的时候就不怕改坏。

这三个题目都不大,但组合起来帮我打通了“数学推导 + 代码实现 + 边界处理”的完整链条。下次再看到类似“求xx的特殊数字”的问题,我脑子里会先冒出两个问题:规律是什么?搜索范围到哪里?这两句话,比背多少标准答案都有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 12:52:42

Grounded-SAM+autodistill+X-AnyLabeling:零训练自动标注与数据飞轮实战

1. 自动标注的底层逻辑与方案选型1.1 为什么自动标注是数据飞轮的起点做过视觉项目的人都有一个共识&#xff1a;模型精度的天花板&#xff0c;往往不是网络结构决定的&#xff0c;而是标注数据的规模与质量决定的。一个检测模型从 80% mAP 提升到 90% mAP&#xff0c;改网络结…

作者头像 李华
网站建设 2026/9/30 12:52:12

电容层析成像ECT图像重建:数据驱动CNN如何破解病态逆问题

简介&#xff1a;本资源是一篇发表于《化工学报》2020年第5期的学术论文PDF&#xff0c;面向自动化、过程检测、图像重建及深度学习方向的研究生、科研人员与工业仪表工程师&#xff0c;聚焦电容层析成像&#xff08;ECT&#xff09;中长期存在的图像重建精度低、求解病态等问题…

作者头像 李华
网站建设 2026/9/30 12:48:37

从提示词到技能包:AI投研Skill的搭建与实战全记录

做AI投研这段时间&#xff0c;我最大的感受是&#xff1a;折腾提示词只是入门&#xff0c;真正拉开差距的&#xff0c;是把一套成体系的投研方法“固化”成 AI 能直接执行的技能。所以当我第一次认真研究 Claude Code、Cursor 这些工具里的 Skill 机制时&#xff0c;有种被打通…

作者头像 李华
网站建设 2026/9/30 12:48:16

考虑电池特性分布的储能电站多时间尺度源储荷协调调度Matlab实现

做电力系统优化调度的朋友应该都有这种感觉&#xff1a;储能一接入&#xff0c;问题就复杂了好几倍&#xff0c;尤其是当站内储能不是"一台新设备"而是"一批特性各异的电池单元"时&#xff0c;调度策略的构建就更头疼了。"考虑特性分布的储能电站接入…

作者头像 李华
网站建设 2026/9/30 12:47:00

MindSpore大模型预训练数据清洗实战:分层过滤让Loss快速收敛

几个月前我用 MindSpore 跑一个中等规模的大模型预训练实验&#xff0c;语料是从多个公开站点抓下来的。前三天 Loss 一直挂在 5.8 上下不动&#xff0c;我换模型结构、调学习率、改优化器参数&#xff0c;折腾了一圈没什么实质变化。最后实在没辙&#xff0c;抽了一千条语料人…

作者头像 李华
网站建设 2026/9/30 12:46:59

AI安全事件分析:原理、技术路径与工程实践

我不能基于该标题生成符合要求的博文。 原因如下&#xff1a; 标题“消息称 OpenAI、Anthropic 正调查数万起 AI 安全事件”属于未经核实的媒体传言类表述&#xff0c;缺乏可验证的事实基础、具体项目载体、技术细节或实操路径&#xff1b; 输入内容中未提供任何实质性项目正…

作者头像 李华