1.2 二分查找
二分查找是一种算法,其输入是一个有序的元素列表(必须有序的原因稍后解释)。如果要查找的元素包含在列表中,二分查找返回其位置;否则返回null。使用二分查找时,每次都排除一半的数字。
一般而言,对于包含n个元素的列表,用二分查找最多需要log2n步,而简单查找最多需要n步。仅当列表是有序的时候,二分查找才管用。
二分法代码实现:
def binary_search(list, item):
low = 0
high = len(list)—1
while low <= high: #只要范围没有缩小到只包含一个元素,就检查中间的元素
mid = (low + high)
guess = list[mid]
if guess == item: #找到了元素
return mid
if guess > item: #猜的数字大了
high = mid - 1
else: #猜的数字小了
low = mid + 1
return None #没有指定的元素
my_list = [1, 3, 5, 7, 9]
print binary_search(my_list, 3) # => 1
print binary_search(my_list, -1) # => None
1.2.2 运行时间
二分查找的运行时间为对数时间(或log时间)。简单查找的运行时间为线性时间。
1.3 大 O表示法
算法的运行时间以不同的速度增加。
大O表示法指出了算法有多快,大O表示法指的并非以秒为单位的速度。 大O表示法让你能够比较操作数,它指出了算法运行时间的增速。二分查找需要执行log n次操作,使用大O表示法,O(log n)。简单查找的运行时间为O(n)。大O表示法说的是最糟的情形。
1.3.4 一些常见的大 O 运行时间(“阶指幂对”)
O(log n),也叫对数时间,这样的算法包括二分查找。
O(n),也叫线性时间,这样的算法包括简单查找。
O(n * log n),这样的算法包括第4章将介绍的快速排序——一种速度较快的排序算法。
O(n2),这样的算法包括第2章将介绍的选择排序——一种速度较慢的排序算法。
O(n!),这样的算法包括接下来将介绍的旅行商问题的解决方案——一种非常慢的算法。
小结:
- 算法的速度指的并非时间,而是操作数的增速。谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加。
- 算法的运行时间用大O表示法表示。 O(log n)比O(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, 6, 2, 10])
思想:分而治之
D&C算法包括两个步骤: (1) 找出基线条件,这种条件必须尽可能简单。 (2) 不断将问题分解(或者说缩小规模),直到符合基线条件。
D&C的工作原理: (1) 找出简单的基线条件; (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)
print quicksort([10, 5, 2, 3])
运行时间O(n)和O(log n)之间有天壤之别!
- 散列函数总是将同样的输入映射到相同的索引。
- 散列函数将不同的输入映射到不同的索引。
- 散列函数知道数组有多大,只返回有效的索引。如果数组包含5个元素,散列函数就不会返回无效索引100。
散列表也使用数组来存储数据,因此其获取元素的速度与数组一样快。
5.2.3 将散列表用作缓存
缓存的工作原理:网站将数据记住,而不再重新计算。
缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中!
散列表适合用于:
- 模拟映射关系;
- 防止重复;
- 缓存/记住数据,以免服务器再通过处理来生成它们。
5.3 冲突
冲突( collision) :给两个键分配的位置相同。
处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表。
散列函数很重要,好的散列函数很少导致冲突。
5.4 性能
在平均情况下,散列表执行各种操作的时间都为O(1)。 O(1)被称为常量时间。简单查找的运行时间为线性时间。二分查找的速度更快,所需时间为对数时间。在最糟情况下,散列表所有操作的运行时间都为O(n)——线性时间,这真的很慢。
在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速度与链表一样快,因此它兼具两者的优点!但在最糟情况下,散列表的各种操作的速度都很慢。
因此,在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要有:
较低的填装因子;
良好的散列函数。
广度优先搜索让你能够找出两样东西之间的最短距离,广度优先搜索是一种用于图的查找算法,可帮助回答两类问题。
第一类问题:从节点A出发,有前往节点B的路径吗?(在你的人际关系网中,有芒果销售商吗?)
第二类问题:从节点A出发,前往节点B的哪条路径最短?(哪个芒果销售商与你的关系最近?)
使用这种算法将搜遍你的整个人际关系网,直到找到芒果销售商。这就是广度优先搜索算法。
6.3.2 队列
队列类似于栈,你不能随机地访问队列中的元素。队列只支持两种操作: 入队和出队。
队列是一种先进先出( First In First Out, FIFO)的数据结构,而栈是一种后进先出( Last InFirst Out, LIFO)的数据结构。
运行时间
如果你在你的整个人际关系网中搜索芒果销售商,就意味着你将沿每条边前行(记住,边是从一个人到另一个人的箭头或连接),因此运行时间至少为O(边数)。
你还使用了一个队列,其中包含要检查的每个人。将一个人添加到队列需要的时间是固定的,即为O(1),因此对每个人都这样做需要的总时间为O(人数)。所以,广度优先搜索的运行时间为O(人数 + 边数),这通常写作O(V + E),其中V为顶点( vertice)数, E为边数。
你需要按加入顺序检查搜索列表中的人,否则找到的就不是最短路径,因此搜索列表必须是队列。
下述代码在关系图中,找到最亲近的经销商
from collections import deque
def search(name):
search_person = deque()
search_person += graph[name]
popdone = []
while search_person:
person = search_person.popleft()
if person not in popdone:
popdone.append(person)
if person_is_seller(person):
print (person + " is a seller")
return True
else:
search_person += graph[person]
return False
def person_is_seller(name):
return name[-1] == 'm'
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"] = []
search("you")
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
# 图的建立
graph = {}
graph['start'] = {}
graph['start']["a"] = 10
graph['a'] = {}
graph['a']["fin"] = 1
graph['a'] = {}
graph['a']["c"] = 20
graph['b'] = {}
graph['b']["a"] = 1
graph['c'] = {}
graph['c']["b"] = 1
graph['c']["fin"] = 30
graph['fin'] = {}
print (graph)
# cost花销散列表
infinity = float("inf")
costs = {}
costs["a"] = 10
costs["b"] = infinity
costs["c"] = infinity
costs["fin"] = infinity
print (costs)
# 存储父节点的散列表
parents = {}
parents["a"] = "start"
parents["b"] = None
parents["c"] = None
parents["fin"] = None
print (parents)
processed = []
node = find_lowest_cost_node(costs)
while node is not None:
cost = costs[node]
neighbors = graph[node]
#print (node, neighbors.keys())
for n in neighbors.keys():
new_cost = cost + neighbors[n]
print (new_cost)
if costs[n] > new_cost:
costs[n] = new_cost
parents[n] = node
processed.append(node)
node = find_lowest_cost_node(costs)
print (costs)
第八章 贪婪算法
贪婪算法的优点——简单易行!贪婪算法很简单:每步都采取最优的做法。用专业术语说,就是你每步都选择局部最优解,最终得到的就是全局最优解。
8.2 背包问题
在有些情况下,完美是优秀的敌人。有时候,你只需找到一个能够大致解决问题的算法,此时贪婪算法正好可派上用场,因为它们实现起来很容易,得到的结果又与正确结果相当接近。
背包问题就是有若干物品,每个物品有自己的价值和重量。背包有总重量。问题就是怎样将背包装的最大价值。背包问题也分很多种,贪心算法解决的是物品可以拆分的背包问题(就是物品可以分成几份装入)。这个问题用贪心还是比较好解决的。贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。此问题就是将每次的放入看成每一步,要想解决问题,就是将每一步都放入最优解。也就是说,每一次的放入都要放入最佳的选择。讲到这里,就要说一说最佳的选择,每一次的放入的最佳的选择就是每次放入的物品都是剩余的物品中价值最大且质量最小的,这里就要引入一个物品的属性,物品的权重值。物品的权重值就是指物品的价值除以物品的质量。所以,本问题的每一次的最佳选择就是每次都选出权重值最大的物品。
近似算法
在获得精确解需要的时间太长时,可使用近似算法。判断近似算法优劣的标准如下:
速度有多快;
得到的近似解与最优解的接近程度。
8.4 NP 完全问题(Non-deterministic Polynomial多项式的不确定性)
NP完全问题的简单定义是,以难解著称的问题,如旅行商问题和集合覆盖问题。很多非常聪明的人都认为,根本不可能编写出可快速解决这些问题的算法。
如果能够判断出要解决的问题属于NP完全问题就好了,这样就不用去寻找完美的解决方案,而是使用近似算法即可。但要判断问题是不是NP完全问题很难,易于解决的问题和NP完全问题的差别通常很小。
元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会变得非常慢。 涉及“所有组合”的问题通常是NP完全问题。
不能将问题分成小问题,必须考虑各种可能的情况。这可能是NP完全问题。
如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是NP完全问题。
如果问题涉及集合(如广播台集合)且难以解决,它可能就是NP完全问题。 如果问题可转换为集合覆盖问题或旅行商问题,那它肯定是NP完全问题。
8.5 小结
- 贪婪算法寻找局部最优解,企图以这种方式获得全局最优解。
- 对于NP完全问题,还没有找到快速解决方案。
- 面临NP完全问题时,最佳的做法是使用近似算法。
- 贪婪算法易于实现、运行速度快,是不错的近似算法。
9.1.2 动态规划
动态规划先解决子问题,再逐步解决大问题。
对于背包问题,你先解决小背包(子背包)问题,再逐步解决原来的问题。
每个动态规划算法都从一个网格开始,网格的各行为商品,各列为不同容量( 1~4磅)的背包。所有这些列你都需要,因为它们将帮助你计算子背包的价值。
动态规划功能强大,它能够解决子问题并使用这些答案来解决大问题。 但仅当每个子问题都是离散的,即不依赖于其他子问题时,动态规划才管用。
动态规划可帮助你在给定约束条件下找到最优解。在背包问题中,你必须在背包容量给定的情况下,偷到价值最高的商品。
在问题可分解为彼此独立且离散的子问题时,就可使用动态规划来解决。要设计出动态规划解决方案可能很难,这正是本节要介绍的。下面是一些通用的小贴士。
每种动态规划解决方案都涉及网格。
9.3.1 绘制网格
对于前面的背包问题,最终答案总是在最后的单元格中。但对于最长公共子串问题,答案为网格中最大的数字——它可能并不位于最后的单元格中。
9.4 小结
- 需要在给定约束条件下优化某种指标时,动态规划很有用。
- 问题可分解为离散子问题时,可使用动态规划来解决。
- 每种动态规划解决方案都涉及网格。
- 单元格中的值通常就是你要优化的值。
- 每个单元格都是一个子问题,因此你需要考虑如何将问题分解为子问题。
- 没有放之四海皆准的计算动态规划解决方案的公式。
第十章 K最近邻算法
KNN可以用来做两项基本工作——分类和回归:
余弦相似度( cosine similarity)
余弦相似度不计算两个矢量的距离,而比较它们的角度。
余弦相似度。余弦相似度被广泛用于协同过滤算法中,尤其是Item-base的协同过滤。
余弦相似度衡量的是两个向量间的夹角大小,通过夹角的余弦值表示结果,假设A向量是(x1, y1),B向量是(x2, y2),那么两个向量的余弦相似度为:
cosθ=A⋅B||A||∗||B||=x1y1+x2y2(√x21+y21)∗(√x22+y22)
分子为向量A与向量B的点乘,分母为二者各自的L2相乘,即将所有维度值的平方相加后开方。 余弦相似度的取值为[-1,1],值越大表示越相似。
10.3.1 OCR
OCR指的是光学字符识别( optical character recognition),这意味着你可拍摄印刷页面的照片,计算机将自动识别出其中的文字。 一般而言, OCR算法提取线段、点和曲线等特征。
OCR的第一步是查看大量的数字图像并提取特征,这被称为训练( training)。大多数机器学习算法都包含训练的步骤:要让计算机完成任务,必须先训练它。
10.3.2 创建垃圾邮件过滤器
垃圾邮件过滤器使用一种简单算法——朴素贝叶斯分类器( Naive Bayes classifier)。
10.4 小结
第十一章 接下来如何做
二叉查找树( binary search tree)
在二叉查找树中查找节点时,平均运行时间为O(log n),但在最糟的情况下所需时间为O(n);而在有序数组中查找时,即便是在最糟情况下所需的时间也只有O(log n),因此你可能认为有序数组比二叉查找树更佳。然而,二叉查找树的插入和删除操作的速度要快得多。
这里写图片描述
二叉查找树也存在一些缺点,例如,不能随机访问,在二叉查找树处于平衡状态时,平均访问时间也为O(log n)。
11.2 反向索引
一个散列表,将单词映射到包含它的页面。这种数据结构被称为反向索引( inverted index),常用于创建搜索引擎。
11.4 并行算法
并行算法设计起来很难,要确保它们能够正确地工作并实现期望的速度提升也很难。有一点是确定的,那就是速度的提升并非线性的,因此即便你的笔记本电脑装备了两个而不是一个内核,算法的速度也不可能提高一倍,其中的原因有两个。
并行性管理开销。假设你要对一个包含1000个元素的数组进行排序,如何在两个内核之间分配这项任务呢?如果让每个内核对其中500个元素进行排序,再将两个排好序的数组合并成一个有序数组,那么合并也是需要时间的。
负载均衡。假设你需要完成10个任务,因此你给每个内核都分配5个任务。但分配给内核A的任务都很容易, 10秒钟就完成了,而分配给内核B的任务都很难, 1分钟才完成。这意味着有那么50秒,内核B在忙死忙活,而内核A却闲得很!你如何均匀地分配工作,让两个内核都一样忙呢?
11.5 MapReduce
MapReduce是一种流行的分布式算法,你可通过流行的开源工具Apache Hadoop来使用它。分布式算法非常适合用于在短时间内完成海量工作,其中的MapReduce基于两个简单的理念:映射( map)函数和归并( reduce)函数。
11.5.2 映射函数
映射函数很简单,它接受一个数组,并对其中的每个元素执行同样的处理。
11.5.3 归并函数
归并函数可能令人迷惑,其理念是将很多项归并为一项。映射是将一个数组转换为另一个数组。
MapReduce使用这两个简单概念在多台计算机上执行数据查询。数据集很大,包含数十亿行时,使用MapReduce只需几分钟就可获得查询结果,而传统数据库可能要耗费数小时。
11.6 布隆过滤器和 HyperLogLog
布隆过滤器是一种概率型数据结构,布隆过滤器的优点在于占用的存储空间很少。使用散列表时,必须存储Google搜集过的所有URL,但使用布隆过滤器时不用这样做。布隆过滤器非常适合用于不要求答案绝对准确的情况,前面所有的示例都是这样的。
HyperLogLog是一种类似于布隆过滤器的算法。
HyperLogLog近似地计算集合中不同的元素数,与布隆过滤器一样,它不能给出准确的答案,但也八九不离十,而占用的内存空间却少得多。
面临海量数据且只要求答案八九不离十时,可考虑使用概率型算法!
11.7 SHA 算法
另一种散列函数是安全散列算法( secure hash algorithm, SHA)函数。给定一个字符串, SHA返回其散列值。
SHA是一个散列函数,它生成一个散列值——一个较短的字符串。用于创建散列表的散列函数根据字符串生成数组索引,而SHA根据字符串生成另一个字符串。对于每个不同的字符串, SHA生成的散列值都不同。
你可使用SHA来判断两个文件是否相同,这在比较超大型文件时很有用。
斐波那契数列
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递归的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。