文章目录
第一章 二分查找
使用二分查找时,必须保证列表有序,每次都排除一半的数字。
仅知道算法需要多长时间才能运行完毕还不够,还需知道运行时间如何随列表增长而增加。这正是大O表示法的用武之地。
大O表示法指出了算法有多快。并非以秒为单位的速度。大O表示法让你能够比较操作数,它指出了算法运行时间的增速。
大O表示法指出了最糟情况下的运行时间。
O(n)时间意味着查看列表中的每个元素一次。
第二章 选择排序
内存管理
数组:浪费内存、转移
链表的优势在插入元素方面,在随机读取数据时,数组的效率很高。而链表只能顺序访问。
第三章 递归
递归只是让解决方案更清晰,并没有性能上的优势。实际上,在有些情况下,使用循环的性能更好。我很喜欢Leigh Caldwell在Stack Overflow上说的一句话:“如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。”
编写递归函数时,必须告诉它何时停止递归。正因为如此,每个递归函数都有两部分:基线 条件(base case)和递归条件(recursive case)。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。
调用栈
用于存储多个函数的变量。所有函数调用都进入调用栈。
调用另一个函数时,当前函数暂停并处于未完成状态。该函数的所有变量的值都还在内存中。
使用栈虽然很方便,但是也要付出代价:
存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。
第四章 快速排序
D&C的工作原理:
(1) 找出简单的基线条件;
(2) 确定如何缩小问题的规模,使其符合基线条件。
编写涉及数组的递归函数时,基线条件通常是数组为空或只包含一个元素。陷入困境时,请检查基线条件是不是这样的。
快速排序是一种常用的排序算法,比选择排序快得多。
快速排序也使用了D&C。
对排序算法来说,最简单的数组就是根本不需要排序的数组。
快速排序的工作原理
首先,从数组中选择一个元素,这个元素被称为基准值。
接下来,找出比基准值小的元素以及比基准值大的元素。这被称为分区。
对这两个子数组进行快速排序,再合并结果。
(1) 选择基准值。
(2) 将数组分成两个子数组:小于基准值的元素和大于基准值的元素。
(3) 对这两个子数组进行快速排序。
快速排序对任何长度的数组都管用。
快速排序的性能高度依赖于你选择的基准值。假设你总是将第一个元素用作基准值,且要处 理的数组是有序的。由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序。
层数为O(log n)(用技术术语说,调用栈的高度为O(log n)),而每层需要的时间为O(n)。因此整个算法需要的时间为O(n) * O(log n) = O(n log n)。这就是最佳情况。
在最糟情况下,有O(n)层,因此该算法的运行时间为O(n) * O(n) = O(n2)。
大O运行时间–最坏---------平均--------最好
选择排序 O(n2)-------O(n2)-------O(n)
快速排序 O(n2)-------O(nlog n)—O(nlog n)
归并排序 O(nlog n)—O(nlog n)–O(nlog n)
第五章 散列表
散列函数:将输入映射到数字。
数组和链表都被直接映射到内存,但散列表更复杂,它使用散列函数来确定元素的存储位置。
应用:
①查找(模拟映射关系)
②防止重复
③缓存数据
缓存的工作原理:网站将数据记住,而不再重新计算。
缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中!
冲突
给两个键分配的位置相同。
处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表。
散列函数很重要,好的散列函数很少导致冲突。最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。
在平均情况下,散列表执行各种操作的时间都为O(1)。O(1)被称为常量时间。它并不意味着马上,而是说不管散列表多大,所需的时间都相同。
在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速 度与链表一样快,因此它兼具两者的优点!但在最糟情况下,散列表的各种操作的速度都很慢。 因此,在使用散列表时,避开最糟情况至关重要。
为此,需要避免冲突。而要避免冲突,需要有:
①较低的填装因子;
②良好的散列函数。
填装因子
填装因子度量的是散列表中有多少位置是空的。
填装因子大于1意味着商品数量超过了数组的位置数。一旦填装因子开始增大,你就需要在散列表中添加位置,这被称为调整长度。
平均而言,即便考虑到调整长度所需的时间,散列表操作所需的时间也为O(1)。
良好的散列函数
良好的散列函数让数组中的值呈均匀分布。如SHA函数。
第六章 广度优先搜索
解决最短路径问题的算法被称为广度优先搜索。
广度优先搜索可回答两类问题。
第一类问题:从节点A出发,有前往节点B的路径吗?(在你的人际关系网中,有芒果销售商吗?)
第二类问题:从节点A出发,前往节点B的哪条路径最短?(哪个芒果销售商与你的关系最近?)
搜索列表:队列
实现图:散列表
广度优先搜索的运行时间为 O(人数 + 边数),这通常写作O(V + E),其中V为顶点(vertice)数,E为边数。
第七章 狄克斯特拉算法
在狄克斯特拉算法中,你给每段都分配了一个数字或权重,因此狄克斯特拉算法找出的是总权重最小的路径。
狄克斯特拉算法包含4个步骤。
(1) 找出最便宜的节点,即可在最短时间内前往的节点。
(2) 对于该节点的邻居,检查是否有前往它们的更短路径,如果有,就更新其开销。
(3) 重复这个过程,直到对图中的每个节点都这样做了。
(4) 计算最终路径。
广度优先搜索用于在非加权图中查找最短路径。
狄克斯特拉算法用于在加权图中查找最短路径。
仅当权重为正时狄克斯特拉算法才管用。
第八章 贪婪算法
贪婪算法很简单:每步都采取最优的做法。用专业术语说,就是你每步都选择局部最优解,最终得到的就是全局最优解。
在有些情况下,完美是优秀的敌人。有时候,你只需找到一个能够大致解决问题的算法,此时贪婪算法正好可派上用场,因为它们实现起来很容易,得到的结果又与正确结果相当接近。
近似算法
NP完全问题
贪婪算法寻找局部最优解,企图以这种方式获得全局最优解。
对于NP完全问题,还没有找到快速解决方案。
面临NP完全问题时,最佳的做法是使用近似算法。
贪婪算法易于实现、运行速度快,是不错的近似算法。
第九章 动态规划
动态规划先解决子问题,再逐步解决大问题。
动态规划功能强大,它能够解决子问题并使用这些答案来解决大问题。但仅当每个子问题都是离散的,即不依赖于其他子问题时,动态规划才管用。
动态规划可帮助你在给定约束条件下找到最优解。
在问题可分解为彼此独立且离散的子问题时,就可使用动态规划来解决。
每种动态规划解决方案都涉及网格。
单元格中的值通常就是你要优化的值。
每个单元格都是一个子问题。
动态规划的应用
①根据最长公共序列来确定DNA链的相似性。
②git diff命令指出两个文件的差异。
③编辑距离指出两个字符串的相似程度。
④断字功能的应用程序确定在什么地方断字以确保行长一致。
第十章 K最近邻算法
KNN用于分类和回归,需要考虑最近的邻居。
创建推荐系统
特征抽取(挑选合适的特征很重要)
特征抽取意味着将物品(如水果或用户)转换为一系列可比较的数字。
计算两位用户的距离,使用距离公式。在实际中经常使用余弦相似度。余弦相似度不计算两个矢量的距离,而比较它们的角度。
回归
使用KNN来做两项基本工作——分类和回归。
分类就是编组;回归就是预测结果(一个数字)。
第十一章 接下来如何做
二叉查找树
对于其中的每个节点,左子节点的值都比它小,而右子节点的值都比它大。
在二叉查找树中查找节点时,平均运行时间 O(log n),但在糟的情况下所需时间为O(n);而在有序数组中查找时,即便是在糟情况下所需的时间也只有O(log n)。然而,二叉查找树的插入和删除操作的速度要快得多。
二叉查找树也存在一些缺点,例如,不能随机访问,在二叉查找树处于平衡状态时,平均访问时间也为O(log n)。
反向索引
常用于创建搜索引擎
创建一个散列表,这个散列表的键为单词,值为包含指定单词的页面。
并行算法
速度的提升并非线性的。原因:并行性管理开销,负载均衡。
MapReduce(分布式算法)
分布式算法非常适合用于在短时间内完成海量工作,其中的MapReduce基于两个简单的理 念:映射(map)函数和归并(reduce)函数。
映射函数
映射函数很简单,它接受一个数组,并对其中的每个元素执行同样的处理。
归并函数
理念是将很多项归并为一项。
MapReduce使用这两个简单概念在多台计算机上执行数据查询。
布隆过滤器
布隆过滤器是一种概率型数据结构,它提供的答案有可能不对,但很可能是正确的。布隆过滤器的优点在于占用的存储空间很少,非常适合用于不要求答案绝对准确的情况。
HyperLogLog
HyperLogLog是一种类似于布隆过滤器的算法。近似地计算集合中不同的元素数,与布隆过滤器一样,它不能给出准确的答案,但也八九不离十,而占用的内存空间却少得多。
SHA算法
给定一个字符串,SHA 返回其散列值。
SHA是一个散列函数,它生成一个散列值——一个较短的字符串。用于创建散列表的散列函数根据字符串生成数组索引,而SHA根据字符串生成另一个字符串。
可使用SHA来判断两个文件是否相同,这在比较超大型文件时很有用。计算它们的SHA散列值,再对结果进行比较。
检查密码
SHA还让你能在不知道原始字符串的情况下对其进行比较。SHA被广泛用于计算密码的散列值。这种散列算法是单向的。
局部敏感的散列算法
SHA还有一个重要特征,那就是局部不敏感的。
使用Simhash,如果你对字符串做细微的修改,Simhash生成的散列值也只存在细微的差别。
Diffie-Hellman密钥交换
Diffie-Hellman算法解决了如下两个问题:
双方无需知道加密算法。他们不必会面协商要使用的加密算法。
要破解加密的消息比登天还难。
Diffie-Hellman使用两个密钥:公钥和私钥。
Diffie-Hellman算法及其替代者RSA依然被广泛使用。
线性规划
线性规划用于在给定约束条件下大限度地改善指定的指标。线性规划是一个宽泛得多的框架,图问题只是其中的一个子集。