我的第一本算法书

内容简介

  • 481 张步骤图详解 26 个算法和 7 个数据结构的基本原理
  • 没有枯燥的理论和复杂的代码,易于理解
  • 采用大量彩色图片,清晰直观,便于记忆
  • 零基础也能轻松掌握,自学算法的好搭档

enter image description here

作者简介

石田保辉,自由职业工程师,现居日本东京。2011年毕业于日本京都大学研究生院。辗转于几个创新型企业后独立,成为自由职业者。2016年,个人制作的面向工程师的学习型 App“算法动画图解”上架,不到1年时间全球下载量即达到50万次,并入选了“App Store 日本区2016年度最佳应用”榜单。

宫崎修一,日本京都大学学术信息媒体中心副教授。1998年从日本九州大学博士生院工学专业毕业后,开始担任日本京都大学研究生院信息学研究科助手,2002年起担任现职。主要研究算法和计算复杂性理论。近期的重点研究对象为相似算法和在线算法。主要著作有《图论入门:基本知识和算法》(日本森北出版社,2015年)。

本书内容

前言

本书以 iOS 和 Android 平台上的应用程序“算法动画图解”为基础,以图配文,详细讲解了各种算法和数据结构的基本原理。如果本书能够帮助大家理解基本算法的操作和特征,那么我将感到十分荣幸。

使用不同的算法解决同一个问题时,就算得到的结果是一样的,算法之间的性质也有很大的差异。比如,某个算法的运行时间很短,但需要占用大量内存;而另一个算法运行时间较长,但内存资源占用较少。学习各种算法可以使我们在编程时有更多的选择。成为优秀程序员的必要条件之一,就是可以根据应用场景选择最合适的算法。

如果您对算法有兴趣,还可以挑战一下“算法理论”这门学科,试着去发现更高效的算法,或者研究目前用算法还无法解决的问题。

石田保辉


算法是解决问题的计算步骤,用于编写程序之前。即使是解决同样的问题,高效算法和低效算法所花费的时间也迥然不同。另外,要想执行高效的算法,还需要使用合适的数据结构。本书的目的就是让初学者也能轻松地理解算法和数据结构。

本书以 iOS 和 Android 平台上的应用程序“算法动画图解”为基础。该应用以动画的形式展示了算法的流程,而本书则采用了大量的图片来分步讲解,尽量保留了原应用易懂的优点。为了配合出版,本书还添加了“什么是算法”“算法的运行时间”“图的基础知识”等应用中没有的章节,相信会让读者对算法的理解更加深刻。

读完本书,不过是站在了算法世界的入口,这个世界还有很多领域等待人们去探索。如果您由此对算法产生了兴趣,请务必继续深入学习。

宫崎修一

序章 算法的基本知识

No.0-1 什么是算法

算法与程序的区别

算法就是计算或者解决问题的步骤。我们可以把它想象成食谱。要想做出特定的料理,就要遵循食谱上的步骤;同理,要想用计算机解决特定的问题,就要遵循算法。这里所说的特定问题多种多样,比如“将随意排列的数字按从小到大的顺序重新排列”“寻找出发点到目的地的最短路径”,等等。

食谱和算法之间最大的区别就在于算法是严密的。食谱上经常会有描述得比较模糊的部分,而算法的步骤都是用数学方式来描述的,所以十分明确。

算法和程序有些相似,区别在于程序是以计算机能够理解的编程语言编写而成的,可以在计算机上运行,而算法是以人类能够理解的方式描述的,用于编写程序之前。不过,在这个过程中到哪里为止是算法、从哪里开始是程序,并没有明确的界限。

就算使用同一个算法,编程语言不同,写出来的程序也不同;即便使用相同的编程语言,写程序的人不同,那么写出来的程序也是不同的。

排列整数的算法:排序

查找最小的数字并交换:选择排序

来看一个具体的算法示例吧。这是一个以随意排列的整数为输入,把它们按从小到大的顺序重新排列的问题。这类排序问题我们将在第2章详细讲解。

{60%}

只解决这一个问题很简单,但是算法是可以应对任意输入的计算步骤,所以必须采用通用的描述。虽然在这个示例中输入的整数个数 n 为8,然而不管 n 多大,算法都必须将问题解决。

那么,你首先想到的方法,是不是先从输入的数字中找出最小的数字,再将它和最左边的数字交换位置呢?在这个示例中就是找到最小数字1,然后将它和最左边的7交换位置。

{60%}

这之后1的位置便固定下来,不再移动。接下来,在剩下的数字里继续寻找最小数,再将它和左边第2个数字交换位置。于是,4和13也交换了位置。

{60%}

