嗡嘛呢叭咪吽

二分查找:

一般而言,对于包含n个元素的列表,用二分查找最多需要log2 n(以2为底,n的对数)步,而简单查找最多需要n步。

仅当列表是有序的时候,二分查找才管用。

函数 binary_search 接受一个有序数组和一个元素。如果指定的元素包含在数组中,这个函数将返回其位置。

low = 0  high = len(list) - 1  mid = (low + high) / 2

guess = list[mid]

如果猜的数字小了,就相应地修改 low 。if guess < item: low = mid + 1

如果猜的数字大了,就修改 high 。if guess > item: high = mid - 1

 1 #!coding:utf8
 2 
 3 def binary_select(item, li):
 4     low = 0
 5     high = len(li) - 1
 6     while low <= high:  # 0 5, 3 5, 4 5, 5 5  # 所以low==high的情况是比较开头或结尾时
 7         mid = int((low+high) / 2)  # 2  4  4
 8         guess = li[mid]  # 5  10  10  # 虽然重复比较了一次,但是索引变了
 9         print(mid, guess)
10         if guess == item:
11             return mid
12         elif guess < item:
13             low = mid + 1
14         else:
15             high = mid - 1
16     return None  # 没找到的情况
17 
18 li = [2, 4, 5, 7, 10, 14]
19 res = binary_select(16, li)
20 print('res: ', res)

运行时间:

最多需要猜测的次数与列表长度相同,这被称为线性时间(linear time)。

二分查找的运行时间为对数时间 (或log时间)。

大O表示法:

仅知道算法需要多长时间才能运行完毕还不够,还需知道运行时间如何随列表增长而增加。它表示为检查长度为n的列表,需要执行log n次操作。大 O 表示法指出了最糟情况下的运行时间。

常见运行时间:

 O(log n),也叫对数时间,这样的算法包括二分查找。
 O(n),也叫线性时间,这样的算法包括简单查找。
 O(n * log n),这样的算法包括第4章将介绍的快速排序——一种速度较快的排序算法。
 O(n 2 ),这样的算法包括第2章将介绍的选择排序——一种速度较慢的排序算法。
 O(n!),这样的算法包括接下来将介绍的旅行商问题的解决方案——一种非常慢的算法。

旅行商问题:要前往这n个城市,同时要确保旅程最短。运行时间为O(n!)。

数组和链表

数组中的元素需要一整块连续的内存来保存。当需要向数组中添加元素时需要另开辟一整块空间。

链表中的元素可存储在内存的任何地方。链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。向链表中添加元素,只要将其放入内存,并将其地址保存到前一个元素即可。

在需要读取链表的最后一个元素时,你不能直接读取,因为你不知道它所处的地址,必须先访问元素#1,从中获取元素#2的地址,再访问元素#2并从中获取元素#3的地址,以此类推,直到访问最后一个元素。需要同时读取所有元素时,链表的效率很高:你读取第一个元素,根据其中的地址再读取第二个元素,以此类推。但如果你需要跳跃,链表的效率真的很低。

数组与此不同:你知道其中每个元素的地址。例如,假设有一个数组,它包含五个元素,起始地址为00,那么元素#5的地址是多少呢?只需执行简单的数学运算就知道:04。需要随机地读取元素时,数组的效率很高,因为可迅速找到数组的任何元素。在链表中,元素并非靠在一起的,你无法迅速计算出第五个元素的内存地址,而必须先访问第一个元素以获取第二个元素的地址,再访问第二个元素以获取第三个元素的地址,以此类推,直到访问第五个元素。

两种访问方式:随机访问和顺序访问。

链表擅长插入和删除,而数组擅长随机访问。

选排:

 假设从小到大排序,遍历整个列表找出最大的元素,将该元素添加到一个新列表并从原有列表删除;以此重复这种操作,直到原有列表中的元素都移到新列表中,新列表即为已排序好的结果。

 1 #!coding:utf8
 2 
 3 def findSmall(li):
 4     smallest = li[0]
 5     smallest_index = 0  # 存储最小元素的索引
 6     for i in range(1, len(li)):
 7         if li[i] < smallest:
 8             smallest = li[i]
 9             smallest_index = i
10     return smallest_index
11 
12 
13 def selectionSort(li):
14     newli = []
15     for i in range(len(li)):
16         smallest = findSmall(li)
17         newli.append(li.pop(smallest))
18     return newli
19 
20 print(selectionSort([5, 3, 6, 2, 10, 4]))

same as this:

 1 #!coding:utf8
 2 
 3 def select_num(li):
 4     num = 0
 5     for i in range(1, len(li)):
 6         if li[num] < li[i]:
 7             num = i
 8     return num
 9 
10 def select_sort(li):
11     new_li = []
12     while li:
13         num = select_num(li)
14         new_li.append(li.pop(num))
15     print('new_list: ', new_li)
16 
17 select_sort([5, 3, 6, 2, 10, 4, 2.5])

递归:

递归只是让解决方案更清晰,并没有性能上的优势。实际上,在有些情况下,使用循环的性能更好。Leigh Caldwell在Stack Overflow上说的一句话:“如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。

编写递归函数时,必须告诉它何时停止递归。正因为如此,每个递归函数都有两部分:基线条件(base case)和递归条件(recursive case)。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。

栈:

一叠便条:插入的待办事项放在清单的最前面;读取待办事项时,你只读取最上面的那个,并将其删除。因此这个待办事项清单只有两种操作:压入(插入)和弹出(删除并读取)。

调用栈:

 1 #!coding:utf8
 2 
 3 def greet(name):
 4     print("hello, " + name + "!")
 5     greet2(name)
 6     print("getting ready to say bye...")
 7     bye()
 8 
 9 
10 def greet2(name):
11     print("how are you, " + name + "?")
12 
13 
14 def bye():
15     print("ok bye!")
16 
17 greet('maggie')

当调用great('maggie')时,计算机首先为该函数调用分配一块内存,接着打印`hello, maggie!`;在调用greet2('maggie')时,计算机同样为该函数分配一块内存,计算机使用一个来表示这些内存块,其中第二个内存块位于第一个内存块上面。当打印完`how are you, maggie?`时,从函数greet2调用返回到greet,此时,栈顶的内存块被弹出;接着打印`getting ready to say bye...`,然后调用函数bye,同样再栈顶添加函数bye的内存块...,像这样,用于存储多个函数的变量的栈,被成为调用栈。

