《算法图解》读后思考+习题精做+课外知识点拓展

前言

  1. 这本算法图解中的主要内容算法在我看之前,我都全部都有学过,但是这次读了还是收获很多,对于递归、散列和动态规划有了更深的理解,书中生动的现实例子帮我真正理解了自己所学过的数据结构和算法的现实意义,让我觉得原来自己学过的东西是这样用的
  2. 虽说这本书书名是算法图解,但我在阅读过程中,那些图倒是没对我起什么帮助,我觉得这本中对于我最大的意义就是那些关于数据结构和算法的生动例子,从生活面切入让我看到了更多和课堂上学到不一样的东西。
  3. 本次博客,主要写一些我读后的思考和旧知识的新理解,以及我觉得一些有意思的习题,我将他们记录下来。同时,在读的过程中,我看到了很多新名词,这边也做一个简单的拓展。这样的话每接触一本新书,就把新东西记录一下,积少成多,最后就啥都略懂一点啦。

第一章 算法简介

  • 我们用大O表示法来表示算法的运行时间(时间复杂度),但是它表示的是算法运行的速度随着操作数据的增加,其也会跟着进行怎么样的速度变化
  • 用这样的方式去权衡算法的速度,可以帮助我们比较不同算法之间的优劣性

在这里插入图片描述


第三章 递归

  • 递归是一种解决问题的思想方法,他并不一定是在算法性能上的好办法,但是他是一种思考问题的好方法。

Leigh Caldwell在Stack Overflow上说的一句话:
“如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。”
“Loops may achieve a performance gain for your program. Recursion may achieve a performance gain for your programmer. Choose which is more important in your situation!”

  • 递归函数都有两部分:基线条件(base case)递归条件(recursive case)
  • 递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己。然后递归执行过程中会涉及到函数调用栈的概念。

1.尾递归

  • 如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。

  • 当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

  • 当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。


第四章 快速排序

  • 通过学习快速排序这种分而治之的思想,我们来进一步去理解递归的应用。
  • 先找出符合问题答案的最简单判断条件,然后再这个条件去不断缩小问题规模直至符合递归出口条件为止。基线条件不好找,你需要考虑最简单极端的情况,然后再一点分解问题。
  • 实现递归时,很重要的思想就是要对问题有一个整体性看待,可以直接先把这个函数已经实现了来看待,然后再去分解问题。反正这是一种很神奇的方法,可能不好理解,但是最后就是能有效解决问题。多积累一些问题的常用递归经验,更能帮助我们去理解。

在这里插入图片描述

快速排序的优点和性能

  • 快速排序的平均运行时间为 O ( n l o g n ) O(n logn) O(nlogn),但其最坏情况为 O ( n 2 ) O(n^2) O(n2),即使如此,快速排序依旧是性能最好的排序算法,因为相对于遇上最糟情况,它遇上平均情况的可能性要大得多,可以说是平均情况下最快的排序算法,而且因为快速算法的常数小(每一次算法运行的固定时间),即使有算法跟他时间复杂度一样,常数也没有快速排序小
  • 快速排序的性能高度依赖于每次选择的基准值,其实可以将快速排序的最坏情况下的时间复杂度提升为 O ( l o g n ) O(log n) O(logn) 只需在每次选择基准值时都进行随机选择,而不是每次都指定一个位置的值作为基准值.

习题练习

  • 在写习题时突然明白了递归的本质,我们需要不断去缩小问题规模,
  • 假设该功能已经实现,然后用整体的思想将问题的求解分解出最小的一步,先把递归条件写出来,然后再考虑最极端最底层的时候,会发生什么,这时再写出基线条件,然后我们这样就有了大概思路了,就算这时代码调试报错我们只要思路正确也能很快调整回来

1. 请编写前述sum函数的代码。(数字求和的递归方法)

  • 这边有一个很巧妙的技巧,如何在递归过程中把列表中的数一个个踢出去?这里使用了序列的切片,不断截取列表中的第1个元素到结尾,而不去截取第0个元素。
#递归求和
def sum(list):
    if list == []:
        return 0
    return list[0] + sum(list[1:])   #把问题分解成一个一个数的相加,这就是最简单的相加方式
    #对数组进行切片的同时,也是在把数组的第0个元素不断剔除出去,这就是实现sum递归的关键难点所在

print(sum([1,2,3,4,5,6,7])) #28

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

#递归函数计算列表包含的元素数
def count(list):
    if len(list) == 0: #list == [] 判断列表为空也行
        return 0
    return 1 + count(list[1:])
