正式讲课之前,我先问你这样一个问题,请你尽可能快速回答。

一张 1 毫米厚度的纸,对折几次后,可以达到地球到月球的距离(39 万公里)?

我在写这篇稿子的时候,问了身边的几个朋友。最小的回答是 1 万次,最大的则是 100 万次。

请问在你的直觉下,你的答案又是多少呢?我猜想无论如何都是上万次吧,毕竟我们离月球有 39 万公里呢。

折纸的过程就是 1 变 2,2 变 4,4 变 8,这样一个翻一倍的过程。聪明的你,会发现其实这就是一个关于指数函数和对数函数的问题。

那么,这与我们的编程有什么关系吗?其实基于这个数学原理,编程中有一种分治法的二分策略。这一讲,我们就来讨论一下如何利用指数爆炸来优化程序。

折纸,飞奔到月球

接下来,我们定义下面的数学符号。n 为折叠的次数,h(n) 为纸张对折 n 次后的厚度。显然,每次对折纸张时,厚度都会增加一倍。

不对折时,纸张的厚度为 h(0)=1mm;每次对折纸张时,厚度都会增加一倍;如果将纸对折 1 次,则厚度为 h(1)=2mm;如果对折 2 次,则厚度为 h(2)=4mm;对折 3 次,厚度为 h(3) = 8mm。

我们耐着性子继续往下计算,可以得到下面的对折次数与厚度的关系表。

到这里我们发现,对折 10 次后,厚度也不过才刚刚达到 1 m。也许你会不仅感慨,以这样的速度,何时才能到达月球啊。

还是耐着性子,我们继续计算,并整理为下面的表格。区别是,这次我们以米(m)为单位。

这时候,也许你会发现一些端倪。对折 10 次是 1 m,对折 20 次竟然到了 1 公里,成长速度非常快。

接着,我们继续耐着性子来计算,并整理为下面的表格。区别是,这次我们以千米(km)为单位。

我们知道地球到月亮的距离是 38 公里,也就是 3.8×105km,对折 30 次后,厚度竟然已经达到了 103km。虽然离月球仍然很远,但结合这个增长速度,感觉已经快到月球了。

我们继续耐着性子来计算,并整理到下面的表格中。区别是,这次我们以 103km 为单位。

此时,你就会看到一个惊天结果。对折 40 次后,厚度达到了 106km。这已经超过了地月距离的 3.8×105km!往回看你会发现,在对折第 39 次时,厚度就已经开始超过地月距离了。原本猜测的至少要对折 10 万次,竟然只需要 39 次就到达了月球。

【飞奔到月球的代码实现】

为了仔细验证上面的结果,我们还可以把 h(n) 当作是一个数列。显然,它是一个首项为 1,公比为 2 的等比数列,它的通项公式为 h(n)=2n (mm)。

如果要计算折叠多少次厚度可达地月距离(约为 3.8×1011mm),可以对上面式子两边,同时取关于 2 的对数,则有 log22n= n = log2(3.4×1011) ≈ 38.47。

因此在第 38 次折叠时,厚度还没有到达月球;但是第 39 次对折时,纸张厚度就可以突破地月距离。

对这个问题,我们可以用以下代码实现计算:

a = 1
h = a
times = 0
while h < 380000000000:h = h * 2times += 1
print times

代码含义为:

  • 第 1 行,定义纸张厚度为 1mm;

  • 第 2、3 行,定义对折 0 次时,厚度为纸张厚度 1mm;

  • 第 4 行,判断当还没有到达月球时;

  • 第 5 行,执行对折的操作,厚度为原来的两倍;

  • 同时第 6 行,对对折次数进行加 1 的操作。

直到达到月球后,跳出循环,并打印出到达月球的次数。

上图中的程序运行结果与刚刚我们的计算一致,都为 39 次。

指数爆炸的反向应用——二分查找

在计算机中,上面的现象也被称作“指数爆炸”。你可以理解为,某个看似不起眼的任务,每次以翻倍的速度进行增长,很快就会达到“星星之火可以燎原”的爆炸式效果和影响面。显然,指数爆炸性质的问题如果在程序中发生,会让系统迅速瘫痪。

不过,如果可以把指数爆炸的思想反过来用,就能对程序的效率进行优化。 具体而言,某个任务虽然很庞大、很复杂,但是每次我们都让这个任务的复杂性减半,那么用不了多久,这个庞大而又复杂的任务就会变成一个非常简单的任务了

所以,指数爆炸思想的反向应用就是分治法,而分治法中的一个经典案例就是二分查找

1.二分查找算法

二分查找是一种查找算法,用于从某个有序数组 a 中,查找目标数字 obj 出现的位置 index。

