算法图解

算法图解

一、算法简介

1.1 引言

算法:是一组完成任务的指令。任何代码片段都可视为算法

1.2 二分查找

二分查找可用于在一个有序的元素列表快速查找某一元素。使用二分查找时,每次都排除一半的数字。如下例:

在某一字典中查找一个单词,字典包含2400000个词,使用二分查找每次排除一半单词,直到最后只剩下一个单词。

在这里插入图片描述

对于包含n个元素的列表,二分查找只需 logn步。````

1.3 大O表示法

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

(2)一些常见的大O运行时间

O ( l o g 2 n ) O(log_2n) O(log2n),也叫对数时间,这样的算法包括二分查找。

O ( n ) O(n) O(n),也叫线性时间,这样的算法包括简单查找。

O ( n ∗ l o g n ) O(n*log_n) O(nlogn),这样的算法包括快速排序,一种速度较快的排序算法。

O ( n 2 ) O(n^2) O(n2),这样的算法包括选择排序,一种速度较慢的排序算法。

O ( n ! ) O(n!) O(n!),这样的算法包括旅行商的解决方法,一种非常慢的方法。
在这里插入图片描述

1.4 小结

在这里插入图片描述

二、选择排序

2.1 内存的工作原理

计算机就行很多抽屉的集合体,每个抽屉都有地址。需要将数据存储到内存时,你请求计算机提供存储空间,计算机给你一个存储地址。需要存储多项数据时,有两种基本方式——数组和链表

2.2 数组和链表

2.2.1 链表

链表中的元素可以存储在内存的任何地方。其中的每个元素都存储着下一个元素的地址,从而使一系列算计的内存地址串在一起。

链表的优势在插入和删除元素方面。

(1)假设我们要存储一系列元素,使用数组存储意味着所有元素在内存中都是相连的。

在这里插入图片描述

2.2.2 数组

数组存储意味着所有元素在内存中都是相连的。如果要插入元素,而后面的位置被占用,此时就需要进行转移。

优势:需要随机地读取元素时,数组的效率很高,因为可迅速找到数组的任何元素。

2.2.3 术语

元素的位置称为索引。因此,不说“元素20的位置为1”,而说“元素20位于索引1处”。

在这里插入图片描述

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

2.3 选择排序

选择排序是一种灵巧的算法,但速度不是很快,一半运行时间为 O ( n 2 ) O(n^2) O(n2)

示例代码:

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_index = findSmallest(arr) # 找出数组中最小的元素,并将其加入的新数组中
        print(smallest_index)
        newArr.append(arr.pop(smallest_index))
        # print(arr.pop(smallest_index))
    return newArr
print(selectionSort([5, 3, 6, 2, 10]))

运行结果:
在这里插入图片描述

2.4 小结

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

三、递归

3.1递归

  1. 举个例子:如果我们要从一个盒子堆中,找一把钥匙,就可采取递归的方法。
    在这里插入图片描述

  2. 方法步骤:

    (1) 检查盒子中的每样东西。
    (2) 如果是盒子,就回到第一步。
    (3) 如果是钥匙,就大功告成!

在这里插入图片描述

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

3.2 极限条件和递归条件

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

一个倒计时函数:

def countdown(i):
    print(i)
    if i <= 0: # 基线条件
        return
    else: # 递归条件
        countdown(i-1)

3.3 栈

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

在这里插入图片描述

这种数据结构称为栈。

3.3.1 调用栈

计算机在内部使用被称为调用栈的栈。

3.3.2 递归调用栈

递归函数也在使用调用栈。看看阶乘的递归函数 fact(3)的调用栈。

代码示意图:

# 阶乘递归函数
def fact(x):
    if x == 1:
        return 1
    else:
        return x * fact(x-1)

分析过程:

在这里插入图片描述

在这里插入图片描述

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

3.4 小结

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

四、快速排序

4.1 快速排序

  1. 快速排序是一种常用的排序算法,比选择排序快得多。

  2. 实例:实现对数组中元素的快速排序。

(1)**基线条件:**数组为空或只含一个元素时,返回原数组,此时不用排序。

def quicksort(array):
	if len(array) < 2:
		return array

(2)如果包含三个或以上元素,此时需要将数组分解,直到满足基线条件。首先,从数组中找出一个元素,称为基准值(pivot)

(3)找出比基准值小的元素以及比基准值大的元素,进行分区(partitioning)。

(4)此时两个子数组是无序的,在对子数组进行快速排序,从而得到有序数组:左边的数组+基准值+右边的数组。

