机器学习----算法图解

第一章、算法简介

1.1、二分查找

简单的说想要找到目标数据,就是在有序排列的数据中先从有相关的数据中查找。
打个比方:

假设要在电话簿中找一个名字以K打头的人,(现在谁还用电话簿!)
可以从头开始翻页,直到进入以K打头的部分。但你很可能不这样做,而
是从中间开始,因为你知道以K打头的名字在电话簿中间。

优化的二分查找,先看例子

假设你要在字典中查找一个单词,而该字典包含240 000个单词,
你认为每种查找最多需要多少步?
如果要查找的单词位于字典末尾,使用简单查找将需要240 000步。使用二分查找时,每次
排除一半单词,直到最后只剩下一个单词。

这里写图片描述

像这样,每次从数据的中间部分,一次判断所需要的数据的范围,用此来缩减区间,大大的节省了的时间

因此,使用二分查找只需18步——少多了!一般而言,对于包含n个元素的列表,用二分查
找最多需要log2n步,而简单查找最多需要n步。
所用的次数如下计算
这里写图片描述
代码用法如下:

low = 0
high = len(list) - 1 
mid = (low + high) / 2
guess = list[mid] 
#low和high不是偶数,Python自动将mid向下圆整。如果猜的数字小了,就相应地修改low。
if guess < item:
   low = mid + 1
#如果猜的数字大了,就修改high。
#low和high用于跟踪要在其中查找的列表部分
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、二分查找则有优点

        如果列表包含100个元素,最多要猜7次;如果列表包含40亿个数字,最多需猜32次。厉害吧?二分查找的运行时间为对数时间(或log时间)。下表总结了我们发现的情况。

1.3、算法的运行时间以不同的速度增加

        假设检查一个元素需要1毫秒。使用简单查找时,Bob必须检查100个元素,因此需要100毫秒才能查找完毕。而使用二分查找时,只需检查7个元素(log2100大约为7),因此需要7毫秒就能查找完毕。然而,实际要查找的列表可能包含10亿个元素,在这种情况下,简单查找需要多长时间呢?二分查找又需要多长时间呢?

        使用包含10亿个元素的列表运行二分查找,运行时间为30毫秒(log21 000 000 000大约为30)。他心里想,二分查找的速度大约为简单查找的15倍,因为列表包含100个元素时,简单查找需要100毫秒,而二分查找需要7毫秒。因此,列表包含10亿个元素时,简单查找需要30 × 15 = 450毫秒,完全符合在10秒内查找完毕的要求。Bob决定使用简单查找。这是正确的选择吗?
不是
不是。实际上,Bob错了,而且错得离谱。列表包含10亿个元素时,简单查找需要10亿毫秒,
相当于11天!为什么会这样呢?因为二分查找和简单查找的运行时间的增速不同。

这里写图片描述

1.4、大O表达式

大O表示法是一种特殊的表示法,指出了算法的速度有多快

        大O表示法指出了算法有多快。例如,假设列表包含n个元素。
简单查找需要检查每个元素,因此需要执行n次操作。使用大O表示法,
这个运行时间为O(n)。单位秒呢?没有——大O表示法指的并非以秒为单位的速度。大O表示法
让你能够比较操作数,它指出了算法运行时间的增速。

                                                                                        这里写图片描述

大 O 表示法指出了最糟情况下的运行时间

        假设你使用简单查找在电话簿中找人。简单查找的运行时间为O(n),这意味着在最糟情况下,必须查看电话簿中的每个条目。如果要查找的是Adit——电话簿中的第一个人,一次就能找到,无需查看每个条目。
简单查找的运行时间总是为O(n)。查找Adit时,一次就找到了,这是最佳的情形,但大O表示法说的是最糟的情形。因此,你可以说,在最糟情况下,必须查看电话簿中的每个条目,对应的运行时间为O(n)。这是一个保证——你知道简单查找的运行时间不可能超过O(n)。

一些常见的大 O 运行时间

下面按从快到慢的顺序列出了你经常会遇到的5种大O运行时间。
● O(log n),也叫对数时间,这样的算法包括二分查找。
● O(n),也叫线性时间,这样的算法包括简单查找。
● O(n * log n),这样的算法包括第4章将介绍的快速排序——一种速度较快的排序算法。
● O(n2),这样的算法包括第2章将介绍的选择排序——一种速度较慢的排序算法。
● O(n!),这样的算法包括接下来将介绍的旅行商问题的解决方案——一种非常慢的算法。