递归调用栈:

1 #!coding:utf8
2 
3 def fact(x):
4     if x == 1:
5         return 1
6     else:
7         return x * fact(x-1)

1

1

1

分而治之

假设有一块土地(长宽为640m、1680m),把其分成均匀的方块(长宽相等),且方块尽可能大。

使用D&C策略!

D&C算法是递归的。使用D&C解决问题的过程包括两个步骤。

(1) 找出基线条件,这种条件必须尽可能简单。

(2) 不断将问题分解(或者说缩小规模),直到符合基线条件。

首先,找出基线条件。最容易处理的情况是,一条边的长度是另一条边的整数倍。如果一边长25 m,另一边长50 m,那么可使用的最大方块为 25 m×25 m。换言之,可以将这块地分成两个这样的方块。

现在需要找出递归条件,这正是D&C的用武之地。根据D&C的定义,每次递归调用都必须缩小问题的规模。如何缩小前述问题的规模呢?我们首先找出这块地可容纳的最大方块。

以此划分。。。直到满足基线条件。

给定一个数字列表,将这些数字相加,最后返回结果。

第一步:找出基线条件。最简单的数组什么样呢?如果数组不包含任何元素或只包含一个元素,计算总和将非常容易。因此这就是基线条件。

第二步:每次递归调用都必须离空数组更近一步。

这两个版本的结果都为12,但在第二个版本中,给函数sum传递的数组更短。换言之,这缩小了问题的规模!

 1 #!coding:utf8
 2 
 3 def sum_num(li):
 4     if len(li) <= 1:  # 基线条件
 5         return sum(li)
 6     else:  # 递归条件
 7         i = li.pop()
 8         return i + sum_num(li)
 9 
10 print(sum_num([i for i in range(998)]))

快排:

基线条件为数组为空或只包含一个元素。在这种情况下,只需原样返回数组——根本就不用排序。

首先,从数组中选择一个元素,这个元素被称为基准值(pivot)。我们暂时将数组的第一个元素用作基准值。

接下来,找出比基准值小的元素以及比基准值大的元素。这被称为分区(partitioning)。现在你有:

 一个由所有小于基准值的数字组成的子数组;
 基准值;
 一个由所有大于基准值的数组组成的子数组。

分区之后得到的两个子数组是无序的。如果子数组是有序的,就可以像下面这样合并得到一个有序的数组:左边的数组 + 基准值 +右边的数组。

 1 def quick_sort(li):
 2     if len(li) <= 1:  # 基线条件为列表为空或只包含一个元素,这时只需原样返回数组--根本不需要排序。
 3         return li
 4     elif len(li) == 2:
 5         if li[0] > li[1]:
 6             li[0], li[1] = li[1], li[0]
 7         return li
 8     elif len(li) >= 3:
 9         # 分区
10         # li1 = li2 = []  # 不能这么写。。
11         li1 = []
12         li2 = []
13         pivot = li[0]
14         for i in li[1:]:
15             if i < pivot:
16                 li1.append(i)
17             else:
18                 li2.append(i)
19         li1 = quick_sort(li1)
20         li2 = quick_sort(li2)
21 
22         return li1 + [pivot] + li2

优化之后:

 1 #!coding:utf8
 2 
 3 def quick_sort(li):
 4     if len(li) <= 1:
 5         return li
 6     else:
 7         pivot = li[0]
 8         li1 = quick_sort([i for i in li[1:] if i < pivot])
 9         li2 = quick_sort([i for i in li[1:] if i >= pivot])
10         return li1 + [pivot] + li2

冒泡排序:

 1 #!coding:utf8
 2 
 3 # 冒泡排序
 4 # 轮  比较次数
 5 # 0   len -1
 6 # 1   len - 2
 7 # 2
 8 # 3
 9 # 4
10 # 。。。
11 # i   len - i - 1
12 def bubble_sort(li):
13     for i in range(len(li)-1):
14         for j in range(len(li)-i-1):
15             if li[j] < li[j+1]:
16                 li[j], li[j+1] = li[j+1], li[j]
17     print(li)
18 
19 bubble_sort([5, 3, 7, 2, 1, 0])

散列函数:O(1)

无论给它什么数据,它都还返回一个数字。即`将输入映射到数字`。

散列函数必须满足一些要求。

 它必须是一致的。例如,假设你输入apple时得到的是4,那么每次输入apple时,得到的都必须为4。如果不是这样,散列表将毫无用处。
 它应将不同的输入映射到不同的数字。例如,如果一个散列函数不管输入是什么都返回1,它就不是好的散列函数。最理想的情况是,将不同的输入映射到不同的数字。

散列表

结合使用散列函数和数组创建了一种被称为散列表(hash table)的数据结构。散列表学习到的第一种包含额外逻辑的数据结构。数组和链表都被直接映射到内存,但散列表更复杂,它使用散列函数来确定元素的存储位置。

散列表也被称为散列映射、映射、字典和关联数组。散列表的速度很快!你可以立即获取数组中的元素,而散列表也使用数组来存储数据,因此其获取元素的速度与数组一样快。

你可能根本不需要自己去实现散列表,任一优秀的语言都提供了散列表实现。Python提供的散列表实现为字典。

散列表的应用:

缓存:缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中!(将页面URL映射到页面数据)

冲突:

假设你有一个数组,它包含26个位置。

这种情况被称为冲突(collision):给两个键分配的位置相同。

如果你将鳄梨的价格存储到这个位置,将覆盖苹果的价格,以后再查询苹果的价格时,得到的将是鳄梨的价格!冲突很糟糕,必须要避免。处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表。

小结:

 散列函数很重要。前面的散列函数将所有的键都映射到一个位置,而最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。

 如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很好,这些链表就不会很长!

性能:

在平均情况下,散列表执行各种操作的时间都为O(1)。O(1)被称为常量时间。它并不意味着马上,而是说不管散列表多大,所需的时间都相同。

在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速度与链表一样快,因此它兼具两者的优点!但在最糟情况下,散列表的各种操作的速度都很慢。因此,在使用散列表时,避开最糟情况至关重要。

为此,需要避免冲突。而要避免冲突,需要有:
 较低的填装因子;
 良好的散列函数。

填装因子:散列表包含的元素数 / 位置总数。

最短路径问题:

广度优先搜索:解决最短路径问题的算法被称为广度优先搜索。