quicksort([15, 10]) + [33] + quicksort([])
> [10, 15, 33] # 一个有序的数组
  1. 图解实例:

在这里插入图片描述

  1. 代码实例:

    # 快速排序
    def quicksort(arr):
        if len(arr) < 2:
            return arr
        else:
            pivot = arr[0]
            less = [i for i in arr[1:] if i <= pivot]
            greater = [i for i in arr[1:] if i >= pivot]
            return quicksort(less) + [pivot] + quicksort(greater)
    print(quicksort([10, 5, 11, 2, 3]))
    

    运行结果:

在这里插入图片描述

4.2 小结

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

五、散列表

Python提供的散列表实现为字典,可使用函数dict来创建散列表。

5.1 散列函数

1.散列函数“将输入映射的数字”,其要满足的要求:

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

在这里插入图片描述

不断重复此过程,即可填满生个数组。

2.使用散列函数和数组创建了一种被称为**散列表(hash table)**的数据结构。

3.散列表小结:

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

5.2 冲突

**冲突(collision)**是指给两个键分配的位置相同,解决办法就是在这个位置上存储一个链表。

在这里插入图片描述

在上图中,apple和avocado映射到了同一个位置,因此在这个位置存储一个链表。

5.3 性能

在平均的情况下,散列表只需各种操作的事件都为 O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1)被称为常量事件

使用散列表时,避开最糟情况至关重要 ,为避免冲突,其需要:

 较低的填装因子;
 良好的散列函数。

5.3.1 填装因子

填 装 因 子 = 散 列 表 包 含 的 元 素 数 位 置 总 数 填装因子=\frac{散列表包含的元素数}{位置总数} =

在这里插入图片描述

5.3.2 良好散列函数

良好散列函数让数组中的值呈均匀分布。

在这里插入图片描述

5.4 小结

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

六、广度优先搜索(breadth-first search,BFS)

BFS可以帮助我们找到两样东西之间的“最短距离”。

6.1 图简介

首先,我们来学习下什么是图。图模拟一组链接。假设你与朋友玩牌,并要模拟谁欠谁钱,可像下面这样指出Alex欠Rama钱。

在这里插入图片描述

在这里插入图片描述

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

6.2 广度优先搜索

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

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

6.2.1 查找最短路径

引入问题:在你的关系网中,寻找芒果商。首先在你的朋友中查找,如果没有,再在朋友的朋友中查找。

6.2.2 队列

队列的工作原理与现实生活中的队列完全相同。假设你与朋友一起在公交车站排队,如果你排在他前面,你将先上车。队列的工作原理与此相同。队列类似于栈,你不能随机地访问队列中的元素。队列只支持两种操作: 入队和出队。

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

6.3 实现图

在这里插入图片描述

在我的朋友中,找到关系最近的芒果经销商。

Python中表示上图的映射关系:

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.4 实现算法

  1. 工作原理:

在这里插入图片描述

2.完整代码

# 广度优先搜索 breadth-first search
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"] = []
print('graph: ',graph)

from collections import deque
# 判断一个人是不是芒果商
def person_is_seller(name):
    return name[-1] == 'm'

# 广度优先搜索函数BFS
def BFS(name):
    search_queue = deque()  # 创建一个队列
    search_queue += graph[name]  # 将你的邻居都加入到这个搜索队列中
    searched = [] # 这个数组用于记录检查过的人
    print('search_queue: ', search_queue)
    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)
                print(search_queue)
    return False
BFS("you")

3.运行结果
在这里插入图片描述

6.5 小结

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

七、狄克斯特拉算法

狄克斯特拉算法( Dijkstra’s algorithm)适用于加权图,找到最快的路径。而BFS是找到段数最少的路径。

狄克斯特拉算法背后的关键理念: 找出图中最便宜的节点,并确保没有到该节点的更便宜的路径!

7.1 使用迪克斯特拉算法

(1) 找出最便宜的节点,即可在最短时间内前往的节点。
(2) 对于该节点的邻居,检查是否有前往它们的更短路径,如果有,就更新其开销。
(3) 重复这个过程,直到对图中的每个节点都这样做了。
(4) 计算最终路径。

7.2 实现

1.以下图为例:

在这里插入图片描述

2.完成代码:

# 狄克斯特算法实现
graph = {}
graph["start"] = {}
graph["start"]["a"] = 6
graph["start"]["b"] = 2
print(graph["start"].keys())
graph["a"] = {}
graph["a"]["fin"] = 1
graph["b"] = {}
graph["b"]["a"] = 3
graph["b"]["fin"] = 5

graph["fin"] = {} # 终点没有任何邻居