二分查找的思路是,将目标数字 obj 与数组 a 的中位数 a[median] 进行比较:

  • 若相等,则查找结束;

  • 如果 obj 小于 a[median],则问题缩小为从 a 数组的左半边查找 obj;

  • 如果 obj 大于 a[median],则问题缩小为从 a 数组的右半边查找 obj。

重复这个过程,直到查找到 index,或 obj 未在数组 a 中出现为止。

我们围绕下面的例子,来使用一下二分查找算法。假设数组 a 的元素如下表所示,要查找的目标值 obj 为 7。

第一轮,数组 a 的中位数为 a[4] = 14。因为目标值 obj 为 7,小于 14,则问题被缩小为在数组 a 的左半边查找 obj。

第二轮,上一轮剩下的 a 数组的查找范围中,新的中位数为 a[1] = 2。因为目标值 obj 为 7,大于 2,则问题缩小为在右半边继续查找 obj。

第三轮,上一轮剩下的 a 数组的查找范围中,新的中位数为 a[2] = 7。因为目标值 obj 为 7,等于 a[2],则说明查找到结果,输出 index 值为 2。

好了,现在我们来复盘一下刚才的执行过程。

在上面的查找过程中,每轮的查找动作都基于 obj 与中位数的大小关系,来作出保留左边或保留右边的决策。这样来看,每轮的查找动作,可以让 obj 的搜索空间减半,这也是二分查找的命名由来。

在利用二分查找后,原本 10 个元素的数组 a,只需要 3 次比较,就找到了 obj 的位置 index。你可能会决策,10 次计算缩减为 3 次,区区几微秒的时间,这对于强大的计算机而言根本不算什么。

可如果数组 a 的元素个数为 3.8×1011个,又会发生什么呢?

还记得这个数字吗?这就是刚刚我们计算的毫米单位的地月距离。

从指数爆炸的反向结论来看,对于这么多个元素的数组 a,你只需要 39 次计算就能完成对 obj 的查找。假设一次查找需要耗时 1μs,则采用二分查找后,节省的时间能达 3.8×1011μs= 3.8×108ms = 3.8×105s ≈ 100h。

2.二分查找算法的代码

不知道你有没有发现,二分查找的每一轮都是在处理同样的问题,区别只不过是数组的查找范围变小了而已。

这是不是很像上一课时讲到的递归的基本操作呢,这里的递归结构如下:

def fun(N,x):if condition(N):xxxelse:fun(N1,x)

递归的两个关键问题是终止条件和递归体。

  • 二分查找的终止条件有以下两个可能。第一,中位数恰好是 obj,说明找到了目标,则打印中位数的索引值 index;第二,查找完发现没有任何一个数字等于 obj,则打印 -1。

  • 递归体需要做两个分支的判断。即如果 obj 比中位数大,则把数组的右半边保留,继续递归调用查找函数;如果 obj 比中位数小,则把数组的左半边保留,继续递归调用查找函数。

这样就可以得到如下代码:

def binary_search(obj,a,begin,end):median = (begin + end) / 2if obj == a[median]:print medianelif begin > end:print -1else:     if obj > a[median]:binary_search(obj,a,median + 1,end)else:binary_search(obj,a,begin,median - 1)
a = [1,2,7,11,14,24,33,37,44,51]
binary_search(7,a,0,9)

【我们对这段代码进行走读】

  • 第 1 行,说明 binary_search 的入参包括查找目标 obj、数组 a、查找范围的开始索引 begin,以及查找范围的终点索引 end。

  • 第 2 行,计算出查找范围内的中位数 median。

接着进行终止条件的判断:

  • 第 3 行,如果 obj 和中位数相等,则直接打印 median;

  • 第 5 行,如果发现开始索引比终止索引更大,则说明没有找到目标值obj,打印 -1。

第 7 行,开始是递归体

  • 第 8 行,判断 obj 和中位数的大小关系;

  • 如果 obj 更大,则第 9 行递归查找数组右半边,更改开始索引为 median + 1;

  • 反之,则第 11 行递归查找数组左半边,更改终止索引为 median - 1。

利用以上程序,在数组 a = [1,2,7,11,14,24,33,37,44,51] 中查找数字 7,因为 a[2] = 7,因此预期的返回结果是 2。

程序的执行结果如下图,结果也为 2,这与我们手动计算的结果是一致,结果正确。

指数爆炸和二分查找的数学基础

指数爆炸为什么那么恐怖?二分查找又为什么那么厉害?其实这都源自两个数学运算,分别是指数运算和对数运算。

1.指数运算