要确定如何从双子峰前往金门大桥,需要两个步骤。

(1) 使用图来建立问题模型。

(2) 使用广度优先搜索解决问题。

图:模拟一组连接。图由节点和边组成。一个节点可能与众多节点直接相连,这些节点被称为邻居。

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

第一类问题,从节点A出发,有前往节点B的路径吗?

第二类问题从节点A出发,前往节点B的哪条路径最短?

如果Alice不是芒果销售商,就将其朋友也加入到名单中。这意味着你将在她的朋友、朋友的朋友等中查找。使用这种算法将搜遍你的整个人际关系网,直到找到芒果销售商。这就是广度优先搜索算法。只有按添加顺序查找时才会达到优先目的。有一个可实现这种目的的数据结构--队列

队列:

队列是一种先进先出(First In First Out,FIFO)的数据结构,而栈是一种后进先出(Last In First Out,LIFO)的数据结构。

实现图:(通过散列表,即字典)

 1 graph = {}
 2 graph["you"] = ["alice", "bob", "claire"]
 3 graph["bob"] = ["anuj", "peggy"]
 4 graph["alice"] = ["peggy"]
 5 graph["claire"] = ["thom", "jonny"]
 6 graph["anuj"] = []
 7 graph["peggy"] = []
 8 graph["thom"] = []
 9 graph["jonny"] = []
10 print(graph)
11 
12 # 结果:
13 {'you': ['alice', 'bob', 'claire'], 'bob': ['anuj', 'peggy'], 'alice': ['peggy'], 'claire': ['thom', 'jonny'], 'anuj': [], 'peggy': [], 'thom': [], 'jonny': []}

Anuj、Peggy、Thom和Jonny都没有邻居,这是因为虽然有指向他们的箭头,但没有从他们出发指向其他人的箭头。这被称为有向图(directed graph),其中的关系是单向的。因此,Anuj是Bob的邻居,但Bob不是Anuj的邻居。无向图(undirected graph)没有箭头,直接相连的节点互为邻居。例如,下面两个图是等价的。

实现算法:

Peggy既是Alice的朋友又是Bob的朋友,因此她将被加入队列两次:一次是在添加Alice的朋友时,另一次是在添加Bob的朋友时。因此,搜索队列将包含两个Peggy。但你只需检查Peggy一次,看她是不是芒果销售商。如果你检查两次,就做了无用功。因此,检查完一个人后,应将其标记为已检查,且不再检查他。如果不这样做,就可能会导致无限循环。

因此,检查一个人之前,要确认之前没检查过他。为此,你可使用一个列表来记录检查过的人。

 1 #!coding:utf8
 2 from collections import deque
 3 
 4 graph = {}
 5 graph["you"] = ["alice", "bob", "claire"]
 6 graph["bob"] = ["anuj", "peggy"]
 7 graph["alice"] = ["peggy"]
 8 graph["claire"] = ["thom", "jonny"]
 9 graph["anuj"] = []
10 graph["peggy"] = []
11 graph["thom"] = []
12 graph["jonny"] = []
13 
14 def searcher(graph):
15     search_queue = deque()
16     search_queue += graph['you']
17     searched = []
18 
19     while search_queue:  # 只要队列不为空
20         person = search_queue.popleft()  # 就取出其中的第一个人
21         if person in searched:  # 如果这个人已经检查过,就跳过检查下一个人
22             continue
23         if person_is_seller(person):  # 检查这个人是否是芒果销售商
24             print(person + ' is a mango serler!')  # 是芒果销售商
25             return True
26         else:
27             search_queue += graph[person]  # 不是芒果销售商。将这个人的朋友都加入搜索队列
28             searched.append(person)  # 已检查过的人添加到检查列表
29     return False  # 如果到达了这里,就说明队列中没人是芒果销售商
30 
31 
32 def person_is_seller(name):
33     return name[-1] == 'm'
34 
35 searcher(graph)

运行时间:

如果你在你的整个人际关系网中搜索芒果销售商,就意味着你将沿每条边前行(记住,边是从一个人到另一个人的箭头或连接),因此运行时间至少为O(边数)。
你还使用了一个队列,其中包含要检查的每个人。将一个人添加到队列需要的时间是固定的,即为O(1),因此对每个人都这样做需要的总时间为O(人数)。所以,广度优先搜索的运行时间为O(人数 + 边数),这通常写作O(V + E),其中V为顶点(vertice)数,E为边数。

拓扑排序:

如果任务A依赖于任务B,在列表中任务A就必须在任务B后面。这被称为拓扑排序,使用它可根据图创建一个有序列表。

树:

树是一种特殊的图,其中没有往后指的边。

狄克斯特拉算法

最快路径:

狄克斯特拉算法包含4个步骤。
(1) 找出“最便宜”的节点,即可在最短时间内到达的节点。
(2) 更新该节点的邻居的开销,其含义将稍后介绍。
(3) 重复这个过程,直到对图中的每个节点都这样做了。
(4) 计算最终路径。

1. 找出最便宜的节点,最便宜的节点为B。

2. 计算(从起点)经节点B前往其(节点B的)各个邻居所需的时间。

对于节点B的邻居,如果找到前往它的更短路径(直接到节点A需要6分钟,经由节点B到节点A需要5分钟),就更新其开销。在这里,你找到了:
 前往节点A的更短路径(时间从6分钟缩短到5分钟);
 前往终点的更短路径(时间从无穷大缩短到7分钟)。

3. 重复

重复第一步,对节点B来说,便宜节点为A,节点A的邻居为终点。

重复第二步,更新节点A的邻居的开销(从节点B经由节点A到终点的时间比直接到达缩短1分钟),

现在,你知道:从起点
 前往节点B需要2分钟;
 前往节点A需要5分钟;
 前往终点需要6分钟。

4. 计算最终路径

有关属于:

权重:每条边上的关联数字。

带权重的图称为加权图(weighted graph),不带权重的图称为非加权图(unweighted graph)。

要计算非加权图中的最短路径,可使用广度优先搜索。要计算加权图中的最短路径,可使用狄克斯特拉算法。

图还可能有环,像下面这样:

环增加了权重,因此,绕环的路径不可能是最短的路径。

无向图意味着两个节点彼此指向对方,其实就是环!

狄克斯特拉算法只适用于有向无环图。

Rama想拿一本乐谱换架钢琴。

