算法图解 Grokking Algorithms —— 笔记

第1章 算法简介

线性时间(linear time):简单查找
对数时间(log time):二分查找

1.3

大O表示法——算法时间的表述:知道运行时间如何随列表增长而增加
大O表示法能够比较操作数,指出了算法运行时间的增速
O(n2)选择排序
O(n!)旅行商问题
O(n
logn)快速排序

三、四 递归
用递归方法求和数组中元素的和

def sum(list):
    if list==[]:
        return 0
    return list[0]+sum(list[1:])

测试:

print sum([3,4,2,3])
12

第2章 选择排序

小结
1)计算机内存犹如一大堆抽屉
2)需要存储多个元素时,可使用数组或链表
3)数组的元素都在一起
4)链表的元素是分开的,其中每个元素都存储了下一个元素的地址
5)数组的读取速度很快
6)链表的插入和删除速度很快
7)在同一个数组中,所有元素的类型都必须相同(都为int、double等)

选择排序
时间 O(n*n)
遍历元素,每次选出最小或最大值,加入新列表

先编写一个用于找出数组中最小元素的函数:

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,4,87,5,3,2])

第3章 递归

小结
1)递归指的是调用自己的函数
2)每个递归函数都有两个条件:基线条件和递归条件
3)数据结构:栈(stack) 有两种操作:压入和弹出
      比如应用在计算 5! = 5 * 4 * 3 * 2 * 1
使用递归调用栈

def fact(x):
	if x == 1:
		return 1
	else:
		return x * fact(x-1)

就要经历压入以及弹出的过程
4)所有函数调用都要进入调用栈(如上所例)
5)调用栈可能很长,这将占用大量的内存

第4章 快速排序

小结
1)分而治之D&C(divide and conquer)将问题逐步分解。使用D&C处理列表时,基线条件很可能是空数组或只包含一个元素的数组
2)实现快速排序时,请随机地选择用作基准值的元素。快速排序的平均运行时间为O(nlogn)
3)大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在
4)比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时,O(logn)的速度比O(n)快得多

快速排序的代码:

def quicksort(array):
	if len(array) < 2:
		return array   # 基线条件:为空或只包含一个元素的数组是“有序”的
	else:
		pivot = array[0]   # 递归条件
		less = [i for i in array[1:] if i <= pivot]  # 由所有小于基准值的元素组成的子数组
		greater = [i for i in array[1:] if i > pivot]  # 由所有大于基准值的元素组成的子数组
		return quicksort(less) + [pivot] + quick(greater)

print(quicksort([10, 5, 2, 3])) 

第5章 散列表

散列函数: 无论你给他什么数据,它都还你一个数字(专业术语表达:将输入映射到数字)
散列函数必须满足的一些要求:

  • 它必须是一致的。每次输入apple,得到的都是4
  • 它应将不同的输入映射到不同的数字。最理想的是,不同的输入映射到不同的数字

Python提供的散列表时限为字典(dict)
1)用于查找

phone_book = dict()
# 或者写作 phone_book = {}
phone_book["jenny"] = 9675309
phone_book["emergency"] = 911

print(phone_book["jenny"])
  1. 防止重复
voted = {}

def check_voter(name):
	if voted.get(name):
		print("kick them out!")
	else:
		voted[name] = True
		print("let them vote!")
	
>>> check_voter('TOM')
let them vote!

3)将散列表用作缓存
缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中!
e.g. Facebook被反复请求做同样的事情:“当我注销时,请向我显示主页。”
有鉴于此,它不让服务器区生成主页,二十将主页存储起来,并在需要时将其直接发送给用户

检查散列表中是否存储了该页面:

cache = {}

def get_page(url):
	if cache.get(url):
		return cache[url]   # 返回缓存的数据
	else:
		data = get_data_from_server(url)
		cache[url] = data     # 先将数据保存到缓存中
		return data
冲突
  • 散列函数很重要
  • 如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很好,这些链表就不会很长
性能

简单查找: 线性时间 O(n)
二分查找:对数时间 O(logn)
散列表: 常量时间 O(1) 最糟糕时是O(n)

良好的散列函数:SHA函数

小结:

  • 你可以结合散列函数和数组来创建散列表
  • 冲突很糟糕,你应该使用最大限度减少冲突的散列函数
  • 散列表的查找、插入和删除都很快
  • 散列表适合用于模拟映射关系
  • 一旦填装因子超过0.7,就应该调整散列表的长度
  • 散列表可用于缓存数据(例如,在Web服务器上)
  • 散列表非常适合用于防止重复

第6章 广度优先搜索

广度优先搜索(Breadth-first search, BFS)是一种用于图的查找算法,可帮助回答两类问题:

  • 从节点A出发,有前往节点B的路径吗
  • 从节点A出发,前往节点B的哪条路径最短
