书名: 算法图解
出版社: 图灵出版社
网址: http://www.ituring.com.cn/book/1864
在学习C语言的时候,理解到了程序由算法+数据结构组成。算法的理解和学习是程序员必不可少的内容。这周看了《图解算法》这本书,书本采用Python语言,通俗易懂,虽然书本上有些算法没有讲解,但通过大量的例子加深对算法的理解,是不错的入门书籍。
下面是我看了书籍后做的读书笔记,便于日后复习加强学习。
图解算法基础
- 大表示法: 是一种特殊的表示方法,指出了算法的速度有多快。在实际应用中,只知道算法需要多长时间才能运行完毕还不够,还需要知道运行时间如果随列表增长而增加,这正是大表示法的作用。
表达的方式为: (n) 其中n表示的是操作数,操作数前的,所以称为大表示法。
常见的大运行时间
常见的运行时间(log)比()快。需要搜索的元素越多,前者比后者就快得越多。
算法运行时间并不以秒为单位,而以操作数为单位,且从算法增速角度度量。
常量对大表示法影响:
大表示法的常量有时候事关重大,如快速排序比合并排序快,因为其大表示运行时间都为(log)。有时常量无关紧要,如简单查找()和二分查找(log)。
最糟情况:调用算法运行,经过最多次操作才得出结果。
最佳情况:调用算法运行时,经过最少次操作就得出结果。
平均情况:结果等同于最佳情况,如快速排序平均运行时间为(log)
-
内存工作原理:
数据存储到内存时,请求计算机提供存储空间,计算机会返回给你一个16进制128位的存储地址,存储地址对应数据的存放位置。内存存储就类似于往抽屉里放东西。 -
递归
递归是一种优雅的问题解决方法。使用递归,可以使程序更容易理解,使用循环,程序的性能可能更高。每个递归函数包括两部分:基线条件(base case)和递归条件(recursive case)。
例如:
def sum(list):
if list == []: #基线条件
return 0
return list[0] + sum(list[1:]) #递归条件
#循环写法
def sumCircle(list):
allSum = 0
for i in list:
allSum = allSum + i
return allSum
-
NP完全问题
简单定义是,以难解著称的问题,如旅行商问题(根据城市,找出所有可能路线,路线算法是采用阶乘函数,输入参数是城市数量)和集合覆盖问题。很多非常聪明的人认为,根本不可能编写出快速解决这些问题的算法。最佳解法是采用近似算法。
特征:
元素较少运行速度非常快,随元素增加,速度变得非常慢
涉及“所有组合”问题通常是NP完全问题
不能将问题分成小问题,必须考虑各种可能情况,可能是NP完全问题
如果问题涉及序列且难以解决,则可能是NP完全问题
如果问题涉及集合且难以解决,则可能是NP完全问题
如果问题涉及旅行商和集合覆盖问题,则肯定是NP完全问题
数据结构
- 数组:是一组数据在内存上采用内存地址连续的空间存储。由于数组知道每个元素位置,所以查找特别迅速,但由于内存空间固定,所以插入和删除都有重新安排空间,比较缓慢。同一数组中,所有元素类型都必须相同
例如途中待办事项就是数组
数组
-
链表:元素可以存储在内存的任何地方。每个元素都存储了下一个元素的地址,从而使一系列随机内存地址串在一起。插入和删除只要修改存储的下一元素的地址,比较快速,而查找则只能从第一个元素开始读取,比较缓慢。
链表
数组链表比较
比较
-
混合结构:同时使用数组和链表,其查找和插入的性能都介于数组和链表之间。
混合结构
-
栈:是一种简单的数据结构,特点是先进后出。在调用函数和递归调用的时候使用的数据结构就是栈。每个程序调用的栈空间都有限,如果用完这些空间,会造成栈溢出而终止运行。
-
散列表:最有用的基本数据结构之一。散列函数是将输入映射到数字。散列函数满足下列要求:
- 返回必须是一致的,即输入的返回值不发生变化
- 将不同的输入映射到不同数字。
散列函数和数组构成散列表。散列表也被称为散列映射、映射、字典和关联数组。Python提供的散列实现为字典,使用函数dict
创建散列表。
散列表应用:
- 通过创建映射,用于查找
- 防止重复
- 将散列表用于缓存
使用散列表,可能会遇到冲突问题,即存储时分配的位置已经被使用。这时,要保证:散列函数很重要,理想情况是散列函数均衡地映射到散列表的不同位置,呈均衡分布;散列表存储的链表不要太长,不然影响效率。
性能:一般情况下,散列表兼具数组和链表的优点,在最糟糕的情况下,各种操作速度都很慢,这时要避免冲突,做到:较低的填装因子和良好的散列函数
填装因子:用于反映数组中被占用的位置数,计算公式为:
填装因子越低,发生冲突的可能性越小,散列表性能越高。经验规则:一旦填装因子大于0.7,就调整散列表长度
- 队列:原理和生活中队列完全一样。队列类似于栈,你不能随机访问队列中的元素。队列只支持两种操作:出队和入队。
队列是一种先进先出(First In First Out, FIFO)的数据结构,而栈是一种后进先出(Last In First Out, LIFO)的数据结构。
-
图
图模拟一组连接。由节点和边组成。一个节点可能与总多节点直接相连,这些节点称为邻居。
图包括有向图和无向图。
有向图:边为箭头,箭头的方向指定了关系的方向,例如 dog animal表示狗属于动物
无向图:边不带箭头,其中的关系是双向的,例如 上海——广州表示上海可以到广州,广州也可以到上海
加权图加权图:每条边都有关联的数字的图,这些数字称为权重(weight)。带权重的图称为加权图(weighted graph),不带权重的图为非加权图(unweighted graph),使用狄克斯特拉算法。
使用代码实现图:
实现图
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'] = []
键值对交换顺序不会有影响,因为散列表是无序的。
拓扑排序:某种程度上说,有向图构成的列表是有序的,如果任务A依赖于任务B,则在列表中任务A必须在任务B后面,这就是拓扑排序,使用它可以根据图创建一个有序列表。
-
集合
集合类似于列表,只是同样的元素只能出现一次,即集合不包含重复的元素。
集合操作:
1)并集意味着集合合并 setA | setB
2)交集意味着找出两个集合中都有的元素 setA & setB
3)差集意味着将从一个集合中剔除出现在另一个集合中的元素 SetA - setB
-
二叉树
结构为:
二叉树
对于其中的每个节点,每个节点最多只能有两个节点(对应二叉树的二),左子节点的值都比它小 ,而右子节点的值都比它大 。
在二叉查找树中查找节点时,平均运行时间为(log ),但在最糟的情况下所需时间为 ();而在有序数组中查找时,即便是在最糟情况下所需的时间也只有(log ),因此你可能认为有序数组比二叉查找树更佳。然而,二叉查找树的插入和删除操作的速度要快得多。
二叉树数组比较二叉树缺点:不能随机访问。此外,分布不平衡的树会造成性能不佳。也有一些处于平衡状态的
特殊二叉查找树,如红黑树。
-
反向索引 (inverted index)
一个散列表,将单词映射到包含它的页面。这种数据结构用于搜索发动机。
策略
-
D&C(divide and conquer) 分而治之
分而治之是一种著名的递归式问题解决方法。提供了一种解决问题的思路。其解决问题一般分两个步骤:- 找出基线条件,这条件必须尽可能简单
- 不断分解问题(或缩小规模),知道符合基线条件
例如,推导出sum函数的递归写法:
分而治之
使用D&C可以推导出递归函数。D&C理念的方法是快速排序。
-
贪婪算法
优点是简单易行。每步都采取最优做法,即每步都选择局部最优解,最终得到的就是全局的最优解。贪婪算法并非在任何情况下都行之有效。有些情况,贪婪算法不能得到最优解,但非常接近。使用贪婪算法的有近似算法(approximation algorithm)、广度优先算法、狄克斯特拉算法等
-
动态规划
是一种解决棘手问题的方法,将问题分成小问题,并先着手解决这些小问题,在逐步解决大问题。缺陷:动态规划时,只能考虑取或者不取,没办法只取一部分,而贪婪算法可以解决这种情况,尽可能多拿价值高的。此外,仅当每个子问题都是离散的,即不依赖于其它子问题时,动态规划才有用。
要设计出动态规划解决方案可能很难,常用“套路”:
每种动态规划解决方案都涉及网格。
单元格中的值通常就是你要优化的值。
每个单元格都是一个子问题,因此你应考虑如何将问题分成子问题,这有助于你找出网格的坐标轴。
使用例子:
输入错别字时,原本输入的判断:如果错输入hish,判断原输入是fish,还vista
最长公共子串
伪代码
if word_a[i] == word_b[i]:
cell[i][j] = cell[i-1][j-1]+1
else:
cell[i][j] = 0
hish和fish的最长公共子串包含三个字母,而hish和vista的最长公共子串包含两个字母。因此很可能原本要输入的是fish。
最长公共子序列
结果:
结果
伪代码
if word_a[i] == word_b[j]: #两个字母相同
cell[i][j] = cell[i-1][j-1] + 1
else: #两个字母不同
cell[i][j] = max(cell[i-1][j], cell[i][j-1])
没有放之四海皆准的计算动态规划解决方案公式。
算法
-
1.二分查找
使用条件是列表是有序的。通过确定中位数,比较中位数于要查找的数值大小,如果大于查找的,就往中位数左侧查找,若小于查找的值,就往中位数右侧查找,循环运行,直到找到值,或没有值匹配。运行时间为(log)
例如在100个数字中查找1,最多需要log100 7步:
代码:
def binary_search(list, item):
low = 0 #low和high用于跟踪要在其中查找的列表部分
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 #没有指定元素
- 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)) #把找到的最小值加入到新数组中
retrun newArr
-
3.快速排序:是一种递归排序,基线条件是数组为空或只包含一个数组,递归条件是从元素中选择一个元素,这个元素为基准值(pivot),找出比基准值小的元素及大的元素,分别构成两个子数组,称为分区(partitioning),对子数组进行同样操作,找出基准值,进行分区,直到数组只有一个元素或没有元素。运行时间为(log)。在C语言标准库中函数
qsort
实现原理就是快速排序。
代码:
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)
- 4.广度优先搜索(breadth-first search, BFS):是一种图算法,能够找出两样东西之间的最短距离。与搜素起始节点相连节点的是一度关系,与一度关系的节点相连的节点是二度关系,一度关系胜过二度关系,二度关系胜过三度关系。
搜索的顺序是先找一度关系,再到二度关系中查找,知道找到目标,该路径是最短路径。运行时间为
(V+E),其中V为顶点(vertie)数,E为边数。
代码:
from collections import deque
search_queue = deque() #创建队列
search_queue += graph["you"] #将邻居加入到搜素队列中
def object_is_target(input): #检查这个是否是查找的目标
return input[-1] == 'm' #假设查找目标以字母m结尾
while search_queue: #只要队列不为空
input = search_queue.popleft() #就取出其中第一对象
if object_is_target(input):
print ('找到目标')
return True
else:
search_queue += graph[input] #如果不是目标,则将该输入的邻居加入到队列
return False #如果到达这里,说明没有找到目标
该代码的缺点是如果图中节点的邻居有重复的现象,可能把重复的对象重复加入到队列中,造成查找恶性循环,进入死循环。
优化代码:
def search(object):
search_queue = deque() #创建队列
search_queue += graph[object] #将邻居加入到搜素队列中
search = [] #记录查找过的对象
while search_queue: #只要队列不为空
input = search_queue.popleft() #就取出其中第一对象
if not input in searched: #仅当这个对象没有被检查过
if object_is_target(input):
print ('找到目标')
return True
else:
search_queue += graph[input] #如果不是目标,则将该输入的邻居加入到队列
searched.append(input) #标记该对象检查过
return False #如果到达这里,说明没有找到目标
- 5.狄克斯特拉算法:能够找出加权图中前往X的最短路径。狄克斯特拉算法只适用于有向无环图(directed acyclic graph, DAG),其关键理念是找出图中最便宜的节点,并确保没有到该节点的更便宜路径。此外,如果有负权边,就不能使用狄克斯特拉算法,因为狄克斯特拉算法假设对于处理过的节点,没有前往该节点的更短路径,而这种假设在仅在没有负权边时才成立。如果图中包含负权重,则使用贝尔曼-福德算法(书本未详细介绍,以后补充)
步骤:
- 找出“最便宜”的节点,即可在最短时间内(最低代价)到达的节点。
- 对于该节点的邻居,检测是否有前往他们的更短路径,如果有,更新该节点的开销。
- 重复这个过程,直到对图中所有节点都这样做了
- 计算最终路径
代码:
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 cost[n] > new_cost: #如果经当前节点前往该邻居更近
cost[n] = new_cost #更新该邻居开销
parents[n] = node #同时将该邻居的父节点设置为当前节点
processed.append(node) #标记节点为已经处理过
node = find_lowest_cost_node(costs) #找出接下处理节点,并循环
- 6.近似算法(approximation algorithm):在获取精确解需要时间太长时使用。判断优劣标准:运算时间和得到近似解和最优解的接近程度。运行时间为(),为元素数量。
代码:
object_need = set(["a","b",...])#创建一个集合,包含所有目标
objects = {} #散列表创建
objects['sone'] = set(['a','c','e'])
objects['stwo'] = set(['b','c','d'])
...#根据图,创建所有集合,保存到objects中
final_objects = set() #创建集合来存储最终选择。
while object_need: #循环需要的目标
best_object = None
object_covered = set()
for s, object in objects.items():
covered = object_need & object #计算交集
if len(covered) > len(object_covered):
best_object = s
object_covered = covered
object_need -= object_covered #去除掉已经覆盖的
final_objects.add(best_object)
贪婪算法
-
7.K最近邻算法: 可以用于分类和回归。需要考虑最近的邻居。
分类就是编组。回归就是预测结果。是机器学习的入门方法,没有显示学习过程。算法原理:通过计算待分类元素与所有已分类元素距离,计算两位元素的距离时,使用的都是距离公式,距离公式有时也可采用余弦相似度,余弦相似度不计算两个矢量的距离,而比较它们的角度,得到与待分类元素最接近的元素所属的分类,就是待分类元素的类别。
特征抽取以为这将物品转换称一系列可比较的数字,能否挑选合适的特征事关KNN算法成败。在挑选合适的特征方面,没有放之四海皆准的法则,你必须考虑到各种需要考虑的因素。
应用:OCR字符识别;创建垃圾邮件过滤器
网友评论