最近学习了《算法图解》这本书,写下一些笔记。
由于第一次发博客,写的很烂望理解嘻嘻嘻。
算法图解
1、算法简介
1.2二分查找
实现代码
`def binary_search(list,item):
low = 0
high = len(list)-1
while low <=high:
mid = (low+high)/2
guess = list[mid]
if(guess == item):
return mid
if(guess >= item):
high = mid-1
else
low = mid +1
return None
`
1.3大O运行时间(时间复杂度)
O(logn):也叫对数时间,例如二分查找
O(n):也叫线性时间,例如简单算法
O(nlogn):例如快速排序——速度较快
O(n^2):例如选择排序——速度较慢
O(n!):例如旅行商算法——非常慢
1.4小结
1、二分查找的速度比简单查找快得多
2、O(logn)比O(n)快,,需要搜索的元素越多,前者比后者就快得多
3、算法运行时间比不以秒为单位
4、算法运行时间是从其增速的角度度量的
5、算法运行时间用大O表示法表示
2、选择排序
2.2数组和链表
存储多项数据时,有两种基本方式——数组和链表
数组 | 链表 | |
读取 | O(1) | O(n) |
插入 | O(n) | O(1) |
删除 | O(n) | O(1) |
访问方式分两种
1、随机访问
2、顺序访问:要从第一个元素开始逐个地读取元素
数组擅长随机访问,链表擅长插入与删除
2.3选择排序
//找出数组中最小的元素
def findSmallest(arr):
smallest = arr[0]
smallest_index = 0
for i in range(1,len(arr)):
if arr[i] > samllest:
smallest = arr[i]
smallest_index = i
return smallest_index
//对数组进行排序
def selectionSort(arr):
newArr = []
for i in range(i,len(arr)):
smallest = findSmallest(arr)
newArr.append(arr.pop(smallest))
return newArr
3、递归
3.1递归
Leigh Caldwell:如果使用循环,程序的性能可能更高,如果使用递归,程序可能更容易理解。
3.2基线条件与递归条件
每个递归函数都有两部分:基线条件与递归条件。
基线条件是指函数不再调用自己,避免形成无限循环。
递归条件是指函数自己调用自己。
3.4小结
1、递归指的是调用自己的函数
2、每个递归函数都有两个条件:基线条件与递归条件
3、栈有两种操作:压入与弹出
4、所有的函数调用都进入调用栈,调用另一个函数时,当前函数暂停并处于未完成的状态
5、调用栈可能很长,这将占用大量的内存
4、快速排序
4.1分而治之
使用分治法解决问题的过程包括两个步骤
(1)找出基线条件,这种条件必须尽可能简单
(2)不断将问题分解,直到符合基线条件
求数组中各元素之和
def sun(list):
if list == []
return 0;
return list[0]+sum(list[1:])
求数组元素个数
def count(list):
if list == []
return 0
return 1 + count(list[1:])
找出列表中最大的数
def max(list):
if len(list) == 2:
return list[0] if list[0] > list[1] else list[1]
sub_max = max(list[1:])
return list[0] if list[0] > sub_max else sub_max
4.2快速排序
快速排序是一种常用的排序算法,比选择排序快得多。快速排序也使用了分治法。
实现代码
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] +quicksort(greater)
快速排序在平均情况下,运行时间为O(nlogn),最糟糕的情况下运行时间为O(n^2)
4.4小结
1、分治法将问题逐步分解。使用分治法处理列表时,基线条件很可能时空数组或者只包含一个元素的数组。
2、实现快速排序时,请随机的选择用作基准值的元素。快速排序的平均运行时间为O(logn)
3、大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因
4、比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时,O(logn)的速度比O(n)快得多
5、散列表
散列表的查找的时间复杂度为O(1)
散列函数:将输入映射到数字
散列函数必须满足一些要求:(1)它必须是一致的,例如输入apple得到4,那么每次输入apple,都要得到4.(2)它应将不同的输入映射到不同的数字。
散列表应用:1、将散列表用于查找
2、防止重复
3、将散列表用作缓存
4、模拟映射关系
5、缓存/记住数据,以免服务器再通过处理来生成他们
好的散列函数很少会导致冲突。
最理想的散列函数将键均匀的映射到散列表的不同位置,并且如果散列表存储的链表很长,散列表的速度将会急剧下降,若有好的散列函数,这些链表就不会很长。
散列表(平均情况) | 散列表(最糟情况) | 数组 | 链表 | |
查找 | 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)良好的散列函数
填装因子=散列表包含的元素数/位置总数
一个不错的经验就是:一旦填装因子大于0.7,就调整散列表的长度。
良好的散列函数让数组中的值呈均匀分布。
小结 1、一般的编程语言提供了散列表的实现 例如Python
phone_book = dict()
phont_book = {} //快捷方式
2、可以结合散列函数与数组来创建散列表
3、冲突很糟糕,你应该使用可以最大限度减少冲突的散列函数
4、散列表的查找、插入和删除速度都非常快
5、散列表适合用于模拟映射关系
6、一旦填装因子大于0.7,就该调整散列表的长度
7、散列表可用于缓存数据(例如,在web服务器上)
8、散列表非常适合用于防止重复
6、广度优先搜
本章介绍图,再介绍第一种图算法——广度优先搜索
广度优先搜索可以找出两样东西之间的最短距离
检查你的朋友中有没有芒果经销商:
用散列表实现图
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"] = []
//散列表是无序的,因此添加键-值对的顺序无关紧要
要按照顺序来进行检查,可以用队列来实现
在检查一个人以前,要确认之前有没有检查过他,需要一个列表来记录检查过的人
总的代码如下
def search(name):
search_queue = deque() //创建一个队列
search_queue += graph[name] //将name的邻居都加入到这个搜索队列中
searched =[] //这个数组用于记录检查过的人
while search_queue: //只要队列不为空
person = search_queue.popleft() 取出队列里的第一个人
if person not in searched: //仅当这个人没检查过时才检查
if person_is_selle(person):
print person + "is a mango seller!"
return True
else
search_queue += graph[person] //将其邻居都加入到搜索队列
searched.append(person) //将这个人标记为检查过
return False //没有人是芒果经销商
def person_is_seller(name): //判断一个人是不是芒果经销商
return name[-1] == 'm' //例如检查这个人的姓名是不是以m结尾
小结 1、广度优先搜索指出是否有从A到B的路径
2、如果有广度优先搜索将找出最短路径
3、面临类似于寻找最短路径的问题,可尝试使用图来建立模型,在使用广度优先搜索来解决问题
4、有向图中的边不带箭头,箭头的方向制定了关系的方向,例如:rama——>adit表示rama欠了adit的钱
5、无向图中的边不带箭头,其中的关系是双向的,例如:ross - rachel 表示ross与rachel约会,而rachel也与ross约会
6、队列是先进先出的(FIFO)
7、栈是先进后出的(LIFO)
8、你需要按加入顺序检查搜索列表中的人,否则找到的就不是最短路径,因此搜索列表必须是队列
9、对于检查过的人,务必不要再次检查,否则可能导致无限循环
7、迪克斯特拉算法
上一章使用了广度优先搜索,找出的是段数最少的路径。本章介绍加权图与迪克斯特拉算法,他可以找出加权图中
前往X的最短路径。
迪克斯特拉算法步骤:(1)找出"最便宜"的节点,即可在最短时间内到达的节点。
(2)更新该节点的邻居的开销。
(3)重复这个过程,知道对图中每个节点都这样做了。
(4)计算最终路径。
迪克斯特拉算法只适用于有向无环图
rama想用乐谱换一架钢琴,如何才能花费最少呢?
最终的表格为
乐谱 | 黑胶唱片 | 5 |
乐谱 | 海报 | 0 |
黑胶唱片 | 低音吉他 | 20 |
黑胶唱片 | 架子鼓 | 25 |
架子鼓 | 钢琴 | 35 |
代码实现
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["b"] = 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 //将该邻居的父节点设置为当前节点
proecssed.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、仅当权重为正时狄克斯特拉算法才管用
4、如果途中包含负边权,请使用贝尔曼-福德算法