算法图解笔记
研一的寒假,在家好好地补一下算法,先看的算法图解,从2022/1/26到2022/2/9
算法简介
算法是一组完成任务的指令
二分查找
二分查找必须有序,思路为:找中间,若没找到则往靠近的一边找,终止条件为左大于右或者右小于左(这里我用的python3,与书上python2不同)
arr_list = [1,6,8,11,24,33,57,89,90,99]
def binary_search(arr,num):
left = 0
right = len(arr)-1
index = -1
while left<=right:
mid = (left+right)//2 # 这里python2和python3语法稍微不同
if arr[mid]==num:
index = mid
break
elif arr[mid]>num:
right = mid - 1
else:
left = mid + 1
return index
result = binary_search(arr_list,8)
print(result) # 2
大O表示法
大O表示法指出最糟情况下的运行时间
常见大O运行时间:
- O(log n):对数时间,如二分查找
- O(n):线性时间,如简单查找
- O(n * log n):如快速排序
- O(n2):如选择排序
- O(n!):如旅行商问题
研究算法运行时间,并不是从具体时间度量考虑,而是从增速的角度度量
选择排序
数组和链表
数组擅长读取,因为数组支持随机访问
链表擅长插入和删除,请注意插入和删除操作为O(1)是有条件的,是说的第一个元素和最后一个元素,因为通常都会记录头和尾
冒泡排序
def bubbleSort(arr):
for i in range(len(arr)-1):
for j in range(len(arr)-i-1):
if arr[j]>arr[j+1]:
temp = arr[j+1]
arr[j+1] = arr[j]
arr[j] = temp
return arr
print(bubbleSort([9,7,4,8,1,3,2,6,0,5]))
选择排序
def selectSort(arr):
for i in range(10):
max_num = -1
k = -1
for j in range(10-i):
if max_num <= arr[j]:
max_num = arr[j]
k = j
temp = arr[9-i]
arr[9-i] = arr[k]
arr[k] = temp
return arr
print(selectSort([9,7,4,8,1,3,2,6,0,5]))
递归
递归就是函数调用自己,递归有基线条件(跳出循环)和递归条件,栈是存储内存块的一种数据结构
函数调用都进入了调用栈
快速排序
分而治之(divide and conquer,D&C):著名的递归式问题解决方法
分治的思想:
- 找出基线条件,这种条件必须尽可能简单
- 不断将问题分解(或者说缩小规模),直到符合基线条件
分治举例1
比如一个1680*640大小的矩形,想要全部分成正方形大小的矩形,目的是矩形最大,如可以全部分成1*1大小的矩形,也可以分成2*2大小的矩形
那么首先可以变成640*640,那么现在问题就变成了从400*640来求这个最大矩形,那么进而变为240*400大小,再变为160*240大小,再变为80*160大小,那么这个矩形大小就变为80*80
问题分析
此问题中,基线条件:找出可容纳的最大矩形
问题分解:不断划分矩形去找最大矩形
分治举例2
对一个数组进行求和,如果用循环,那么遍历就可以,如果用分治,则思想如下:
将数组分为第一个数和剩下的数组,剩下的数组则可以继续进行划分为一个数和另外的数组,只需要把这些数相加即可
def sum(arr):
total = 0
if len(arr) != 0:
temp = arr.pop(0)
total = temp + sum(arr)
return total
print(sum([1,2,3,4,5,6,7,8,9,10]))
快速排序
对一个数组进行快速排序,先找一个基准,然后分成3部分内容:小于基准的数组、基准、大于基准的数组
def quickSort(arr):
if len(arr)<2:
return arr
else:
pivot = arr[0]
smaller = [i for i in arr[1:] if i<=pivot]
bigger = [i for i in arr[1:] if i>pivot]
return quickSort(smaller)+[pivot]+quickSort(bigger)
print(quickSort([9,7,4,8,1,3,2,6,0,5]))
快速排序中,最糟情况为O(n2),平均情况为O(n log n),合并排序时间复杂度为O(n log n),这样看好像只要选合并排序就好。但是平均情况下,快速排序要优于合并排序(复杂度相同情况下,常量决定)
快速排序是最快的排序算法之一,是D&C典范
散列表
散列函数:给出一个数据,返回一个数字
散列表:散列函数结合数组的数据结构
在python中,散列表为字典
字典举例
book = dict()
book["apple"] = 0.67
book["milk"] = 1.49
book["avacado"] = 1.49
print(book)
# {'apple': 0.67, 'milk': 1.49, 'avacado': 1.49}
缓存
缓存的数据一般用散列表进行存储
冲突
散列表是散列函数+数组的数据结构,如果两个数据经过散列函数得到的值是相同的,那么在数组中会冲突
如果冲突,在同一个位置处存储一个链表,但设计时最好不要让链表太长
填装因子
$填装因子=\frac{散列表包含的元素数}{位置总数}$
填装因子大于1意味着商品数量超过了数组的位置数,一旦填装因子开始增大,则需要在散列表中添加位置,称为调整长度,通常是将数组增长一倍
一半填装因子大于0.7,就需要调整散列表的长度
优缺点
查找、删除、插入速度都非常快
散列表适合模拟映射关系
散列表适合用于防止重复
广度优先搜索
解决最短路径问题的算法被称为广度优先搜索(BFS)
图
图由节点和边组成,图用于模拟不同的东西是如何相连的
广度优先搜索
BFS解决两类问题:
- 第一类问题:从节点A出发,有前往节点B的路径吗?
- 第二类问题:从节点A出发,前往节点B的哪条路径最短
查找最短路径
先在一度关系中搜索,如果一度关系中没有想要的结果,再在二度关系中搜索
广度优先搜索不仅查找A到B的路径,而且找到的是最短的路径
广度优先搜索中,需要满足按添加顺序进行检查,这时需要队列
队列
队列是一种先进先出(FIFO)的数据结构,栈是一种后进先出(LIFO)的数据结构
有向图&无向图
有向图有箭头,无向图无箭头
广度优先举例
# 创建图
graph = {}
graph["you"] = ["alice","bob","claire"]
graph["bob"] = ["anuj","peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["thom","jonny"]
graph["anuj"] = []
graph["peggy"] = []
graph["thom"] = []
graph["jonny"] = []
# 创建队列
from collections import deque
search_queue = deque()
search_queue += graph["you"]
# 进行搜索
def person_is_seller(name):
return name[-1] == "m"
while search_queue:
person = search_queue.popleft()
if person_is_seller(person):
print(person+" is a mango seller!")
break
else:
search_queue += graph[person]
这个例子中,使用的判断语句比较简单,并且如果是双向循环,则可能导致无限循环,故最好再添加一个列表来只进行一次判断
graph = {}
graph["you"] = ["alice","bob","claire"]
graph["bob"] = ["anuj","peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["thom","jonny"]
graph["anuj"] = []
graph["peggy"] = []
graph["thom"] = []
graph["jonny"] = []
from collections import deque
def person_is_seller(name):
return name[-1] == "m"
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!")
break
else:
search_queue += graph[person]
search("you")
# thom is a mango seller
运行时间
广度优先搜索的时间为:O(V+E)(verticle+edge)
某任务必须在某任务之后,这被称为拓扑排序
树是图的子集,树都是图
狄克斯特拉算法
广度优先搜索解决最短路径问题(段数),狄克斯特拉算法解决最快路径问题(总权重)
算法过程
- 找出最便宜的节点,即可在最短时间内前往的节点
- 对该节点的邻居,检查是否有前往他们的更短路径,如果有,则更新其开销
- 重复上述两步,直到对图里的每个节点都如此
- 计算最终路径
带权重的为加权图,不带权重的图为非加权图
计算非加权图的最短路径,可使用BFS,加权图的最短路径,可使用狄克斯特拉算法
绕环的路径不可能是最短的路径
无向图意味着两个节点彼此指向对方,其实就是环
在无向图中,每条边都是一个环,狄克斯特拉算法只适用于有向无环图(DAG)
狄克斯特拉算法关键理念:找出图中最便宜的节点,并确保没有到该节点的更便宜的路径
负权边
如果有负权边,就不能使用狄克斯特拉算法,因为负权边会导致狄克斯特拉算法不管用
对于含有负权边的图,可使用贝尔曼-福德算法
# 整个的图
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 = []
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_node = node
return lowest_cost_node
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)
print(costs)
# {'a': 5, 'b': 2, 'fin': 6}
print(parents)
# {'a': 'b', 'b': 'start', 'fin': 'a'}
贪婪算法
贪婪算法:每一步都选择局部最优解,最终得到的就是全局最优解
贪婪算法并非任何情况下有效,但易于实现
问题描述
有几个地方(8个州),然后有若干个广播电台(5个),每个广播电台能播放若干地方(1-3个州),选出若干广播电台,要求如下:
- 每个广播电台包含的州尽可能多
- 合起来的电台能够覆盖所有的州
实现
states_needed = set(["mt","wa","or","id","nv","ut","ca","az"])
stations = {}
stations["kone"] = set(["id","nv","ut"])
stations["ktwo"] = set(["wa","id","mt"])
stations["kthree"] = set(["or","nv","ca"])
stations["kfour"] = set(["nv","ut"])
stations["kfive"] = set(["ca","az"])
final_stations = set()
while states_needed:
# best_station = None
states_covered = set()
for station,states in stations.items():
covered = states_needed & states
if len(covered) > len(states_covered):
best_station = station
states_covered = covered
states_needed -= states_covered
final_stations.add(best_station)
print(final_stations)
# {'kthree', 'ktwo', 'kone', 'kfive'}
结果可能有不同,因为我们实现的是先找到广播最多的州的电台,然后覆盖所有州
贪婪算法复杂度
此例中,n个广播电台,每次对电台进行循环,每次循环时选择最大覆盖,那么复杂度为O(n2)
NP完全问题
多项式复杂程度的非确定性问题,以难解著称的问题
NP完全问题:
- 组合问题大概率是NP完全问题
- 序列问题、集合问题、覆盖问题、旅行商问题等大概率是NP完全问题
面对NP完全问题时,最佳做法是使用近似算法
动态规划
动态规划从小问题着手,逐步解决大问题
特点
- 在约束条件下找到最优解,背包问题中在背包容量给定情况下偷到价值最高商品
- 问题可分解为彼此独立且离散的子问题时,可使用动态规划解决
- 每种动态规划解决方案都涉及网格
- 每个网格值都是要优化的值,背包问题中,单元格的值为商品的价值
- 每个单元格都是一个子问题,因此需要考虑如何将问题分成子问题
费曼学习法
Concept (概念)、Teach (教给别人)、Review (回顾)、Simplify (简化)
费曼算法
(1)将问题写下来
(2)好好思考
(3)将答案写下来
最长公共序列和最长公共子串稍有不同
K最近邻算法
计算距离时,可以使用距离公式,也可以使用预先相似度
其他
并行算法
并行算法涉及很难,确保正确工作并实现期望的速度提升也很难,原因如下:
- 并行性管理开销
- 负载均衡