Alex说:“这是我最喜欢的乐队Destroyer的海报,我愿意拿它换你的乐谱。如果你再加5美元,还可拿乐谱换我这张稀有的Rick Astley黑胶唱片。”

Amy说:“哇,我听说这张黑胶唱片里有首非常好听的歌曲,我愿意拿我的吉他或架子鼓换这张海报或黑胶唱片。”

Beethoven惊呼:“我一直想要吉他,我愿意拿我的钢琴换Amy的吉他或架子鼓。”

那么问题来了,用乐谱换钢琴,怎么个换法花钱最少。

创建一个表格,列出各个节点的开销,即到达节点需要额外支付多少钱。

在执行狄克斯特拉算法的过程中,你将不断更新这个表。为计算最终路径,还需在这个表中添加表示父节点的列。

第一步:找出最便宜的节点。在这里,换海报最便宜,不需要支付额外的费用。还有更便宜的换海报的途径吗?这一点非常重要,你一定要想一想。Rama能够通过一系列交换得到海报,还能额外得到钱吗?想清楚后接着往下读。答案是不能,因为海报是Rama能够到达的最便宜的节点,没法再便宜了。

第二步:计算前往该节点的各个邻居的开销。

第三步:

再次执行第一步:下一个最便宜的节点是黑胶唱片——需要额外支付5美元。(和之前想的还不一样)
再次执行第二步:更新黑胶唱片的各个邻居的开销。

更新了架子鼓和吉他的开销!这意味着经“黑胶唱片”前往“架子鼓”和“吉他”的开销更低,因此你将这些乐器的父节点改为黑胶唱片。

下一个最便宜的是吉他,因此更新其邻居的开销。

最后,对最后一个节点——架子鼓,做同样的处理。

如果用架子鼓换钢琴,Rama需要额外支付的费用更少。因此,采用最便宜的交换路径时,Rama需要额外支付35美元。

负权边:

假设黑胶唱片不是Alex的,而是Sarah的,且Sarah愿意用黑胶唱片和7美元换海报。换句话说,换得Alex的海报后,Rama用它来换Sarah的黑胶唱片时,不但不用支付额外的费用,还可得7美元。对于这种情况,如何在图中表示出来呢?

从黑胶唱片到海报的边的权重为负!即这种交换让Rama能够得到7美元。现在,Rama有两种
获得海报的方式。

第二种方式更划算——Rama可赚2美元!你可能还记得,Rama可以用海报换架子鼓,但现在

有两种换得架子鼓的方式。

第二种方式的开销少2美元,他应采取这种方式。然而,如果你对这个图运行狄克斯特拉算法,Rama将选择错误的路径——更长的那条路径。如果有负权边,就不能使用狄克斯特拉算法。

实现:

以下面的图为例

要编写解决这个问题的代码,需要三个散列表。

随着算法的进行,你将不断更新散列表costs和parents。

首先,需要实现这个图,可以使用一个散列表。

在前一章中,你像下面这样将节点的所有邻居都存储在散列表中。

graph["you"] = ["alice", "bob", "claire"]

 1 #!coding:utf8
 2 
 3 # 权重图,当前节点到邻居节点的映射
 4 graph = {}
 5 graph["start"] = {}
 6 graph["start"]["a"] = 6
 7 graph["start"]["b"] = 2
 8 
 9 graph["a"] = {}
10 graph["a"]["fin"] = 1
11 
12 graph["b"] = {}
13 graph["b"]["a"] = 3
14 graph["b"]["fin"] = 5
15 
16 graph["fin"] = {}
17 
18 # print(graph)
19 
20 # 开销
21 infinity = float("inf")
22 costs = {}
23 costs["a"] = 6
24 costs["b"] = 2
25 costs["fin"] = infinity
26 # print(costs)
27 # 父节点
28 parents = {}
29 parents["a"] = "start"
30 parents["b"] = "start"
31 parents["fin"] = None
32 
33 # 一个数组,记录处理过的节点
34 processed = []
35 
36 def find_lowest_cost_node(costs):
37     lowest_cost = float('inf')
38     lowest_cost_node = None
39     for node in costs:
40         cost = costs[node]
41         if cost < lowest_cost and node not in processed:
42             lowest_cost = cost
43             lowest_cost_node = node
44     return lowest_cost_node
45 
46 # 在未处理的节点中找出开销最小的节点,最开始是节点B,然后是节点A,然后是终点,再然后就是空了
47 node = find_lowest_cost_node(costs)
48 while node is not None:
49     cost = costs[node]
50     neighboors = graph[node]
51     for n in neighboors.keys():  # 遍历当前节点的所有邻居
52         new_cost = cost + neighboors[n]
53         if costs[n] > new_cost:  # 如果经当前节点前往该邻居更近
54             costs[n] = new_cost  # 就更新该邻居的开销
55             parents[n] = node  # 同时将该邻居的父节点设置为当前节点
56     processed.append(node)  # 将当前节点标记为处理过
57     node = find_lowest_cost_node(costs)  # 找出来接下来要处理的节点并循环
58 
59 # 路线
60 paths = ['fin']
61 before = parents['fin']
62 for n in range(len(parents)-1):
63     paths.insert(0, before)
64     before = parents[before]
65 paths.insert(0, 'start')
66 print(paths)  # 路线
67 print(costs['fin'])  # 最短距离

