第一章 算法简介
二分查找
- 必须是有序的数组
- 对于n个元素的列表,使用二分查找最多需要 log2 n步,用普通的查找最多需要n步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def binary_search(l, item):
low = 0
high = len(l) - 1
while low <= high:
mid = int((low + high) / 2) # 这里需要转成int,不然没办法做index
if l[mid] == item:
return mid
elif l[mid] < item:
low = mid + 1
else:
high = mid - 1
return None
运行时间
- 大O表示法表示了操作数,表示的是这个算法的增量
- 大O表示了在最糟情况下的运行时间 -> 但是也是需要讨论平均时间的
- 常见的大O时间
- logN 对数时间 Logarithmic
- N 线性时间 Linear
- N * logN 包括快速排序等,速度较快
- N ^ 2 选择排序等,速度较慢
- N! Factorial time 非常慢
旅行商问题 travelling salesman problem, TSP
- 一个搞不好可以用n!来解决的问题
- 你要去五个不同的地方,需要规划怎么样路线最短,最开始的思路就是把每种可能性都列出来,然后对每种路线进行计算。这样的话五个地方就是120种,地方越多越呈阶乘增长
第二章 选择排序
数组和链表
- 数组需要的空间是固定的(也就是说必须连在一起),所以如果后面加进去了其他东西的话就不行了,需要转移位置,或者预留空间
- 链表的每一个位置都会有到下一个位置的指针,所以不需要移动元素。
- 但是链表的问题在于,如果想直接找后面的东西的时候需要一个接着一个读取
- 需要读取整个数据的时候链表效率很高,需要跳跃的时候链表效率很低。而数组在读取随机元素的时候效率很高
- 数组插入 / 删除线性,读取常数。
- 链表读取线性,插入 / 删除常数。
- 在实际中,因为数组支持随机访问(但是链表只支持顺序访问),所以数组的适用范围大一些
选择排序
- 实现方法:每次都从所有的里面选出最大 / 最小,然后放在最开头
- 时间复杂度 n ^ 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def select_sort(l):
newArr = []
for j in range(len(l)):
smallest = float('inf')
index = None
for i, item in enumerate(l):
if item < smallest:
smallest = item
index = i
newArr.append(l.pop(index)) # 注意这里需要把l的大小改变了
# 但是是在带l的loop外面变得所以没有关系
return newArr
第三章 递归 recursion
何为递归
- 函数自己调用自己
- 递归和循环的作用效果是相同的,没有性能上的优势,但是可以让方案更加清晰
- base case:告诉函数什么时候不再调用自己,停止循环
- recursive case:函数调用自己
栈 stack
- 先进后出的数据结构,push(压入)和pop(读取和删除)
- 在调用另一个函数的时候,当前函数暂停并且处于未完成状态,函数的所有变量都储存在内存里
- 使用递归的一个问题:如果调用栈的时候很长,会占据大量内存
- 这种时候需要重新编写代码使用循环
- 或者使用尾递归(并不是所有语言都支持)
第四章 快速排序(分而治之 divide and conquer)
分治
- 核心:把一个问题分成子问题,再把子问题分成更小的子问题,最后的子问题可以直接求解,这样的话原问题的解就是子问题的解的合并
- 比如把n规模的问题分成k份,然后在k个问题里面分别再分开
- 特征
- 问题缩小规模可以轻松解决
- 可以分解成若干个小问题
- 分解出的子问题可以再合并(如果不满足这条,应该考虑贪心或者DP)
- 分解出的各个子问题是独立的(不满足应该考虑DP)
- 不用循环而用递归:函数式编程里面没有循环(Haskell)
1 |
|
- 注意:
- 需要找好基本条件,如果找最大值的base就是还剩下两个值
- 注意return的内容
快速排序
- 比选择排序速度快很多
- base条件,一个元素或者空的数组就不需要排序了
- 需要设定一个基本值(比如取第一个值,根据这个值把原数组分成两部分),这样这三个大块就分类完成了。然后对于每个小块,再继续分组
- 比基准小的子数组
- 基准
- 比基准大的子数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def QuickSort(l):
if len(l) < 2:
return l
else:
piv = l[0]
# 这里是快读得到比他大和比他小的写法,主要从l[1:]开始
less_part = [i for i in l[1:] if i < piv]
more_part = [i for i in l[1:] if i > piv]
return QuickSort(less_part) + [piv] + QuickSort(more_part)
# 注意这里piv是个int,所以连接的时候需要改成list
时间复杂度
- 快速排序的速度取决于选择的piv的值的大小,也就是说如果完全排好的情况下,复杂度是n2
- 合并排序的速度是 nlogn,快速排序的平均速度是 nlogn,最佳情况是 logn
- 快速排序需要logn层,每层需要把n个元素全都遍历一次,所以最终的结果是nlogn
- 在计算复杂度的时候,复杂度是操作的次数,而这个次数需要乘一个每次操作的常量,在快速排序的时候常量更小,而且快速排序没那么容易遇到最糟情况
第五章 散列表(hash表)
hash函数
- 最终的目的是查找的时候时间复杂度是 1
- 函数构造:把输入映射到一个数字
- 无论你什么时候输入,输出的数字是一致的
- 将不同的输入映射到不同的数字
- 知道整个存储的范围有多大,不会返回超过这个大小的数字
应用:缓存
- 网站的缓存数据就存在hash表里面,这样访问速度更快,网站本身需要做的工作更少
- 访问一个网页 -> 是否在缓存里 -> 有的话调用缓存 -> 没有的话存进缓存
冲突
- 虽然假设的时候认为每个东西都被映射到不同地方,其实会产生冲突
- 这种情况下要在hash后面加上list
- hash函数需要把内容比较平均分分配
- 如果储存的链表很长,那么性能就会急剧下降
性能
- 最佳性能,1
- 最糟性能,插入,删除,查询全都是n
- 装填因子: 装填的元素数 / 元素总数
- 一旦超过0.7就需要调整hash的长度了
第六章 BFS:最短路径问题
BFS
- 用于图的查找算法,可以解决两种问题
- 从A出发有前往B的路径吗
- 从A出发到B的路径最短是什么
- 实现图 -> hash表,需要将node映射到所有的邻居
- 在python里面使用deque创建双端队列
- 算法实现(广度搜索):
- 创建一个队列,储存用于查找的人
- 首先把初始化的人载入队列
- 从队列里弹出一个人,查找他是不是(查过之后标记成已检查,列表记录),不是的话把这个人的相邻加入队列(一直重复)
- 标记成已检查非常重要,因为不标记的话可能会陷入无限循环
- 如果最后队列空了还没找到,那就是没有
1 |
|
更新版本,不但可以搜索还可以计算长度
1 | def BFS(name, graph): |
- 注意点:
- 增加了distance这个dict来储存开始点到这个点的距离
- 在初始化的时候只在已搜索队列里添加了第一个点的信息,在后面的循环里才添加后面的点
- 对于每个从queue里面拿出来的点,如果不在已经查找的点里就一定需要加进去,并且计算距离,距离即是和上一点的距离+1
- 计算过距离之后再判断是不是要找的点
运行时间
- 需要沿着每条边前进,所以在边上的运行时间是 O(E)
- 把每个人加到queue里面也需要时间,每个人的时间是常数,所以人数的时间是 O(V)
- 总的运行时间 E + V
拓扑排序
- 如果任务A依赖于任务B,那么任务A就必须排在B的后面,这种就是拓扑排序
第七章 狄克斯特拉算法
- 之前的图找的是最短路径,现在需要给图加权,找加权之后的最短路径
- 加权之后的最短不一定是边数最短
- 负的权重不顶用,因为负权重不能确定没有比目前消耗更小的
- 书里写的错的地方:可以有环,没环都是树了!有向无环图可以直接拓扑排序
- 核心思想:找到到这个点消耗最少的路径,并且确保没有路径比这个小了
算法流程
- 找出消耗最低的点
- 更新这个点相邻点的开销
- 重复这个过程,直到对所有点都做了(即A点最小之后更新B点,然后更新C点,以此类推)
- 计算最终流程
具体实现
- 创建一个表格
- 包含了每一项和每一项的具体开销,目前不知道的开销标记成inf
- 需要包含每个点的父节点,才能保证最后可以计算流程
- 不停的更新这个表,从开销最低的一直更新到开销最高的,如果更新了开销的话同样需要更新父节点
- 在确定路径的时候,从结尾的地方开始找,然后一路找到开头
一个问题:关于图在python里面的表示
- 实际就是一个dict叠dict,如果直接访问G[a][b]就可以直接得到这两个点的距离
如果没有加权的话,可以直接dict叠list,因为不需要记录距离了
注意点
- 最好是最开始指定了第一个消耗最低点,然后再循环里面最后找消耗最低点
- 初始化的时候注意:cost里面初始为0,father里面去掉这个点,Node(cost最低点)初始化成这个点,循环的条件是Node不是空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42def dijkstra(graph, src, target):
# 需要创建一个cost表和一个父节点表
cost = {}
father = {}
visited = []
for i in graph.keys(): # 访问这个图里面所有的key,就是所有的店
cost[i] = float('inf')
father[i] = None
cost[src] = 0 # 起始点的cost是0
father.pop(src) # 起始点不需要父节点
Node = src
# 最小开销点还存在的时候更新所有的点
while Node is not None:
for near in graph[Node]:
new_distance = cost[Node] + graph[Node][near]
if new_distance < cost[near]:
cost[near] = new_distance
father[near] = Node
visited.append(Node)
# 找到最小开销的点(放在循环里面好一点)
Node = None
smallest = float('inf')
for i in cost:
if i not in visited:
if cost[i] < smallest:
smallest = cost[i]
Node = i
# 跳出循环,得到结果
N = {
"start": {"a": 2, "b": 6},
"a": {"fin": 1},
"b": {"a": 3, "fin": 5},
"fin": {}
}
# print(N["start"]["a"])
dijkstra(N, "start", "fin")
第八章 贪心算法
- 处理npc问题,即没有快速算法的解法的问题
- 对NPC问题找到近似解
例子问题
- 课表问题:
- 希望尽可能多的课在这个屋子里面上,但是上课时间冲突,如何排课
- 先找到这个教室里最早开始的课,然后找到这节课结束之后最早开始的课
- 装东西
- 背着包去装东西,容量有限,如何装到最大价值的
- 从最大的开始装,但是得到的不是最优解,最优解应该动态规划
- 核心思想:每一步都用局部最优解,得到的结果就是全局最优解。即使得不到最优解,也可以得到近似最优解
只能用贪心的问题
- 集合覆盖问题,比如想在全部区域广播,但是每家电台只覆盖特定区域,如果才能花费最小的计划
- 列出所有的可能性的话,复杂度是 2^n
- 近似算法:从覆盖最多地方的电台开始找,一直到覆盖所有地方
- 贪心算法的复杂度是 n^2,n是广播台的数量
- 因为每次都需要遍历一次找出最需要的那个(n),这样的操作需要进行最多n次(万一每次都覆盖不上)
NPC问题:从旅行商问题详解
- 旅行商问题和求路径问题的区别在于,旅行商问题需要访问一遍所有的地方,找到最短路线
- 旅行商问题需要查看所有的可能的路线
- 从简单问题出发
- 两个地方的时候,可能的路线有两条(一来一回)
- 三个地方的时候 6条
- 需要计算出所有的解才有可能从中选出最短的路线 -> NPC问题
- 近似方法:每次都去离得最近的地方
识别问题
- 元素较少的时候速度快,随着元素增加非常慢
- 涉及所有组合
- 不能分成小问题,必须考虑所有情况
- 涉及序列(比如旅行商中的城市序列),集合(集合覆盖问题)且难以解决。或者可以转换成这种问题的
第九章 动态规划
核心思想
- 把一个大的问题拆分成很多小问题的解的合并
- 最终的结果其实类似一个表,每次再得到新的结果的时候,其实是在比较
- 同等大小下上一个的值(即上一行的值,这个值已经是之前储存下来的最大的值了)
- 当前放新的东西的价值+放完这个东西之后剩下空间可以放的东西的最大价值(这个最大价值同样也被记录了)
- DP处理问题的时候只能整件的处理,也就是说分布应该是离散的而不是连续的
- 而且DP处理问题的时候各个子问题之间不能互相依赖,如果互相依赖了就很复杂了
最长公共子串
- 比如搜索引擎误输入,怎么判断相近词
- 每种动态规划都涉及网络,每个网格里面的值就是我需要优化的值
- 每个单元格里是什么
- 每个单元格是这个格子之前相同的字母数量
- 如何把这个问题分成子问题
- 如果相同,就是前一个的字母数+1,如果不同就是0
- 网络的坐标轴是什么
- 在这个问题中,是两个单词的各个字母
- 每个单元格里是什么
- 注意在这个问题里面,最终答案不是在最后出现的
- 最长公共字序列!!和子串不一样,对比的是序列而不是字母的个数
- 当字母不同的时候,选择上边或者左边最大的那个
应用
- levenshtein distance(字符串的相似程度,也就是上面的公共子序列)
- 指出文件的差异,DNA的相似性,确定什么地方断字
- 核心思想:需要把两个str里面所有的substr的combination都计算一下
- 实现
- 初始化条件:插入
- 实际上来说一个格子有三种操作,删除,插入和置换,这个格子需要改变的操作是这三个操作的最小值
- 删除 = d[i - 1][j] + 1
- 插入 = d[i][j - 1] + 1 (相当于这个单词的前一个字母,加上一个插入,这两个没有本质区别)
- 替换 = d[i - 1][j - 1] + cost,其中这个cost,如果两个相同就是0,两个不同就是1
- 具体说明见levenshtein distance说明
第十章 K最近邻
创建推荐系统
- 向这个人推荐电影,找到离这个人最近的五个用户
- 如何判断最近:特征抽取
第十一章 接下来如何
树(数据库 + 高级数据结构)
- 二叉查找树:对于每一个节点,左节点都比他小,右节点都比他大
- 时间复杂度 logn
- 平衡问题
- 延伸: B树,红黑树,堆,延展树
反向索引
- 搜索引擎的工作原理
- 把网页创建hash表,key是单词,value是包含单词的页面
傅里叶变换
- 信号处理
并行算法(提高速度)
- 对速度的提升是非线性的
- 并行管理开销
- 负载均匀
MapReduce
- 分布式算法
- 把一台电脑上面处理的工作分布到多台电脑,处理大量数据
- 分布+归并
布隆过滤器
- 概率型数据结构:可能出现错报,但是不可能出现漏报
- 储存空间很少
HyperLogLog
- 类似于布隆过滤器的算法,不能给出准确的答案但是占据的空间很小
SHA
- 另一种hash,安全散列算法,给定一个字符串返回hash
- 比较大型文件
- 检查密码
- 局部不敏感,修改其中一个字符,会改变很多
Diffie-Hellman 密钥交换
- 如何对消息加密,让只有收件人看得懂
线性规划
- 在给定的约束条件下最大限度的改善指定的指标