指数运算,即幂运算,写作 an,其中 a 为底数,n 为指数:

  • 当 n 为正数时,an 表示含义为 n 个 a 相乘的积;

  • 当 n 为 0 时,a0=1;

  • 当 n 为负数时,an = 1/a-n

除此以外,指数运算还有下面三个关键性质:

an∙ am=an+m

an∙ bn= (ab)n

(bn)m=bnm

2.对数运算

对数运算是指数运算的逆运算,设幂运算 an = y,此幂运算的逆运算为 n=logay。

其中 a 是对数运算的底,而 n 就是 y 对于底数 a 的对数。

对数有下面三个重要性质:

logb(x ∙ y) = logbx +logby

logbxy=y ∙logbx

logb1 = 0

接着,我们从计算机运行的复杂度来看一下。我们先把对数函数、线性函数、指数函数在一张图中画出来。假设对数函数和指数函数的底数选择为 2,线性函数选择为 y = x,其函数图如下所示。

其中,灰色线为指数函数 y = 2x 的图像,橙色线为函数 y = x 的图像,蓝色线为对数函数 y = log2x 的图像,图中的这三条线,刻画了自变量 x 和因变量 y 之间的变化趋势关系,其中需要你重点关注的是指数函数和对数函数。

  • 指数函数

对于指数函数而言,自变量 x 的增加会让因变量 y 快速达到“爆炸”状态。如果程序的复杂度与数据量是指数爆炸的趋势,那么随着数据量的增加,系统可能很快就会陷入瘫痪的状态。

现实中也有与之类比的案例。比如,人们常说的一传十、十传百就是一种指数爆炸;又比如,2020 年开始的疫情,之所以要所有人隔离,就是要避免又传染带来的指数爆炸。

  • 对数函数

反之,对于对数函数而言,自变量 x 的增加对因变量 y 增加的趋势影响非常小。

程序员应该多利用这个思想来进行程序优化。例如,刚刚讲解的二分策略的程序,即使任务量很大,也可以在很少的计算时间内完成运算。

现实中也有与之类比的场景。例如,你要在一个英文词典里面查找某个单词。虽然词典的厚度可能达到成百上千页,但因为单词排列有序,你完全可以通过二分查找去找到某个单词的所在位置。同时,即使某天人们新造出很多单词,哪怕是单词数量翻倍,也不会让查单词的复杂度有明显提高。

指数爆炸的正向应用——密码学

指数爆炸的反向应用是程序的优化,而指数爆炸的正向应用就是密码学。

决定密码安全性的一个重要因素,就是密码的搜索空间 S。假设大漂亮做了个密码系统,在这个系统中,密码的每一位都由 0~9 的数字构成时。这样,密码的每一位就有 10 个可能性。

如果密码的长度为 n,则密码的搜索空间为 S = 10n。假设 n 为 5,则密码共有 105 = 1 万种可能性。要想破译密码,无异于万里挑一。

可见,要想把密码做得很复杂,一个可行的方法是,利用指数爆炸不断增加位数,来获得更大的搜索空间;除了增加密码尾数的方式外,将单个密码位上的构成可能增加也是一种提升安全性的手段。

例如,如果把每一位的密码,由先前的数字调整为数字或区分大小写的字母,则意味着密码的搜索空间由 S = 10n,提高到 S = 62n

26 个小写字母、26 个大写字母、10 个数字,合在一起是 62 个可能性。

所以,增加每一位密码的可能性时,搜索空间 S 也可以获得提高。

小结

这一课时,我们了解了指数爆炸(运算)与对数运算,以及它们在程序和生活中的应用。而指数爆炸的思维过程就是“折纸,分奔到月球”的过程,其正向应用就是密码学。

而指数爆炸的反向应用有二分查找算法(也就是基于对数函数性质),二分查找算法是提高程序效率的重要手段,其前提条件是搜索空间有序,其实现方法需要采用上一讲所学的递归思想,需要预先定义递归的终止条件和递归体。

最后,我们留个课后习题,在上面的内容中,我们介绍了对数和指数的一些关键性质,你可以试着从数学的角度来证明这些性质的成立。

下一课时,我将向你讲解“17 | 动态规划:如何利用最优子结构解决问题?”别忘来听课~


精选评论

