【算法入门】《图解算法》知识点总结

说明

本文记录了笔者自学《图解算法》的过程,旨在帮助学习过的读者复习这本书的知识,或者帮助未学过的读者入门。整体内容按照书中的目录顺序展开,主要以分点和表格的形式记录概念,混淆点等,为之后刷算法题打基础。

  • 完成任务

算法简介

  1. 算法是一组完成任务的指令。
  2. 算法的速度指的并非时间,而是操作数的增速。(算法的速度表示随着输入的增加,其运行时间将以怎样的速度增加)
  3. 算法的运行时间用大O表示,大O表示法指出了最糟情况下的运行时间。
  4. 按照由快到慢的顺序列出常见的五种大O运行时间:
    在这里插入图片描述
大O运行时间描述算法
O(n)线性时间简单查找
O(log n)对数时间二分查找
O(n*log n)二次方时间快速排序
O(n^2)指数时间选择排序
O(n!)阶乘时间旅行商问题

选择排序

  1. 将多项数据存储到内存时,有两种方式:数组和链表,数组中的数据在内存中地址相连,所以要求连续的内存空间,而链表的每个元素都存储了下一个元素的地址,可以使随机的内存地址串联。对比数组和链表操作的运行时间:
操作数组链表
读取O(1),数组支持随机访问O(n),链表只能顺序访问
插入O(n),必须将后面的元素都向后移O(1),只需修改前面元素指向的地址
删除O(n),必须将所有后面的元素都向前移O(1),只需修改前面元素指向的地址
  1. 选择排序算法:每次检查n个元素,需要的时间为O(n),这样的操作需要执行n次,才能得到一个有序列表,所以排序算法的运行时间是O(n^2)。示例:将数组元素按照从小到大的顺序排列:
def findSmallest(arr):
	smallest = arr[0]
	smallest_index = 0 
	for i in range(1,len(arr)):
		if arr[i] < smallest:
		smallest = arr[i]
		smallest_index = i
	return smallest_index

def selectionSort(arr):
	newArr = []
	for i in range(len(arr)):
		smallest = findSmallest(arr)
		newArr.append(arr.pop(smallest))
	return newArr

print(selectionSort([5,3,6,2,10]))

递归

  1. 基线条件和递归条件:每个递归函数都有两部分组成,递归条件(recursive case)是指函数调用自己,基线条件(base case)指函数不再调用自己,从而避免陷入无限循环。
  2. 调用栈(call stack):一种简单的数据结构,只有两种操作:压入(插入)和弹出(删除并读取),栈可以看作是一个待办事项清单,插入的事项放在最上面,读取待办事项时,只读取最上面的事项并将其删除。
  3. 所有函数调用都进入调用栈。
  4. 如果递归函数进入无限循环,会占用大量内存,直至栈溢出而终止运行。

快速排序

  1. 分而治之(divide and conquer, D&C):一种广泛使用的递归策略,分为两步:
    找出尽可能简单的基线条件;(编写数组相关的递归问题时,常常使用的基线条件是数组为空或只包含一个元素);
    必须不断缩小问题规模,直到符合基线条件。
  2. 使用D&C策略的几个实例:
# 使用D&C策略实现列表内所有元素求和:
def sum(lst):
	if lst == []:
		return 0
	else:
		return lst[0] + sum(lst[1:])
		
# 编写一个递归函数计算列表包含的元素数:
def count(lst):
	if lst == []:
		return 0
	else:
		return 1 + count(lst[1:])		

# 找出列表中最大的数字:
def max(lst):
	if len(lst) == 2:
		return lst[0] if lst[0] > lst[1] else lst[1]
	sub_max = max(lst[1:])
	return lst[0] if lst[0] > sub_max else sub_max
  1. 二分查找的基线条件和递归条件:二分查找的基线条件是数组只包含一个元素,如果要查找的值与这个元素相同,就找到了,否则说明它不在数组中。在二分查找的递归条件中,把数组分成两半,一半丢弃,对另一半执行二分查找。
  2. 快速排序的基准条件是数组为空或只包含一个元素。快速排序用D&C策略,工作原理如下:首先选出一个基准值(pivot),然后将其他值与基准值比较分区(partitioning)得到小于基准值和大于基准值组成的两个无序子数组,最后对两个子数组递归地调用快速排序。