数据结构:队列

队列类似于栈,你不能随地访问队列中的元素。队列只支持两种操作:入队和出队
队列是一种先进先出(First In First Out, FIFO)
而栈是后进先出(Last In First Out, LIFO)

代码:

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(person):   # 仅当这个人没检查过时才检查
			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")

小结:

  • 广度优先搜索指出是否又从A到B的路径
  • 如果有,广度优先搜索将找出最短路径
  • 面临类似于寻找最短路径的问题时,可尝试使用图来建立模型,再使用广度优先搜索来解决问题
  • 有向图中的边卫箭头,箭头的方向指定了关系的方向
  • 无向图中的边不带箭头,其中的关系是双向的
  • 队列是先进先出(FIFO)的
  • 栈硕士后进先出(LIFO)的
  • 你需要按加入顺序检查搜索列表中的人,否则找到的就不是最短路径,因此搜索列表必须是队列
  • 对于检查过的人,务必不要再去检查,否则可能导致无限循环

第7章 狄克斯特拉算法(Dijkstra’s algorithm)

小结

  • 广度优先搜索用于在非加权图中查找最短路径
  • Dijkstra算法用于在加权图中查找最短路径
  • 仅当权重为正时,Dijkstra算法才管用
  • 如果途中包含负权边,请使用Belman-fodd(贝尔曼-福德)算法
    函数find_lowest_cost_node找出开销最低的节点:
    用python表示无穷大: infinity = float(‘inf’)

我学过《运筹学》,这里下面的python代码与《运筹学》书上图解法思路完全一致:

  • 先计算起点start的邻居的cost,其他所有点的cost都标记为’inf’无穷大,起点标记为已处理processed
  • 获取所有未被处理点的最小cost的节点。更新其邻居的cost,如果有邻居的cost被更新,同时更新其父节点
  • 标记目前的这个点为已处理processed
  • 只要还有未被标记处理的点,就继续重复以上几步

点的cost在不断更新中,所以只要给出一开始start相连邻居的cost和其余inf即可,当然邻居有边的关系graph和对应的有向边的权重不能少

'''本算法,只要给出'''
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"] = {}   # 终点没有任何邻居

# the costs table
infinity = float("inf")
costs = {}
costs["a"] = 6
costs["b"] = 2
costs["fin"] = infinity

# the parents table
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 = cost
            lowest_cost_node = node
    return lowest_cost_node

node = find_lowest_cost_node(costs)   # 在未处理的节点中找出开销最小的节点
while node is not None:        # 这个while循环在所有节点都被处理过后结束
    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     # 同时将该邻居的父节点设置为当前节点
            #print(parents)
    processed.append(node)        # 将当前节点标记为处理过
    node = find_lowest_cost_node(costs)

第8章 贪婪算法

贪婪算法其实就是NP完全问题的近似解法。
每步都采取最优的做法。每步都选择局部最优解,最终得到的就是全局最优解!!!

8.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()
# 遍历广播台,从中选择覆盖最多的未覆盖州的广播台,存储在best_statio
while states_needed:
	best_station = None
	states_covered = set()
	for station, states_for_station in stations.items():
		covered = states_needed & states_for_station
		if len(covered) > len(states_covered):
			best_station = station
			states_covered = covered
	# for循环结束时,选出了最佳station,这时要更新states_needed
	states_needed -= states_covered
	final_stations.add(best_station)

print(final_stations)

e.g. 旅行商问题、集合覆盖问题——计算所有的解,并从中选出最小/最短的那个

近似解法:随便选择出发城市,然后每次选择要去的下一个城市时,都选择还没去过的最近的城市

很多人认为:根本不可能编写出可快速解决这些问题的算法

识别:比如教练挑选根据球队的不同需求及球员能力挑选球员组成组队

找出A → \rightarrow B的最短路径,是易于解决的最短路问题,但是如果要找出经由几个点的最短路径,就是旅行商问题——NP完全问题

判断方法:

  • 元素较少时算法的运行速度很快,但随着元素数量增加,速度会变得非常慢
  • 涉及‘所有组合’的问题通常是NP完全问题
  • 不能将问题分成小问题,必须考虑各种可能的情况,这可能是NP完全问题
  • 如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是NP完全问题
  • 如果问题涉及集合(如广播台集合)且难以解决,它可能就是NP完全问题
  • 如果问题可转换为集合覆盖问题或旅行商问题,那它肯定是NP完全问题

邮递员送信也是…

小结:

  • 贪婪算法寻找局部最优解,企图以这种方式获得全局最优解
  • 对于NP完全问题,还没有找到快速解决方案
  • 面临NP完全问题时,最佳的做法是使用近似算法
  • 贪婪算法易于实现、运行速度快,是不错的近似算法

第9章 动态规划

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值