《算法图解》笔记(完结)

看书还得写笔记,文字的写不来,还是写电子的,自己的字跟狗爬一样,打出来的字好多了。

后续把自己看的基本关于网络的书也写点博客,一便于查寻,二便于加强记忆,要不然跟小说一样,看了就忘了。

 

第1章:算法介绍

理解大O表示法,并非以秒为单位。大O表示法让你能够比较操作数,它指出了算法运行时间的增速。

 

大O表示法说的是在查找情况中最糟的情形。

从快到慢晕倒的5种大O运行时间。

O(log n),也叫对数时间,这样的算法包括二分查找。

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

O(n * log n),这样的算法包括快速排序---一种比较快的排序算法

O(n2)【表示n的平方】, 这样的算法包括选择排序---一种速度较慢的排序算法

O(n!), 这样的算法包括旅行商的解决方案---一种非常慢的算法

 

第一章主要理解:

算法的速度指的并非时间,而是操作数的增速。

讨论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加。

算法的运行时间用大O表示法表示。

O(log n)比O(n)快,当需要搜索的元素越多时,前者比后者快很多。

 

小结:

二分查找的速度比简单查找快很多。

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

算法运行时间并不以秒为单位。

算法运行时间是从其增速的角度度量的。

算法运行时间用大O表示法表示

 

最后上书中的二分查找代码

def binary_search(list, item):
    # 初始化序列的开始序列号,为末尾的序列号
    low = 0
    high = len(list) - 1
    # 只有在开始的序列号小于等于结束的序列号,才执行2分,否则就是找不到元素
    while low <= high:
        # 地板除取出中间值
        middle = (low + high) // 2
        # 取出中间值的值
        guess = list[middle]
        # 如果是的话,就返回这个索引
        if guess == item:
            return middle
        # 当取出来的中间值比要帅选的值大,按取出来中间值的前一位索引就是下一次寻找的结尾。
        elif guess > item:
            high = middle - 1
        # 反之,下一次查找的开始索引中间值的后一位索引,这里我还是比较容易搞混的
        else:
            low = middle + 1
    return None


if __name__ == '__main__':
    print(binary_search('123456', '2'))

 

第2章 选择排序

主要学习数组与链表

数组与链表的运行时间

        数组    链表

读取   O(1)   O(n)

插入    O(n)   O(1)

删除     O(n)   O(1)

 

这里指出一下,仅当能够立即访问要删除的元素时,删除操作的运行时间才为O(1)。通常我们都记录了链表的第一个元素和最后一个元素,因此删除这些元素时运行时间为O(1)

 

选择排序的时间:O(n2)【表示n的平方】

小结:

计算机的内存犹如一大堆抽屉。

需要存储多个元素时,可使用数组或者链表

数组的元素都在一起

链表的元素是分开的,其中每个元素都存储了下一个元素的地址

数组的读取速度很快

链表的插入和删除速度很快

在同一个数组中,所有元素的类型都必须相同(都为int,double等)

 

代码:

这是我自己写的:

def my_sort(arr):
    # 首相取选址范围,从大到小取,最大为最大索引,最小为0
    for i in range(len(arr) - 1, -1, -1):
        # 开始循环对数组的数据进行比较
        for i in range(i):
            # 如果前面的数字大于后面的数字,两个数字互相,确保后面的数字大
            if arr[i] > arr[i + 1]:
                arr[i], arr[i + 1] = arr[i + 1], arr[i]
    return arr


if __name__ == '__main__':
    print(my_sort([9, 3, 33, 3, 2, 1, 5, 6]))

 

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

if __name__ == '__main__':
    print(selectionSort(list('6754345678987654')))

 选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

我写的不用额外占用内存空间,书中的代码还是需要额外新建一个新列表,但书中的代码更加容易理解,而且逻辑也很漂亮

 

第三章 递归

递归时我最讨厌的主题,希望书中学完,能够让我爱上它一点

 

实际使用中,使用循环的性能更好。高手在Stark Overflow上说过:如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要

 

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

书中举例了一个好简单的例子,真的很基础,但讲的不错。

