第一章 算法简介
1.1引言
算法是一组完成任务的指令。
1.2二分查找
二分查找是一种算法,其输入是一个有序的元素列表(必须有序的原因稍后解释)。如果要查找的元素包含在列表中,二分查找返回其位置;否则返回null。
对于包含n个元素的列表,用二分查找最多需要log2n步,而简单查找最多需要n步。
对数运算是幂运算的逆运算。例如:log10100 = 2
仅当列表是有序的时候,二分查找才管用。
一般而言,应选择效率最高的算 法,以最大限度地减少运行时间或占用空间。
最多需要猜测的次数与列表长度相同,这被称为线性 时间(linear time)。
二分查找的运行时间为对数时间(或log时间)。
1.3大O表示法
大O表示法是一种特殊的表示法,指出了算法的速度有多快。
两种算法的运行时间呈现不同的增速。也就是说,随着元素数量的增加,二分查找需要的额外时间并不多,而简单查找需要的额外时间却很多。
下面按从快到慢的顺序列出了你经常会遇到的5种大O运行时间:
a) O(log n),也叫对数时间,这样的算法包括二分查找。
b) O(n),也叫线性时间,这样的算法包括简单查找。
c) O(n * log n),这样的算法包括快速排序——一种速度较快的排序算法。
d) O(n2),这样的算法包括选择排序——一种速度较慢的排序算法。
e) O(n!),这样的算法包括旅行商问题的解决方案——一种非常慢的算法。
我们获得的主要启示如下。
a) 算法的速度指的并非时间,而是操作数的增速。
b) 谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加。
c) 算法的运行时间用大O表示法表示。
d) O(log n)比O(n)快,当需要搜索的元素越多时,前者比后者快得越多。
第二章 选择排序
2.1 内存的工作原理
需要将数据存储到内存时,你请求计算机提供存储空间,计算机给你一个存储地址。需要存储多项数据时,有两种基本方式——数组和链表。
2.2 数组和链表
链表中的元素可存储在内存的任何地方。
链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。
在需要读取链表的最后一个元素时,你不能直接读取,因为你不知道 它所处的地址,必须先访问元素#1,从中获取元素#2的地址,再访问元素#2并从中获取元素#3的地址,以此类推,直到访问最后一个元素。
需要随机地读取元素时,数组的效率很高,因为可迅 速找到数组的任何元素。
数组的元素带编号,编号从0而不是1开始。
元素的位置称为索引。
需要在中间插入元素时,插入元素很简单,只需修改 它前面的那个元素指向的地址。而使用数组时,则必须将后面的元素都向后移。
当需要在中间插入元素时,链表是更好的选择。
如果你要删除元素呢?链表也是更好的选择,因为只需修改前一个元素指向的地址即可。而使用数组时,删除元素后,必须将后面的元素都向前移。
数组和链表哪个用得更多呢?显然要看情况。但数组用得很多,因为它支持随机访问。有两种访问方式:随机访问和顺序访问。顺序访问意味着从第一个元素开始逐个地读取元素。链表只能顺序访问:要读取链表的第十个元素,得先读取前九个元素,并沿链接找到第十个元素。随机 10访问意味着可直接跳到第十个元素。
下面是常见数组和链表操作的运行时间。
2.3 选择排序
你要将这个列表按播放次数从多到少的顺序排列,从而将你喜欢的乐队排序。该如何做呢?一种办法是遍历这个列表,找出作品播放次数最多的乐队,并将该乐队添加到一个新列表中。再次这样做,找出播放次数第二多的乐队。继续这样做,你将得到一个有序列表。
要找出播放次数最多的乐队,必须检查列表中的每个元素。需要的总时间为 O(n × n),即O(n2)。
选择排序是一种灵巧的算法,但其速度不是很快。
第三章 递归
3.1 递归
这个盒子里有盒子,而盒子里的盒子又有盒子。钥匙就在某个盒子中。为找到钥匙,你将使用什么算法?
下面是一种方法。
1) 创建一个要查找的盒子堆。
2) 从盒子堆取出一个盒子,在里面找。
3) 如果找到的是盒子,就将其加入盒子堆中,以便以后再查找。 (4) 如果找到钥匙,则大功告成!
4) 回到第二步。
下面是另一种方法。
1) 检查盒子中的每样东西。
2) 如果是盒子,就回到第一步。
3) 如果是钥匙,就大功告成!
第一种方法使用的是while循环:只要盒子堆不空,就从中 取一个盒子,并在其中仔细查找。第二种方法使用递归——函数调用自己。
递归只是让解决方案更清晰,并 没有性能上的优势。实际上,在有些情况下,使用循环的性能更好。
3.2 基线条件和递归条件
编写递归函数时,必须告诉它何时停止递归。正因为如此,每个递归函数都有两部分:基线条件(base case)和递归条件(recursive case)。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。
3.3 栈
调用另一个函数时,当前函数暂停 并处于未完成状态。
这个栈用于存储多个函数的变量,被称为调用栈。
递归函数也使用调用栈!来看看递归函数factorial的调用栈。factorial(5)写作5!,其定义如下:5! = 5 * 4 * 3 * 2 * 1。同理,factorial(3)为3 * 2 * 1。下面是计算阶乘的递归函数。
def fact(x):
if x == 1:
return 1
else:
return x * fact(x-1)
使用栈虽然很方便,但是也要付出代价:存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。在这种情况下,你有两种选择。
a) 重新编写代码,转而使用循环。
b) 使用尾递归。
第四章 快速排序
4.1分而治之
本章的重点是使用学到的新技能来解决问题。我们将探索分而治之(divide and conquer,D&C)——一种著名的递归式问题解决方法。
如何将一块地均匀地分成方块,并确保分出的方块是最大的呢?使用D&C策略!D&C是递归的。使用D&C解决问题的过程包括两个步骤。
(1) 找出基线条件,这种条件必须尽可能简单。
(2) 不断将问题分解(或者说缩小规模),直到符合基线条件。
首先,找出基线条件。最容易处理的情况是,一条边的长度是另一条边的整数倍。
现在需要找出递归条件,这正是D&C的用武之地。根据D&C的定义,每次递归调用都必须缩小问题的规模。如何缩小前述问题的规模呢?我们首先找出这块地可容纳的最大方块。同时余下一小块地。现在是顿悟时刻:何不对余下的那一小块地使用相同的算法呢?
这里重申一下D&C的工作原理:
(1) 找出简单的基线条件;
(2) 确定如何缩小问题的规模,使其符合基线条件。 D&C并非可用于解决问题的算法,而是一种解决问题的思路。
4.2 快速排序
快速排序是一种常用的排序算法,比选择排序快得多。例如,C语言标准库中的函数qsort实现的就是快速排序。快速排序也使用了D&C。
下面来使用快速排序对数组进行排序。
首先,从数组中选择一个元素,这个元素被称为基准值(pivot)。
接下来,找出比基准值小的元素以及比基准值大的元素。
4.3 再谈大O表示法
快速排序的独特之处在于,其速度取决于选择的基准值。在讨论快速排序的运行时间前,我们再来看看最常见的大O运行时间。
还有一种名为合并排序(merge sort)的排序算法,其运行时间为O(n log n),比选择排序快得多!快速排序的情况比较棘手,在最糟情况下,其运行时间为O(n2)。
与选择排序一样慢!但这是最糟情况。在平均情况下,快速排序的运行时间为O(n log n)。
第五章 散列表
5.1 散列函数
散列函数“将输入映射到数字”。
在你将学习的复杂数据结构中,散列表可能是最有用的,也被称为散列映射、映射、字典和关联数组。散列表的速度很快!
你可能根本不需要自己去实现散列表,任一优秀的语言都提供了散列表实现。Python提供的散列表实现为字典,你可使用函数dict来创建散列表。
5.2 应用案例
缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中!
散列表适合用于:
a) 模拟映射关系;
b) 防止重复;
c) 缓存/记住数据,以免服务器再通过处理来生成它们。
5.3 冲突
这种情况被称为冲突(collision):给两个键分配的位置相同。
处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表。
这里的经验教训有两个。
a) 散列函数很重要。前面的散列函数将所有的键都映射到一个位置,而最理想的情况是, 散列函数将键均匀地映射到散列表的不同位置。
b) 如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很好,这些链表就不会很长!
5.4 性能
在平均情况下,散列表执行各种操作的时间都为O(1)。O(1)被称为常量时间。你以前没有见过常量时间,它并不意味着马上,而是说 4不管散列表多大,所需的时间都相同。
在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。需要有:
a) 较低的填装因子
b) 良好的散列函数
散列表的填装因子很容易计算:散列表包含的元素数/位置总数
良好的散列函数让数组中的值呈均匀分布。糟糕的散列函数让值扎堆,导致大量的冲突。
第六章 广度优先搜索
广度优先搜索让你能够找出两样东西之间的最短距离,不过最短距离的含义有很多!使用广度优先搜索可以:
a) 编写国际跳棋AI,计算最少走多少步就可获胜;
b) 编写拼写检查器,计算最少编辑多少个地方就可将错拼的单词改成正确的单词,如READED改为READER需要编辑一个地方;
c) 根据你的人际关系网络找到关系最近的医生。
6.1 图简介
假设你居住在旧金山,要从双子峰前往金门大桥。你想乘公交车前往,并希望换乘最少。可 乘坐的公交车如下。
前往金门大桥的 最短路径需要三步。这种问题被称为最短路径问题(shorterst-path problem)。你经常要找出最短路径,这可能是前往朋友家的最短路径,也可能是国际象棋中把对方将死的最少步数。解决最短路径问题的算法被称为广度优先搜索。
6.2 图是什么
图由节点和边组成。一个节点可能与众多节点直接相连,这些节点被称为邻居。
6.3 广度优先搜索
广度优先搜索是一种用于图的查找算法,可帮助回答两类问题。
a) 第一类问题:从节点A出发,有前往节点B的路径吗?
b) 第二类问题:从节点A出发,前往节点B的哪条路径最短?
下面来尝试回答第二类问题——谁是关系最近的芒果销 售商。例如,朋友是一度关系,朋友的朋友是二度关系。假设你经营着一个芒果农场,需要寻找芒果销售商,以便将芒果卖给他。
在广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,即先检查一度关系,再检查二度关系。
你需要按添加顺序进行检查。有一个可实现这种目的的数据结构,那就是队列(queue)。
队列只支持两种操作:入队和出队。
队列是一种先进先出(First In First Out,FIFO)的数据结构,而栈是一种后进先出(Last In First Out,LIFO)的数据结构。
6.4 实现图
图不过是一系列的节点和边,表示它的Python代码如下。
Anuj、Peggy、Thom和Jonny都没有邻居,这是因为虽然有指向他们的箭头,但没有从他们出发指向其他人的箭头。这被称为有向图(directed graph),其中的关系是单向的。因此,Anuj是Bob的邻居,但Bob不是Anuj的邻居。无向图(undirected graph)没有箭头,直接相连的节点互为邻居。例如,下面两个图是等价的。
6.5 实现算法
先概述一下这种算法的工作原理。
检查一个人之前,要确认之前没检查过他,这很重要。为此,你可使用一个列表来记录检查过的人。
考虑到这一点后,广度优先搜索的最终代码如下。
第七章 狄克斯特拉算法
在前一章使用了广度优先搜索,它找出的是段数最少的路径。如果你要找出最快的路径,该如何办呢?为此,可使用另一种算法——狄克斯特拉算法(Dijkstra’s algorithm)。
7.1 使用狄克斯特拉算法
下面来看看如何对下面的图使用这种算法。
其中每个数字表示的都是时间,单位分钟。为找出从起点到终点耗时最短的路径,你将使用狄克斯特拉算法。
狄克斯特拉算法包含4个步骤。
a) 找出“最便宜”的节点,即可在最短时间内到达的节点。
b) 更新该节点的邻居的开销,其含义将稍后介绍。
c) 重复这个过程,直到对图中的每个节点都这样做了。
d) 计算最终路径。
7.2 术语
狄克斯特拉算法用于每条边都有关联数字的图,这些数字称为权重(weight)。
带权重的图称为加权图(weighted graph),不带权重的图称为非加权图(unweighted graph)。
要计算非加权图中的最短路径,可使用广度优先搜索。要计算加权图中的最短路径,可使用狄克斯特拉算法。图还可能有环,而 环类似右面这样。
狄克斯特拉算法只适用于有向无环图。
7.5 实现
第八章 贪婪算法
8.1 教室调度问题
你希望在这间教室上尽可能多的课。如何选出尽可能多且时间不冲突的课程呢?
这个问题好像很难,不是吗?实际上,算法可能简单得让你大吃一惊。具体做法如下。
a) 选出结束最早的课,它就是要在这间教室上的第一堂课。
b) 接下来,必须选择第一堂课结束后才开始的课。同样,你选择结束最早的课,这将是要 在这间教室上的第二堂课。
重复这样做就能找出答案!
贪婪算法很简单:每步都采取最优的做法。
你每步都选择局部最优解,最终得到的就是全局最优解。
8.2 背包问题
你力图往背包中装入价值最高的商品,你会使用哪种算法呢?
同样,你采取贪婪策略,这非常简单。
a) 盗窃可装入背包的最贵商品。
b) 再盗窃还可装入背包的最贵商品,以此类推。
在有些情况下,完美是优秀的敌人。有时候,你只需找到一个能够大致解决问题的算法,此时贪婪算法正好可派上用场,因为它们实现起来很容易,得到的结果又与正确结果相当接近。
8.3 集合覆盖问题
假设你办了个广播节目,要让全美50个州的听众都收听得到。为此,你需要决定在哪些广播台播出。在每个广播台播出都需要支付费用,因此你力图在尽可能少的广播台播出。
每个广播台都覆盖特定的区域,不同广播台的覆盖区域可能重叠。
如何找出覆盖全美50个州的最小广播台集合呢?听起来很容易,但其实非常难。具体方法如下。
(1) 列出每个可能的广播台集合,这被称为幂集(power set)。可能的子集有2n个。
(2) 在这些集合中,选出覆盖全美50个州的最小集合。
贪婪算法可化解危机!使用下面的贪婪算法可得到非常接近的解。
(1) 选出这样一个广播台,即它覆盖了最多的未覆盖州。即便这个广播台覆盖了一些已覆盖 的州,也没有关系。
(2) 重复第一步,直到覆盖了所有的州。
这是一种近似算法(approximation algorithm)。在获得精确解需要的时间太长时,可使用近
似算法。判断近似算法优劣的标准如下:
a) 速度有多快;
b) 得到的近似解与最优解的接近程度。
第九章 动态规划
略
第十章 K最近邻算法
10.1 橙子还是柚子
如果判断这个水果是橙子还是柚子呢? 一般而言,柚子更大、更红。一种办法是看它的邻居。来看看离它最近的三个邻居。
在这三个邻居中,橙子比柚子多,因此这个水果很可能是橙子。祝贺你,你刚才就是使用K 最近邻(k-nearest neighbours,KNN)算法进行了分类!这个算法非常简单。
KNN算法虽然简单却很有用!要对东西进行分类时,可首先尝试这种算法。
10.2 创建推荐系统
特征抽取
在前面的水果示例中,你根据个头和颜色来比较水果,换言之,你比较的特征是个头和颜色。现在假设有三个水果,你可抽取它们的特征。
再根据这些特征绘图。
从上图可知,水果A和B比较像。下面来度量它们有多像。要计算两点的距离,可使用毕达哥拉斯公式。
回归
你将使用KNN来做两项 基本工作——分类和回归:
a) 分类就是编组;
b) 回归就是预测结果(如一个数字)。
挑选合适的特征
使用KNN时,挑选合适的特征进行比较至关重要。
在挑选合适的特征方面,没有放之四海皆准的法则,你必须考虑到各种需要考虑的因素。
10.3 机器学习简介
OCR
OCR指的是光学字符识别(optical character recognition),这意味着你可拍摄印刷页面的照片,计算机将自动识别出其中的文字。
OCR的第一步是查看大量的数字图像并提取特征,这被称为训练(training)。大多数机器学习算法都包含训练的步骤:要让计算机完成任务,必须先训练它。
创建垃圾邮件过滤器
垃圾邮件过滤器使用一种简单算法——朴素贝叶斯分类器(Naive Bayes classifier)
第十一章 接下来如何做
略
<完结>
备注:该文章为个人读书学习笔记,仅供学习之用,未经允许禁止转载。