zoukankan      html  css  js  c++  java
  • 《算法图解》笔记(完结)

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

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

    第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数据结构与算法分析。

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

  • 相关阅读:
    Java中的国际化
    springcloud介绍
    SpringMVC之请求和响应
    JAVA坦克大战系列10-高效雷达(下)
    CF920F SUM and REPLACE
    luoguP4141 消失之物
    luoguP2843 暗杀
    luoguP5521 [yLOI2019] 梅深不见冬
    CF940E Cashback
    CF1051D Bicolorings
  • 原文地址:https://www.cnblogs.com/sidianok/p/12446291.html
Copyright © 2011-2022 走看看