程序员的算法趣题

内容简介

本书是一本解谜式的趣味算法书,从实际应用出发,通过趣味谜题的解谜过程,引导读者在愉悦中提升思维能力、掌握算法精髓。此外,本书作者在谜题解答上,通过算法的关键原理讲解,从思维细节入手,发掘启发性算法新解,并辅以 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张图片

1 IQ

本书问题的难度逐章递增,每道题的IQ就是一个更加明确的难度提示。

2 目标时间

解题需要的标准思考时间。

3 问题的背景

为让读者更容易理解,这里描述了问题的背景。

4 问题

这里是问题描述,在读者了解背景后设问,引导读者编程并解题。

5 提示

有助于解题的提示。

讲解页

程序员的算法趣题_第2张图片

6 下载文件名

本书中的源代码均可下载,具体信息请参考下文的“下载相关”部分。

7 源代码

解题的源代码示例。本书中的问题基本都会附上基于不同方法来实现的不同的源代码。

8 人物

本书中将有三个人物出场,一同来思考问题。

9 关键点

解题的关键思路。

10 答案

问题的答案。

11 专栏

讲解与该问题相关的内容,或者与编程和算法相关的内容。

出场人物介绍

程序员的算法趣题_第3张图片

吉田

在 SE 股份有限公司上班的年轻程序员。文科出身,偶然间撞见前辈在兴致高昂地编程,深感震撼,并立志成为工程师。好不容易才掌握了基本的编程技能,但是从学生时代起数学就是他的短板。

程序员的算法趣题_第4张图片

山崎

吉田的上司。在进度管理方面很严苛,但总是能耐心和大家交流。喝酒也很豪爽,深受部下尊敬。和吉田相反,山崎从小就喜欢数学,是公司“数学之美座谈会”(会员两名)的主力。

程序员的算法趣题_第5张图片

前辈

SE 股份有限公司前员工,自由职业者。现在经常以业余活动参与者的身份出入公司,常常给公司的后辈灌输编程的乐趣。从前在公司工作时,因为超人的编程速度,留下了“手指比一般人多两根”“晚上的时候第三只眼会睁开”的传说。

下载相关

本书讲解页上记载的源代码可以通过以下链接下载(点击“随书下载”)

URL http://www.ituring.com.cn/book/1814

下载文件的著作权为作者以及出版社(翔泳社)所有。未经允许,不能通过网络传播,或者发布在Web站点上。

此外,源代码在以下执行环境中验证过:

  • Ruby 2.2.3

  • JavaScript 1.8

  • C 语言 C99 (GCC)

第1章 入门篇 · 尝试用编程解决问题

二进制和十进制

有不少人虽然听说过计算机内部是基于二进制进行处理的,但没办法进一步理解这个概念。事实上,屏幕上显示的文字、图像,或者音乐、视频等,在计算机中都是基于二进制存储的。这里先介绍一下二进制。

首先想想日常使用的数字。数数时我们都是 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。

Q1 回文十进制数

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 的最小值。

例)

{55%}

※ 本例中的十进制数 9 小于 10,因此不符合要求。

程序员的算法趣题_第6张图片

思路

因为是二进制的回文数,所以如果最低位是 0,那么相应地最高位也是 0。但是,以 0 开头肯定是不恰当的,由此可知最低位为 1。

如果用二进制表示时最低位为 1,那这个数一定是奇数,因此只考虑奇数的情况就可以。接下来可以简单地编写程序,从 10 的下一个数字 11 开始,按顺序搜索。譬如用 Ruby 就可以通过下面的代码找到符合条件的数(代码清单 01.01)。

代码清单 01.01q01_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

程序员的算法趣题_第7张图片

下面试着用 JavaScript 实现同样的逻辑。JavaScript 里没有内置把字符串逆序的标准函数,因此首先需要封装一个返回逆序字符串的方法,其他流程则和代码清单 01.01 中的一致。JavaScript 版本的实现如代码清单 01.02 所示。

代码清单 01.02q01_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;}

程序员的算法趣题_第8张图片

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")

程序员的算法趣题_第9张图片

答案

程序员的算法趣题_第10张图片