7.1实例A图:不是所有节点都要走

  1 #!coding:utf8
  2 
  3 # 权重图,当前节点到邻居节点的映射
  4 graph = {}
  5 graph["start"] = {}
  6 graph["start"]["a"] = 5
  7 graph["start"]["b"] = 2
  8 
  9 graph["a"] = {}
 10 graph["a"]["c"] = 4
 11 graph["a"]["d"] = 2
 12 
 13 graph["b"] = {}
 14 graph["b"]["a"] = 8
 15 graph["b"]["d"] = 7
 16 
 17 graph["c"] = {}
 18 graph["c"]["d"] = 6
 19 graph["c"]["fin"] = 3
 20 
 21 graph["d"] = {}
 22 graph["d"]["fin"] = 1
 23 
 24 graph["fin"] = {}
 25 
 26 # print(graph)
 27 
 28 # 开销
 29 infinity = float("inf")
 30 costs = {}
 31 costs["a"] = 5
 32 costs["b"] = 2
 33 costs["c"] = infinity
 34 costs["d"] = infinity
 35 costs["fin"] = infinity
 36 # print(costs)
 37 # 父节点
 38 parents = {}
 39 parents["a"] = "start"
 40 parents["b"] = "start"
 41 parents["c"] = None
 42 parents["d"] = None
 43 parents["fin"] = None
 44 
 45 # 一个数组,记录处理过的节点
 46 processed = []
 47 
 48 def find_lowest_cost_node(costs):
 49     lowest_cost = float('inf')
 50     lowest_cost_node = None
 51     for node in costs:
 52         cost = costs[node]
 53         if cost < lowest_cost and node not in processed:
 54             lowest_cost = cost
 55             lowest_cost_node = node
 56     return lowest_cost_node
 57 
 58 # 在未处理的节点中找出开销最小的节点,最开始是节点B,然后是节点A,然后是终点,再然后就是空了
 59 node = find_lowest_cost_node(costs)
 60 # print(node)
 61 while node is not None:
 62     cost = costs[node]
 63     neighboors = graph[node]
 64     for n in neighboors.keys():  # 遍历当前节点的所有邻居
 65         new_cost = cost + neighboors[n]
 66         if costs[n] > new_cost:  # 如果经当前节点前往该邻居更近
 67             costs[n] = new_cost  # 就更新该邻居的开销
 68             parents[n] = node  # 同时将该邻居的父节点设置为当前节点
 69         # print('==', node, n, costs, cost, parents)
 70     processed.append(node)  # 将当前节点标记为处理过
 71     # print('===', node, processed)  # 通过打印这条信息可以发现,到节点`fin`时没有处理
 72     node = find_lowest_cost_node(costs)  # 找出来接下来要处理的节点并循环
 73 
 74 print(parents)
 75 # 路线
 76 # paths = ['fin']
 77 # before = parents['fin']
 78 # # for n in range(len(parents)-1):  # 对于有未经过的节点不适用
 79 # # while parents[before] != 'start':  # 这个把起点的子节点丢失了
 80 # for n in range(len(parents)-1):
 81 #     paths.insert(0, before)
 82 #     before = parents[before]
 83 #     if before == 'start':
 84 #         paths.insert(0, before)
 85 #         break
 86 
 87 k = 'fin'
 88 paths = [k]
 89 for n in range(len(parents)):
 90     before = parents[k]
 91     if before == 'start':
 92         paths.insert(0, before)
 93         break
 94     paths.insert(0, before)
 95     k = before
 96 
 97 print(paths)  # 路线
 98 print(costs['fin'])  # 最短距离
 99 
100 
101 # == b a {'a': 5, 'b': 2, 'c': inf, 'd': inf, 'fin': inf} 2 {'a': 'start', 'b': 'start', 'c': None, 'd': None, 'fin': None}
102 # == b d {'a': 5, 'b': 2, 'c': inf, 'd': 9, 'fin': inf} 2 {'a': 'start', 'b': 'start', 'c': None, 'd': 'b', 'fin': None}  # 到节点D还有更近的,但这一步只更新到这里
103 # == a c {'a': 5, 'b': 2, 'c': 9, 'd': 9, 'fin': inf} 5 {'a': 'start', 'b': 'start', 'c': 'a', 'd': 'b', 'fin': None}
104 # == a d {'a': 5, 'b': 2, 'c': 9, 'd': 7, 'fin': inf} 5 {'a': 'start', 'b': 'start', 'c': 'a', 'd': 'a', 'fin': None}  # 又更新了节点D
105 # == d fin {'a': 5, 'b': 2, 'c': 9, 'd': 7, 'fin': 8} 7 {'a': 'start', 'b': 'start', 'c': 'a', 'd': 'a', 'fin': 'd'}  # 接下来是`fin`,但是`fin`没有邻居
106 # == c d {'a': 5, 'b': 2, 'c': 9, 'd': 7, 'fin': 8} 9 {'a': 'start', 'b': 'start', 'c': 'a', 'd': 'a', 'fin': 'd'}
107 # == c fin {'a': 5, 'b': 2, 'c': 9, 'd': 7, 'fin': 8} 9 {'a': 'start', 'b': 'start', 'c': 'a', 'd': 'a', 'fin': 'd'}

7.1实例B:有环

1 # == a c {'a': 10, 'b': inf, 'c': 30, 'fin': inf} 10 {'a': 'start', 'b': None, 'c': 'a', 'fin': None}
2 # == c b {'a': 10, 'b': 31, 'c': 30, 'fin': inf} 30 {'a': 'start', 'b': 'c', 'c': 'a', 'fin': None}
3 # == c fin {'a': 10, 'b': 31, 'c': 30, 'fin': 60} 30 {'a': 'start', 'b': 'c', 'c': 'a', 'fin': 'c'}
4 # == b a {'a': 10, 'b': 31, 'c': 30, 'fin': 60} 31 {'a': 'start', 'b': 'c', 'c': 'a', 'fin': 'c'}  # 有换也能处理,环不更新花销

7.1实例C:有负数(也有环)

1 # == a b {'a': 2, 'b': 2, 'c': inf, 'fin': inf} 2 {'a': 'start', 'b': 'start', 'c': None, 'fin': None}  # 节点A和节点B花销相等
2 # == b c {'a': 2, 'b': 2, 'c': 4, 'fin': inf} 2 {'a': 'start', 'b': 'start', 'c': 'b', 'fin': None}
3 # == b fin {'a': 2, 'b': 2, 'c': 4, 'fin': 4} 2 {'a': 'start', 'b': 'start', 'c': 'b', 'fin': 'b'}
4 # == c a {'a': 2, 'b': 2, 'c': 4, 'fin': 4} 4 {'a': 'start', 'b': 'start', 'c': 'b', 'fin': 'b'}  # 带有`-1`的环负数没有起到作用,节点A的父节点仍然是起点
5 # == c fin {'a': 2, 'b': 2, 'c': 4, 'fin': 4} 4 {'a': 'start', 'b': 'start', 'c': 'b', 'fin': 'b'}

贪婪算法:

教室调度问题:

假设有如下课程表,你希望将尽可能多的课程安排在某间教室上。

具体做法如下。
(1) 选出结束最早的课,它就是要在这间教室上的第一堂课。
(2) 接下来,选择第一堂课结束后才开始,并且结束最早的课,这将是要在这间教室上的第二堂课。
重复这样做就能找出答案!

贪婪算法的思想:每步都选择局部最优解,最终得到的就是全局最优解。

