算法图解笔记

书名: 算法图解
出版社: 图灵出版社
网址: http://www.ituring.com.cn/book/1864

在学习C语言的时候,理解到了程序由算法+数据结构组成。算法的理解和学习是程序员必不可少的内容。这周看了《图解算法》这本书,书本采用Python语言,通俗易懂,虽然书本上有些算法没有讲解,但通过大量的例子加深对算法的理解,是不错的入门书籍。

下面是我看了书籍后做的读书笔记,便于日后复习加强学习。

图解算法

基础

  • O表示法: 是一种特殊的表示方法,指出了算法的速度有多快。在实际应用中,只知道算法需要多长时间才能运行完毕还不够,还需要知道运行时间如果随列表增长而增加,这正是大O表示法的作用。

表达的方式为: O(n) 其中n表示的是操作数,操作数前的O,所以称为大O表示法。

常见的大O运行时间

常见的运行时间

O(logn)比O(n)快。需要搜索的元素越多,前者比后者就快得越多。

算法运行时间并不以秒为单位,而以操作数为单位,且从算法增速角度度量。

常量对大O表示法影响:
O表示法的常量有时候事关重大,如快速排序比合并排序快,因为其大O表示运行时间都为O(nlogn)。有时常量无关紧要,如简单查找O(n)和二分查找O(nlogn)。

最糟情况:调用算法运行,经过最多次操作才得出结果。
最佳情况:调用算法运行时,经过最少次操作就得出结果。
平均情况:结果等同于最佳情况,如快速排序平均运行时间为O(nlogn)

  • 内存工作原理:
    数据存储到内存时,请求计算机提供存储空间,计算机会返回给你一个16进制128位的存储地址,存储地址对应数据的存放位置。内存存储就类似于往抽屉里放东西。

  • 递归
    递归是一种优雅的问题解决方法。使用递归,可以使程序更容易理解,使用循环,程序的性能可能更高。每个递归函数包括两部分:基线条件(base case)递归条件(recursive case)

例如:

def sum(list):
    if list == []: #基线条件
        return 0
    return list[0] + sum(list[1:]) #递归条件
    
#循环写法
def sumCircle(list):
    allSum = 0
    for i in list:
        allSum = allSum + i
    return allSum
  • NP完全问题
    简单定义是,以难解著称的问题,如旅行商问题(根据城市,找出所有可能路线,路线算法是采用阶乘函数,输入参数是城市数量)和集合覆盖问题。很多非常聪明的人认为,根本不可能编写出快速解决这些问题的算法。最佳解法是采用近似算法。

特征:

元素较少运行速度非常快,随元素增加,速度变得非常慢

涉及“所有组合”问题通常是NP完全问题

不能将问题分成小问题,必须考虑各种可能情况,可能是NP完全问题

如果问题涉及序列且难以解决,则可能是NP完全问题

如果问题涉及集合且难以解决,则可能是NP完全问题

如果问题涉及旅行商和集合覆盖问题,则肯定是NP完全问题

数据结构

  • 数组:是一组数据在内存上采用内存地址连续的空间存储。由于数组知道每个元素位置,所以查找特别迅速,但由于内存空间固定,所以插入和删除都有重新安排空间,比较缓慢。同一数组中,所有元素类型都必须相同

例如途中待办事项就是数组


数组
  • 链表:元素可以存储在内存的任何地方。每个元素都存储了下一个元素的地址,从而使一系列随机内存地址串在一起。插入和删除只要修改存储的下一元素的地址,比较快速,而查找则只能从第一个元素开始读取,比较缓慢。
    链表

数组链表比较


比较
  • 混合结构:同时使用数组和链表,其查找和插入的性能都介于数组和链表之间。
    混合结构
  • :是一种简单的数据结构,特点是先进后出。在调用函数和递归调用的时候使用的数据结构就是栈。每个程序调用的栈空间都有限,如果用完这些空间,会造成栈溢出而终止运行。

  • 散列表:最有用的基本数据结构之一。散列函数是将输入映射到数字。散列函数满足下列要求:

  1. 返回必须是一致的,即输入的返回值不发生变化
  2. 将不同的输入映射到不同数字。