# 实现快速排序
def quicksort(array):
	if len(array) < 2:
		return array
	else:
		pivot = array[0]
		less = [ i for i in array[1:] <= pivot]
		greater = [ i for i in array[1:] > pivot]
		return quicksort(less) + [pivot] + quicksort(greater)
  1. 快速排序的最糟运行时间和平均运行时间:快速排序的性能高度依赖于基准值的选择,如果总是将第一个元素作为基准值,且要处理的数组是有序的,这会导致调用栈很长,为O(n);在调用栈的每一层的操作时间都为O(n),所以最糟运行时间为O(n^2)。而如果每次都随机选择一个元素作为基准值,栈长就会变成O(logn),此时快速排序算法的最佳运行时间为O(nlogn),最佳情况也是平均情况。

6.快速排序和归并排序的异同:
归并排序和快速排序都是利用分治的思想,代码都通过递归来实现,过程非常相似。
归并排序非常稳定,时间复杂度始终都是O(nlogn),但不是原地排序;快速排序虽然最坏情况下时间复杂度为 O(n^2),但平均情况下时间复杂度为 O(nlogn),最糟情况发生的概率也比较小,而且是原地排序算法,因此应用得更加广泛。

散列表

  1. 散列表是一种功能强大的数据结构,散列表通过散列函数模拟映射关系,散列函数将不同的输入一致地映射到不同的索引,python提供的散列表就是字典
  2. 散列表的查找、插入和删除非常快,可用于查找、缓存/记住数据、防止重复。
  3. 散列表的性能:良好的散列函数(能否将键均匀地映射到散列表的不同位置),填装因子较低(散列表包含的元素数/位置总数,一旦填装因子超过0.7,就该调整散列表的长度)。
散列表(平均情况)散列表(最糟情况)数组链表
查找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. 广度优先搜索是一种用于图的查找算法,回答两个问题:从A出发能否到B以及如果有,最短路径是什么。
  3. 图由节点(node)和边(edge)构成,直接相连的点互为邻居。
  4. 有向图中的边为箭头,箭头方向指定了关系的方向;无向图的边不带箭头,其中关系是双向的。
  5. 队列(queue)是一种先进先出的数据结构,类似于栈(栈是后进先出的数据结构),不支持随机访问队列中的元素,只支持两种操作:入队和出队。
  6. 需要按照顺序检查列表中的元素,否则找到的就不是最短路径,因此搜素列表必须是队列,而且对于检查过的人务必不要再去检查,否则进入无限循环。
  7. 广度优先搜索的运行时间为O(V+E),V为节点数,E为边数。
# 搜索整个人际关系网找到芒果销售商
def search(name):
	search_queue = deque() # 创建一个双端队列
	search_queue = graph[name] # 将邻居加入搜索队列中
	searched = [] # 记录检查过的人
	while search_queue: # 只要队列不为空
		person = search_queue.popleft() # 取出第一个人
		if person not in searched: 
			if person_is_seller(person):
				print(person + "is a mango seller")
				return true
			else:
				search_queue += graph[person] #如果不是芒果商人就把这个人的邻居都加入搜索队列
				searched.append(person) #标记查过了
	return False

search("you")

迪克斯特拉算法

  1. 在迪科斯特拉算法中,每条边都分配了权重,因此该算法找到的是总权重最小的路径。
  2. 迪科斯特拉算法包括四个步骤:
    (1) 找出最便宜的节点,即可在最短时间内前往的节点;
    (2)对于该节点的邻居,检查是否有前往它们的更短路径,如果有,就更新其开销;
    (3)重复这个过程,直到对每个节点都这样做;
    (4)计算最终路径。
  3. 迪科斯特拉算法只适用于有向(无环)图。
  4. 不能将迪科斯特拉算法用于包含负权边的图,因为迪科斯特拉算法假设:对于处理过的节点,没有前往该节点的最短路径,这种假设仅在没有负权边时才成立。
  5. 代码实现迪科斯特拉算法:其中,节点的开销指的是从起点出发到该节点需要多久,
