本书是一本解谜式的趣味算法书,从实际应用出发,通过趣味谜题的解谜过程,引导读者在愉悦中提升思维能力、掌握算法精髓。此外,本书作者在谜题解答上,通过算法的关键原理讲解,从思维细节入手,发掘启发性算法新解,并辅以 Ruby、JavaScript 等不同语言编写的源代码示例,使读者在算法思维与编程实践的分合之间,切实提高编程能力。
本书适合已经学习过排序、搜索等知名算法,并想要学习更多有趣算法以提升编程技巧、拓展程序设计思路的程序员,以及对挑战算法问题感兴趣、爱好解谜的程序员阅读。
增井敏克,1979年生于奈良,毕业于大阪府立大学研究生院。增井 IT 工程师事务所代表、注册工程师(信息工程学方向)。从事旨在“将商务、数学和 IT 结合以正确、高效使用计算机”的技能提升指导、软件开发以及信息安全咨询等工作。掌握 C/C++、C#、Java、PHP 和 Ruby 等20多种编程语言。著作有《在家就能学会的安全基础》等。目前在面向 IT 工程师提供业务技能评估服务的平台 CodeIQ 上负责人气栏目“每周算法”的出题和评审工作。
作为程序员,大家也许会有这样的“小洁癖”:特别不能忍受重复劳动,特别讨厌“人肉运维”。因此,只要做某件事需要花 90 秒以上的时间,那么就一定要通过写程序来完成这件事,哪怕写程序要花费半个小时。乍一听,这似乎是在浪费时间,然而这正是大部分优秀程序员的特质。一方面,如果是做重复的事情,计算机通常做得比人更快,准确率也更高;另一方面,写成程序之后,这些重复的流程更易于变更、管理和复用。事实上,正因为无数“有洁癖”的前辈们的伟大工作,才有了编译器,才有了百花齐放的编程语言,才有了欣欣向荣的 IT 产业。
不过,如果只是养成了“一言不合就写脚本”的习惯,与真正优秀的程序员仍然有很大的差距。同样是排序,不同的数据规模、不同的算法实现,性能表现都相差巨大。同样地,做同一件事,不同的程序员的解法和效率也天差地别。程序员圈内一直流传这样的说法:“优秀程序员的生产力可以达到普通程序员的十倍甚至成百上千倍。”ACM 圈子里的高手,各种复杂精巧的算法信手拈来,应对极其复杂的问题时编码也如庖丁解牛,行云流水般顺畅;顶级的程序员甚至能创造世界级的工具,或者开创一种流派,影响大部分程序员的工作和思维方式。这种差距,真就像不同算法之间复杂度的差距一样明显,让人望而生畏。
见贤思齐。要怎么做才能步入“优秀程序员”的行列呢?抛开数学、各种计算机理论的基础不谈,也许最能量化程序员能力的就是“代码量”了。读更多优秀的代码,就能知道更多好的架构、好的算法;写更多的代码,解决问题的速度就更快,生产力也就更高。提高代码量这个简单粗暴的方法,效果的确立竿见影,于是乎一大批在线编程解题网站应运而生。而本书正是源于日本一个 IT 服务网站 CodeIQ 上的在线编程解题栏目“本周算法”。这个栏目的主编就是本书作者,他“寓教于题”,通过精心设计的问题向大家传授了很多算法、程序优化技巧甚至工程架构方面的经验等。
本书可以看作是一本算法书,与其他编程类、算法类图书最大的不同有两点:其一是所有问题都贴近生活和实际应用,兼具实用性和趣味性;其二是以虚拟的人物形象和实际的代码进行讲解,重点向读者演示不同思路、不同解决方案之间的区别和差距。公交车上如果设置自动找零的装置,应该怎么实现?怎样实现一个简单的扫地机器人,让它尽量不要重复清扫某一个角落?如何串联和并联组合一堆电阻,使得最终电阻值逼近黄金分割值?像这样接地气、有意思的问题,书中比比皆是。讲解求斐波那契数列某一项的问题时,作者先由递归切入,后讲查表法优化,最后引出实际实现时需要处理数值溢出的问题。全书的讲解都像这样层层深入、条分缕析。
本书共 4 章,每一章都由很多问题构成。第 1 章讲的是最基础的二进制,通过实例帮助大家理解二进制,进而用二进制解决实际问题;第 2 章~ 第 4 章则分别从工程、算法和架构几个方面切入不同的算法优化案例。此外,个别问题下还会设置专栏,穿插一些作者在软件工程甚至人才培育等方面的理解和经验。
关于本书,最推荐的阅读方式是读完题先停下来想想解法。此时最好能打开电脑,打开编辑器,先试着把题做出来。做完之后再往下读,顺着作者的分析和解答细细体会问题背后的算法思路。书中每一个问题都汇集了作者以及 CodeIQ 网站大量用户的集体智慧,相信做完题再作对比,一定可以收获不少新的体会。如果您发现了更好的解法,希望您可以到图灵社区(http://www.ituring.com.cn/)或者本书的代码仓库(https://github.com/leungwensen/70-math-quizs-for-programmers)上和大家分享交流,共同进步。
最后,成书不易,非常感谢图灵各位编辑的帮助和指导,也感念这将近一年时间里家人的理解和包容。
绝云
2017 年 4 月 5 日于杭州
计算机的世界每天都在发生着深刻的变化。新操作系统的发布、CPU 性能的提升、智能手机和平板电脑的流行、存储介质的变化、云的普及……这样的变化数不胜数。
在这样日新月异的时代中,“算法”是不变的重要基石。要编写高效率的程序,就需要优化算法。无论开发工具如何进化,熟识并能灵活运用算法仍然是对程序员的基本要求。
程序员的工作说白了就是把需求变为程序。人们希望计算机做的事情就是“需求”,实现需求的就是“程序”。能满足需求的程序肯定不止一种,我们需要从中挑选出最优的程序。
这里的难点在于,怎样判断一个程序是不是最优的。不同的人对“优秀的算法”有着不同的理解。我认为,优秀的算法需要满足以下 3 点。
(1)高速
即使是简单实现后在处理上会花很长时间的程序,有时候转换一下角度进行优化,就能得到一个高速的版本。根据算法内容的不同,有时候优化的效果不仅仅是速度提升 2 倍、3 倍,甚至提升 100 倍、1000 倍的情况也不少见。
(2)简化
如何简化输入条件将会决定最终代码的复杂度。越是简单的程序,可维护性越高。
(3)通用
如果我们在实现程序时有意识地把通用的处理封装起来,那么就能把源代码用于其他问题或者工作需求上。如果实现的程序即便输入值或者参数发生变更,代码改动也很小,那么测试往往也能简化。
我很喜欢这么一句话:阅读量决定了学习能力的上限,写作量决定了学习能力的下限。这是因提出“百格计算” 而闻名的岸本裕史先生说的话,个人觉得这对编程也是适用的。要想磨练编程技巧只有两个途径:一是阅读代码,二是编写代码。
不存在没有读过其他人写的代码的程序员。很显然,也不存在没有写过代码的程序员。越是编程技巧高超的开发者,读过的代码越多,写过的代码也越多。
数据结构和算法的学习尤为重要。多了解堪称无数先驱智慧结晶的算法,多亲身体会这些算法的效果对程序员非常重要。
本书为那些已经学习过排序、搜索等知名算法,并想要学习更多有趣的算法,进一步提升编程技巧的工程师准备了 69 道数学谜题形式的问题。
当然,本书中的算法并不是最优的。请怀着这样的心态,努力去思考更加优秀的算法。
在提供 IT 工程师业务技能评估服务的平台 CodeIQ(https://codeiq.jp)上有一个名为“本周算法”的栏目。我在这个栏目中担任出题人,这一年多来每周都会公布一个算法趣题。本书中的问题就是出自这个栏目。当然,对于原问题,这里进行了些许修改和补充。感谢策划了该栏目的大成弘子女士、每周在我出题后进行检查的峯亚由美女士,以及其他 CodeIQ 相关的工作人员。
最后,还要感谢积极参与“本周算法”挑战的各位答题者。正是因为有你们,我才能持续不断地出题,最终让本书得以付梓。真的非常感谢大家。
增井敏克
2015 年 10 月
本书为 69 道数学谜题编写了解题程序。每个问题大致分为“问题页”和“讲解页”两部分,“问题页”从单页起。请各位先通读问题描述,并动手编写程序尝试解题。在这个过程中,具体的实现方法是其次,更重要的是思考“通过哪些步骤来实现才能够解决问题”。
翻过问题页就能看到思路讲解和源代码示例了。请留意自己编程时在处理速度、可读性等方面进行的优化,和本书上的源代码示例有什么不同。如果事先看了思路讲解和答案,就会失去解题的乐趣,所以这里建议大家先编程解题,再看讲解页。
1 IQ
本书问题的难度逐章递增,每道题的IQ就是一个更加明确的难度提示。
2 目标时间
解题需要的标准思考时间。
3 问题的背景
为让读者更容易理解,这里描述了问题的背景。
4 问题
这里是问题描述,在读者了解背景后设问,引导读者编程并解题。
5 提示
有助于解题的提示。
6 下载文件名
本书中的源代码均可下载,具体信息请参考下文的“下载相关”部分。
7 源代码
解题的源代码示例。本书中的问题基本都会附上基于不同方法来实现的不同的源代码。
8 人物
本书中将有三个人物出场,一同来思考问题。
9 关键点
解题的关键思路。
10 答案
问题的答案。
11 专栏
讲解与该问题相关的内容,或者与编程和算法相关的内容。
吉田
在 SE 股份有限公司上班的年轻程序员。文科出身,偶然间撞见前辈在兴致高昂地编程,深感震撼,并立志成为工程师。好不容易才掌握了基本的编程技能,但是从学生时代起数学就是他的短板。
山崎
吉田的上司。在进度管理方面很严苛,但总是能耐心和大家交流。喝酒也很豪爽,深受部下尊敬。和吉田相反,山崎从小就喜欢数学,是公司“数学之美座谈会”(会员两名)的主力。
前辈
SE 股份有限公司前员工,自由职业者。现在经常以业余活动参与者的身份出入公司,常常给公司的后辈灌输编程的乐趣。从前在公司工作时,因为超人的编程速度,留下了“手指比一般人多两根”“晚上的时候第三只眼会睁开”的传说。
下载相关
本书讲解页上记载的源代码可以通过以下链接下载(点击“随书下载”):
URL http://www.ituring.com.cn/book/1814
下载文件的著作权为作者以及出版社(翔泳社)所有。未经允许,不能通过网络传播,或者发布在Web站点上。
此外,源代码在以下执行环境中验证过:
Ruby 2.2.3
JavaScript 1.8
C 语言 C99 (GCC)
有不少人虽然听说过计算机内部是基于二进制进行处理的,但没办法进一步理解这个概念。事实上,屏幕上显示的文字、图像,或者音乐、视频等,在计算机中都是基于二进制存储的。这里先介绍一下二进制。
首先想想日常使用的数字。数数时我们都是 0,1,2,…,9,10,11,…,98,99,100,101 这样数下去的。把这里用到的数字拆开来之后,会发现就只有 0~9 这 10 个数字。十进制就是使用这 10 个数字来表示数的数字系统。
与此类似,二进制只使用 0 和 1 来表示数。即使位数增加,每个数位上也只会是这两个数字之一,因此用二进制来数数则是 0,1,10,11,100,101,110,111,1000,1001,…。
十进制数 3984 由 3 个 1000(=103)、9 个 100(=102)、8 个 10(= 101) 和 4 个 1(= 100)组成。同样地,二进制数 1011 由 1 个 8(= 23)、0 个 4(= 22)、1 个 2(= 21)和 1 个 1(= 20)组成。
也就是说,二进制数 1011 是 8 + 0 + 2 + 1,对应十进制数 11。反过来,知道十进制数要求二进制数时,则是用这个数除以 2,得到的商再除以 2,再用这一步得到的商除以 2,直到商变为 0。最后,把过程中得到的余数逆序排列就能得到相应的二进制数。
举个例子,如果给定十进制数 19,则求对应的二进制数的过程如下。
19÷2 = 9 余 1
9÷2 = 4 余 1
4÷2 = 2 余 0
2÷2 = 1 余 0
1÷2 = 0 余 1
从下往上排列余数后就可以得到二进制数 10011。
IQ:70 目标时间:10 分钟
如果把某个数的各个数字按相反的顺序排列,得到的数和原来的数相同,则这个数就是“回文数”。譬如 123454321 就是一个回文数。
表 1 十进制数、二进制数和八进制数示例
十进制数 | 二进制数 | 八进制数 |
---|---|---|
0 | 0 | 0 |
1 | 1 | 1 |
2 | 10 | 2 |
3 | 11 | 3 |
4 | 100 | 4 |
5 | 101 | 5 |
6 | 110 | 6 |
7 | 111 | 7 |
8 | 1000 | 10 |
9 | 1001 | 11 |
10 | 1010 | 12 |
11 | 1011 | 13 |
12 | 1100 | 14 |
13 | 1101 | 15 |
14 | 1110 | 16 |
15 | 1111 | 17 |
16 | 10000 | 20 |
求用十进制、二进制、八进制表示都是回文数的所有数字中,大于十进制数 10 的最小值。
例)
※ 本例中的十进制数 9 小于 10,因此不符合要求。
因为是二进制的回文数,所以如果最低位是 0,那么相应地最高位也是 0。但是,以 0 开头肯定是不恰当的,由此可知最低位为 1。
如果用二进制表示时最低位为 1,那这个数一定是奇数,因此只考虑奇数的情况就可以。接下来可以简单地编写程序,从 10 的下一个数字 11 开始,按顺序搜索。譬如用 Ruby 就可以通过下面的代码找到符合条件的数(代码清单 01.01)。
代码清单
01.01
(q01_01.rb
)
# 从11 开始搜索num = 11while true if num.to_s == num.to_s.reverse && num.to_s(8) == num.to_s(8).reverse && num.to_s(2) == num.to_s(2).reverse puts num break end # 只搜索奇数,每次加2 num += 2end
下面试着用 JavaScript 实现同样的逻辑。JavaScript 里没有内置把字符串逆序的标准函数,因此首先需要封装一个返回逆序字符串的方法,其他流程则和代码清单 01.01 中的一致。JavaScript 版本的实现如代码清单 01.02 所示。
代码清单
01.02
(q01_02.js
)
/* 为字符串类型添加返回逆序字符串的方法 */String.prototype.reverse = function (){ return this.split("").reverse().join("");}/* 从11 开始搜索 */var num = 11;while (true){ if ((num.toString() == num.toString().reverse()) && (num.toString(8) == num.toString(8).reverse()) && (num.toString(2) == num.toString(2).reverse())){ console.log(num); break; } /* 只搜索奇数,每次加2 */ num += 2;}
Point
很多语言都提供了把整数转换成二进制数或者八进制数的方法。表 2 汇总了代表性语言的相关函数或者方法,不过 C 语言并没有提供直接转换的接口。
表 2 各编程语言中进制转换的接口
语言 二进制数 八进制数 十六进制数 Ruby to_s(2) to_s(8) to_s(16) PHP decbin decoct dechex Python bin oct hex JavaScript toString(2) toString(8) toString(16) Java toBinaryString toOctalString toHexString C# Convert.ToString Convert.ToString Convert.ToString 或者 ToString("X")
IQ:70 目标时间:10 分钟
大家小时候可能也玩过“组合车牌号里的 4 个数字最终得到 10”的游戏。
组合的方法是在各个数字之间插入四则运算的运算符组成算式,然后计算算式的结果(某些数位之间可以没有运算符,但最少要插入 1 个运算符)。
例)
1234 → 1 + 2×3 - 4 = 3
9876 → 9×87 + 6 = 789
假设这里的条件是,组合算式的计算结果为“将原数字各个数位上的数逆序排列得到的数”,并且算式的运算按照四则运算的顺序进行(先乘除,后加减)。
那么位于 100~999,符合条件的有以下几种情况。
351-→-3×51 = 153
621-→-6×21 = 126
886-→-8×86 = 688
求位于 1000~9999,满足上述条件的数。
解决这个问题时,“计算算式的方法”会影响实现方法。如果要实现的是计算器,那么通常会用到逆波兰表示法,而本题则是使用编程语言内置的功能来实现更为简单。
很多脚本语言都提供了类似 eval 这样的标准函数。譬如用 JavaScript 实现时,可以用代码清单 02.01 解决问题。
代码清单
02.01
(q02_01.js
)
var op = ["+", "-", "*", "/", ""];for (i = 1000; i < 10000; i++){ var c = String(i); for (j = 0; j < op.length; j++){ for (k = 0; k < op.length; k++){ for (l = 0; l < op.length; l++){ val = c.charAt(3) + op[j] + c.charAt(2) + op[k] + c.charAt(1) + op[l] + c.charAt(0); if (val.length > 4){ /* 一定要插入1 个运算符 */ if (i == eval(val)){ console.log(val + " = " + i); } } } } }}
第 10 行中的 eval 就是本题的关键点,接下来只是选择和设置运算符了。虽然有比较深的循环嵌套,但只要确定了位数就没有问题。
基于这样的考虑,如果把代码第 1 行的 op 变量设置成以下值,可以进一步提高程序执行效率。
var op = ["*", ""];
Point
如果用其他语言实现同样逻辑,需要对 0 进行特别处理。例如在 Ruby 中,“以 0 开头的数”会被当作八进制数来处理,因此必须排除以 0 开头的数。此外,也需要排除除数为 0 的情况。
5931(5 * 9 * 31= 1395)
Column
本书以 Ruby 为主要语言编写源代码,但也有像本题一样用 JavaScript 实现的情况。凡用 JavaScript 实现时,结果都用 console.log 来输出,这个结果可以用浏览器来确认。用 Mozilla Firefox 浏览器打开加载了 JavaScript 源代码的 HTML 文件,然后打开“开发者”→“Web 控制台”(如果用 Google Chrome 浏览器,则是打开“更多工具”→“开发者工具”),就可以确认代码的执行结果了。
eval 函数的危险性
本题使用了 eval 函数。这个函数在计算算式等场景下非常方便,但 eval 可以做到的事情不止于此。例如,eval 还可以用来执行指令。
如果在 Web 应用中直接用 eval 执行用户输入的内容,那么用户可能会输入并让程序执行任意指令,包括不恰当的指令。举个例子,假设存在下面这样的用 PHP 编写的 Web 页面(代码清单 02.02)。
代码清单
02.02
(q02_02.php
)
计算器 这个页面的功能就是计算表单中输入的类似“1 + 2*3”这样的算式,并且显示结果。正常输入算式的情况当然没有问题,但根据输入内容不同,我们还可以执行 PHP 脚本。
举个例子,如果输入 phpinfo( ),那么 PHP 的版本等信息会被打印出来。从安全的角度来看,这是非常危险的。
阅读全文: http://gitbook.cn/gitchat/geekbook/5a56c81de286423809d471e2