散列函数和数组构成散列表。散列表也被称为散列映射、映射、字典和关联数组。Python提供的散列实现为字典,使用函数dict创建散列表。

散列表应用:

  1. 通过创建映射,用于查找
  2. 防止重复
  3. 将散列表用于缓存

使用散列表,可能会遇到冲突问题,即存储时分配的位置已经被使用。这时,要保证:散列函数很重要,理想情况是散列函数均衡地映射到散列表的不同位置,呈均衡分布;散列表存储的链表不要太长,不然影响效率

性能:一般情况下,散列表兼具数组和链表的优点,在最糟糕的情况下,各种操作速度都很慢,这时要避免冲突,做到:较低的填装因子和良好的散列函数

散列表性能

填装因子:用于反映数组中被占用的位置数,计算公式为:

\frac {\text{散列表包含的元素数}}{\text{位置总数}}
填装因子越低,发生冲突的可能性越小,散列表性能越高。经验规则:一旦填装因子大于0.7,就调整散列表长度

  • 队列:原理和生活中队列完全一样。队列类似于栈,你不能随机访问队列中的元素。队列只支持两种操作:出队和入队。
队列

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


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

图包括有向图和无向图。

有向图:边为箭头,箭头的方向指定了关系的方向,例如 dog \to animal表示狗属于动物

无向图:边不带箭头,其中的关系是双向的,例如 上海——广州表示上海可以到广州,广州也可以到上海

加权图:每条边都有关联的数字的图,这些数字称为权重(weight)。带权重的图称为加权图(weighted graph),不带权重的图为非加权图(unweighted graph),使用狄克斯特拉算法。

加权图

使用代码实现图:


实现图
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'] = []

键值对交换顺序不会有影响,因为散列表是无序的。

拓扑排序:某种程度上说,有向图构成的列表是有序的,如果任务A依赖于任务B,则在列表中任务A必须在任务B后面,这就是拓扑排序,使用它可以根据图创建一个有序列表。

  • 集合
    集合类似于列表,只是同样的元素只能出现一次,即集合不包含重复的元素。

集合操作:

1)并集意味着集合合并 setA | setB

2)交集意味着找出两个集合中都有的元素 setA & setB

3)差集意味着将从一个集合中剔除出现在另一个集合中的元素 SetA - setB

  • 二叉树
    结构为:
    二叉树

对于其中的每个节点,每个节点最多只能有两个节点(对应二叉树的二),左子节点的值都比它小 ,而右子节点的值都比它大 。

在二叉查找树中查找节点时,平均运行时间为O(log n ),但在最糟的情况下所需时间为O (n);而在有序数组中查找时,即便是在最糟情况下所需的时间也只有O(logn ),因此你可能认为有序数组比二叉查找树更佳。然而,二叉查找树的插入和删除操作的速度要快得多。

二叉树数组比较

二叉树缺点:不能随机访问。此外,分布不平衡的树会造成性能不佳。也有一些处于平衡状态的
特殊二叉查找树,如红黑树。

  • 反向索引 (inverted index)
    一个散列表,将单词映射到包含它的页面。这种数据结构用于搜索发动机。

策略

  • D&C(divide and conquer) 分而治之
    分而治之是一种著名的递归式问题解决方法。提供了一种解决问题的思路。其解决问题一般分两个步骤:

    1. 找出基线条件,这条件必须尽可能简单
    2. 不断分解问题(或缩小规模),知道符合基线条件

    例如,推导出sum函数的递归写法:


    分而治之