程序员的数学课16 二分法:如何利用指数爆炸优化程序?相关推荐

  1. 【程序员必修数学课】->基础思想篇->递归(下)->分而治之从归并排序到MapReduce

    递归(下) 前言 归并排序中的分治思想 分布式系统中的分治思想 1.数据分割和映射 2.归约 3.合并 总结 前言 在上一篇中,我介绍了如何使用递归,来处理迭代法中比较复杂的数值计算.但是我们知道,有 ...

  2. 干货 | 程序员必备的16个实用的网站

    最近看到很多网友分享了好多比较酷炫的网站,好多都放进小艾的收藏夹了,(__) 嘻嘻--看的我也忍不住想分享了,因为是IT行业,所以分享几个收集的比较实用而且酷炫的网站O(∩_∩)O~ 1." ...

  3. 月均数据_程序员月均薪多少,2019全国互联网行业程序员就业大数据报告

    <2019全国互联网行业程序员就业大数据报告>,该报告针对程序员画像.专业背景.职能供需分布.城市分布特征和薪资优势等方面进行分析.作者:子瑜说IT 下面,一起来看看,2019年1月-9月 ...

  4. 程序员7年和我的7点感想――我的程序人生

    程序员 7 年和我的 7 点感想 ――我的程序人生 我是1986年第一次接触计算机的,当时刚上大学,用的是VAX11-780小型机运行Basic程序,一个学期下来,算是学点皮毛.1989年,在大学因& ...

  5. 程序员7年和我的7点感想 ――我的程序人生

    程序员7年和我的7点感想 ――我的程序人生 我是1986年第一次接触计算机的,当时刚上大学,用的是VAX11-780小型机运行Basic程序,一个学期下来,算是学点皮毛.1989年,在大学因<微 ...

  6. 程序员的职业规划_从菜鸡到大佬——程序员们,请收下这份职业规划全攻略!...

    作者:阿诺,有删改 引言 John Z. Sonmez是一位来自硅谷的杰出程序员,2016年他出版了<软技能:代码之外的生存指南>一书.这本书在中国翻译出版之后,引起了国内广大程序员的热烈 ...

  7. 利用系统缓存优化程序的运行效率

    Buffer和Cache对系统性能有很大影响,在软件开发的过程中,也可以利用这一点,来优化I/O的性能,提生应用程序的运行效率. 缓存命中率 想利用缓存来提升程序的运行效率,应该怎么评估这个效果尼?换 ...

  8. 写代码犹如写文章: “大师级程序员把系统当故事来讲,而不是当做程序来写” | 如何架构设计复杂业务系统? 如何写复杂业务代码?

    写代码犹如写文章: "大师级程序员把系统当故事来讲,而不是当做程序来写" | 如何架构设计复杂业务系统? 如何写复杂业务代码? Kotlin 开发者社区 "大师级程序员把 ...

  9. 甜蜜暴击!程序员为媳妇儿做了个记录心情的小程序

    甜蜜暴击!程序员为媳妇儿做了个记录心情的小程序 web前端一大咖学习群 闲暇之余,听媳妇嘀咕说要给她做一个能表达她每日心情的小程序,只能她在上面发东西.既然媳妇发话了,就花点心思做一个吧,因为没有 U ...

最新文章

  1. #include quot;*.cquot;文件的妙用
  2. mysql 非交互查询 存入execl
  3. linux下安装java环境(ubuntu和centos)
  4. jsp中session 和 cookies区别
  5. win7怎么安装nodejs_怎么解决win7安装软件提示
  6. TypeError: Data must not be unicode
  7. 用脚本整理Leetcode题解
  8. 白鸦:我印象中的Keso
  9. python遗传算法最短路径问题有几种类型_用遗传算法求解最短路径问题.pdf
  10. 蓝桥杯单片机——“”彩灯控制器”的程序设计
  11. 知网如何快速引用参考文献
  12. win11 快捷键无法使用?键盘win无法呼出?win+d无法显示桌面?
  13. 手机html点击按钮复制,网页文字无法复制?按下手机这个键即可复制!网友:厉害了...
  14. 七年老安卓的九十月小结
  15. u, v风和风速风向的相互转换
  16. 如何评价架构的优劣(转)
  17. java获取文件夹下所有的文件
  18. 基于Matlab模拟用于海况海洋学研究的 X 波段雷达系统(附源码)
  19. G-Rilling EMD工具箱
  20. AD原理图右下角Title Block信息实时更新的方法(AD20)多图教程

热门文章

  1. 计算机技术与软件资格证书有哪些,请问计算机技术与软件专业技术资格的证书有什么用..._出版资格_帮考网...
  2. 科技公司 IPO,利润不重要,证据在这里
  3. 基于ssm的驾校管理系统
  4. trie树——【吴传之火烧连营】
  5. c#开发电子商务网站---我的笔记
  6. Android中Service的使用详解和注意点(LocalService)
  7. Win10系统电脑开机黑屏一直转圈无法进入桌面怎么办?
  8. 好惨一恐龙:在最后时刻,它们将所有灾难都经历个遍
  9. 一位6年的测试老鸟工作感悟,以及对现阶段的测试行业的分析
  10. C#模拟西门子S7服务