这里写图片描述
● 算法的速度指的并非时间,而是操作数的增速。
● 谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加。
● 算法的运行时间用大O表示法表示。
● O(log n)比O(n)快,当需要搜索的元素越多时,前者比后者快得越多。

附表:各排序和大O表达式的关系

这里写图片描述

1.5、旅行熵

上来就是案例图解
这里写图片描述

        例如一个人去五个城市,大O算法对于每种顺序,他都计算总旅程,再挑选出旅程最短的路线。5个城市有120种不同的排列方式。因此,在涉及5个城市时,解决这个问题需要执行120次操作。涉及6个城市时,需要执行720次操作(有720种不同的排列方式)。涉及7个城市时,需要执行5040次操作!

小结

● 二分查找的速度比简单查找快得多。
● O(log n)比O(n)快。需要搜索的元素多,前者比后者就快得越多。
● 算法运行时间并不以秒为单位。
● 算法运行时间是从其增速的角度度的。
● 算法运行时间用大O表示法表示。


第二章、选择排序

        需要将数据存储到内存时,你请求计算机提供存储空间,计算机给你一个存储地址。需要存储多项数据时,有两种基本方式——数组和链表

如是你本来就有4个代办项目,但是紧挨着能连在一起的空位只有三个,那么就要将原来的三个位置的格子移到另外的位置。这个就非常的麻烦
这里写图片描述

2.1、数组和链表

        当前只有3个待办事项,也请计算机提供10个位置,以防需要添加待办事项。这样,只要待办事项不超过10个,就无需转移。这是一个不错的权变措施,但你应该明白,它存在如下两个缺点。
● 你额外请求的位置可能根本用不上,将浪费内存。你没有使用,别人也用了。
● 待办事项超过10个后,你还得转移。
因此,这种权宜措施虽然不错,但绝非完美的解决方案。对于这种问题,可使用链表来解决。

2.1.1、链表

链表中的元素可存储在内存的任何地方。
这里写图片描述
链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。使用链表时,根本就不需要移动元素。
链表缺点

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

2.1.2、数组

数据则解除了链表中的不足,只需要查看一次数组,就能得到数据中的所有的元素。
        这里写图片描述

需要随机地读取元素时,数组的效率很高,因为可迅速找到数组的任何元素。在链表中,元素并非靠在一起的,你无法迅速计算出第五个元素的内存地址,而必须先访问第一个元素以获取第二个元素的地址,再访问第二个元素以获取第三个元素的地址,以此类推,直到访问第五个元素。

2.2在中间插入

需要在中间插入元素时,数组和链表哪个更好呢?

使用链表时,插入元素很简单,只需修它前面的那个元素指向的地址。而使用数组时,则必须将后面的元素都向后移。
这里写图片描述

如果没有足够的空间,可能还得将整个数组复制到其他地方!因此,当需要在中间插入元素
时,链表是更好的选择。
这里写图片描述

2.3、删除

          如果你要删除元素呢?链表也是更好的选择,因为只需修改前一个元素指向的地址即可。而使用数组时,删除元素后,必须将后面的元素都向前移。
          不同于插入,删除元素总能成功。如果内存中没有足够的空间,插入操作可能失败,但在任何情况下都能够将元素删除。

          数组和链表哪个用得更多呢?显然要看情况。
          数组用得很多,因为它支持随机访问。有两种访问方式:随机访问和顺序访问。顺序访问意味着从第一个元素开始逐个地读取元素。
          链表只能顺序访问:要读取链表的第十个元素,得先读取前九个元素,并沿链接找到第十个元素。随机访问意味着可直接跳到第十个元素。

2.4、选择排序

