学算法认准 labuladong

东哥带你手把手撕力扣????

点击下方卡片即可搜索????

读完本文,你可以去 LeetCode 上拿下如下题目:

172、阶乘后的零(难度 Easy

793、阶乘后 K 个零(难度 Hard

笔试题中经常看到阶乘相关的题目,今天说两个最常见的题目:

1、输入一个非负整数n,请你计算阶乘n!的结果末尾有几个 0

比如说输入n = 5,算法返回 1,因为5! = 120,末尾有一个 0。

函数签名如下:

int trailingZeroes(int n);

2、输入一个非负整数K,请你计算有多少个n,满足n!的结果末尾恰好有K个 0

比如说输入K = 1,算法返回 5,因为5!,6!,7!,8!,9!这 5 个阶乘的结果最后只有一个 0,即有 5 个n满足条件。

函数签名如下:

int preimageSizeFZF(int K);

我把这两个题放在一起,肯定是因为它们有共性,可以连环击破。下面我们来逐一分析。

题目一

肯定不可能真去把n!的结果算出来,阶乘增长可是比指数增长都恐怖,趁早死了这条心吧。

那么,结果的末尾的 0 从哪里来的?我们有没有投机取巧的方法计算出来?

首先,两个数相乘结果末尾有 0,一定是因为两个数中有因子 2 和 5,因为 10 = 2 x 5。

也就是说,问题转化为:n!最多可以分解出多少个因子 2 和 5

比如说n = 25,那么25!最多可以分解出几个 2 和 5 相乘?这个主要取决于能分解出几个因子 5,因为每个偶数都能分解出因子 2,因子 2 肯定比因子 5 多得多。

25!中 5 可以提供一个,10 可以提供一个,15 可以提供一个,20 可以提供一个,25 可以提供两个,总共有 6 个因子 5,所以25!的结果末尾就有 6 个 0。

现在,问题转化为:n!最多可以分解出多少个因子 5

难点在于像 25,50,125 这样的数,可以提供不止一个因子 5,怎么才能不漏掉呢?

这样,我们假设n = 125,来算一算125!的结果末尾有几个 0:

首先,125 / 5 = 25,这一步就是计算有多少个像 5,15,20,25 这些 5 的倍数,它们一定可以提供一个因子 5。

但是,这些足够吗?刚才说了,像 25,50,75 这些 25 的倍数,可以提供两个因子 5,那么我们再计算出125!中有 125 / 25 = 5 个 25 的倍数,它们每人可以额外再提供一个因子 5。

够了吗?我们发现 125 = 5 x 5 x 5,像 125,250 这些 125 的倍数,可以提供 3 个因子 5,那么我们还得再计算出125!中有 125 / 125 = 1 个 125 的倍数,它还可以额外再提供一个因子 5。

这下应该够了,125!最多可以分解出 20 + 5 + 1 = 26 个因子 5,也就是说阶乘结果的末尾有 26 个 0。

理解了这个思路,就可以理解解法代码了:

int trailingZeroes(int n) {int res = 0;long divisor = 5;while (divisor <= n) {res += n / divisor;divisor *= 5;}return res;
}

这里divisor变量使用 long 型,因为假如n比较大,考虑 while 循环的结束条件,divisor可能出现整型溢出。

上述代码可以改写地更简单一些:

int trailingZeroes(int n) {int res = 0;for (int d = n; d / 5 > 0; d = d / 5) {res += d / 5;}return res;
}

这样,这道题就解决了,时间复杂度是底数为 5 的对数级,也就是O(logN),我们看看下如何基于这道题的解法完成下一道题目。

题目二

现在是给你一个非负整数K,问你有多少个n,使得n!结果末尾有K个 0。

一个直观地暴力解法就是穷举呗,因为随着n的增加,n!肯定是递增的,trailingZeroes(n!)肯定也是递增的,伪码逻辑如下:

int res = 0;
for (int n = 0; n < +inf; n++) {if (trailingZeroes(n) < K) {continue;}if (trailingZeroes(n) > K) {break;}if (trailingZeroes(n) == K) {res++;}
}
return res;

前文 二分搜索只能用来查找元素吗? 说过,对于这种具有单调性的函数,用 for 循环遍历,可以用二分查找进行降维打击,对吧?

搜索有多少个n满足trailingZeroes(n) == K,其实就是在问,满足条件的n最小是多少,最大是多少,最大值和最小值一减,就可以算出来有多少个n满足条件了。

那不就是二分查找「搜索左侧边界」和「搜索右侧边界」这两个事儿嘛?

先不急写代码,因为二分查找需要给一个搜索区间,也就是上界和下界,上述伪码中n的下界显然是 0,但上界是+inf,这个正无穷应该如何表示出来呢?

首先,数学上的正无穷肯定是无法编程表示出来的,我们一般的方法是用一个非常大的值,大到这个值一定不会被取到。比如说 int 类型的最大值INT_MAX(2^31 - 1,大约 31 亿),还不够的话就 long 类型的最大值LONG_MAX(2^63 - 1,这个值就大到离谱了)。

那么我怎么知道需要多大才能「一定不会被取到」呢?这就需要认真读题,看看题目给的数据范围有多大

这道题目实际上给了限制,K是在[0,10^9]区间内的整数,也就是说,trailingZeroes(n)的结果最多可能达到10^9

然后我们可以反推,当trailingZeroes(n)结果为10^9时,n为多少?这个不需要你精确计算出来,你只要找到一个数hi,使得trailingZeroes(hi)10^9大,就可以把hi当做正无穷,作为搜索区间的上界。

刚才说了,trailingZeroes函数是单调函数,那我们就可以猜,先算一下trailingZeroes(INT_MAX)的结果,比10^9小一些,那再用LONG_MAX算一下,远超10^9了,所以LONG_MAX可以作为搜索的上界。

注意为了避免整型溢出的问题,trailingZeroes函数需要把所有数据类型改成 long

// 逻辑不变,数据类型全部改成 long
long trailingZeroes(long n) {long res = 0;for (long d = n; d / 5 > 0; d = d / 5) {res += d / 5;}return res;
}

现在就明确了问题:

n属于区间[0,LONG_MAX]我们要寻找满足trailingZeroes(n) == K的左侧边界和右侧边界

根据前文 二分查找算法框架,可以直接把搜索左侧边界和右侧边界的框架 copy 过来:

/* 主函数 */
int preimageSizeFZF(int K) {// 左边界和右边界之差 + 1 就是答案return right_bound(K) - left_bound(K) + 1;
}/* 搜索 trailingZeroes(n) == K 的左侧边界 */
long left_bound(int target) {long lo = 0, hi = LONG_MAX;while (lo < hi) {long mid = lo + (hi - lo) / 2;if (trailingZeroes(mid) < target) {lo = mid + 1;} else if (trailingZeroes(mid) > target) {hi = mid;} else {hi = mid;}}return lo;
}/* 搜索 trailingZeroes(n) == K 的右侧边界 */
long right_bound(int target) {long lo = 0, hi = LONG_MAX;while (lo < hi) {long mid = lo + (hi - lo) / 2;if (trailingZeroes(mid) < target) {lo = mid + 1;} else if (trailingZeroes(mid) > target) {hi = mid;} else {lo = mid + 1;}}return lo - 1;
}

如果对二分查找的框架有任何疑问,建议好好复习一下前文 二分查找算法框架,这里就不展开了。

现在,这道题基本上就解决了,我们来分析一下它的时间复杂度吧。

时间复杂度主要是二分搜索,从数值上来说LONG_MAX是 2^63 - 1,大得离谱,但是二分搜索是对数级的复杂度,log(LONG_MAX) 是一个常数 63;每次二分的时候都会调用一次trailingZeroes(n)函数,它的复杂度 O(logN)。

那么算法的复杂度就是 O(logN) 吗?其实你会发现 trailingZeroes 函数传入的参数 n 也是在区间[0,LONG_MAX]之内的,所以我们认为这个 O(logN) 最多也不过 63,所以说可以认为时间复杂度为 O(1)。

综上,由于我们根据 K 的大小限制了数据范围,用大 O 表示法来说,整个算法的时间复杂度为 O(1)。

当然,如果说不考虑数据范围,这个算法的复杂度应该是 O(logN*logN),也就是二分查找的复杂度乘 trailingZeroes 的复杂度。

往期推荐 ????

二叉树的题,就那几个框架,枯燥至极????

状态压缩技巧:动态规划的降维打击

一个函数秒杀 2Sum 3Sum 4Sum 问题

回溯算法和动态规划,到底谁是谁爹

东哥手写正则通配符算法,结构清晰,包教包会!

_____________

学好算法全靠套路,认准 labuladong 就够了。

知乎、B站搜索:labuladong

《labuladong的算法小抄》即将出版,公众号后台回复关键词「pdf」下载,回复「进群」可加入刷题群。

阶乘相关的算法题,东哥又整活儿了相关推荐

  1. 链表相关的算法题大汇总 — 数据结构之链表奇思妙想

    http://blog.csdn.net/lanxuezaipiao/article/details/22100021 基本函数(具体代码实现见后面) 1,构造节点 //定义节点类型 struct N ...

  2. java算法题——豪哥打牌

    豪哥打牌 题目以及要求: 样例 题目以及要求: 题目描述 著名ACM选手豪哥退役后迷上了打扑克.对对碰是一种十分有趣的双人游戏,游戏开始时分别发给玩家数量相等的牌,玩家均不能看到自己和对方的牌(即盲打 ...

  3. 东哥手把手带你刷二叉树|第三期

    学算法认准 labuladong 后台回复进群一起力扣???? 读完本文,你可以去力扣解决: 652.寻找重复子树(Medium) 接前文 手把手带你刷二叉树(第一期)和 手把手带你刷二叉树(第二期) ...

  4. LeetCode算法题8:递归和回溯1

    文章目录 前言 回溯算法: 一.合并两个有序链表(简单,可略过) 迭代遍历 一开始没有想到的递归解法 二.反转链表 迭代遍历(头插法): 递归: 三.组合 回溯: 四.全排列 回溯(交换): 回溯: ...

  5. LeetCode算法题7:DFS和BFS

    文章目录 前言 深度优先搜索算法伪代码: 广度优先搜索算法伪代码: 一.图像渲染 DFS: BFS: 上面BFS算法存在的问题: 修改 1: 修改 2: 二.岛屿的最大面积 DFS: BFS : 三. ...

  6. LeetCode算法题6:滑动窗口*

    文章目录 前言 一.无重复字符的最长子串 思路 1: 思路 2: 二.字符串的排列 思路 1: 思路 2: 思路 3: 思路 3 plus: 思路 3 plus plus: 思路 4: 思路 5: 总 ...

  7. LeetCode算法题5:双指针

    文章目录 前言 一.有序数组的平方 二.轮转数组 三.移动零 四.两数之和 II - 输入有序数组 五.反转字符串 六.反转字符串中的单词 III 七.链表的中间结点 八.删除链表的倒数第 N 个结点 ...

  8. LeetCode算法题4:二分查找及扩展应用

    文章目录 前言 一.二分查找 二.第一个错误的版本 三.搜索插入位置 总结 前言 Leetcode算法系列:https://leetcode-cn.com/study-plan/algorithms/ ...

  9. 啊这,一道数组去重的算法题把东哥整不会了…

    学算法认准 labuladong 东哥带你手把手撕力扣???? 点击下方卡片即可搜索???? 想啥呢?labuladong 怎么可能被整不会?只是东哥又发现了一个有趣的套路,所以写了篇文章分享给大家~ ...

最新文章

  1. 使用CleanIISLog清除IIS记录
  2. python安装教程32位-python为什么要装32位的
  3. python安装requests第三方模块
  4. Re-Order Buffer
  5. jQuery 插件使用记录
  6. [原]批量删除VSS产生的scc文件
  7. python创建类mymath_构建DLL(MyMathFuncs)以在Python Ctypes中使用
  8. CI520读卡芯片 软硬件兼容替换CV520
  9. 【python学习笔记】Python对经纬度处理
  10. Mac电脑怎样网络在线重装系统
  11. mysql的month_MySQL month()函数
  12. c语言中的内存4区域模型(堆,栈,全局区,代码区)
  13. matplotlib设置颜色、标记、线条,让你的图像更加丰富
  14. 获取所有复选框选中状态的id
  15. Excel分组行转列
  16. aliPay支付宝APP支付操作流程
  17. 租房小程序怎么样?租房小程序可以提供哪些价值?
  18. wordpress个人博客申请Let’s Encrypt免费SSL证书
  19. 制造业管理系统如何帮助企业做好物料编码管理?
  20. 如何向公众号添加的文档的链接

热门文章

  1. 好用的java图形验证码
  2. 使用 CSS3 jQuery 制作漂亮的书签动画
  3. OpenSSL密码库算法笔记——第1.2.2章 comba乘法
  4. 3D全景具备哪些特点?又有哪些有趣的交互功能?
  5. 软件测试岗一位小伙伴的~同花顺一面(面经)
  6. 前端/运维电话面试被问到的问题
  7. 苹果home键在哪里设置_五毛特效?苹果X的Home键又回来了!
  8. cyq.data mysql_终于等到你:CYQ.Data V5系列 (ORM数据层)最新版本开源了
  9. 翁恺老师全套C语言课程笔记(本菜鸟正在学习)
  10. w ndows7如果连接宽带,win7怎么创建桌面宽带连接?