# 和递归实现sum函数一样的思路

print(count( [1,2,3,4,5,6,7] )) #7

3. 找出列表中最大的数字。

  • 由于我Python用的没有C++多也不够熟悉一些用法,所以自己只写了一个残缺版本,虽然我是get到了这个问题实现递归的关键,但是有些细节我没处理好,只能实现在正数中找到最大值。
  • 同时在设计这个算法过程中,我才知道Python中并没有C++中的? :三目运算符,而是使用已有的if else语句改进的三目运算符。使用 if else 实现三目运算符(条件运算符)的格式如下:exp1 if contion else exp2,如果 if 中的条件成立,那么就实现exp1表达式,否则就实现 exp2表达式。
def max1(list): #只能比较正数中的最大值
    if list == []:
        return -1  # 我不知道怎么找到最小的值,让任何数和它相比都比它大
        
    return max(list[1:]) if list[0] < max(list[1:]) else list[0]
# 在想着使用?:三目运算符时,发现Python没有引入? :这个新的运算符,
# 而是使用已有的 if else 关键字来实现相同的功能。 exp1 if contion else exp2
  • 作者的写法更简洁,把情况分得更细了,这样可以对任何列表进行取最大值了,他这边是把列表中只剩一个数字作为递归出口,但我自己的写法是列表为空,或许以后想不通问题可以多往前走一步。
# 更简洁的做法,把情况分得更细了,这样可以对任何列表进行取最大值了
def max_(lst):
  if len(lst) == 0:  # 其实不会发生这种情况,因为当列表中只剩一个数的时候,其实就已经开始递归return上去了
    return None
  if len(lst) == 1:  #只剩下一个数,这样列表中最大的数肯定就是这个数了,这时就是最简单的情况了
    return lst[0]
  else:
    sub_max = max_(lst[1:])
    return lst[0] if lst[0] > sub_max else sub_max

print( max([1,2,3,4,5,6,7,8,-1]))

4. 找出二分查找算法的基线条件和递归条件

def search_recursive(list, low, high, item):
    # Check base case
    if high >= low:   #递归二分的做法其实和循环二分差不多,不过在二分上用不着递归实现

        mid = (high + low) // 2
        guess = list[mid]

        # If element is present at the middle itself
        if guess == item:
            return mid

            # If element is smaller than mid, then it can only
        # be present in left subarray
        elif guess > item:
            return search_recursive(list, low, mid - 1, item)

            # Else the element can only be present in right subarray
        else:
            return search_recursive(list, mid + 1, high, item)

    else:
        # Element is not present in the array 没找到
        return None


1. 可汗学院

  • 这个名词我不也是第一次听说了,之前在《白话机器学习的数学》中也有被提及到,然后可汗学院也有着专门的中国网站,上面都是汉化好的内容。

可汗学院(Khan Academy),是由孟加拉裔美国人萨尔曼·可汗创立的一家教育性非营利组织,主旨在于利用网络影片进行免费授课,现有关于数学、历史、金融、物理、化学、生物、天文学等科目的内容,教学影片超过2000段,机构的使命是加快各年龄学生的学习速度。

  • 可以理解为免费的高质量学习网站,可汗学院在美国影响十分的大,甚至一些大学课程都直接让学生自己在可汗学院上自学,然后教师负责一些问题的答疑。比尔·盖茨曾说,可汗的成功“令人难以置信”:“我和孩子也经常使用‘可汗学院’。他是一个先锋,他借助技术手段,帮助大众获取知识、认清自己的位置,这简直引领了一场革命

2. 函数式编程语言Haskell

函数式编程是种编程方式,它将电脑运算视为函数的计算。函数编程语言最重要的基础是λ演算(lambda calculus),而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值)。
指令式编程相比,函数式编程强调函数的计算比指令的执行重要。
过程化编程相比,函数式编程里函数的计算可随时调用。

简单说,“函数式编程"是一种"编程范式”(programming paradigm),也就是如何编写程序的方法论。它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。


  • Haskell就是一门纯粹的函数式语言。

Haskell(发音为 /ˈhæskəl/)是一种标准化的,通用纯函数式编程语言,有非限定性语义和强静态类型。它的命名源自美国逻辑学家Haskell Brooks Curry,他在数学逻辑方面的工作使得函数式编程语言有了广泛的基础。在Haskell中,“函数是一等公民”。