Q2 数列的四则运算

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,满足上述条件的数。

程序员的算法趣题_第11张图片

思路

解决这个问题时,“计算算式的方法”会影响实现方法。如果要实现的是计算器,那么通常会用到逆波兰表示法,而本题则是使用编程语言内置的功能来实现更为简单。

很多脚本语言都提供了类似 eval 这样的标准函数。譬如用 JavaScript 实现时,可以用代码清单 02.01 解决问题。

代码清单 02.01q02_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 就是本题的关键点,接下来只是选择和设置运算符了。虽然有比较深的循环嵌套,但只要确定了位数就没有问题。

程序员的算法趣题_第12张图片

基于这样的考虑,如果把代码第 1 行的 op 变量设置成以下值,可以进一步提高程序执行效率。

var op = ["*", ""];

Point

如果用其他语言实现同样逻辑,需要对 0 进行特别处理。例如在 Ruby 中,“以 0 开头的数”会被当作八进制数来处理,因此必须排除以 0 开头的数。此外,也需要排除除数为 0 的情况。

程序员的算法趣题_第13张图片

答案

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.02q02_02.php

 计算器

这个页面的功能就是计算表单中输入的类似“1 + 2*3”这样的算式,并且显示结果。正常输入算式的情况当然没有问题,但根据输入内容不同,我们还可以执行 PHP 脚本。

举个例子,如果输入 phpinfo( ),那么 PHP 的版本等信息会被打印出来。从安全的角度来看,这是非常危险的。

Q3 翻牌
Q4 切分木棒
Q5 还在用现金支付吗
Q6(改版)考拉兹猜想
Q7 日期的二进制转换
Q8 优秀的扫地机器人
Q9 落单的男女
Q10 轮盘的最大值
第2章 初级篇 · 解决简单问题 体会算法效果
Q11 斐波那契数列
Q12 平方根数字
Q13 有多少种满足字母算式的解法
Q14 世界杯参赛国的国名接龙
Q15 走楼梯
Q16 3 根绳子折成四边形
Q17 挑战 30 人 31 足
Q18 水果酥饼日
Q19 朋友的朋友也是朋友吗
Q20 受难立面魔方阵
Q21 异或运算三角形
Q22 不缠绕的纸杯电话
Q23 二十一点通吃
Q24 完美的三振出局
Q25 鞋带的时髦系法
Q26 高效的立体停车场
Q27 禁止右转也没关系吗
Q28 社团活动的最优分配方案
Q29 合成电阻的黄金分割比
Q30 用插线板制作章鱼脚状线路
第3章 中级篇 · 优化算法 实现高速处理
Q31 计算最短路径
Q32 榻榻米的铺法
Q33 飞车与角行的棋步
Q34 会有几次命中注定的相遇
Q35 0 和 7 的回文数
Q36 翻转骰子
Q37 翻转 7 段码
Q38 填充白色
Q39 反复排序
Q40 优雅的 IP 地址
Q41 只用 1 个数字表示 1234
Q42 将牌洗为逆序
Q43 让玻璃杯水量减半
Q44 质数矩阵
Q45 排序交换次数的最少化
Q46 唯一的 ○× 序列
Q47 格雷码循环
Q48 翻转得到交错排列
Q49 欲速则不达
Q50 完美洗牌
Q51 同时结束的沙漏
Q52 糖果恶作剧
Q53 同数包夹
Q54 偷懒的算盘
Q55 平分蛋糕
第4章 高级篇 · 改变思路 让程序速度更快
Q56 鬼脚图中的横线
Q57 最快的联络网
Q58 丢手绢游戏中的总移动距离
Q59 合并单元格的方式
Q60 分割为同样大小
Q61 不交叉 , 一笔画下去
Q62 日历的最大矩形
Q63 迷宫会合
Q64 麻烦的投接球
Q65 图形的一笔画
Q66 设计填字游戏
Q67 不挨着坐是一种礼节吗
Q68 异性相邻的座次安排
Q69 蓝白歌会

阅读全文: http://gitbook.cn/gitchat/geekbook/5a56c81de286423809d471e2

你可能感兴趣的:(程序员的算法趣题)