算法知识笔记
=========================== 第一章 算法简介 ==========================
大O表示法表示了最糟糕情况下的操作次数(运行时间)
O(log n) 对数时间 包括 二分查找(省略了2的底数)
O(n) 线性时间 简单查找
O(n * log n) 快速排序 (速度较快)
O(n^2) 选择排序 (速度较慢)
O(n!) 旅行商的解决方案(非常慢)
========================= 第二章 选择排序 ============================
算法的速度并非时间,而是操作数的增速
数组 | 链表 | |
读取 | O(1) | O(n) |
插入 | O(n) | O(1) |
删除 | O(n) | O(1) |
======================== 第三章 递归 ===============================
递归只是让解决方案更清晰, 并没有性能上的优势, 实际上在某些情况下使用 while 循环的性能更好.
(如果使用循环 程序的性能会更高, 如果使用递归,程序可能更容易理解)
出处:performance - Recursion or Iteration? - Stack Overflow https://stackoverflow.com/questions/72209/recursion-or-iteration/72694#72694
编写递归函数需要注意两点:基线条件和递归条件 告诉它何时停止.
递归条件是指:函数调用自己
基线条件是指: 函数不再调用自己(避免形成无限循环)
栈:调用栈(call stack) 调用栈不仅对编程很重要,使用递归也必须理解
计算机在内部使用被称为 调用栈 的 栈。 如果一个栈被用来存储多个函数的变量 被称为调用栈。
虽然栈很方便但是存储详尽的信息会占用大量的内存,每个函数调用都会占用内存,如果栈很高,意味着计算机存储了大量的函数调用的信息,这种情况下: (1)重新编写代码 转而使用循环 (2)使用 尾递归 (高级递归主题)
总结:
每个递归都有两个条件: 基线条件 和 递归条件
栈有两种操作: 压入 和 弹出
所有函数调用 都进入调用栈
调用栈 可能很长 将占用大量内存
======================= 第四章 快速排序 =========================
快速排序(一种优雅的排序算法)使用 分而治之 (divide and conquer D&C)的策略,分而治之是递归的。
使用 D&C 解决问题需要两个步骤:
(1)、找出基线条件, 这种条件必须尽可能简单
(2)、 不断将问题分解(缩小规模), 直到符合基线条件。
====================== 第五章: 散列表 ==========================
散列函数: 即无论给它什么数据, 都还你一个数字(将输入映射到数字)
散列函数必须满足一些要求:
输出一致: 即输入相同时候, 输出必须一致
应将不同的输入映射到不同的数字(映射到不同的索引) # (防止冲突)
散列函数知道数组有多大,只返回有效的索引,如果数组包含5个元素就不会返回无效索引100.
python 提供的 散列表 实现为 字典。
用途:
(1) 将散列表用于查找——电话簿
(2) 防止重复
(3) 用作缓存
缓存是一种常用的加速方式, 所有大型网站都使用缓存,而缓存的数据则存储在散列表中。
总结:
散列表适用于:
模拟映射关系
防止重复
缓存/记住数据
冲突:
散列函数总是将不同的键映射到数组的不同位置,但是实际上不可能编写出这样的散列函数。
解决办法:
冲突很糟糕必须要避免,最简单的办法是: 如果两个键映射到了同一个位置,就在这个位置存储一个链表。(但是链表不能太长,否则效果也不好)
经验教训:
散列函数很重要:前面的散列函数将所有的键都映射到了一个位置, 而最理想的情况是散列函数将键均匀的映射到散列表的不同位置。
如果散列表存储的链表很长,散列表的速度将急剧下降,然而 如果使用的散列函数好, 这些链表就不会很长。
散列函数很重要,好的散列函数很少导致冲突。
散列表(平均情况) | 散列表(最糟情况) | 数组 | 链表 | |
查找 | O(1) | O(n) | O(1) | O(n) |
插入 | O(1) | O(n) | O(n) | O(1) |
删除 | O(1) | O(n) | O(n) | O(1) |
在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速度与链表一样快,因此兼具两者的优点,在最糟糕情况下散列表各种操作的速度都很慢, 因此:
使用散列函数时避开最糟糕的情况很重要
(1)较低的填装因子
(2)良好的散列函数
填装因子 = 散列表包含的元素数 / 位置总数(分配的位置总数)
散列表使用数组来存储数据。
良好的散列函数让数组中的值呈均匀分布。(SHA函数)
总结:
散列表示一种功能强大的数据结构,操作速度快,还能以不同的方式建立数据模型。
可以结合散列函数和数组来创建自己的散列表
冲突很糟糕,应该使用可以最大限度的减少冲突的散列函数
散列表适用于模拟映射关系
一旦填装因子超过 0.7 就该扩充散列表的长度了。
散列表可以用于缓存数据(web服务器上)
散列表非常适合用于防止重复
======================= 第六章:广度优先搜索 ======================
解决最短路径问题的算法称为 广度优先搜索。
如果确定从一个地方到另一个地方的最短路径:
(1) 使用图建立问题模型
(2) 使用广度优先搜索解决问题
广度优先搜索的执行过程: 搜索范围从起点开始向外延伸, 即先检查一度关系,再检查二度关系..
队列是一种 先进先出(FIFO)的数据结构
栈是一种 后进先出 (LIFO)的数据结构
更新队列时, 使用术语: 入队 和 出队 (压入, 弹出)
======================= 第七章 狄克斯特拉算法 ======================
狄克斯特拉算法主要包含4步骤:
(1) 找出开销最小的节点,并认为此节点已经被处理过(以防止负权边的形成,狄克斯特拉算法不支持负权边)
(2) 对于找出的最小开销节点的邻居节点,并检查是否有前往它们的更短路径,如果有就更新到其的开销
(3) 重复这个过程直到对图中的所有节点这样检查过了
(4) 计算最终路径
要计算非加权图中的最短路径可使用 广度优先算法, 要计算加权图中的最短路径可使用狄克斯特拉算法。
在无向图中, 每条边都是一个环, 狄克斯特拉算法只适用于有向无环图。
总结:
广度优先搜索用于在非加权图中查找最短路径
狄克斯特拉算法用于在加权图中查找最短路径
仅当权重为正时狄克斯特拉算法才有用
如果图中有负权边, 使用 贝尔曼-福德算法
====================== 第八章:贪婪算法 =========================
贪婪算法:
简单易行:每一步都采用最优的做法,(n个局部最优解合起来也是全局最优解)
教室调度问题可以使用 贪婪算法, 但是背包问题就不行了,还要考虑其价值因素。
近似算法:
在获得精确解需要很长时间时,可使用近似算法。
判断近似算法优劣的标准:
(1) 速度有多快
(2) 得到的近似解与最优解的接近程度
NP完全问题:
旅行商和集合覆盖问题有一些共同之处:就是需要计算所有的解, 并从中选出最小/最短的那个, 这两个问题都属于 NP 完全问题。
NP 完全问题的简单定义是, 以难解著称的问题, 如旅行商和集合覆盖问题, 很多非常聪明的人都认为, 根本不可能编写出可快速解决这些问题的算法。
判断一个问题是否是 NP 完全问题:
(1)、 元素较少时算法的运行速度很快,但是随着元素数量的增加,速度会越来越慢。
(2)、 涉及“所有组合” 的问题通常是 NP完全问题。
(3)、 不能将问题分成小问题, 必须考虑各种可能的情况, 这可能就是 NP完全问题。
(4)、 如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是 NP完全问题。
(5)、 如果问题涉及集合(如广播台集合)且难以解决, 可能就是NP完全问题
(6)、 如果问题可转换为集合覆盖问题或旅行商问题, 肯定是 NP完全问题。
小结:
(1)、 贪婪算法寻找局部最优解, 企图以这种方式获得全局最优解
(2)、 对于 NP完全问题, 还没有找到快速解决方案
(3)、 面临 NP完全问题时, 最佳做法是使用 近似算法
(4)、 贪婪算法易于实现, 运行速度快, 是不错的近似算法。
====================== 第九章: 动态规划 =====================
背包问题:...
动态规划: 先解决子问题, 再逐步解决大问题。
动态规划功能强大, 能够解决子问题并使用这些答案来解决大问题, 但仅当每个子问题都是离散的, 即不依赖于其他子问题时,动态规划才管用。
计算最终的解时会涉及两个以上的子背包吗?
为获得背包问题的最优解, 可能需要两件以上的商品, 但是根据动态规划算法的设计,最多只需合并两个子背包, 不过子背包可能包含子背包。
最长公共字串:
在问题可分解为彼此独立且离散的子问题时, 就可使用动态规划来解决。
example: git diff 等比较两个文件的差异也是使用动态规划实现的。
小结:
(1)、 需要在给定约束条件下优化某种指标时, 动态规划很有用。
(2)、 问题可分解为离散子问题时, 可使用动态规划解决
(3)、 每种动态规划解决方案都涉及网络
(4)、 单元格中的值通常就是你要优化的值
(5)、 ..
====================== 第10章: K最近邻算法 =====================
使用KNN 做两项工作:
(1)、 分类就是编组
(2)、 回归就是预测结果(比如一个数字)
如果在以后算法中使用 KNN 就最好研究一下 余弦相似度, 比如有两个人的口味都差不多,但是评分标准不一样, 一个较为严格,所以对电影评分的时候一个给了 5分 一个却给了三分, 所以这时候为了获取最精确的结果应该使用 余弦相似度来衡量。
OCR的第一步是查看大量的数字图像并提取特征, 被称为训练, 大多数机器学习算法都包含训练的步骤, 要让计算机完成任务必须先训练它。
创建垃圾邮件过滤器:
垃圾邮件过滤器使用一种简单算法—— 朴素贝叶斯分类器。
小结:
(1)、 KNN用于分类和回归, 需要考虑最近的邻居。
(2)、 分类就是编组。
(3)、 回归就是预测结果(数字)
(4)、 特征抽取意味着将物品转换为一系列可比较的数字
(5)、 能否挑选合适的特征事关KNN算法的成败。
==================== 第十一章: 接下来.. ======================
二叉树:..
B数是一种特殊的二叉树, 数据库常用它来存储数据。
如果对数据库或高级数据结构兴趣可以研究下: 数据结构 B树, 红黑树, 堆, 伸展树。
红黑树是处于平衡状态的特殊二叉查找树。
反向索引: 类似于一个散列表,将单词映射到包含它的页面, 这种数据结构被称为 反向索引, 常用于创建搜索引擎。
傅里叶变换:Better Explained 是一个杰出的网站, 致力于以通俗易懂的语言解释数学。
比如:能告诉我们一杯冰沙含有哪些成分, 给点一首歌曲, 傅里叶变换能够将其中的各种频率分离出来。
傅里叶变换非常合适用于处理信号, 可使用它来压缩音乐。
并行算法:
为提高算法的速度, 可以让代码在多个内核中并行的执行。
并行算法设计起来很难, 要确保它们能够正确地工作并实现期望的速度提升也很难,有一点是确定的,那就是速度的提升并非线性的, 因此即使笔记本电脑装备了两个而不是一个内核, 算法的速度也不可能提高一倍, 其中的原因有两个:
(1)、 并行性管理开销: 假设对一个对1000个元素的数组排序, 如何在两个内核之间分配这项任务呢, 如果让内核对其中 500个元素进行排序, 再将两个排好序的数组合并成一个有序数组, 那么合并也是需要时间的。
(2)、负载均衡: 假设你需要完成10个任务, 因此你给每个内核都分配5个任务, 但是分配给内核A的任务很容易10秒钟完成了,而内核B却1分钟才能完成,意味着A闲了50秒,而B却忙死忙活,如何均匀的分配任务,让两个内核一样忙呢。
要改善性能和可扩展性, 并行算法是不错的选择。
MapReduce:
有一种特殊的并行算法很流行, 就是分布式算法, 在并行算法只需要两到四个内核时,完全可以在笔记本电脑上运行它, 但是如果需要数百个内核呢, 在这种情况下,可让算法在多台计算机上运行, MapReduce是一种流行的分布式算法, 可以在流行的开源工具 Apache Hadoop使用它。
分布式算法为何有用?
假设有一个数据库表,包含数十亿乃至数万亿行, 需要对其执行复杂的SQL查询, 在这种情况下,不能使用MySQL,因为数据表的行数超过数十亿后, 处理起来将很吃力, 相反需要通过 Hadoop来使用 MapReduce。
分布式算法非常适合用于在短时间内完成海量任务, 其中MapReduce基于两个简单的理念:映射(map)和归并(reduce)函数。
映射函数:如 arr1 = [1,2,3] arr2 = map(lambda x: 2 * x, arr1) 将数组每个元素翻倍。
归并函数: 其理念就是将很多项归并为一项, 映射是将一个数组转换为另一个数组。
MapReduce使用这两个简单概念在多台计算机上执行数据查询, 数据集很大,包含数十亿行时, 使用MapReduce只需几分钟就可获得结果,而传统数据库需要耗费数小时。
布隆过滤器和 HyperLogLog
给定一个元素,需要判断是否包含在这个集合中,为快速做出判断可使用散列表。 但是当数据量非常大,导致散列表占用大量存储空间的时候,需要另一种创造性的解决方案!
布隆过滤器: 是一种概率性数据结构, 所以提供的答案有可能不对,但也有可能是正确的,google为了判断某一个网页是否收集过,可不使用散列表而是使用布隆过滤器, 使用散列表时答案绝对可靠, 而是用布隆过滤器时答案可能是正确的。
布隆过滤器的优点就是占用的存储空间很少, 使用散列表时必须存储google搜集过的所有的URL, 但是使用布隆过滤器不用这样做, 布隆过滤器非常适用于不要求答案绝对准确的情况。
HyperLogLog 是一种类似于布隆过滤器的算法, 近似的计算集合中不同的元素数..
面临海量数据只要求答案八九不离十时, 可考虑使用概率型算法。
SHA算法(secure hash algorithm):给定一个字符串,SHA返回其散列值。
可使用 SHA 判断两个文件是否相同。
检查密码: .. 网站一般注册保存的用户的原始密码字符串的散列值, 反向不能计算出原始密码,增强了安全性。
SHA实际上是一系列算法: SHA-0, SHA-1, SHA-2, SHA-3,目前 SHA-0, SHA-1已被发现存在一些缺陷, 当前最安全的密码散列函数是 bcrypt。
SHA是一个局部不敏感的散列算法, 比如 kuing -> adtf03e2.. 如果修改了 king -> e34fo7b8.. 可以看出再次生成的散列值截然不用,而不是局部有所差别, 因此无法让攻击者通过比较散列值的相似性而破解密码。但是有时候却希望结果相反, 也就是局部敏感, 这种情况下可使用 Simhash, 如果对字符串做细微的修改,Simhash生成的散列值也只存在细微的差别, 这可以通过比较散列值来判断两个字符串的相似程度。
(1)、 google 可使用 simhash 判断网页是否收集
(2)、 论文查重
(3)、 上传核查是否为侵权内容
Diffie-Hellman 密钥交换: 也就是如何对消息加密, 以便只有收件人才能看懂。
Diffie-Hellman 算法解决了两个问题:
(1)、双方无需知道加密算法, 不必协商要使用的加密算法
(2)、要破解加密的消息比登天还难
使用两个密钥, 公钥和私钥, 公钥是公开的,任何人可用来加密消息, 然后传给接收的一方, 而接收方使用只有自己知道的私钥进行解密。
Diffie-Hellman算法以及替代者 RSA依然被广泛应用。
线性规划:
作者认为线性规划是最酷的算法之一, 所以留到最后讲。
线性规划用于在给定约束条件下最大限度的改善指定的指标。
线性规划使用 Simplex算法..
========================== over ===========================