最近快速阅读了《图解算法》这本算法的入门书,对其中的一些知识点做了总结。
O(log n),也叫对数时间,如二分查找
O(n),也叫线性时间,如简单查找
O(n * log n),快排
O(n*n),选择
O(n!),旅行商问题
算法的速度指的并非时间,而是操作数的增速
讨论算法的速度时,我们说的是随着输入的增加,指其运行时间将以什么样的速度增加
算法的运行时间用大O表示法表示
O(logn)比O(n)快,当需要搜索的元素越多时,前者比后者快得越多
递归是在函数里面调用自己
迭代是不断调用另一个值
基线条件,指的是函数何时不再调用自己,结束递归
递归条件,指的是,函数何时调用自己
递归函数如果一直运行:
栈将不断地增大,每个程序可使用的调用栈空间有限,程序用完这些空间(终将如此)后,将因栈溢出而终止
递归指的是调用自己的函数
所有函数的调用都进入调用栈
调用栈可能很长,这将占用大量内存
对于这样的一个函数
def greet(name):
print("hello" + name + "!")
greet2(name)
print("getting ready to say bye")
bye()
def greet2(name):
print("how are you" + name + "?")
def bye():
print("ok bye!")
当调用greet("mary")的时候,计算机首先为该函数调用分配一块内存
使用这些内存来存储变量
当再次调用greet2("mary")的时候,计算机也为这个函数的调用分配一块内存。
计算机使用一个栈来表示这些内存块,其中第二个内存块位于第一个内存块上面,当函数调用返回的时候,栈顶的内存块弹出
当调用greet2的时候,greet只执行了一部分。调用另一个函数的时候,当前函数暂停并处于未完成状态。该函数的所有变量的值都还在内存中。
执行玩函数greet2后,回到greet函数,并从离开的地方接着向下执行。
打印完后调用函数bye
函数bye打印完毕后,又回到greet执行最后的功能
这个栈用于存储多个函数的变量,称为调用栈
一种著名的递归式问题解决方法
不是某种解决问题的算法,而是一种解决问题的思路
解决问题的两个步骤
找出基线条件,这种条件尽可能的简单
不断将问题分解,或者说缩小规模,直到符合基线条件
D&C将问题逐步分解。使用D&C处理列表时,基线条件很可能是空数组或只包含一个元素的数组
大O表示法中的常量有时候事关重大,这就是快速排序比归并排序快的原因所在
比较算法快慢的时候,首先比较算法的运算级数,之后再考虑常量大小
过程
选择基准值
将数组分成两个子数组:小于基准值的元素组成的子数组和大于基准值的元素组成的子数组
对这两个子数组进行快速排序
性能高度依赖于选择的基准值
最坏情况下,基准值有序从小到大
最佳情况下,基准值能将数组分成平均的两部分
在最佳情况下
调用栈的高度为O(log n),每层涉及O(n)个元素,需要的操作时间为O(n),因此运行时间为O(log n) * O(n) = O(n * log n)
在最坏情况下
调用栈的高度为O(n),每层涉及元素O(n),所以运行时间为O(n*n)
实现快速排序的时候,请随机地选择用作基准值的元素。
就是高中时候的归纳法,证明算法成立
分为基线条件和归纳条件
基线条件,就是在问题规模为1的时候,算法成立
归纳条件,在已知问题规模为n-1的时候,证明问题规模为n的时候依然成立
散列函数是这样的函数,即无论你给他什么数据,它都还你一个数字。将输入映射到数字
散列函数必须满足一些要求:
它必须是一致的。例如,假设你输入apple时,得到的是4,那么每次输入apple时,得到的都必须为4,如果不是这样,那么散列表将毫无用处
它应将不同的输入映射到不同的数字。例如,如果一个散列函数,不管输入是什么都返回1,它就不是好的散列函数。最理想的情况是,将不同的输入映射到不同的数字
散列函数能够准确指出索引位置的原因:
散列函数总是将同样的输入映射到相同的索引。每次输入apple时,得到的都是同一个数字。因此,可以首先使用它来确定苹果的价格存储在什么地方,并在以后使用它来确定苹果的价格存储在什么地方
散列函数将不同的输入映射到不同的索引。apple映射索引4,milk映射索引0。每种商品都映射到数组的不同位置,让你能够将其价格存储到这里
散列函数知道数组有多大,只返回有效的索引。
可以使用散列函数和数组来创建散列表
将散列表用于查询。如DNS解析,散列表是提供这种功能的方式之一
防止重复,例如投票
缓存。将常用的网页保存在本地,是一种常见的加速方式
用户能够更快地看到网页。
服务器端需要做的工作更少
以上三种案例分别是:模拟映射关系;防止重复;缓存/记录数据
散列函数总是将不同的键映射到数组的不同位置。但是几乎不可能编写出这样的散列函数
如果两个键映射到同一个位置,这种情况叫冲突
处理冲突的方法:
在冲突的位置存储一个链表
冲突的经验教训
散列函数很重要。最理想的情况是散列函数将键均匀地映射到不同的位置
如果散列表存储的链表很长,那么散列表的速度会急剧下降。但是如果使用的散列函数很好,这些链表就不会很长。
散列表的查找、插入和删除速度都非常快
在使用散列表的时候,避开最坏情况至关重要。为了避免冲突:
较低的填装因子。填装因子 = 散列表包含的元素书 / 位置总数。如果填装因子超过0.7,就要调整列表的长度
良好的散列函数
广度优先搜索 breadth-first search BFS
图是由节点和边组成,一个节点可能与众多节点直接相连,这些节点称为邻居
图分为有向图和无向图。有向图的关系是单向的,即只有指向一个方向的箭头。如果两个节点互相指向,则等于无向图。
广度优先搜索可以回答两种问题
第一类,从节点A出发,是否有前往节点B的路径
第二类,从节点A出发,前往节点B的哪条路径最短
队列 queue 是一种先进先出的结构 first in first out FIFO
栈 stack 是一种后劲先出的结构 last in first out LIFO
实现图
可以用散列表,在python里是字典。每个键值对表示从键指向值
实现BFS
创建一个队列,用于存储要检查的人
从队列中弹出一个人
检查这个人是否是目标
是的话返回
不是的话就将其所有指向,即其在字典里的所有值添加到队列中,回到第二步
如果队列为空,就说明目标不存在
为了防止形成无限循环,应该添加一个列表用于存储记录检查过的人
需要按加入顺序检查搜索列表中的人,否则找到的就不是最短路径,因此搜索列表必须是队列。
广度优先搜索用于在非加权图中查找最短路径
狄克斯特拉算法用于在加权图中查找最短路径
仅当权重没有负值的时候,狄克斯特拉算法才有效
如果图中包含负权边,使用贝克曼-福德算法
包含四个步骤
找出当前权值最小的节点
对于该节点的邻居,检查是否有前往它们的更短的路径,如果有,就更新其开销
重复这个过程,直到对图中的每个点都这样做了
计算最终路径
术语
权重,每条边的度量指标
加权图,带权重的图称为加权图
非加权图,不带权重的图称为非加权图
狄克斯特拉算法假设:对于处理过的海报节点,没有前往该节点的更短的路径,这种假设仅在没有负权边的时候才成立。因此这个算法不能用于包含负权边的图。在包含负权边的图中,要找出最短路径,可以使用贝尔曼-福德算法
算法的实现
定义三个散列表,graph、costs、parents
graph 存储图信息,每个键值对也是一个散列表,键为当前节点,包括当前节点的邻居节点和前往邻居节点的开销,即边的权重
costs 存储从出发点到当前节点的开销
parents 存储着最小开销时,其父节点
定义一个列表 processed
用于存储处理过的节点
graph = {}
graph["start"] = {}
graph["start"]["a"] = 6
graph["start"]["b"] = 2
graph["a"] = {}
graph["a"]["end"] = 1
graph["b"] = {}
graph["b"]["a"] = 3
graph["b"]["end"] = 5
graph["end"] = {}
costs = {}
costs["a"] = 6
costs["b"] = 2
costs["end"] = 999
parents = {}
parents["a"] = "start"
parents["b"] = "start"
parents["end"] = None
processed = []
node = find_lowest_cost_node(costs)
while node is not None:
cost = costs[node] # 取出当前节点目前为止的总开销
neighbors = graph[node] # neighbors是一个字典,包含着node的所有邻居与权重
for n in neighbors.keys():
new_costs = cost + neighbors[n] # 计算当前节点到邻居节点的总开销
if new_costs < costs[n]: # 如果这个总开销比邻居节点之前的总开销小的话
costs[n] = new_costs # 就将小值赋给邻居节点的总开销,并
parents[n] = node # B并且将邻居节点的父节点更新为当前节点
processed.append(node) # 将当前节点放入以处理过的节点中
node = find_lowest_cost_node(costs, processed) # 找到下一个要处理的节点,直到全部节点都处理过
def find_lowest_cost_node(costs, processed):
lowest_cost = 999 # 最小开销
lowest_cost_node = None # 最小开销时的节点
for node in costs.keys(): # 遍历costs字典,寻找下一个最小开销值
cost = costs[node] # 将当前开销记为cost
# 如果当前开销小于之前的最小开销并且不在processed中
if cost < lowest_cost and (node not in processed):
lowest_cost = cost # 将最小开销记为当前节点的开销
lowest_cost_node = node # 将要返回的最小开销节点记为当前节点
return(Lowest_cost_node)
NP完全问题是一种没有快速算法的问题
使用近似算法,求解NP完全问题的近似解
近似算法
在获得精确解需要的时间太长时,可使用近似算法
近似算法的优劣标准:
速度有多快
得到的近似解与最优解的接近程度
贪婪算法,每步都采取局部最优解,最终得到全局最优解的近似解。贪婪算法易于实现、运行速度快,是不错的近似算法。
广度优先搜索、狄克斯特拉算法都是贪婪算法
如何识别NP完全问题
元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会非常慢
涉及“所有组合”的问题通常是NP完全问题
不能将问题分成小问题,必须考虑各种可能的情况。这可能是NP完全问题
如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能是NP完全问题
如果问题涉及集合(如广播台集合)且难以解决,它可能是NP完全问题
如果问题可转换为集合覆盖问题或旅行商问题,那它肯定是NP完全问题
这是一个求解最优解的算法,将问题分成小问题,然后求解小问题的最优解
以背包问题为例,如何能够在不超过背包空间的情况下,装价值总和最大的物品
简单的说,就是在是否装这个物品的时候,选择以下两个值中的较大值
上一个单元的值
当前物品的价值 + (背包空间 - 当前物品需要的空间)这个剩余空间的最大价值
但是动态规划有一个假设,当且仅当每个子问题都是离散的时候,即不依赖于其它子问题的时候,动态规划才管用
通过背包问题可以发现:
动态规划可以在给定约束条件下,找到最优解。如在背包问题中,必须在背包容量给定的情况下,偷到价值最高的商品
在问题可以分解为彼此独立且离散的子问题时,就可以使用动态规划来解决
如何设计动态规划解决方案:
每种动态规划解决方案都设计网格
单元格中的值通常就是要优化的值
每个单元格都是一个子问题,考虑如何将问题分成子问题,这有助于找出网格的坐标轴
绘制网格需要考虑
单元格中的值是什么
如何将这个问题划分为子问题
网格的坐标轴是什么
最长公共子串
如果两个字母不相同,则为0
如果两个字母相同,值为左上角的值+1
最长公共子序列
如果两个字母不同,就选择上方和左方邻居中较大的那个(其实就是A[m-1][n]和A[m][n-1])
如果两个字母相同,值为左上角的值+1
KNN用于分类和回归
分类就是编组
回归就是预测
特征提取意味着将样本转换为一系列可比较的数字
能够挑选合适的特征事关KNN算法的成败
毕达哥拉斯公式
在计算样本之间距离的时候,除了使用距离公式,还可以使用预先相似度,计算两个样本之间的角度而不是距离
在前面的二分查找中,每当有新数据的时候,必须将数据插入到合适的位置,使插入后的数组依然有序
二叉查找树:左子节点都比根节点小,右子节点都比根节点大
二叉查找树不能随机访问
有一些处于平衡状态的特殊二叉查找树:B树(数据库常用来存储数据)、红黑树、堆、伸展树
反向索引
散列表,将内容映射到索引,如网页将网页内容映射到url,这种数据结构称为反向索引
傅里叶变换
为了提高算法的速度,需要它们在多个内核中并行的执行
并行算法速度的提升不是线性的,原因有:
并行管理开销,两个内核对总共1000个数据,分别排序500个数据后,需要将2个有序数组合并成一个有序数组,这个过程也是需要时间的
负载均衡,在分配任务的时候,如何均匀的分配任务使得两个内核能够几乎同时结束
分布式算法。分布式算法适合用于在短时间内完成海量的工作,其中MapReduce基于两个简单的理念:映射(map)函数和归并(reduce)函数
映射函数,接受一个数组,然后对数组中的每个元素都执行同样的处理
归并函数,将很多项归并为一项。将一个数组转换为1个函数
布隆过滤器是一种概率型数据结构,它提供的答案有可能不对,但很可能是正确的。
布隆过滤器的优点在于占用的存储空间很少。比如使用散列表虽然可以得到绝对正确的答案,但是需要存储的空间很大。
布隆过滤器可能出现错报的情况,即无说成有,但不可能出现漏报的情况,即有说成无
HyperLogLog,是一种类似于布隆过滤器的算法。近似的计算集合中不同的元素数,与布隆过滤器一样,不能给出准确的答案,但也相近,而且占用空间少
一种算列函数是安全散列函数(secure hash algorithm,SHA)函数。给定一个字符串,SHA返回其散列值
SHA是一个散列函数,它生成一个散列值——一个较短的字符串。
用于创建散列表的散列函数根据字符串生成数组索引,而SHA根据字符串生成另一个字符串
用于文件比较、检查密码
SHA有一个重要的特征是局部不敏感,即使只修改字符串中的一个字符,得到的散列值也截然不同
Simhash 是一种局部敏感的算法,对字符串做细微的修改的话,生成的散列值也只存在细微的差别
用于检查两项内容的相似程度
Diffie - Hellman算法解决了如下两个问题:
双方无需知道加密算法。不必协商要使用的加密算法
要破解加密的信息很难
公钥加密,私钥解密
RSA是Diffie - Hellman的替代者
也是一种最优解,用于在给定约束条件下,最大限度地改善指定的指标
线性规划使用Simplex算法