使用D&C可以推导出递归函数。D&C理念的方法是快速排序

  • 贪婪算法
    优点是简单易行。每步都采取最优做法,即每步都选择局部最优解,最终得到的就是全局的最优解。贪婪算法并非在任何情况下都行之有效。有些情况,贪婪算法不能得到最优解,但非常接近。

    使用贪婪算法的有近似算法(approximation algorithm)、广度优先算法、狄克斯特拉算法等

  • 动态规划
    是一种解决棘手问题的方法,将问题分成小问题,并先着手解决这些小问题,在逐步解决大问题。

    缺陷:动态规划时,只能考虑取或者不取,没办法只取一部分,而贪婪算法可以解决这种情况,尽可能多拿价值高的。此外,仅当每个子问题都是离散的,即不依赖于其它子问题时,动态规划才有用。

    要设计出动态规划解决方案可能很难,常用“套路”:

    每种动态规划解决方案都涉及网格。

    单元格中的值通常就是你要优化的值。

    每个单元格都是一个子问题,因此你应考虑如何将问题分成子问题,这有助于你找出网格的坐标轴。

使用例子:
输入错别字时,原本输入的判断:如果错输入hish,判断原输入是fish,还vista

最长公共子串

最长公共子串

伪代码

if word_a[i] == word_b[i]:
    cell[i][j] = cell[i-1][j-1]+1
else:
    cell[i][j] = 0

hish和fish的最长公共子串包含三个字母,而hish和vista的最长公共子串包含两个字母。因此很可能原本要输入的是fish。

最长公共子序列

最长公共子序列

结果:


结果

伪代码

if word_a[i] == word_b[j]: #两个字母相同
    cell[i][j] = cell[i-1][j-1] + 1
else:  #两个字母不同
    cell[i][j] = max(cell[i-1][j], cell[i][j-1])

没有放之四海皆准的计算动态规划解决方案公式。

算法

  • 1.二分查找
    使用条件是列表是有序的。通过确定中位数,比较中位数于要查找的数值大小,如果大于查找的,就往中位数左侧查找,若小于查找的值,就往中位数右侧查找,循环运行,直到找到值,或没有值匹配。运行时间为O(logn)

例如在100个数字中查找1,最多需要log100 \approx 7步:

二分查找

代码:

def binary_search(list, item):
    low = 0 #low和high用于跟踪要在其中查找的列表部分
    high = len(list) - 1
    while low <= high: #只要范围没缩小到只剩一个元素
        mid = (low + high)/2 #检测中间元素
        guess = list[mid] 
        if guess == item: #找到元素
            return mid
        if guess > item: #猜测的数字大了
            high = mid - 1
        else:  #猜测的数字小了
            low = mid + 1 
    return None #没有指定元素
  • 2.选择排序:循环查找,第一次循环找到最大的,第二次循环找到第二大的数值,循环知道排序完成。运行时间为O(n^2),此处省略了n^2系数\frac {1}{2}

代码:

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)) #把找到的最小值加入到新数组中
    retrun newArr
  • 3.快速排序:是一种递归排序,基线条件是数组为空或只包含一个数组,递归条件是从元素中选择一个元素,这个元素为基准值(pivot),找出比基准值小的元素及大的元素,分别构成两个子数组,称为分区(partitioning),对子数组进行同样操作,找出基准值,进行分区,直到数组只有一个元素或没有元素。运行时间为O(nlogn)。在C语言标准库中函数qsort实现原理就是快速排序。

代码:

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)
  • 4.广度优先搜索(breadth-first search, BFS):是一种图算法,能够找出两样东西之间的最短距离。与搜素起始节点相连节点的是一度关系,与一度关系的节点相连的节点是二度关系,一度关系胜过二度关系,二度关系胜过三度关系。

搜索的顺序是先找一度关系,再到二度关系中查找,知道找到目标,该路径是最短路径。运行时间为
O(V+E),其中V为顶点(vertie)数,E为边数。

代码:

from collections import deque
search_queue = deque() #创建队列
search_queue += graph["you"] #将邻居加入到搜素队列中
def object_is_target(input): #检查这个是否是查找的目标
    return input[-1] == 'm' #假设查找目标以字母m结尾
while search_queue: #只要队列不为空
    input = search_queue.popleft() #就取出其中第一对象
    if object_is_target(input):
        print ('找到目标')
        return True
    else:
        search_queue += graph[input] #如果不是目标,则将该输入的邻居加入到队列
return False #如果到达这里,说明没有找到目标