def fact(x):
    if x == 1:
        return 1
    else:
        return x * fact(x-1)

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

书p39页,结合盒子的例子。这个栈包含未完成的函数调用,每个函数调用都包含未检查完的盒子。使用栈很方便,因为你无需自己跟踪盒子堆-栈替你这样做了。

原来Python确实有递归次数限制,默认最大次数为998

 

小结:

递归指的是调用自己的函数

每个递归函数都有两个条件:基线条件和递归条件

栈有两种操作:压入和弹出

所有函数调用都进入调用栈

调用栈可能很长,这将占用大量的内存

 

第4章 快速排序

学习分而治之和快速排序。分而治之是本书学习的第一种通用的解决方法。

学习快速排序---一种常用的优雅的排序算法。快速排序使用分而治之的策略。

 

分而治之D&G(divide and cpnquer)

工作原理:

找出基线条件,这个条件必须尽可能的简单

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

 

涉及数组的递归函数时,基线条件通常是数组为空或只包含一个元素。陷入困境时,请检查基线条件是不是这样的。

 

书中的3道编程题,没能写出来,只能抄答案了。

请编写书中要求sum函数的代码

def sum(arr):
    if len(arr) == 0:
        return 0
    # 把第一个值取出来,后面的进行递归,当只有一个元素的arr会满足基线条件
    return arr[0] + sum(arr[1:])

if __name__ == '__main__':
    print(sum(list(range(997))))

 编写一个递归函数来计算列表包含的元素数:

def count(arr):
    if arr == []:
        return 0
    return 1 + count(arr[1:])

if __name__ == '__main__':
    print(count(list(range(100))))

 跟第一个原理差不多

找出列表中最大的数字

def find_max_num(arr):
    # 当两个元素的时候,进行比较,返回最大值,基线条件
    if len(arr) == 2:
        return arr[0] if arr[0] > arr[1] else arr[1]
    # 递归条件,拆分后面索引1的元素到最后的元素进行递归条件。
    sun_max = find_max_num(arr[1:])
    return arr[0] if arr[0] > sun_max else sun_max

# 这个我自己真心写不出来,看的我都有点绕了
if __name__ == '__main__':
    print(find_max_num([1, 2, 3, 4, 99, 5]))

 

正式进入快速排序:

def quick_sort(array):
    '''快速排序'''
    # 基线条件,当只有一个或0个元素的时候饭返回本身
    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 quick_sort(less) + [pivot] + quick_sort(greater)


if __name__ == '__main__':
    print(quick_sort([1, 5, 3, 11, 6, 6, 3, 2, ]))

 

小结:

D&C将问题逐步分解。使用D&C处理列表时,基线条件很可能是空数组或只包含一个元素的数组

实现快速排序时,请随机的选择用作基准值的元素。快速排序的平均运行时间为O(nlongn)。

大O表示法中的常量有时候事关重大,这就时快速排序比合并排序快的原因所在

比较简单查找和二分查找,常量几乎无关紧要,因为列表很长时,O(logn)的速度比O(n)快很多。

 

第五章 散列表

散列表---最有用的基本数据结构之一。

 

散列函数是这样的函数,既无论你给它什么数据,它都还你一个数字。

专业术语表达的话,散列函数"将输入映射到数字"

 

小的小结

散列表适合用于(书中介绍的其实就时Python中的字典数据格式)

模拟映射关系

防止重复

缓存/记住数据,以免服务器再通过处理来生成它们

 

散列冲突,既两个键映射到了同一个位置,最简单的解决方法,在这个位置存储一个链表。

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

 

散列表在平均情况下,操作速度与数组一样快,而插入和删除的速度与链表一样快。但在槽糕的情况下,散列表的各种操作就慢了。

所以为了避免冲突,需要较低的填装因子,良好的散列函数。

 

装填因子越低,发生冲突的可能性越小,一般装填因子大于0.7,就调整散列表的长度。

 

本章小结

散列表时一种功能强大的数据结构,其操作速度快,还能让你以不同的方式建立数据模型。

你可以结合散列函数和数组来创建散列表。