作为函数式编程语言,主要控制结构是函数。Haskell语言是1990年在编程语言Miranda的基础上标准化的,并且以λ演算(Lambda-Calculus)为基础发展而来。具有“证明即程序、结论公式即程序类型”的特征。这也是Haskell语言以希腊字母「λ」(Lambda)作为自己标志的原因。Haskell语言的最重要的两个应用是GHC(Glasgow
Haskell Compiler)和Hugs(一个Haskell语言的解释器)


第五章 散列表

  • 散列函数+数组= 散列表,散列表是一种包含额外逻辑的数据结构。散列表可以用于:模拟映射关系;防止重复;缓存数据。
  • 为了避免散列函数冲突,我们需要有:较好的填装因子和良好的散列函数。

填装因子

  • 散列表的填装因子表示散列表中数据的饱和程度或说空间的拥挤程度。用散列表中包含的元素数在散列表中的位置总数的占比就可以表示散列表了。

在这里插入图片描述

  • 填装因子度量的是散列表中有多少位置是被装填的,填装因子大于一意味着存储元素已经超过了数组的位置。一旦填装因子开始增大,你就需要在散列表中添加位置,这被称为调整长度。

  • 填装因子越低,发生冲突的可能性越小,散列表的性能越高。一个不错的经验规则是:一旦填装因子大于0.7,就调整散列表的长度。

  • 调整长度的开销很大,但平均而言,即便考虑到调整长度所需的时间,散列表操作所需的时间也为 O ( 1 ) O(1) O(1) 。因为从整体上来看,散列表的长度调整的开销大的缺点,抵不过散列表的元素映射功能过于强大的有点。

在这里插入图片描述


第七章 Dijkstra 迪杰斯特拉算法

  • 广度优先搜索不适合用于带权值的图,或者说BFS适用于权值为1的加权图,BFS算出来的最短路径只能是路径段数最短,若是路径带权值的话,就不一定是最短路径了。
  • 此时我们可以使用Dijstra算法,它可以用于计算加权图的最短路径,但它并不是万能的,它只适应于有向无环图,而且所有权值都要是正的才行。同时该算法是单源最短路,即只能计算从某一点出发到其他所有点的最短路径。

算法总结

这个算法我很早就学过了,这次看了这部分的内容介绍后又有更生动的理解。在没有负权边的情况下,迪杰斯特拉算法所做的就是 “步步为营”,只要是该算法每次循环处理过的点,就是该点的最终状态了、


1. Bellman-Ford 算法

  • 当权值有负边时,使用Dijstra算法就有可能陷入负无穷的情况,这时就可以使用Bellman-Ford算法,它可以通过迭代来判断是否有负环,其实更好的解决负权边问题是使用SPFA算法,虽然在这边书没有提到过,但是我以前准备算法比赛的时候,还是都有学过,这边不做过多赘述了。

第八章 NP完全问题

  • NP就是Non-deterministic Polynomial的问题,也即是多项式复杂程度的非确定性问题。而如果任何一个NP问题都能通过一个多项式时间算法转换为某个NP问题,那么这个NP问题就称为NP完全问题(Non-deterministic Polynomial complete problem)。NP完全问题也叫做NPC问题

1. 非确定性问题

  • 有些问题的答案,是无法直接计算得到的(或者说直接计算代价过于大,以至于无法实现),只能通过间接的 “猜算” 来得到结果。这就是非确定性问题

2. 多项式非确定问题

  • 而这些问题的通常有个算法,它不能直接告诉你答案是什么,但可以告诉你,某个可能的结果是正确的答案还是错误的。(只能针对性运算,就像考研做高数填空题时,我们可以轻易通过题目条件和经验猜想出符合题目条件的数学等式,但是我们并不能直接猜出一般性通用等式)
  • 这个可以告诉你“猜算”的答案正确与否的算法,假如可以在 多项式时间 内算出来,就叫做 多项式非确定性问题。而如果这个问题的所有可能答案,都是可以在多项式时间内进行正确与否的验算的话,就叫 完全多项式非确定问题

3. 如何解决这类问题?

  • 完全多项式非确定性问题可以用穷举法得到答案,一个个检验下去,最终便能得到结果。但是这样算法的复杂程度,是指数关系,因此计算的时间随问题的复杂程度成指数的增长,很快便变得不可计算了。

