《算法图解》笔记(完结)
看书还得写笔记,文字的写不来,还是写电子的,自己的字跟狗爬一样,打出来的字好多了。
后续把自己看的基本关于网络的书也写点博客,一便于查寻,二便于加强记忆,要不然跟小说一样,看了就忘了。
第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数据结构与算法分析。
为最后的搬砖脚本添加算法基础。