该代码的缺点是如果图中节点的邻居有重复的现象,可能把重复的对象重复加入到队列中,造成查找恶性循环,进入死循环。
优化代码:

def search(object):
    search_queue = deque() #创建队列
    search_queue += graph[object] #将邻居加入到搜素队列中
    search = [] #记录查找过的对象
    while search_queue: #只要队列不为空
        input = search_queue.popleft() #就取出其中第一对象
        if not input in searched: #仅当这个对象没有被检查过
            if object_is_target(input):
                print ('找到目标')
                return True
            else:
                search_queue += graph[input] #如果不是目标,则将该输入的邻居加入到队列
                searched.append(input) #标记该对象检查过
return False #如果到达这里,说明没有找到目标
  • 5.狄克斯特拉算法:能够找出加权图中前往X的最短路径。狄克斯特拉算法只适用于有向无环图(directed acyclic graph, DAG),其关键理念是找出图中最便宜的节点,并确保没有到该节点的更便宜路径。此外,如果有负权边,就不能使用狄克斯特拉算法,因为狄克斯特拉算法假设对于处理过的节点,没有前往该节点的更短路径,而这种假设在仅在没有负权边时才成立。如果图中包含负权重,则使用贝尔曼-福德算法(书本未详细介绍,以后补充)

步骤:

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

代码:

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
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 cost[n] > new_cost: #如果经当前节点前往该邻居更近
            cost[n] = new_cost #更新该邻居开销
            parents[n] = node #同时将该邻居的父节点设置为当前节点
    processed.append(node) #标记节点为已经处理过
    node = find_lowest_cost_node(costs) #找出接下处理节点,并循环
  • 6.近似算法(approximation algorithm):在获取精确解需要时间太长时使用。判断优劣标准:运算时间和得到近似解和最优解的接近程度。运行时间为O(n^2),n为元素数量。

代码:

object_need = set(["a","b",...])#创建一个集合,包含所有目标
objects = {} #散列表创建
objects['sone'] = set(['a','c','e'])
objects['stwo'] = set(['b','c','d'])
...#根据图,创建所有集合,保存到objects中
final_objects = set() #创建集合来存储最终选择。
while object_need: #循环需要的目标
    best_object = None
    object_covered = set()
    for s, object in objects.items():
        covered = object_need & object #计算交集
        if len(covered) > len(object_covered):
            best_object = s
            object_covered = covered
    object_need -= object_covered #去除掉已经覆盖的
    final_objects.add(best_object)
贪婪算法
  • 7.K最近邻算法: 可以用于分类和回归。需要考虑最近的邻居。
    分类就是编组。回归就是预测结果。是机器学习的入门方法,没有显示学习过程。

    算法原理:通过计算待分类元素与所有已分类元素距离,计算两位元素的距离时,使用的都是距离公式,距离公式有时也可采用余弦相似度,余弦相似度不计算两个矢量的距离,而比较它们的角度,得到与待分类元素最接近的元素所属的分类,就是待分类元素的类别。

    特征抽取以为这将物品转换称一系列可比较的数字,能否挑选合适的特征事关KNN算法成败。在挑选合适的特征方面,没有放之四海皆准的法则,你必须考虑到各种需要考虑的因素。

    应用:OCR字符识别;创建垃圾邮件过滤器

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 第一章 算法简介 按从快到慢的顺序列出5种大O运行时间: O(log n),也叫对数时间,这样的算法包括二分查找。...
    yytester阅读 454评论 0 0
  • 每次在写东西之前看东看西的,跟朋友聊天,聊的不亦乐乎,看看朋友圈,明明知道还有今天的文章没写,可就是不想打开简书的...
    7c5489ad8902阅读 163评论 1 3
  • 卸尽浮华只为君 一处别院度晨昏 那堪风雨催落花 香魂一缕何处寻
    浮生小姿阅读 891评论 27 23
  • 有可能你看到标题就会想骂傻逼,这个世界上有不敢幸福的人吗?特么净是想而不能好么?我不想生活美满、事业有成吗?我不想...
    深海鱼不吃鱼阅读 318评论 2 1