#创建散列表
graph = {}
#创建散列表保存邻居和前往邻居的开销
graph["start"] = {}
graph["start"]["a"] = 6
graph["start"]["b"] = 2
#添加其他节点及其邻居
graph["a"] = {}
graph["a"]["fin"] = 1
graph["b"] = {}
graph["b"]["a"] = 3
graph["b"]["fin"] = 5
graph["fin"] = {} #终点没有邻居

#创建一个散列表储存每个节点的开销
infinity  = float("inf")
costs = {}
costs["a"] = 6
costs["b"] = 2
costs["fin"] = infinity

#还需要一个储存父节点的散列表:
parents = {}
parents["a"] = "start"
parents["b"] = "start"
parents["fin"] = None 

#需要一个数组记录处理过的节点
processed = []

#迪科斯特拉算法: 
node = find_lowest_cost_node(costs)#在未处理的节点中找到开销最小的节点
while node is not None:
	cost = costs[node]
	neighbors = graph[node]
	for n in neighbors.keys():#遍历当前节点的所有邻居
		new_cost = cost + neighbors[n]
		if costs[n] > new_cost:#如果经当前节点前往该邻居更近
			costs[n] = new_cost#就更新该邻居的开销
			parents[n] = node #同时将该邻居的父节点设置为当前节点
	processed.append(node)#将当前节点标记处理过
	node = find_lowest_cost_node(costs)
			
def find_lowest_cost_node(costs):
	lowest_cost = float("inf")
	lowest_cost_node = None
	for node in costs:#遍历所有节点
		cost = costs[node]
		if cost < lowest_cost and node not in processed:#如果当前节点的开销更低而且未处理过
			lowest_cost = cost
			lowest_cost_node = node
	return lowest_cost_node

贪婪算法

  1. 贪婪算法是一种近似算法,广度优先搜索和迪科斯特拉算法都是贪婪算法。
  2. 贪婪算法寻找局部最优解,企图用这种方式获得全局最优解。
  3. 对于NP完全问题,还没有快速解决方案,最佳的做法就是使用近似算法。
  4. 判断是不是NP完全问题的方法:
    (1)元素较少时算法运行非常快,但随着元素数量增加,算法速度会变得非常慢;
    (2)涉及“所有组合”的问题通常是NP完全问题;
    (3)不能将问题分成小问题,必须考虑各种可能的情况,可能是NP完全问题;
    (4)如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,可能是NP完全问题;
    (5)如果问题涉及集合(如广播台集合)且难以解决,可能是NP完全问题;
    (6)如果问题可以转换为集合覆盖问题或者旅行商问题,那肯定是NP完全问题。

动态规划

  1. 需要在给定约束条件下优化某种指标时,动态规划很有用,每种动态规划的解决方案都涉及网格;网格中的值就是要优化的值,每个格子都是一个子问题。
  2. 需要注意的是,以背包问题为例:各行的排列顺序(放东西的先后顺序)无关紧要;增加更小的商品需要调整网格的粒度;动态规划没法判断是否拿走商品的一部分,但贪婪算法可以解决;当且仅当每个子问题都是离散的时,动态规划才有用。

K最近邻算法

  1. 要使用K最近邻算法,一定要了解余弦相似度。
  2. 在创建推荐系统时,如果有N个用户,应该考虑sqrt(N)个邻居。

接下来如何做

本书未介绍的十种算法:

  1. 二叉查找树:对于其中的每个节点,左子节点的值都比它小,右子节点的值都比它大,但二叉查找树不支持随机访问。
二叉查找树数组
查找O(logn)O(logn)
插入O(logn)O(n)
删除O(logn)O(n)
  1. 反向索引:搜索引擎的工作原理
  2. 傅立叶变换
  3. 并行算法
  4. 分布式算法
  5. 布隆过滤器和HyperLogLog
  6. SHA算法:安全散列算法,SHA根据字符串生成另一个字符串,可以用来判断两个文件是否相同,也可以用来检查密码(单向,可以根据字符串计算出散列值,但无法根据散列值推断出原始字符串)
  7. 局部敏感的散列算法:Simhash算法,可以通过比较散列值来判断两个字符串的相似程度
  8. Diffie-Hellman密钥交换:一种加密算法
  9. 线性规划:所有的图算法都可以用线性规划实现,线性规划使用Simplex算法。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值