第1章 算法简介
线性时间(linear time):简单查找
对数时间(log time):二分查找
1.3
大O表示法——算法时间的表述:知道运行时间如何随列表增长而增加
大O表示法能够比较操作数,指出了算法运行时间的增速
O(n2)选择排序
O(n!)旅行商问题
O(nlogn)快速排序
三、四 递归
用递归方法求和数组中元素的和
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"])
- 防止重复
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完全问题时,最佳的做法是使用近似算法
- 贪婪算法易于实现、运行速度快,是不错的近似算法