背包问题:

假设你是个贪婪的小偷,背着可装35磅(1磅≈0.45千克)重东西的背包,在商场伺机盗窃各种可装入背包的商品。你力图往背包中装入价值最高的商品,例如,你可盗窃的商品有下面三种。

根据贪婪算法的原则,音响最贵,应该拿音响,但是之后就没有空间装其他东西了,如果拿电脑和吉他反而价值更高。

在这里,贪婪策略显然不能获得最优解,但非常接近。不过小偷去购物中心行窃时,不会强求所偷东西的总价最高,只要差不多就行了。

从这个示例你得到了如下启示:在有些情况下,完美是优秀的敌人。有时候,你只需找到一个能够大致解决问题的算法,此时贪婪算法正好可派上用场,因为它们实现起来很容易,得到的
结果又与正确结果相当接近。

练习中的问题:

(1). 你在一家家具公司工作,需要将家具发往全国各地,为此你需要将箱子装上卡车。每个箱子的尺寸各不相同,你需要尽可能利用每辆卡车的空间,为此你将如何选择要装上卡车的箱子呢?

(2). 你要去欧洲旅行,总行程为7天。对于每个旅游胜地,你都给它分配一个价值——表示你有多想去那里看看,并估算出需要多长时间。你如何将这次旅行的价值最大化?这显然不是狄克特拉斯算法。

集合覆盖问题:

假设你办了个广播节目,要让全美50个州的听众都收听得到。为此,你需要决定在哪些广播台播出。在每个广播台播出都需要支付费用,因此你力图在尽可能少的广播台播出。现有广播台名单如下。

每个广播台都覆盖特定的区域,不同广播台的覆盖区域可能重叠。

具体方法如下。
(1) 列出每个可能的广播台集合,这被称为幂集(power set)。可能的子集有2n个。

(2) 在这些集合中,选出覆盖全美50个州的最小集合。

问题是计算每个可能的广播台子集需要很长时间。由于可能的集合有2的n次方个,因此运行时间为O(2的n次方)。

使用下面的贪婪算法可得到非常接近的解。

(1) 选出这样一个广播台,即它覆盖了最多的未覆盖州。即便这个广播台覆盖了一些已覆盖的州,也没有关系。
(2) 重复第一步,直到覆盖了所有的州。

在这个例子中,贪婪算法的运行时间为O(n的2次方),其中n为广播台数量。

 1 #!coding:utf8
 2 
 3 # 首先,创建一个列表,其中包含要覆盖的州。
 4 states_needed = set(["mt", "wa", "or", "id", "nv", "ut", "ca", "az"])
 5 
 6 # 可供选择的广播台清单
 7 stations = {}
 8 stations["kone"] = set(["id", "nv", "ut"])
 9 stations["ktwo"] = set(["wa", "id", "mt"])
10 stations["kthree"] = set(["or", "nv", "ca"])
11 stations["kfour"] = set(["nv", "ut"])
12 stations["kfive"] = set(["ca", "az"])
13 
14 # 使用一个集合来存储最终选择的广播台。
15 final_stations = set()
16 
17 # 不断地循环,直到states_needed为空。
18 while states_needed:
19     # 遍历所有的广播台,从中选择覆盖了最多的未覆盖州的广播台。将这个广播台存储在best_station中。
20     best_station = None
21     states_covered = set()  # 包含该广播台覆盖的所有未覆盖的州
22     for station, states_for_station in stations.items():
23         covered = states_needed & states_for_station  # 表示当前广播台覆盖的还未覆盖的周
24         if len(covered) > len(states_covered):
25             best_station = station
26             states_covered = covered
27     print('==', best_station)
28     final_stations.add(best_station)  # 将best_station添加到最终的广播台列表中。
29     states_needed -= states_covered  # 由于该广播台覆盖了一些州,因此不用再覆盖这些州。
30 
31 print(final_stations)

NP完全问题:

回看旅行商问题:

阶乘

方法:随便选择出发城市,然后每次选择要去的下一个城市时,都选择还没去的最近的城市。假设旅行商从马林出发。

旅行商问题和集合覆盖问题有一些共同之处:你需要计算所有的解,并从中选出最小/最短的那个。这两个问题都属于NP完全问题。

NP问题的识别:

Jonah正为其虚构的橄榄球队挑选队员。他列了一个清单,指出了对球队的要求:优秀的四分卫,优秀的跑卫,擅长雨中作战,以及能承受压力等。他有一个候选球员名单,其中每个球员都满足某些要求。

Jonah需要组建一个满足所有这些要求的球队,可名额有限。

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

P类问题和NP类问题:

P类问题是指可以在多项式时间复杂度内解决的问题。NP类问题是指可以在多项式时间复杂度内验证的问题。因为可解决的问题一定可验证,因此所有P类问题都是NP类问题,但是,可验证的问题不一定可解决,如果用集合表示的话就是集合P属于集合NP,但是集合NP是否也属于集合P还未解决,更进一步说P是否等于NP还未解决,即`P=NP?`,作为千禧年七大数学难题之一的问题,值100万美元呢,解决了它就能娶你喜欢的人了。

动态规划

即使贪婪算法也无法解决背包问题。

动态规划,一种解决棘手问题的方法,将问题分解成小问题,并着手解决这些小问题。

解决背包问题:

简单算法:尝试各种可能的商品组合,并找出价值最高的组合。时间复杂度为O(2的n次方),所以行不通。

动态规划:先解决小问题,再逐步解决大问题。

每个动态规划算法都从一个网格开始,背包问题的网格如下。

向空单元格里填数,填满时问题就解决了。在每一行,可填的商品都为当前行的商品以及之前各行的商品。

填第3个歌时就需要用到公式了:

填完之后的样子:

最终答案就是笔记本和吉他。

背包问题FAQ

(1). 再增加一件商品将如何呢

此时需要重新执行前面所做的计算,只需要简单地在后面加上一行即可。

最终结果:

(2). 行的排列顺序发生变化时结果将如何

结果不变

(3). 可以逐列而不是逐行填充网格吗?

就这个问题而言,这没有任何影响,但对于其他问题,可能有影响。

(4). 增加一件更小的商品将如何呢? 假设你还可以偷一条项链,它重0.5磅,价值1000美元。

 由于项链的加入,你需要考虑的粒度更细,因此必须调整网格。

(5). 可以偷商品的一部分吗?

