基础算法图解介绍

文章摘自《算法图解》 作者Aditya Bhargava,这是本机器学习入门好书,简洁易懂,还能让人提起读下去的兴趣。兴趣是个好东西。。。

一、二分查找

1、二分查找

定义:二分查找是一种算法,其输入是一个有序的元素列表(必须有序的原因稍后解释)。如果要查找的元素包含在列表中,二分查找返回其位置;否则返回null
例子
如果要查找的单词位于字典末尾,使用简单查找将需要240 000步。使用二分查找时,每次排除一半单词,直到最后只剩下一个单词
这里写图片描述
因此,使用二分查找只需18步——少多了!一般而言,对于包含n个元素的列表,用二分查找最多需要log2n步,而简单查找最多需要n步。

计算公式
n是总量,结果就是f(n)次

f(n) = log_2n

代码:

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

2、大 O 表示法

大O表示法是一种特殊的表示法,指出了算法的速度有多快
简单查找:O(n)
二分查找:

O ( l o g 2 n )

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

二、选择排序

1、链表

链表中的元素可存储在内存的任何地方,链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起
例子
这犹如寻宝游戏。你前往第一个地址,那里有一张纸条写着“下一个元素的地址为123”。因
此,你前往地址123,那里又有一张纸条,写着“下一个元素的地址为847”,以此类推。在链表
中添加元素很容易:只需将其放入内存,并将其地址存储到前一个元素中。
所以:链表的优势在插入元素方面

2、数组

数组与此不同:数组知道其中每个元素的地址。
例如,假设有一个数组,它包含五个元素,起始地址为00,那么元素#5的地址是04?
只需执行简单的数学运算就知道:04。需要随机地读取元素时,数组的效率很高,因为可迅速找到数组的任何元素。在链表中,元素并非靠在一起的,你无法迅速计算出第五个元素的内存地址,而必须先访问第一个元素以获取第二个元素的地址,再访问第二个元素以获取第三个元素的地址,以此类推,直到访问第五个元素。
所以:数组的优势在读取元素方面(随机访问)
这里写图片描述
数组和链表哪个用得更多呢?
显然要看情况。但数组用得很多,因为它支持随机访问。有两种访问方式: 随机访问和顺序访问。顺序访问意味着从第一个元素开始逐个地读取元素。链表只能顺序访问:要读取链表的第十个元素,得先读取前九个元素,并沿链接找到第十个元素。随机访问意味着可直接跳到第十个元素。所以说数组的读取速度更快,这是因为它们支持随机访问。很多情况都要求能够随机访问,因此数组用得很多。
示例代码
前面没有列出对乐队进行排序的代码,但下述代码提供了类似的功能:将数组元素按从小到大的顺序排列。先编写一个用于找出数组中最小元素的函数。

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])

三、递归

1、递归

递归代码:
递归——函数调用自己,这种方法的伪代码如下

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!"

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

2、基线条件和递归条件

每个递归函数都有两部分:基线条件( base case)和递归条件( recursive case) 。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。

3、栈

栈是一种简单的数据结构

调用栈

计算机在内部使用被称为调用栈的栈。我们来看看计算机是如何使用调用栈的。下面是一个
简单的函数。

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!

使用栈虽然很方便,但是也要付出代价:存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。在这种情况下,你有两种选择。
- 重新编写代码,转而使用循环。
- 使用尾递归。这是一个高级递归主题,不在本书的讨论范围内。另外,并非所有的语言都支持尾递归

四、快速排序

原理:
首先,从数组中选择一个元素,这个元素被称为基准值( pivot)。接下来,找出比基准值小的元素以及比基准值大的元素。
- 一个由所有小于基准值的数字组成的子数组;
- 基准值;
- 一个由所有大于基准值的数组组成的子数组。
代码:

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])

五、散列表()

1、散列表

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

2、冲突

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

在最糟情况下,散列表所有操作的运行时间都为O(n)——线性时间,这真的很慢。现将散列表同数组和链表比较:
这里写图片描述
在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速度与链表一样快,因此它兼具两者的优点!但在最糟情况下,散列表的各种操作的速度都很慢。因此,在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要有:
- 较低的填装因子;
- 良好的散列函数。

