笔记:《图解算法》(已完结)

第一章 算法简介

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)



第十一章 接下来如何做

<完结>


备注:该文章为个人读书学习笔记,仅供学习之用,未经允许禁止转载。

你可能感兴趣的:(笔记:《图解算法》(已完结))