使用动态规划时不行。但是可以使用贪婪算法。首先,尽可能多地拿价值最高的商品;如果拿光了,再尽可能多地拿价值次高的商品,以此类推。

(6). 旅游行程最优化

假设你要去伦敦度假,假期两天,但你想去游览的地方很多。你没法前往每个地方游览,因此你列个单子。

这也是一个背包问题!但约束条件不是背包的容量,而是有限的时间;不是决定该装入哪些商品,而是决定该去游览哪些名胜。(在时间尽量少的情况下,评分最高)

剩余的部分找的是上一行,因为不可重复拿。

(7). 处理相互依赖的情况

假设你还想去巴黎,因此在前述清单中又添加了几项。

其中从伦敦到巴黎需要0.5天时间,已经包含在每项中。到达巴黎后,每个地方都只需1天时间。因此玩这3个地方需要的总时间为3.5天。

因为仅当每个子问题都是离散的,即不依赖于其他子问题时,动态规划才管用。这意味着使用动态规划算法解决不了去巴黎玩的问题。

(8). 计算最终的解时会涉及两个以上的子背包吗?

根据动态规划算法的设计,最多只需合并两个子背包,即根本不会涉及两个以上的子背包。不过这些子背包可能又包含子背包。

(9). 最优解可能导致背包没装满吗?
完全可能。假设你还可以偷一颗钻石。这颗钻石非常大,重达3.5磅,价值100万美元,比其他商品都值钱得多。你绝对应该把它给偷了!但当你这样做时,余下的容量只有0.5磅,别的什么都装不下。

最长公共子串

通过前面的动态规划问题,你得到了哪些启示呢?
 动态规划可帮助你在给定约束条件下找到最优解。在背包问题中,你必须在背包容量给定的情况下,偷到价值最高的商品。
 在问题可分解为彼此独立且离散的子问题时,就可使用动态规划来解决。
要设计出动态规划解决方案可能很难。下面是一些通用的小贴士。
 每种动态规划解决方案都涉及网格。
 单元格中的值通常就是你要优化的值。在前面的背包问题中,单元格的值为商品的价值。
 每个单元格都是一个子问题,因此你应考虑如何将问题分成子问题,这有助于你找出网格的坐标轴。

假设你管理着网站dictionary.com。用户在该网站输入单词时,你需要给出其定义。但如果用户拼错了,你必须猜测他原本要输入的是什么单词。例如,Alex想查单词fish,但不小心输入了hish。在你的字典中,根本就没有这样的单词,但有几个类似的单词。在这个例子中,只有两个类似的单词,真是太小儿科了。实际上,类似的单词很可能有数千个。Alex输入了hish,那他原本要输入的是fish还是vista呢?

第一步,绘制表格

用于解决这个问题的网格是什么样的呢?要确定这一点,你得回答如下问题。
 单元格中的值是什么?
 如何将这个问题划分为子问题?
 网格的坐标轴是什么?

在动态规划中,你要将某个指标最大化(比如背包问题中就是价值要求最大;旅程最优化问题中就是评分要求最大)。

在这个例子中,你要找出两个单词的最长公共子串。hish和fish都包含的最长子串是什么呢?hish和vista呢?这就是你要计算的值。

单元格中的值通常就是你要优化的值。在这个例子中,这很可能是一个数字:两个字符串都包含的最长子串的长度。

如何将这个问题划分为子问题呢?你可能需要比较子串:不是比较hish和fish,而是先比较his和fis。每个单元格都将包含这两个子串的最长公共子串的长度。这也给你提供了线索,让你觉得坐标轴很可能是这两个单词。因此,网格可能类似于下面这样。

第二步,填充表格

现在,你很清楚网格应是什么样的。填充该网格的每个单元格时,该使用什么样的公式呢?由于你已经知道答案——hish和fish的最长公共子串为ish,因此可以作点弊。

实际上,根本没有找出计算公式的简单办法,你必须通过尝试才能找出管用的公式。有些算法并非精确的解决步骤,而只是帮助你理清思路的框架。

请尝试为这个问题找到计算单元格的公式。给你一点提示吧:下面是这个单元格的一部分。

其他单元格的值呢?别忘了,每个单元格都是一个子问题的值。为何单元格(3, 3)的值为2呢?又为何单元格(3, 4)的值为0呢?

想法是(1, 1)对应的是两个单词的子句`F`和`H`,(1, 2)对应的子句为`F`和`HI`,得出来的答案一致,但是不好落实到代码上。。。这是个问题啊!!

答案揭晓,mmp...

 

查找单词hish和vista的最长公共子串时,网格如下。

对于前面的背包问题,最终答案总是在最后的单元格中。但对于最长公共子串问题,答案为网格中最大的数字——它可能并不位于最后的单元格中。

我们回到最初的问题:哪个单词与hish更像?hish和fish的最长公共子串包含三个字母,而hish和vista的最长公共子串包含两个字母。因此Alex很可能原本要输入的是fish。

最长公共子序列:

假设Alex不小心输入了fosh,他原本想输入的是fish还是fort呢?

伪代码如下:

最长公共子序列的应用:

 生物学家根据最长公共序列来确定DNA链的相似性,进而判断度两种动物或疾病有多相似。最长公共序列还被用来寻找多发性硬化症治疗方案。
 你使用过诸如git diff等命令吗?它们指出两个文件的差异,也是使用动态规划实现的。
 前面讨论了字符串的相似程度。编辑距离(levenshtein distance)指出了两个字符串的相似程度,也是使用动态规划计算得到的。编辑距离算法的用途很多,从拼写检查到判断用户上传的资料是否是盗版,都在其中。
 你使用过诸如Microsoft Word等具有断字功能的应用程序吗?它们如何确定在什么地方断字以确保行长一致呢?使用动态规划!

K最近邻算法:

如果判断这个水果是橙子还是柚子呢?一种办法是看它的邻居。来看看离它最近的三个邻居。

在这三个邻居中,橙子比柚子多,因此这个水果很可能是橙子。祝贺你,你刚才就是使用K最近邻(k-nearest neighbours,KNN)算法进行了分类

创建推荐系统:

假设你是Netflix,要为用户创建一个电影推荐系统。从本质上说,这类似于前面的水果问题!你可以将所有用户都放入一个图表中。这些用户在图表中的位置取决于其喜好,因此喜好相似的用户距离较近。假设你要向Priyanka推荐电影,可以找出五位与他最接近的用户。假设在对电影的喜好方面,Justin、JC、Joey、Lance和Chris都与Priyanka差不多,因此他们喜欢的电影很可能Priyanka也喜欢!