六、广度优先搜索

广度优先搜索让你能够找出两样东西之间的最短距离,不过最短距离的含义有很多!使用广
度优先搜索可以:
- 编写国际跳棋AI,计算最少走多少步就可获胜;
- 编写拼写检查器,计算最少编辑多少个地方就可将错拼的单词改成正确的单词,如将READED改为READER需要编辑一个地方;
- 根据你的人际关系网络找到关系最近的医生。

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

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
search("you")

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

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

七、狄克斯特拉算法

简介:
广度优先搜索:找出的是段数最少的路径
狄克斯特拉算法(Dijkstra’salgorithm):要找出最快的路径

狄克斯特拉算法用于每条边都有关联数字的图,这些数字称为权重( weight)。
这里写图片描述
带权重的图称为加权图( weightedgraph),不带权重的图称为非加权图( unweightedgraph)。
这里写图片描述
要计算非加权图中的最短路径,可使用广度优先搜索。要计算
加权图中的最短路径,可使用狄克斯特拉算法。
狄克斯特拉算法只适用于有向无环图( directed acyclic
graph, DAG)
小结:
- 广度优先搜索用于在非加权图中查找最短路径。
- 狄克斯特拉算法用于在加权图中查找最短路径。
- 仅当权重为正时狄克斯特拉算法才管用。
- 如果图中包含负权边,请使用贝尔曼福德算法。

八、贪婪算法

1、贪婪算法
贪婪算法很简单,每步都采取最优的做法。用专业术语说,就是你每步都选择局部最优解,最终得到的就是全局最优解。
2、近似算法
当参数过多时,贪婪算法需要计算很长时间,这样就会用到近似算法,局部最优,一直循环(条件结束),得到近似最优的结果

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

九、动态规划

动态规划功能强大,它能够解决子问题并使用这些答案来解决大问题。 但仅当每个子问题都是离散的,即不依赖于其他子问题时,动态规划才管用。

  • 动态规划可帮助你在给定约束条件下找到最优解。在背包问题中,你必须在背包容量给定的情况下,偷到价值最高的商品。
  • 在问题可分解为彼此独立且离散的子问题时,就可使用动态规划来解决。

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

十、K最近邻算法(KNN)

图解:
这里写图片描述
小结:
- KNN用于分类和回归,需要考虑最近的邻居。
- 分类就是编组。
- 回归就是预测结果(如数字)。
- 特征抽取意味着将物品(如水果或用户)转换为一系列可比较的数字。
- 能否挑选合适的特征事关KNN算法的成败。

十一、多种算法

1、树
2、反向索引
3、傅里叶变换
举例理解:
给它一杯冰沙,它能告诉你其中包含哪些成分。换言之,给定一首歌曲,傅里叶变换能够将其中的各种频率分离出来。
3、并行算法
4、分布式计算系统
5、布隆过滤器和 HyperLogLog
6、SHA算法
应用:比较文件和检查密码
SHA实际上是一系列算法: SHA-0、 SHA-1、SHA-2和SHA-3。本书编写期间, SHA-0和SHA-1已被发现存在一些缺陷。如果你要使用SHA算法来计算密码的散列值,请使用SHA-2或SHA-3。当前,最安全的密码散列函数是bcrypt,但没有任何东西是万无一失的
7、局部敏感的散列算法
- Google使用Simhash来判断网页是否已搜集。
- 老师可以使用Simhash来判断学生的论文是否是从网上抄的。
- Scribd允许用户上传文档或图书,以便与人分享,但不希望用户上传有版权的内容!这个
- 网站可使用Simhash来检查上传的内容是否与小说《哈利·波特》类似,如果类似,就自动拒绝。

8、Diffie-Hellman 密钥交换
Diffie-Hellman算法解决了如下两个问题:
- 双方无需知道加密算法。他们不必会面协商要使用的加密算法。
- 要破解加密的消息比登天还难。

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

猜你喜欢

转载自blog.csdn.net/sinat_30353259/article/details/80869772