(但是除了穷举法暴力求解,我们是很难找到方法求出精确解的,在获得精确解需要的时间太长时,这时可使用近似算法,例如本书提到的贪心算法,虽然贪心算法也有严格的使用条件)

  • 人们发现,所有的完全多项式非确定性问题,都可以转换为一类叫做满足性问题的逻辑运算问题。既然这类问题的所有可能答案,都可以在多项式时间内计算,人们于是就猜想,是否这类问题存在一个确定性算法,可以在多项式时间内直接算出或是搜寻出正确的答案呢?这就是著名的NP=P?的猜想。

  • 解决这个猜想,无非两种可能,一种是找到一个这样的算法,只要针对某个特定NP完全问题找到一个算法,所有这类问题都可以迎刃而解了,因为他们可以转化为同一个问题。另外的一种可能,就是这样的算法是不存在的。那么就要从数学理论上证明它为什么不存在。


第九章 动态规划

  • 动态规划先解决子问题,再解决大问题,是一种以空间换时间的算法,跟算法题打表有点像
  • 这本书用了一个小偷去商场偷东西用4磅背包意图使得背包内物品价值最大的例子,来说明动态规划的具体执行过程。书中用网格法来表示每次动态规划的结果。虽说我们要求的是4磅背包的最大价值,但是我们还是要从最底层的情况开始慢慢往上算。

在这里插入图片描述

  • 那么网格中的每一个单元格是如何得出的呢?具体如下,我在书中做好了详细的说明解释。

在这里插入图片描述

动态规划中的细节

  1. 每次迭代时,存储的都是当前的最大价值,所以单元格内的值只会越来越大。(当前的值 >= {上一轮的值,本轮+剩余重量的最大价值})
  2. 网格中行的排列顺序发生变化并不会对最终结果产生影响,因为在算法执行过程中每一个物品都会被轮流到,所以先后顺序无所谓,因为这是最值问题。但是若是列的排列顺序发生变化,就可能对最终答案有影响。
  3. 网格的列的基本单位,是受到商品价值的粒度所影响的。要是新加入商品的价值很小,这时就需要考虑修改网格列的粒度了。
  4. 只有当问题分解后的子问题都是离散的,即子问题之间不能够相互依赖、相互影响,我们才能用动态规划解决问题,动态规划解决不了子问题相互依赖的情况。

习题练习

在这里插入图片描述

  • 详细解释如下

在这里插入图片描述


第十章 K最近邻算法

  • 将所有训练数据的进行特征提取,然后如果要把某一点进行分类,在特征空间中上我们可以其最邻近的K个数据的分类来判断该点的分类。这是一种很简单的分类算法,比较麻烦的地方在于如何选择合适的特征进行比较不同点之间的相似度。

邻近算法,或者说K最邻近(KNN,K-NearestNeighbor)分类算法是数据挖掘分类技术中最简单的方法之一。所谓K最近邻,就是K个最近的邻居的意思,说的是每个样本都可以用它最接近的K个邻近值来代表。近邻算法就是将数据集合中每一个记录进行分类的方法 。

核心思想

  • KNN算法的核心思想是,如果一个样本在特征空间中的K个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。该方法在确定分类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。KNN方法在类别决策时,只与极少量的相邻样本有关。

1. 余弦相似度

余弦相似度不计算两个矢量的距离,而是比较较它们的角度,书中的对水果进行分类的例子,使用的是距离公式,在实际工作中,使用更多的余弦相似度。

  • 余弦相似性通过测量两个向量的夹角的余弦值来度量它们之间的相似性。0度角的余弦值是1,而其他任何角度的余弦值都不大于1;并且其最小值是-1。从而两个向量之间的角度的余弦值确定两个向量是否大致指向相同的方向。
  • 两个向量有相同的指向时,余弦相似度的值为1;两个向量夹角为90°时,余弦相似度的值为0;两个向量指向完全相反的方向时,余弦相似度的值为-1。这结果是与向量的长度无关的,仅仅与向量的指向方向相关。余弦相似度通常用于正空间,因此给出的值为-1到1之间。
  • 注意这上下界对任何维度的向量空间中都适用,而且余弦相似性最常用于高维正空间。例如在信息检索中,每个词项被赋予不同的维度,而一个维度由一个向量表示,其各个维度上的值对应于该词项在文档中出现的频率。余弦相似度因此可以给出两篇文档在其主题方面的相似度。
  • 另外,它通常用于文本挖掘中的文件比较。此外,在数据挖掘领域中,会用到它来度量集群内部的凝聚力。

参考资料

  1. 作者个人博客
  2. 出版社官网
  3. GIthub 书中代码示例
  4. NP完全问题的百度百科
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值