我们将这样的一次交换称为“1轮”。到了第 k 轮的时候,就把剩下的数字中最小的一个,与左边开始第 k 个数字进行交换。于是在结束第 k 轮后,从左数的 k 个数字便都按从小到大的顺序排列了。只要将这个步骤重复 n 次,那么所有的数字都将按从小到大的顺序排列。

这便是我们将在2-3节中介绍的选择排序。不管输入的数字是什么、n 有多大,都可以用这个算法解决问题。

用计算机能理解的方式构思解法:算法的设计

计算机擅长高速执行一些基本命令,但无法执行复杂的命令。此处的“基本命令”指的是“做加法”或者“在指定的内存地址上保存数据”等。

计算机是以这些基本命令的组合为基础运行的,面对复杂的操作,也是通过搭配组合这些基本命令来应对的。上文中提到的“对 n 个数字进行排序”对计算机来说就是复杂的操作。如何设计算法来解决这个排序问题,也就等同于构思如何搭配组合计算机可以执行的那些基本命令来实现这个操作。

如何选择算法

能解决排序问题的算法不止选择排序这一个。那么,当有多个算法都可以解决同一个问题时,我们该如何选择呢?在算法的评判上,考量的标准也各有不同。

比如,简单的算法对人来说易于理解,也容易被写成程序,而在运行过程中不需要耗费太多空间资源的算法,就十分适用于内存小的计算机。

不过,一般来说我们最为重视的是算法的运行时间,即从输入数据到输出结果这个过程所花费的时间。

对50个数字排序所花的时间竟然比宇宙的历史还要长吗

使用全排列算法进行排序

为了让大家体会一下低效率算法的效果,这里来看看下面这个排序算法。

① 生成一个由 n 个数字构成的数列(不和前面生成的数列重复)

② 如果①中生成的数列按从小到大的顺序排列就将其输出,否则回到步骤①

我们就把这个算法称为“全排列算法”吧。全排列算法列出了所有的排列方法,所以不管输入如何,都可以得到正确的结果。

那么,需要等多久才能出结果呢?若运气好,很快就能出现正确排列的话,结果也就立马出来了。然而,实际情况往往并不如我们所愿。最差的情况,也就是直到最后才出现正确排列的情况下,计算机就不得不确认所有可能的排列了。

n 个数字有 n! 种不同的排列方法 (n!=n(n-1)(n-2)\cdots3\cdot2\cdot1)。现在,我们来看看 n=50 时是怎样一种情况吧。

<img src="http://private.codecogs.com/gif.latex?\begin{aligned}&\textcircled{1}~50!=50~\cdot~49~\cdot~48\cdots3~\cdot~2~\cdot~1\&\textcircled{2}~~~~~~~~~50~\cdot~49~\cdot~48\cdots3~\cdot~2~\cdot~1%3E50~\cdot~49~\cdot~48\cdots13~\cdot~12~\cdot~11\&\textcircled{3}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~50~\cdot~49~\cdot~48\cdots13~\cdot~12~\cdot~11%3E10^{40}\end{aligned}" alt="\begin{aligned}&\textcircled{1}~50!=50~\cdot~49~\cdot~48\cdots3~\cdot~2~\cdot~1\&\textcircled{2}~~~~~~~~~50~\cdot~49~\cdot~48\cdots3~\cdot~2~\cdot~1>50~\cdot~49~\cdot~48\cdots13~\cdot~12~\cdot~11\&\textcircled{3}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~50~\cdot~49~\cdot~48\cdots13~\cdot~12~\cdot~11>10^{40}\end{aligned}" />

公式①中,50!即为数字1到数字50的乘积。为了便于计算,我们通过公式②③将结果近似转换为10的 n 次方的形式。公式②右边部分去掉了10以下的数字,因此小于50!。公式③左右都是40个数字的乘积,但左边数字都大于10,因此大于右边的 10^{40}。接下来我们就用 10^{40} 近似代表50个数字的所有排列情况来进行计算。

假设1台高性能计算机1秒能检查1万亿(=10^{12})个数列,那么检查 10^{40} 个数列将花费的时间为 10^{40}\div10^{12}=10^{28} 秒。1年为31536000秒,不到 10^{8} 秒。因此,10^{28} 秒> 10^{20} 年。

从大爆炸开始宇宙已经经历了约137亿年,即便如此也少于 10^{11} 年。也就是说,仅仅是对50个数字进行排序,若使用全排列算法,就算花费宇宙年龄的 10^{9} 倍时间也得不出答案。

{80%}

使用选择排序算法进行排序

那么,使用前文提到的选择排序算法,情况又将如何呢?