就是遍历每个次数,放新的列表
这里写图片描述
选择排序是一种灵巧的算法,但其速度不是很快。快速排序是一种更快的排序算法,其运行时间为O(n log 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 

2.5、小节

● 计算机内存犹如一大堆抽屉。
● 需要存储多个元素时,可使用数组或链表。
● 数组的元素都在一起。
● 链表的元素是分开的,其中每个元素都存储了下一个元素的地址。
● 数组的读取速度很快。
● 链表的插入和删除速度很快。
● 在同一个数组中,所有元素的类型都必须相同(都为int、double等)。


第三章、递归

3.1、递归

来了例题,找钥匙开箱子

这里写图片描述
下面是另一种方法。
(1) 检查盒子中的每样东西。
(2) 如果是盒子,就回到第一步。
(3) 如果是钥匙,就大功告成!
代码部分:

#在你看来,哪种方法更容易呢?
#第一种方法用的是while循环:只要盒子堆不空,就从中取一个盒子,并在其中仔细查找。
def look_for_key(main_box):
 pile = main_box.make_a_pile_to_look_through()
 while pile is not empty:
     box = pile.grab_a_box()
 for item in box:
if item.is_a_box():
    pile.append(item)
elif item.is_a_key():
    print "found the key!"
#第二种方法使用递归——函数调用自己
def look_for_key(box):
 for item in box:
 if item.is_a_box():
    look_for_key(item)
 elif item.is_a_key():
print "found the key!" 

3.2 基线条件和递归条件

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

def countdown(i):
 print i
 if i <= 0:
     return
else:
     countdown(i-1) 

这里写图片描述

3.3、栈

通俗的讲就是把代办项目放在最前面,然后读取,完成后便删除,类似于便利贴
这里写图片描述

3.3.1、调用栈

◆ 调用栈(调用函数)
def greet(name):
    print ("hello, " + name + "!")
    greet2(name)
    print ("getting ready to say bye...")
    bye()

#这个函数问候用户,再调用另外两个函数。这两个函数的代码如下:
def greet2(name):
    print ("how are you, " + name + "?")
def bye():
    print ("ok bye!")

greet('zhangsan')

输出:hello, zhangsan!
     how are you, zhangsan?
     getting ready to say bye...
     ok bye!

计算机使用一个栈来表示这些内存块,其中第二个内存块位于第一个内存块上面。你打印how are you, maggie?,然后从函数调用返回。此时,栈顶的内存块被弹出。

这里写图片描述

3.3.2、递归调用栈

#递归函数也使用调用栈!来看看递归函数factorial的调用栈。factorial(5)写作5!,其
#定义如下:5! = 5 * 4 * 3 * 2 * 1
#同理,factorial(3)为3 * 2 * 1。下面是计算阶乘的递归函数。
def fact(x):
 if x == 1:
 return 1
 else:
 return x * fact(x-1) 

具体运行程序如下:
这里写图片描述

注意,每个fact调用都有自己的x变量。在一个函数调用中不能访问另一个的x变量。

● 递归指的是调用自己的函数。
● 每个递归函数都有两个条件:基线条件和递归条件。
● 栈有两种操作:压入和弹出。
● 所有函数调用都进入调用栈。
● 调用栈可能很长,这将占用大量的内存。

3.4递归和迭代的区别

1、递归的基本概念:程序调用自身的编程技巧称为递归,是函数自己调用自己.一个函数在其定义中直接或间接调用自身的一种方法。
2、迭代:利用变量的原值推算出变量的一个新值.如果递归是自己调用自己的话,迭代就是A不停的调用B。

3、递归中一定有迭代,但是迭代中不一定有递归,大部分可以相互转换.能用迭代的不用递归,递归调用函数,浪费空间,并且递归太深容易造成堆栈的溢出。


第四章、快速排序

4.1、分而治之(D&C)

上来先来个图例讲原理
这里写图片描述

D&C的工作原理:
(1) 找出简单的基线条件;
(2) 确定如何缩小问题的规模,使其符合基线条件。
D&C并非可用于解决问题的算法,而是一种解决问题的思路。
代码实现如下:

def sum(arr):
 total = 0
 for x in arr:
     total += x
 return total 

4.2、快速排序

现对于D&C排序,快速排序要简单的多
1、首先,从数组中选择一个元素,这个元素被称为基准值(pivot)
2、接下来,找出比基准值小的元素以及比基准值大的元素
● 一个由所有小于基准值的数字组成的子数组;
● 基准值;
● 一个由所有大于基准值的数组组成的子数组。
这里只是进行了分区,得到的两个子数组是无序的。但如果这两个数组是有序的,对整个数组进行排序将非常容易。
3.重复上述两次的步骤,便可排序。
这里写图片描述
将子数组排序后,将它们合并,得到一个有序数组

4.3、再论大O表达法

这里写图片描述
                  上述图表中的时间是基于每秒执行10次操作计算得到的。这些数据并不准确,这里提供它们只是想让你对这些运行时间的差别有大致认识。
                  实际上,计算机每秒执行的操作远不止10次。对于每种运行时间,本书还列出了相关的算法。来看看第2章介绍的选择排序,其运行时间为O(n2),速度非常慢。
                  还有一种名为合并排序(merge sort)的排序算法,其运行时间为O(n log n),比选择排序快得多!快速排序的情况比较棘手,在最糟情况下,其运行时间为O(n2)。

4.3.1 、比较合并排序和快速排序

c是算法所需的固定时间量,被称为常量

from time import sleep
def print_items2(list):
 for item in list:
 sleep(1)
 print item 

这里写图片描述
意思就是说,数值小,当大O时间相同的时,考虑c常量的变化,
当数值非常大的时候,合并排序O(n log n)的速度更快,C值影响可以忽略
                  快速查找的常量比合并查找小,因此如果它们的运行时间都为O(n log n),快速查找的速度将更快。
                  实际上,快速查找的速度确实更快,因为相对于遇上最糟情况,它遇上平均情况的可能性要大得多。

4.3.2、平均情况和最糟情况

快速排序的性能高度依赖于你选择的基准值。
假设你总是将第一个元素用作基准值,且要处理的数组是有序的。由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序。
这里写图片描述

                  在这个示例中,层数为O(log n)(用技术术语说,调用栈的高度为O(log n)),而每层需要的时间为O(n)。因此整个算法需要的时间为O(n) * O(log n) = O(n log n)。
                  这就是最佳情况。在最糟情况下,有O(n)层,因此该算法的运行时间为O(n) * O(n) = O(n2)。
                  知道吗?这里要告诉你的是,最佳情况也是平均情况。只要你每次都随机地选择一个数组元素作为基准值,快速排序的平均运行时间就将为O(n log n)。快速排序是最快的排序算法之一,也是D&C典范。

4.4、小结

● D&C将问题逐步分解。使用D&C处理列表时,基线条件很可能是空数组或只包含一个元
素的数组。
● 实现快速排序时,请随机地选择用作基准值的元素。快速排序的平均运行时间为O(n log n)。
● 大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在。
● 比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时,O(log n)的速度比O(n)
快得多。

第五章、散列表

就是一组无序的数据

5.1、散列函数

这里写图片描述
                如果用专业术语来表达的话,我们会说,散列函数“将输入映射到数字”。你可能认为散列
函数输出的数字没什么规律,但其实散列函数必须满足一些要求。
● 它必须是一致的。例如,假设你输入apple时得到的是4,那么每次输入apple时,得到的都
必须为4。如果不是这样,散列表将毫无用处。
● 它应将不同的输入映射到不同的数字。例如,如果一个散列函数不管输入是什么都返回1,
它就不是好的散列函数。最理想的情况是,将不同的输入映射到不同的数字。
事例如下:
                                                                                                  这里写图片描述

● 散列函数总是将同样的输入映射到相同的索引。每次你输入avocado,得到的都是同一个
数字。因此,你可首先使用它来确定将鳄梨的价格存储在什么地方,并在以后使用它来
确定鳄梨的价格存储在什么地方。
● 散列函数将不同的输入映射到不同的索引。avocado映射到索引4,milk映射到索引0。每
种商品都映射到数组的不同位置,让你能够将其价格存储到这里。
● 散列函数知道数组有多大,只返回有效的索引。如果数组包含5个元素,散列函数就不会
返回无效索引100。

数组和链表都被直接映到内存,但散列表更复杂,它使用散列函数来确定元素的存储位置。
可使用函数dict来创建散列表

5.2、应用案例

5.2.1、将散列表用于查找

手机都内置了方便的电话簿,其中每个姓名都有对应的电话号码。
这里写图片描述
假设你要创建一个类似这样的电话簿,将姓名映射到电话号码。该电话簿需要提供如下功能。
还有类似电脑的IP地址:
这里写图片描述

5.2.2、防止重复

行政建议的投票,不能重复
这里写图片描述
简化代码如下:

voted = {}
def check_voter(name):
 if voted.get(name):
 print "kick them out!"
 else:
 voted[name] = True
 print "let them vote!" 

5.2.3 将散列表用作缓存

这里写图片描述

cache = {}
def get_page(url):
 if cache.get(url):
 return cache[url]
 else:
 data = get_data_from_server(url)
 cache[url] = data
 return data 

5.2.4、小结

● 模拟映射关系;
● 防止重复;
● 缓存/记住数据,以免服务器再通过处理来生成它们。

5.3、冲突

这里写图片描述

                除第一个位置外,整个散列表都是空的,而第一个位置包含一个很长的列表!换言之,这个散列表中的所有元素都在这个链表中,这与一开始就将所有元素存储到一个链表中一样糟
糕:散列表的速度会很慢。

这里的经验教训有两个。
● 散列函数很重要。前面的散列函数将所有的键都映射到一个位置,而最理想的情况是,
散列函数将键均匀地映射到散列表的不同位置。
● 如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很
好,这些链表就不会很长!

5.4、性能

在平均情况下,散列表执行各种操作的时间都为O(1)。O(1)被称为常量时间。
用图让你知道散列表的优点
                                          这里写图片描述
                  在最糟情况下,散列表所有操作的运行时间都为O(n)——线性时间,这真的很慢。我们来将散列表同数组和链表比较一下。
                                                                                这里写图片描述

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

5.4.1 填装因子

计算
                                                            这里写图片描述

5.4.2 良好的散列函数

良好的散列函数让数组中的值呈均匀分布
这里写图片描述

5.5 小结

● 你可以结合散列函数和数组来创建散列表。
● 冲突很糟糕,你应使用可以最大限度减少冲突的散列函数。
● 散列表的查找、插入和删除速度都非常快。
● 散列表适合用于模拟映射关系。
● 一旦填装因子超过0.7,就该调整散列表的长度。
● 散列表可用于缓存数据(例如,在Web服务器上)。
● 散列表非常适合用于防止重复。


第六章、广度优先搜索

定义:广度优先搜索让你能够找出两样东西之间的最短距离,广度优先搜索是一种用于图的查找算法
                假设你居住在旧金山,要从双子峰前往金门大桥。你想乘公交车前往,并希望换乘最少。可乘坐的公交车如下。
                        这里写图片描述
最短的路劲是:
                            这里写图片描述

6.1、广度优先搜索步骤

这里写图片描述
比如查找一个人,得先有查找的人员选择吧,然后就进行下一步:

6.1.1、查找最短路径

谁是关系最近的芒果销售商。例如,朋友是一度关系,朋友的朋友是二度关系。
这里写图片描述
                广度优先搜索在这里的意义就是说按顺序依次检查名单中的每个人,看看他是否是芒果销售商。这将先在一度关系中查找,再在二度关系中查找,因此找到的是关系最近的芒果销售商。广度优先搜索能找到的是最短的路径。

6.1.2、 队列

定义:队列的工作原理与现实生活中的队列完全相同.队列类似于栈,你不能随机地访问队列中的元素。
队列只支持两种操作:入队和出队
这里写图片描述
队列是一种先进先出(First In First Out,FIFO)的数据结构,而栈是一种后进先出(Last In First Out,LIFO)的数据结构。

6.2 实现图

代码表示方式:

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"] = []

6.3、实现算法

原理图:
                      这里写图片描述

###普通思维的代码部分
from collections import deque   
search_queue = deque()           #创建一个队列
search_queue += graph["you"]     #将你的邻居都加入到这个搜索队列中
while search_queue:              #只要队列不为空
 person = search_queue.popleft() #就取出其中的第一个人
 if person_is_seller(person):
    print person + " is a mango seller!" #不是芒果销售商。将这个人的朋友都加入搜索队列
    return True
 else:
    search_queue += graph[person]#如果到达了这里,就说明队列中没人是芒果销售商
    return False 
def person_is_seller(name):
    return name[-1] == 'm'

####广度优先搜索思维
search_queue = deque()
def search(name):
    search_queue = deque()
    search_queue += graph[name]
    searched = []    #这个数组用于记录检查过的人
    while search_queue:
        person = search_queue.popleft()
        if not person in searched:
            if person_is_seller(person):  #这一步骤是去处已经检查的人,
                                          #不去除的话会一直会死循环下去
                print person + " is a mango seller!"
                return True
            else:
                search_queue += graph[person]
                searched.append(person)
    return False

6.5、小结

● 广度优先搜索指出是否有从A到B的路径。
● 如果有,广度优先搜索将找出最短路径。
● 面临类似于寻找最短路径的问题时,可尝试使用图来建立模型,再使用广度优先搜索来
      解决问题。
● 有向图中的边为箭头,箭头的方向指定了关系的方向,例如,rama→adit表示rama欠adit钱。
● 无向图中的边不带箭头,其中的关系是双向的,例如,ross - rachel表示“ross与rachel约
      会,而rachel也与ross约会”。
● 队列是先进先出(FIFO)的。
● 栈是后进先出(LIFO)的。
● 你需要按加入顺序检查搜索列表中的人,否则找到的就不是最短路径,因此搜索列表必
      须是队列。
● 对于检查过的人,务必不要再去检查,否则可能导致无限循环。

第七章、狄克斯特拉算法

定义:找到耗时最少的路径或者权重
广度优先搜索,它找出的是段数最少的路径。如果你要找出最快的路径,该如何办呢?为此,可使用另一种算法——狄克斯特拉算法(Dijkstra’s algorithm)。
这里写图片描述
                广度优先搜索来查找两点之间的最短路径,那时“最短路径”的意思是段数最少。在狄克斯特拉算法中,你给每段都分配了一个数字或权重,因此狄克斯特拉算法找出的是总权重最小的路径。

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

7.1、负权边

这里写图片描述
如果有负权边,就不能使用狄克斯特拉算法。因为负权边会导致这种算法不管用。

7.2、实现

这里写图片描述

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  #同时将该邻居的父节点设置为当前节点
        processed.append(node)    #将当前节点标记为处理过
    node = find_lowest_cost_node(costs)  #找出接下来要处理的节点,并循环
#函数find_lowest_cost_node找出开销最低的节点,其代码非常简单,如下所示。
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 

7.3、小结

● 广度优先搜索用于在非加权图中查找最短路径。
● 狄克斯特拉算法用于在加权图中查找最短路径。
● 仅当权重为正时狄克斯特拉算法才管用。
● 如果图中包含负权边,请使用贝尔曼福德算法。

八、贪婪算法

首先了解一下

8.1、什么是P问题、NP问题和NPC问题

            Hamilton回路。问题是这样的:给你一个图,问你能否找到一条经过每个顶点一次且恰好一次(不遗漏也不重复)最后又走回来的路(满足这个条件的路径叫做Hamilton回路),这个问题现在还没有找到多项式级的算法。事实上,这个问题就是我们后面要说的NPC问题。
            P类问题的概念:如果一个问题可以找到一个能在多项式的时间里解决它的算法,那么这个问题就属于P问题。
            NP问题不是非P类问题。NP问题是指可以在多项式的时间里验证一个解的问题。NP问题的另一个定义是,可以在多项式的时间里猜出一个解的问题。通常只有NP问题才可能找到多项式的算法。

很显然,所有的P类问题都是NP问题。也就是说,能多项式地解决一个问题,必然能多项式地验证一个问题的解——既然正解都出来了,验证任意给定的解也只需要比较一下就可以了。

为了说明NPC问题,我们先引入一个概念——约化(Reducibility,有的资料上叫“归约”)。
            简单地说,一个问题A可以约化为问题B的含义即是,可以用问题B的解法解决问题A,或者说,问题A可以“变成”问题B。
下面来看npc问题的解释:

            通过对某些问题的不断约化,我们能够不断寻找复杂度更高,但应用范围更广的算法来代替复杂度虽然低,但只能用于很小的一类问题的算法。再回想前面讲的P和NP问题,联想起约化的传递性,自然地,我们会想问,如果不断地约化上去,不断找到能“通吃”若干小NP问题的一个稍复杂的大NP问题,那么最后是否有可能找到一个时间复杂度最高,并且能“通吃”所有的 NP问题的这样一个超级NP问题?答案居然是肯定的。也就是说,存在这样一个NP问题,所有的NP问题都可以约化成它。换句话说,只要解决了这个问题,那么所有的NP问题都解决了。这种问题的存在难以置信,并且更加不可思议的是,这种问题不只一个,它有很多个,它是一类问题。这一类问题就是传说中的NPC 问题,也就是NP-完全问题。

            NPC问题的定义非常简单。同时满足下面两个条件的问题就是NPC问题。首先,它得是一个NP问题;然后,所有的NP问题都可以约化到它。证明一个问题是 NPC问题也很简单。先证明它至少是一个NP问题,再证明其中一个已知的NPC问题能约化到它(由约化的传递性,则NPC问题定义的第二条也得以满足

8.2、贪婪算法解释

用专业术语说,就是你每步都选择局部最优解,最终得到的就是全局最优解
这里写图片描述

8.3、背包问题

            在有些情况下,完美是优秀的敌人。有时候,你只需找到一个能够大致解决问题的算法,此时贪婪算法正好可派上用场,因为它们实现起来很容易,得到的结果又与正确结果相当接近。
            打个比方
这里写图片描述
你偷到了价值3000美元的东西。且慢!如果不是偷音响,而是偷笔记本电脑和吉他,总价将
为3500美元!
在这里,贪婪策略显然不能获得最优解,但非常接近。下一章将介绍如何找出最优解。不过
小偷去购物中心行窃时,不会强求所偷东西的总价最高,只要差不多就行了。

8.4、 集合覆盖问题(近似算法原理)

类似广播问题
找出覆盖全美50个州的最小广播台集合,找到最少的广播台,这样极大的缩减了运行之间,在这个例子中,一开始运行的是是O(2^n),用近似之后就是O(n^2)。
这里写图片描述

8.5、NP 完全问题

8.5.1、旅行熵问题

这里写图片描述
旅行商问题和集合覆盖问题有一些共同之处:你需要计算所有的解,并从中选出最小/最短的那个。这两个问题都属于NP完全问题。
找出经由指定几个点的的最短路径,就是旅行商问题——NP完全问题

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

8.6小结

● 贪婪算法寻找局部最优解,企图以这种方式获得全局最优解。
● 对于NP完全问题,还没有找到快速解决方案。
● 面临NP完全问题时,最佳的做法是使用近似算法。
● 贪婪算法易于实现、运行速度快,是不错的近似算法。

第九章、动态规划

9.1、背包问题–动态规划

这里写图片描述

9.1.1、处理相互依赖的情况

这里写图片描述
                从一个地方去上述地方都需要1.5天,但是,这三个地方的相连的路程只有1天的时间。去这些地方游览需要很长时间,因为你先得从伦敦前往巴黎,这需要半天时间。如果这3个地方都去玩,是不是要4.5天呢?
                不是的,因为不是去每个地方都得先从伦敦到巴黎。到达巴黎后,每个地方都只需1天时间。因此玩这3个地方需要的总时间为3.5天(半天从伦敦到巴黎,每个地方1天),而不是4.5天。

9.1.2、计算最终的解时会涉及两个以上的子背包吗

为获得前述背包问题的最优解,可能需要偷两件以上的商品。但根据动态规划算法的设计,最多只需合并两个子背包,即根本不会涉及两个以上的子背包。不过这些子背包可能又包含子背包。
这里写图片描述

9.1.3、最优解可能导致背包没装满吗

这里写图片描述

9.1.4、小总结

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

9.2 最长公共子串和最长公共子序列(前者范围大)

对比两个单词的子串
这里写图片描述

9.3、用途

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

9.4、小结

● 需要在给定约束条件下优化某种指标时,动态规划很有用。
● 问题可分解为离散子问题时,可使用动态规划来解决。
● 每种动态规划解决方案都涉及网格。
● 单元格中的值通常就是你要优化的值。
● 每个单元格都是一个子问题,因此你需要考虑如何将问题分解为子问题。

第十章、K最近邻算法(KNN)

K最近邻其实就是按距离的分类,哪种的比例和他距离近,就分为哪一类。单独做了专题博客
详情点击这里

第十一章、其他算法

11.1、树

                在一个庞大的数组中查找,核实其中是否包含指定的数据。前面说过,在这种数组中查找时,最快的方式是二分查找,单数有数据进来的时候,就得重新排序。因为二分查找仅在数组有序时才管用。如果能将数据插入到数组的正确位置就好了,这样就无需在插入后再排序。为此,有人设计了一种名为二叉查找树(binary search tree)的数据结构。
这里写图片描述

11.2、反向索引

                搜索引擎发现页面A和B包含hi,因此将这些页面作为搜索结果呈现给用户。现在假设用户搜索there。你知道,页面A和C包含它。非常简单,不是吗?这是一种很有用的数据结构:一个散
列表,将单词映射到包含它的页面。这种数据结构被称为反向索引(inverted index),常用于创
建搜索引擎。

简单的说就是key–>values到values–>key,因为是映射查找,可能会有多个回馈。

11.3、傅里叶变换

                因为内容太多,后期会给出详细的解释
                绝佳的比喻:给它一杯冰沙,它能告诉你其中包含哪些成分①。换言之,给定一首歌曲,傅里叶变换能够将其中的各种频率分离出来。
                因为内容太多,后期会给出详细的解释

                这种理念虽然简单,应用却极其广泛。例如,如果能够将歌曲分解为不同的频率,就可强化你关心的部分,如强化低音并隐藏高音。傅里叶变换非常适合用于处理信号,可使用它来压缩音
乐。为此,首先需要将音频文件分解为音符。傅里叶变换能够准确地指出各个音符对整个歌曲的
贡献,让你能够将不重要的音符删除。这就是MP3格式的工作原理!

                图像经过二维傅里叶变换后,其变换系数矩阵具有如下性质:若变换矩阵原点设在中心,其频谱能量集中分布在变换系数短阵的中心附近(图中阴影区)。若所用的二维傅里叶变换矩阵的原点设在左上角,那么图像信号能量将集中在系数矩阵的四个角上。这是由二维傅里叶变换本身性质决定的。同时也表明一股图像能量集中低频区域。
                图像灰度变化缓慢的区域,对应它变换后的低频分量部分;图像灰度呈阶跃变化的区域,对应变换后的高频分量部分。除颗粒噪音外,图像细节的边缘、轮廓处都是灰度变化突变区域,它们都具有变换后的高频分量特征。

11.4、 并行算法

                来看一个简单的例子。在最佳情况下,排序算法的速度大致为O(n log n)。众所周知,对数组进行排序时,除非使用并行算法,否则运行时间不可能为O(n)!对数组进行排序时,快速排序的
并行版本所需的时间为O(n)。

                笔记本电脑和台式机转而采用多核处理器。为提高算法的速度,你需要让它们能够在多个内核中并行地执行!

11.5、MapReduce

MapReduce是一种流行的分布式算法
假设你有一个数据库表,包含数十亿乃至数万亿行,需要对其执行复杂的SQL查询。在这种情况下,你不能使用MySQL,因为数据表的行数超过数十亿后,它处理起来将很吃力。相反,你需要通过Hadoop来使用MapReduce!
分布式算法非常适合用于在短时间内完成海量工作,其中的MapReduce基于两个简单的理念:映射(map)函数和归并(reduce)函数。

11.5.2、映射函数

这里写图片描述
映射函数很简单,它接受一个数组,并对其中的每个元素执行同样的处理。例如,下面的映射函数将数组的每个元素翻倍。

arr1 = [1, 2, 3, 4, 5]
arr2 = map(lambda x: 2 * x, arr1)
[2, 4, 6, 8, 10]

11.5.3、归并函数

归并函数可能令人迷惑,其理念是将很多项归并为一项。映射是将一个数组转换为另一个数组。
而归并是将一个数组转换为一个元素,类似累积
这里写图片描述

arr1 = [1, 2, 3, 4, 5]
reduce(lambda x,y: x+y, arr1)
15

11.6 布隆过滤器和 HyperLogLog

11.6.1、布隆过滤器

                布隆过滤器是一种概率型数据结构,它提供的答案有可能不对,但很可能是正确的。为判断网页以前是否已搜集,可不使用散列表,而使用布隆过滤器。使用散列表时,答案绝对可靠,而使用布隆过滤器时,答案却是很可能是正确的。
                布隆过滤器的优点在于占用的存储空间很少。使用散列表时,必须存储Google搜集过的所有URL,但使用布隆过滤器时不用这样做。布隆过滤器非常适合用于不要求答案绝对准确的情况。

11.6.2、HyperLogLog

                HyperLogLog是一种类似于布隆过滤器的算法。如果Google要计算用户执行的不同搜索的量,或者Amazon要计算当天用户浏览的不同商品的数量,要回答这些问题,需要耗用大量的空间!对Google来说,必须有一个日志,其中包含用户执行的不同搜索。有用户执行搜索时,Google 必须判断该搜索是否包含在日志中:如果答案是否定的,就必须将其加入到日志中。

11.7 SHA 算法(安全散列算法(secure hash algorithm,SHA)函数)

11.7.1、比较文件

SHA是一个散列函数,它生成一个散列值——一个较短的字符串。
用于创建散列表的散列函数根据字符串生成数组索引,而SHA根据字符串生成另一个字符串。
你可使用SHA来判断两个文件是否相同
                                                                                      这里写图片描述

11.7.2、检验密码

                SHA还让你能在不知道原始字符串的情况下对其进行比较。例如,假设Gmail遭到攻击,攻击者窃取了所有的密码!你的密码暴露了吗?没有,因为Google存储的并非密码,而是密码的SHA散列值!你输入密码时,Google计算其散列值,并将结果同其数据库中的散列值进行比较。
这里写图片描述

11.8、局部敏感的散列算法

这里写图片描述

在这种情况下,可使用Simhash。如果你对字符串做细微的修改,Simhash生成的散列值也只存在细微的差别。这让你能够通过比较散列值来判断两个字符串的相似程度

11.9、Diffie-Hellman 密钥交换

解决的问题:对消息进行加密,以便只有收件人才能看懂这里写图片描述

Diffie-Hellman使用两个密钥:公钥和私钥。顾名思义,公钥就是公开的,可将其发布到网站上,通过电子邮件发送给朋友,或使用其他任何方式来发布。你不必将它藏着掖着。有人要向你发送消息时,他使用公钥对其进行加密。加密后的消息只有使用私钥才能解密。只要只有你知道私钥,就只有你才能解密消息!
Diffie-Hellman算法及其替代者RSA依然被广泛使用。如果你对加密感兴趣,先着手研究Diffie-Hellman算法是不错的选择:它既优雅又不难理解。

11.10、线性规划

线性规划用于在给定约束条件下最大限度地改善指定的指标。简单的说就是在约束条件下,目标最大化的计划。
详细理解点这里


总结于书《算法图解》(人民邮电出版社)

猜你喜欢

转载自blog.csdn.net/sakura55/article/details/80887748