有了这样的图表以后,创建推荐系统就将易如反掌:只要是Justin喜欢的电影,就将其推荐给Priyanka。但还有一个重要的问题没有解决。在前面的图表中,相似的用户相距较近,但如何确定两位用户的相似程度呢?

特征提取

在前面的水果示例中,你根据个头和颜色来比较水果,换言之,你比较的特征是个头和颜色。现在假设有三个水果,你可抽取它们的特征。

再根据这些特征绘图。

从上图可知,水果A和B比较像。

下面来度量它们有多像。要计算两点的距离,可使用毕达哥拉斯公式(只有两个参数时就是勾股定理)。

距离,

这个距离公式印证了你的直觉:A和B很像。

在推荐系统中,就需要以某种方式将所有用户放到图中。因此,你需要将每位用户都转换为一组坐标,就像前面对水果所做的那样。这样就可以计算用户之间的距离了。

下面是一种将用户转换为一组数字的方式。用户注册时,要求他们指出对各种电影的喜欢程度。这样,对于每位用户,都将获得一组数字!

Priyanka和Justin都喜欢爱情片且都讨厌恐怖片。Morpheus喜欢动作片,但讨厌爱情片(他讨厌好好的动作电影毁于浪漫的桥段)。前面判断水果是橙子还是柚子时,每种水果都用2个数字表
示,在这里,每位用户都用5个数字表示。经计算,Priyanka和Justin的距离的距离为2,Priyanka和Morpheus的距离为24,上述距离表明,Priyanka的喜好更接近于Justin而不是Morpheus。现在要向Priyanka推荐电影将易如反掌:只要是Justin喜欢的电影,就将其推荐给Priyanka,反之亦然。你这就创建了一个电影推荐系统!

如果你是Netflix用户,Netflix将不断提醒你:多给电影评分吧,你评论的电影越多,给你的推荐就越准确。现在你明白了其中的原因:你评论的电影越多,Netflix就越能准确地判断出你与哪些用户类似。

练习
10.1 在Netflix示例中,你使用距离公式计算两位用户的距离,但给电影打分时,每位用户标准并不都相同。假设你有两位用户——Yogi和Pinky,他们欣赏电影的品味相同,但Yogi给喜欢的电影都打5分,而Pinky更挑剔,只给特别好的电影打5分。他们的品味一致,但根据距离算法,他们并非邻居。如何将这种评分方式的差异考虑进来呢?

10.2 假设Netflix指定了一组意见领袖。例如,Quentin Tarantino和Wes Anderson就是Netflix的意见领袖,因此他们的评分比普通用户更重要。请问你该如何修改推荐系统,使其偏重于意见领袖的评分呢?

回归

假设你不仅要向Priyanka推荐电影,还要预测她将给这部电影打多少分。

假设你在伯克利开个小小的面包店,每天都做新鲜面包,需要根据如下一组特征预测当天该烤多少条面包:
 天气指数1~5(1表示天气很糟,5表示天气非常好);
 是不是周末或节假日(周末或节假日为1,否则为0);
 有没有活动(1表示有,0表示没有)。
你还有一些历史数据,记录了在各种不同的日子里售出的面包数量。

今天是周末,天气不错(坐标为(4, 1, 0))。根据这些数据,预测你今天能售出多少条面包呢?

设置K为4,距离如下,因此最近的邻居为A、B、D和E。

将这些天售出的面包数平均,结果为218.75。这就是你今天要烤的面包数!

余弦相似度:

前面计算两位用户的距离时,使用的都是距离公式。但在实际工作中,经常使用余弦相似度(cosine similarity)。假设有两位品味类似的用户,但其中一位打分时更保守。他们都很喜欢Manmohan Desai的电影Amar Akbar Anthony,但Paul给了5星,而Rowan只给4星。如果你使用距离公式,这两位用户可能不是邻居,虽然他们的品味非常接近。余弦相似度不计算两个矢量的距离,而比较它们的角度,因此更适合处理前面所说的情况。本书不讨论余弦相似度,但如果你要使用KNN,就一定要研究研究它!

通过计算两个向量的夹角余弦值来评估它们的相似度。余弦相似度将向量根据坐标值绘制到向量空间中,如常见的二维空间。

计算方法,

挑选合适的特征

所谓合适的特征,就是:
 与要推荐的电影紧密相关的特征;
 不偏不倚的特征(例如,如果只让用户给喜剧片打分,就无法判断他们是否喜欢动作片)。

在挑选合适的特征方面,没有放之四海皆准的法则,你必须考虑到各种需要考虑的因素。。。

机器学习简介

OCR
OCR指的是光学字符识别(optical character recognition),这意味着你可拍摄印刷页面的照片,计算机将自动识别出其中的文字。Google使用OCR来实现图书数字化。

OCR是如何工作的呢?我们来看一个例子。请看下面的数字。

如何自动识别出这个数字是什么呢?可使用KNN。
(1) 浏览大量的数字图像,将这些数字的特征提取出来。
(2) 遇到新图像时,你提取该图像的特征,再找出它最近的邻居都是谁!

这与前面判断水果是橙子还是柚子时一样。一般而言,OCR算法提取线段、点和曲线等特征。

遇到新字符时,可从中提取同样的特征。

与前面的水果示例相比,OCR中的特征提取要复杂得多,但再复杂的技术也是基于KNN等简单理念的。这些理念也可用于语音识别和人脸识别。你将照片上传到Facebook时,它有时候能够自动标出照片中的人物,这是机器学习在发挥作用!
OCR的第一步是查看大量的数字图像并提取特征,这被称为训练(training)。大多数机器学习算法都包含训练的步骤:要让计算机完成任务,必须先训练它。下一个示例是垃圾邮件过滤器,其中也包含训练的步骤。

垃圾邮件过滤器:

垃圾邮件过滤器使用一种简单算法——朴素贝叶斯分类器(Naive Bayes classifier)... 太过简单,跟没说一样mmp...

预测股票市场:

在根据以往的数据来预测未来方面,没有万无一失的方法。未来很难预测,由于涉及的变数太多,这几乎是不可能完成的任务。我尼玛。。。

原文地址:https://www.cnblogs.com/yangxiaoling/p/9692272.html