目录
10.2 NP 完全问题(Non-deterministic Polynomial多项式的不确定性)
12.1 余弦相似度( cosine similarity)
13.1 二叉查找树( binary search tree)
花了一周时间读完了《算法图解》,没有集中大块的时间读书,收益就不是很好!以后读书要专心,高效才行。现将本书内容整理如下:
本书的算法部分包含了最简单最经典的排序和查找,还有递归这种编程思想。然后介绍了本质是数学映射的散列表,以及广度搜索和深度搜索算法(深度搜索算法应该就是Dijkstra)。随后介绍了解决NP问题的常用优化算法:贪心算法。贪心算法有的时候并不能解决NP问题,这时候动态规划算法就上场了。介绍了最简单的机器学习算法——KNN算法,有点类似于Kmeans。各章节的主要内容如下:
1、二分查找
二分查找是一种算法,其输入是一个有序的元素列表,如果要查找的元素在其中 二分查找就返回其原位置;否则返回null。通俗的讲就是折半查找,线性表采用顺序存储的结构,表中元素按关键字有序排列,将表中间的关键字与查找关键字比较,如果相等就查找成功,如果不相等就以中间位置为界限将表分成前后两个子表,如果中间位置的关键字大于查找的数则查前一表单,如小于则查后一表单,不断缩小查找范围。
对于包含n个元素的列表,用二分查找最多需要log2n(以2为底n的对数)而简单查找最多需要n步。
2、大O表示法
计算机科学中,大O表示法被用来描述一个算法的性能或复杂度。大O表示法可以用来描述一个算法的最差情况,或者一个算法执行的耗时或占用空间(例如内存或磁盘占用)。即大O表示法指的并不是以秒为单位的速度,是比较操作数,指出了算法运行时间的增速。
O(操作数) O(log n) <log n就表示log2n>
常见大O运行时间:
· O(log n ),也叫对数时间,这样的算法包括二分查找。
· O (n),也叫线性时间,这样的算法包括简单查找。
· O (n* log n ),这样的算法包括快速排序——一种速度较快的排序算法。
· O (n2 ),这样的算法包括选择排序——一种速度较慢的排序算法。
· O (n!),这样的算法是一种非常慢的算法。包括旅行商问题的解决方案。
算法的速度指的并不是时间 而是操作数的增速。
3、排序
数组,链表
数组的话意味着所有的元素在内存中都是连在一起的。一般为了添加新元素会有 预留座位 但是会有可能额外请求的内存用不上 浪费了内存,或者预留不够 这两个缺点
链表的每个元素都储存了下一个元素的地址,内存是随机的,因此添加元素只需要将其放入内存中,将其地址储存到前一个元素中。
链表的优势在于添加元素方面,数组在于地址方面。
当需要读取链表的最后一个元素时,不能够直接读取,因为不知道它的地址只能挨个访问元素,直至访问到最后一个元素。需要读取所有元素时 链表的效率很高,但如果需要跳跃,链表的效率很低。而 数组则可以知道每个元素的地址,随机读取数据时数组的效率很高
数组的元素带编号,从0开始而不是1.元素的位置称为索引,如说 元素20的位于索引1处
读取时间 插入时间
数组 O(1) O(n)
链表 O(n) O(1)
在中间插入
使用链表时 插入元素只需要修改它前面的那个元素指向的地址,而使用数组则需要将后面的元素都后移,空间不够还得将整个数组复制到别的地方。 删除元素同理,链表也是更好的选择。
读取 插入 删除
数组 O(1) O(n) O(n)
链表 O(n) O(1) O(1)
需要指出的是,仅当能够立即访问要删除的元素时,删除操作的运行时间才为O(1)。通常我们都记录了链表的第一个元素和最后一个元素 因此删除这些元素时运行时间为O (1)。
访问方式:分 随机访问 和 顺序访问
链表只能顺序访问 数组支持随机访问
实例:实际上,Facebook存储用户信息时使用的既不是数组也不是链表。假设Facebook使用的是一种混合数据:链表数组。这个数组包含26个元素,每个元素都指向一个链表。例如,该数组的第一个元素指向的链表包含所有以A打头的用户名,第二个元素指向的链表包含所有以B打头的用户名,以此类推。
4、选择排序
找出最多的然后添加到一个新列表中
需要总时间为 O(n*n),即O(n²) {并非每次都需要检查n 个元素。第一次需要检查n 个元素,但随后检查的元素数依次为n- 1, n-2, …, 2和1。平均每次检查的元素数为1/2×n,因此运行时间为O (n× 1/2 × n)。但大O表示法省略诸如1/2这样的常数(有关这方面的完整讨论,请参阅第4章),因此简单地写作O (n× n)或O (n²)。}
5、递归
伪代码是对手头问题的简要描述,看着像代码,但其实更接近自然语言。
编写递归函数时,必须告诉它何时停止递归。正因为如此,每个递归函数都有两部分:基线条件(base case)和递归条件(recursive case)。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。
递归指的是调用自己的函数。每个递归函数都有两个条件:基线条件和递归条件。栈有两种操作:压入和弹出。所有函数调用都进入调用栈。调用栈可能很长,这将占用大量的内存。
6、快速排序
分而治之-递归式问题解决方法
再谈大 O 表示法
快速排序的独特之处在于,其速度取决于选择的基准值。选择排序,其运行时间为O(n2),速度非常慢。
合并排序( merge sort) 的排序算法,其运行时间为O(n log n)。比选择排序快得多!快速排序的情况比较棘手,在最糟情况下,其运行时间为O(n2)。
D&C将问题逐步分解。使用D&C处理列表时,基线条件很可能是空数组或只包含一个元素的数组。
实现快速排序时,请随机地选择用作基准值的元素。快速排序的平均运行时间为O (nlog n)。
大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在。
比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时,O(log n)的速度比O (n)快得多。
7、散列表
散列函数“将输入映射到数字”
散列函数要求:1.必须是一致的。每次输入同样的数字输出结果都必须一致。
2.它应该将不同的输出映射到不同的数字中。输入不同最好输出都不同
缓存的工作原理:网站将数据记住,而不再重新计算。如果你登录了Facebook,你看到的所有内容都是为你定制的。你每次访问facebook.com,其服务器都需考虑你感兴趣的是什么内容。但如果你没有登录,看到的将是登录页面。每个人看到的登录页面都相同。Facebook被反复要求做同样的事情:“当我注销时,请向我显示主页。”有鉴于此,它不让服务器去生成主页,而是将主页存储起来,并在需要时将其直接发送给用户。
散列表适合用于:
- 模拟映射关系;
- 防止重复;
- 缓存/记住数据,以免服务器再通过处理来生成它们。
散列表冲突:给两个键分配的位置相同。
如果两个键映射到了同一个位置,就在这个位置存储一个链表。
在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速度与链表一样快,因此它兼具两者的优点!但在最糟情况下,散列表的各种操作的速度都很慢。因此,在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要有:
- 较低的填装因子;
- 良好的散列函数
8、广度优先搜索
广度优先搜索能够找出两样东西之间的最短距离。
图由节点和边组成,一个节点可能与众多节点直接相连,这些节点被称为邻居
广度优先搜索是一种用于图的查找算法。解决两种问题:1.从节点A出发有前往B节点的路径嘛? 2.从节点A出发前往节点B的哪条路径最短
按添加顺序进行检查——队列,与栈类似,不能随机的访问队列中的元素。队列只支持两种操作:入队 出队
队列是一种先进后出的数据结构,栈是一种后进先出的数据结构。
散列表能够将键映射到值,添加键-值对的顺序不重要,因为散列表是无序的
有向图:其中的关系是单向的;
无向图:没有箭头,直接相连的节点互为邻居。
广度优先搜索的运行时间为O(人数+边数),通常写作O(V+E),其中V为顶点数,E为边数
拓扑排序,可以用来创建一个有序列表
树:树是一种特殊的图,其中没有往后指的边
在数据结构中树的特点是一对多,链表是一对一,图是多对多。
无向图中的边不带箭头,其中的关系是双向的
9、狄克斯特拉算法
广度优先搜索 将得到最少的路径,
狄克斯特拉 将得到最短时间的路径 。这个算法给每段都分配了一个数字或者权重找出的是总权重最小的路径。
狄克斯特拉算法对于每条边都有关联数字的图——权重。带权重的图叫 加权图,不带则叫 非加权图。
环:意味着绕圈圈,环增加权重。绕环的路径不可能是最短的路径。无向图意味着两个节点彼此指向对方其实就是环,在无向图中 每条边都是一个环。狄克斯特拉算法只适用于有向无环图。
如果有负权边则不能使用狄克斯特拉算法,在包含负权边的图里面,可使用 贝尔曼-福德算法。
10、贪婪算法
简单易行。每步都采取最优的做法,每步选择局部最优解,得到的就是全局最优解。显然,贪婪算法不是在任何情况下都行之有效。贪婪算法,不仅简单而且通常运行速度很快。
10.1 背包问题
在有些情况下,完美是优秀的敌人。有时候,你只需找到一个能够大致解决问题的算法,此时贪婪算法正好可派上用场,因为它们实现起来很容易,得到的结果又与正确结果相当接近。
背包问题就是有若干物品,每个物品有自己的价值和重量。背包有总重量。问题就是怎样将背包装的最大价值。背包问题也分很多种,贪心算法解决的是物品可以拆分的背包问题(就是物品可以分成几份装入)。这个问题用贪心还是比较好解决的。贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。此问题就是将每次的放入看成每一步,要想解决问题,就是将每一步都放入最优解。也就是说,每一次的放入都要放入最佳的选择。讲到这里,就要说一说最佳的选择,每一次的放入的最佳的选择就是每次放入的物品都是剩余的物品中价值最大且质量最小的,这里就要引入一个物品的属性,物品的权重值。物品的权重值就是指物品的价值除以物品的质量。所以,本问题的每一次的最佳选择就是每次都选出权重值最大的物品。
近似算法
在获得精确解需要的时间太长时,可使用近似算法。判断近似算法优劣的标准如下:
速度有多快;
得到的近似解与最优解的接近程度。
10.2 NP 完全问题(Non-deterministic Polynomial多项式的不确定性)
NP完全问题的简单定义是,以难解著称的问题,如旅行商问题和集合覆盖问题。很多非常聪明的人都认为,根本不可能编写出可快速解决这些问题的算法。
如果能够判断出要解决的问题属于NP完全问题就好了,这样就不用去寻找完美的解决方案,而是使用近似算法即可。但要判断问题是不是NP完全问题很难,易于解决的问题和NP完全问题的差别通常很小。
元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会变得非常慢。
涉及“所有组合”的问题通常是NP完全问题。
不能将问题分成小问题,必须考虑各种可能的情况。这可能是NP完全问题。
如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是NP完全问题。
如果问题涉及集合(如广播台集合)且难以解决,它可能就是NP完全问题。
如果问题可转换为集合覆盖问题或旅行商问题,那它肯定是NP完全问题。
10.3 小结
1. 贪婪算法寻找局部最优解,企图以这种方式获得全局最优解。
2. 对于NP完全问题,还没有找到快速解决方案。
3. 面临NP完全问题时,最佳的做法是使用近似算法。
4. 贪婪算法易于实现、运行速度快,是不错的近似算法。
11 动态规划
动态规划先解决子问题,再逐步解决大问题。
对于背包问题,你先解决小背包(子背包)问题,再逐步解决原来的问题。
每个动态规划算法都从一个网格开始,网格的各行为商品,各列为不同容量( 1~4磅)的背包。所有这些列你都需要,因为它们将帮助你计算子背包的价值。
动态规划功能强大,它能够解决子问题并使用这些答案来解决大问题。 但仅当每个子问题都是离散的,即不依赖于其他子问题时,动态规划才管用。
动态规划可帮助你在给定约束条件下找到最优解。在背包问题中,你必须在背包容量给定的情况下,偷到价值最高的商品。
在问题可分解为彼此独立且离散的子问题时,就可使用动态规划来解决。要设计出动态规划解决方案可能很难,这正是本节要介绍的。下面是一些通用的小贴士。
每种动态规划解决方案都涉及网格。
11.1 绘制网格
对于前面的背包问题,最终答案总是在最后的单元格中。但对于最长公共子串问题,答案为网格中最大的数字——它可能并不位于最后的单元格中。
11.2 小结
1. 需要在给定约束条件下优化某种指标时,动态规划很有用。
2. 问题可分解为离散子问题时,可使用动态规划来解决。
3. 每种动态规划解决方案都涉及网格。
4. 单元格中的值通常就是你要优化的值。
5. 每个单元格都是一个子问题,因此你需要考虑如何将问题分解为子问题。
6. 没有放之四海皆准的计算动态规划解决方案的公式。
12 K最近邻算法
KNN可以用来做两项基本工作——分类和回归:
1. 分类就是编组;
2. 回归就是预测结果(如一个数字)。
12.1 余弦相似度( cosine similarity)
余弦相似度不计算两个矢量的距离,而比较它们的角度。
余弦相似度。余弦相似度被广泛用于协同过滤算法中,尤其是Item-base的协同过滤。
余弦相似度衡量的是两个向量间的夹角大小,通过夹角的余弦值表示结果,假设A向量是(x1, y1),B向量是(x2, y2),那么两个向量的余弦相似度为:
cosθ=A⋅B||A||∗||B||=x1y1+x2y2(√x21+y21)∗(√x22+y22)
分子为向量A与向量B的点乘,分母为二者各自的L2相乘,即将所有维度值的平方相加后开方。 余弦相似度的取值为[-1,1],值越大表示越相似。
12.2 OCR
OCR指的是光学字符识别( optical character recognition),这意味着你可拍摄印刷页面的照片,计算机将自动识别出其中的文字。 一般而言, OCR算法提取线段、点和曲线等特征。
OCR的第一步是查看大量的数字图像并提取特征,这被称为训练( training)。大多数机器学习算法都包含训练的步骤:要让计算机完成任务,必须先训练它。
12.3 创建垃圾邮件过滤器
垃圾邮件过滤器使用一种简单算法——朴素贝叶斯分类器( Naive Bayes classifier)。
12.4 小结
1. KNN用于分类和回归,需要考虑最近的邻居。
2. 分类就是编组。
3. 回归就是预测结果(如数字)。
4. 特征抽取意味着将物品(如水果或用户)转换为一系列可比较的数字。
5. 能否挑选合适的特征事关KNN算法的成败。
13 接下来如何做
13.1 二叉查找树( binary search tree)
在二叉查找树中查找节点时,平均运行时间为O(log n),但在最糟的情况下所需时间为O(n);而在有序数组中查找时,即便是在最糟情况下所需的时间也只有O(log n),因此你可能认为有序数组比二叉查找树更佳。然而,二叉查找树的插入和删除操作的速度要快得多。
这里写图片描述
二叉查找树也存在一些缺点,例如,不能随机访问,在二叉查找树处于平衡状态时,平均访问时间也为O(log n)。
13.2 反向索引
一个散列表,将单词映射到包含它的页面。这种数据结构被称为反向索引( inverted index),常用于创建搜索引擎。
13.3 并行算法
并行算法设计起来很难,要确保它们能够正确地工作并实现期望的速度提升也很难。有一点是确定的,那就是速度的提升并非线性的,因此即便你的笔记本电脑装备了两个而不是一个内核,算法的速度也不可能提高一倍,其中的原因有两个。
并行性管理开销。假设你要对一个包含1000个元素的数组进行排序,如何在两个内核之间分配这项任务呢?如果让每个内核对其中500个元素进行排序,再将两个排好序的数组合并成一个有序数组,那么合并也是需要时间的。
负载均衡。假设你需要完成10个任务,因此你给每个内核都分配5个任务。但分配给内核A的任务都很容易, 10秒钟就完成了,而分配给内核B的任务都很难, 1分钟才完成。这意味着有那么50秒,内核B在忙死忙活,而内核A却闲得很!你如何均匀地分配工作,让两个内核都一样忙呢?
13.4 MapReduce
MapReduce是一种流行的分布式算法,你可通过流行的开源工具Apache Hadoop来使用它。分布式算法非常适合用于在短时间内完成海量工作,其中的MapReduce基于两个简单的理念:映射( map)函数和归并( reduce)函数。
13.4.1 映射函数
映射函数很简单,它接受一个数组,并对其中的每个元素执行同样的处理。
13.4.2 归并函数
归并函数可能令人迷惑,其理念是将很多项归并为一项。映射是将一个数组转换为另一个数组。
MapReduce使用这两个简单概念在多台计算机上执行数据查询。数据集很大,包含数十亿行时,使用MapReduce只需几分钟就可获得查询结果,而传统数据库可能要耗费数小时。
13.5 布隆过滤器和 HyperLogLog
布隆过滤器是一种概率型数据结构,布隆过滤器的优点在于占用的存储空间很少。使用散列表时,必须存储Google搜集过的所有URL,但使用布隆过滤器时不用这样做。布隆过滤器非常适合用于不要求答案绝对准确的情况,前面所有的示例都是这样的。
HyperLogLog是一种类似于布隆过滤器的算法。
HyperLogLog近似地计算集合中不同的元素数,与布隆过滤器一样,它不能给出准确的答案,但也八九不离十,而占用的内存空间却少得多。
面临海量数据且只要求答案八九不离十时,可考虑使用概率型算法!
13.6 SHA 算法
另一种散列函数是安全散列算法( secure hash algorithm, SHA)函数。给定一个字符串, SHA返回其散列值。
SHA是一个散列函数,它生成一个散列值——一个较短的字符串。用于创建散列表的散列函数根据字符串生成数组索引,而SHA根据字符串生成另一个字符串。对于每个不同的字符串, SHA生成的散列值都不同。
你可使用SHA来判断两个文件是否相同,这在比较超大型文件时很有用。