# 创建开销表
infinitely = float("inf") # 表示无穷大
costs={}
costs["a"] = 6
costs["b"] = 2
costs["fin"] = infinitely

# 存储父节点的散列表
parents = {}
parents["a"] = "start"
parents["b"] = "start"
parents["fin"] = None

# 数组 记录读取过的节点
processed = []

# 找出开销最低的节点
def find_lowest_cost_node(costs):
    lowest_cost = float("inf")
    lowest_cost_node = None
    for node in costs: # 遍历所有节点
        # print(node)
        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)  # 在未处理的节点中找开销最小的节点
print(node)
while node is not None:  # 此while循环在所有节点被处理过结束
    cost = costs[node]
    neighbors = graph[node]
    print('neighbors', neighbors)
    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)  # 找出接下来要处理的节点,并循环
    print(costs)

3.运行结果

在这里插入图片描述

可看出最低路程最短为6。

7.3 小结

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

八、贪婪算法

贪婪算法:每步都采取最优的做法。用专业术语来说,就是每步都选择局部最优解,最终得到就是全局最优解。

8.1 背包问题

假设有一个贪婪的小偷,背着可装35磅( 1磅≈ 0.45千克)重东西的背包,他力图王背包中装入加入价值最高的商品,应该采用哪种算法呢?

此时采用贪婪算法,就非常简单:

(1) 盗窃可装入背包的最贵商品。
(2) 再盗窃还可装入背包的最贵商品,以此类推。

但是在在以下情况,贪婪算法就可能不太好用了:
在这里插入图片描述

在这里插入图片描述

可以看出,贪婪算法显然不能获得最优解,但非常接近。

8.2 集合覆盖问题

**1.问题叙述:**假设你办了个广播节目,要让全美50个州的听众都收听得到。为此,你需要决定在哪些广播台播出。在每个广播台播出都需要支付费用,因此你力图在尽可能少的广播台播出。 每个广播台都覆盖特定的区域,不同广播台的覆盖区域可能重叠。

**2.近似算法:**下面使用贪婪算法获得近似解,

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

这是一种近似算法( approximation algorithm) 。在获得精确解需要的时间太长时,可使用近
似算法。判断近似算法优劣的标准如下:
 速度有多快;
 得到的近似解与最优解的接近程度。

3.代码仿真实现

# 贪婪算法的近似算法
# 创建一个列表,其中包含要覆盖的州
# 传入一个数组,它被转换为集合 集合不能包含重复的元素
states_needed = set(['mt', 'wa', 'or', 'id', 'nv', 'ut', 'ca', 'az'])
print(states_needed)
stations = {}
stations['kone'] = set(["id", "nv", "ut"])
stations["ktwo"] = set(["wa", "id", "mt"])
stations["kthree"] = set(["or", "nv", "ca"])
stations["kfour"] = set(["nv", "ut"])
stations["kfive"] = set(["ca", "az"])

# 使用集合来寻出最终选择的广播电台
final_stations = set()

# 不断循环,知道states_needed为空
while states_needed:
    best_station = None
    states_covered = set()
    for station, states_for_station in stations.items():
        covered = states_needed & states_for_station # 计算交集
        if len(covered) > len(states_covered):
            best_station = station  # 将best_station设置为当前广播台。
            states_covered = covered
    final_stations.add(best_station)  # best_station添加到最终的广播台列表中
    states_needed -= states_covered  # 更新states_needed,广播台覆盖了一些州,因此不用再覆盖这些州
print(final_stations)

4.运行结果

在这里插入图片描述

8.3 小结

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

九、动态规划

9.1 背包问题

同第八章问题,此时小偷的背包可装4磅的东西,可盗窃的商品如下:

在这里插入图片描述

为了让盗窃的商品价值最高,你该选择哪些商品?

9.1.1 简单算法

最简单算法就是尝试各种可能组合,找出价值最高的组合,不过速度非常慢。在三种商品情况下,你需要计算8个不同的集合;有4件商品时,你需要计算16个集合。每增加一件商品,需要计算的集合数都将翻倍!这种算法的运行时间为O(2n) 。

9.1.2 动态规划

动态规划先解决子问题,在逐步解决大问题。每个动态规划算法都从一个网格开始,网格如下:

网格的各行为商品,各列为不同容量( 1~4磅)的背包。在每一行,可偷的商品都为当前行的商品以及之前各行的商品。

在这里插入图片描述

9.2 最长公共子串

1.启示:

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

2.计算两个英文单词的最长公共子串
在这里插入图片描述

9.3 小结

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

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值