首先,为了在第1轮找到最小的数字,需要从左往右确认数列中的数字,只要查询 n 个数字即可。在接下来的第2轮中,需要从 n-1 个数字中寻找最小值,所以需要查询 n-1 个数字。将这个步骤进行到第 n 轮的时候,需要查询的次数如下。

n+(n-1)+(n-2)+\cdots3+2+1=\frac{n(n+1)}{2}\leqslant n^2

n=50 的时候 n^2=2500。假设1秒能确认1万亿(=10^{12})个数字,那么 2500\div10^{12}=0.000~000~002~5 秒便能得出结果,比全排列算法的效率高得多。

No.0-2 运行时间的计算方法

了解输入数据的量和运行时间之间的关系

上一节在结尾说明了算法的不同会导致其运行时间产生大幅变化,本节将讲解如何求得算法的运行时间。

使用相同的算法,输入数据的量不同,运行时间也会不同。比如,对10个数字排序和对1000000个数字排序,大家很容易就想到后者的运行时间更长。那么,实际上运行时间会长多少呢?后者是前者的100倍,还是1000000倍?就像这样,我们不光要理解不同算法在运行时间上的区别,还要了解根据输入数据量的大小,算法的运行时间具体会产生多大的变化。

如何求得运行时间

那么,如何测算不同输入所导致的运行时间的变化程度呢?最为现实的方法就是在计算机上运行一下程序,测试其实际花费的时间。但是,就算使用同样的算法,花费的时间也会根据所用计算机的不同而产生偏差,十分不便。

所以在这里,我们使用“步数”来描述运行时间。“1步”就是计算的基本单位。通过测试“计算从开始到结束总共执行了多少步”来求得算法的运行时间。

作为示例,现在我们试着从理论层面求出选择排序的运行时间。选择排序的步骤如下。

① 从数列中寻找最小值

② 将最小值和数列最左边的数字进行交换,排序结束。回到①

如果数列中有 n 个数字,那么①中“寻找最小值”的步骤只需确认 n 个数字即可。这里,将“确认1个数字的大小”作为操作的基本单位,需要的时间设为 T_c,那么步骤①的运行时间就是 n\times T_c

接下来,把“对两个数字进行交换”也作为操作的基本单位,需要的时间设为 T_s。那么,①和②总共重复 n 次,每经过“1轮”,需要查找的数字就减少1个,因此总的运行时间如下。

\begin{aligned}&(n\times T_c+T_s)+((n-1)\times T_c+T_s)+((n-2)\times T_c+T_s)+\cdots+(2\times T_c+T_s)+(1\times T_c+T_s)\&=\frac{1}{2}T_cn(n+1)+T_sn\&=\frac{1}{2}T_cn^2+(\frac{1}{2}T_c+T_s)n\end{aligned}

虽说只剩最后1个数字的时候就不需要确认了,但是方便起见还是把对它的确认和交换时间计算在内比较好。

运行时间的表示方法

虽说我们已经求得了运行时间,但其实这个结果还可以简化。T_cT_s 都是基本单位,与输入无关。会根据输入变化而变化的只有数列的长度 n,所以接下来考虑 n 变大的情况。n 越大,上式中的 n^2 也就越大,其他部分就相对变小了。也就是说,对式子影响最大的是 n^2。所以,我们删掉其他部分,将结果表示成下式右边的形式。

enter image description here

通过这种表示方法,我们就能大致了解到排序算法的运行时间与输入数据量 n 的平方成正比。同样地,假设某个算法的运行时间如下。

5T_x~n^3+12T_y~n^2+3T_z~n

那么,这个结果就可以用 O(n^3) 来表示。如果运行时间为

3n\log n+2T_yn

这个结果就可以用 enter image description here 来表示。

O 这个符号的意思是“忽略重要项以外的内容”,读音同 Order。O(n^2) 的含义就是“算法的运行时间最长也就是 n^2 的常数倍”,准确的定义请参考相关专业书籍。重点在于,通过这种表示方法,我们可以直观地了解算法的时间复杂度[1]

比如,当我们知道选择排序的时间复杂度为 O(n^2)、快速排序的时间复杂度为 O(n\log n) 时,很快就能判断出快速排序的运算更为高速。二者的运行时间根据输入 n 产生的变化程度也一目了然。

关于算法的基本知识就介绍到这里了。从下一章开始,我们就来具体学习各种算法吧。


[1] 时间复杂度是一个可以描述算法运行时间的函数,常用大 O 符号来表述。——译者注

第1章 数据结构
第2章 排序
第3章 数组的查找
第4章 图的搜索
第5章 安全算法
第6章 聚类
第7章 其他算法

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

猜你喜欢

转载自blog.csdn.net/valada/article/details/86547927