冲突很糟糕,你应该使用可以最大限度减少冲突的散列函数。

散列表的查找,插入和删除速度都非常快。

散列表适合用于模拟映射关系。

一旦装填因子超过0.7,就该调整散列表的长度.

散列表可用于缓存数据。

散列表非常适用于防止重复。

 

第六章 广度优先搜索

广度优先主要用于非加权图寻找最短路径。

 

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

 

单线箭头的叫有向图,双向箭头或者直线为无向图

 

队列跟栈不同,一个先进先出,一个先进后出

 

书中的广度优先代码,用collections.deque的双端队列。

from collections import deque

searched = []
def search(name):
    search_queue = deque()
    # 将要查寻的数据放入队列
    search_queue += graph(name)
    # 只要有数据就一直执行
    while search_queue:
        # 取出一个数据
        person = search_queue.popleft()
        # 判断是否符合条件
        if person not 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

 

运行时间大O表示法为O(V+E)V为端点的数量,E为边数

 

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

 

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

 

小结

广度优先搜索指出是否有从A到B的路径,如果有,官渡优先可搜索出最短路径

面临类似于寻找最短路径的问题时,可尝试使用图来建立模型,再使用广度优先搜索来解决问题。

有向图中的边为箭头,箭头的方向指定了关系的方向。

无向图中的边不带箭头,其中的关系是双向的。

队列是先进先出FIFO的,栈是先进后出的FILO的

你需要按加入顺序检查搜索列表中的人,否则找到的就不是最短路径,因此搜索列表必须的是队列。

对于检查过的人,务必不要再去检查,否则可能导致无限循环。

 

第七章 迪克斯特拉算法

计算加权图的最短路径。

迪克斯特拉的关键4个步骤:

找出最便宜的节点,即可在最短时间内前往的节点

对于该节点的邻居,检查是否有前往它们的更短路径,如果有,就更新其开销

重复这个过程,直到对图中的每个节点都这样做了。

计算最终路径

 

专业术语介绍:

迪克斯特拉算法用于每条边都有关联数字的图,这些数字称为权重。

带权重的图成为加权图,不带权重的图称为非加权图。

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

 

迪克斯特拉算法只适用与有向无环图

 

书中的案例,我觉的最关键的是在重复操作每个节点的时候,是寻找最便宜的节点,对于起点的节点默认开销为无穷大float(inf)

 

迪克斯特拉算法不能用于包含负权边的图,在包含负权边的图中,使用贝尔曼-富德算法。

 

代码实现:

参考这个吧:https://www.jianshu.com/p/629e6c99dfca

代码我后续自己在抄写一下。

 

processed = []

# costs={} costs['a'] = xx, coats['b'] = yy...
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



'''
graph = {}
graph["Start"] = {}
graph["Start"]["A"] = 6
graph["Start"]["B"] = 2
graph["A"] = {}
graph["A"]["End"] = 4
graph["B"] = {}
graph["B"]["C"] = 1
'''


'''
parents = {}
parents["A"] = "Start"
parents["B"] = "Start"
parents["C"] = None
parents["End"] = None
'''
node = find_lowest_cost_node(costs)
# 只要返回节点
while node is not None:
    cost = costs[node]
    # 每一个节点都市字典形式,保存的邻居的信息与到邻居的开销
    neigbors = graph[node]
    # 遍历所有的邻居
    for n in neigbors.keys():
        # 邻居的从起点到邻居节点的新开销值
        new_cost = cost + neigbors[n]
        # 如果新开销值小于原来的开销值
        if costs[n] > new_cost:
            costs[n] = new_cost
            # 更新父节点字典
            parents[n] = node
    # 放入已经处理节点
    processed.append(node)
    # 继续执行
    node = find_lowest_cost_node(costs)
    

 每一行都注释了,很巧妙的算法,读取每一个节点的信息,算法是理解了,不知道能记住多久

迪杰斯特拉算法(Dijkstra)是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。牛逼

 

小结:

广度优先搜索用于在非加权图中查找最短路径

迪杰斯特拉算法用于在加权图中寻找最短路径

迪克斯特拉算法不能用于包含负权边的图,在包含负权边的图中,使用贝尔曼-富德算法。

 

第8章 贪婪算法

 

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

 

书中一个集合覆盖问题,用贪婪算法实现。

states_needed = set(['mt', 'wa', 'or', 'id', 'nv', 'ut', 'ca', 'za'])

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', 'za'])

final_stations = set()

# 只要还有空确的元素
while states_needed:
    best_stations = None
    states_covered = set()
    # 循环读取每个站点
    for station, states in stations.items():
        covered = states_needed & states
        # 将元素最多的站点先加入
        if len(covered) > len(states_covered):
            best_stations = station
            states_covered = covered
    # 需要的元素减去已经有的元素的站点,剩下需要的元素
    states_needed -= states_covered
    # 将该站点加入
    final_stations.add(best_stations)

# 答案不唯一,set为无序,dict也为无序的
print(final_stations)

 

广度优先搜索与迪克斯特拉算法都算贪婪算法

 

NP完整问题主要就是判断什么是NP完整问题,如果是NP完整问题,就可以使用近似算法既可。

 

判断NP方法的一些条件:

元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会变得非常慢。

涉及"所有组合"的问题通常是NP完全问题。

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

如果问题涉及序列(如旅行商问题中的城市)且难以解决,它可能就是NP完全问题。

如果问题涉及集合(如广播台集合)且难以解决,它可能就是NP完全问题。

如果问题可转换为集合覆盖问题或旅行商问题,那它肯定是NP完全问题。

 

小结:

贪婪算法选择局部最优解,企图以这种方式获得全局最优解。

对于NP完全问题,还没有找到快速解决方案。

面临NP完全问题时,最佳的做法是使用近似算法。

贪婪算法易于实现、运行速度快,是不错的近似算法。

 

第9章 动态规划

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

 

每个动态规划的算法都是从一个网格开始。尝试用书中的背包问题解决方法,手动写的话,确实比较累。

使用动态规划时,要么考虑拿走整件商品,要么考虑不拿,而没法判断该不该拿走商品的一部分。

 

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

 

最长公共子串与最长公共子序列都是使用表格法解决的实例。

代码如下,相对来说,最长公共子序列的匹配度更好。

先上最佳公共子串

 

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

 

 对于最长子串问题,答案为网格中最大的数字---它可能并不位于最后的单元格中

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

 在最长子序列,如果两个字母不同,就选择上方或者左边邻居中较大的那个。

 

小结:

需要在给定约束条件下优化某种指标时,动态规划很有用。

问题可分解为离散子问题时,可使用动态规划来解决。

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

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

每个单元格都时一个子问题,因此你需要考虑如何将问题分解为子问题。

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

 

第10章 K最近邻算法

KNN(k-nearest neighbours)算法

完成两项基本的工作:

分类就时编组

回归就时预测结果

使用KNN经常使用的时余弦相似度。

 

选择邻居个数一般为sqrt(n)个数,n为总量

 

小结:

KNN用于分类和回归,需要考虑最近的邻居

分类就时编组

回归就时预测结果

特征抽取意味着将物品装换为一系列可比较的数字

能否挑选何时的特征事关KNN算法的成败。

 

第11章 接下来如何做

 

二叉树,对于其中的每一个节点,左子节点的值都比它小,而右子节点的值都比它大

二叉查找书,平均运算时间为O(logn),但在最糟糕的情况下需要的时间为O(n)

数组的查找,插入、删除,大O表示法为:O(logn),O(n),O(n)

二叉查找树都为O(logn)

数据库或者高级数据库使用B树,红黑树,堆,伸展树。(不懂)

 

后面简单介绍了一些概念,并行执行,等等,讲的非常浅,就不写了。

 

整体4天断断续续把这本书看完,对我的基础认识有不少提高,但后续讲的太少了,很多一笔带过。前面的基础讲的很仔细,好书推荐。

后面准备看Python数据结构与算法分析。

为最后的搬砖脚本添加算法基础。

 

posted @ 2020-03-09 01:57  就是想学习  阅读(1706)  评论(0编辑  收藏  举报