欢迎关注WX公众号:【程序员管小亮】

python学习之路 - 从入门到精通到大师

文章目录

  • 欢迎关注WX公众号:【程序员管小亮】
    • [python学习之路 - 从入门到精通到大师](https://blog.csdn.net/TeFuirnever/article/details/90017382)
    • 一、分而治之
      • 1)例子一
      • 2)例子二
    • 二、快速排序
    • 三、再谈大O 表示法
      • 1)比较合并排序和快速排序
      • 2)平均情况和最糟情况
    • 四、总结
    • 参考文章

一、分而治之

《算法图解》学习笔记(三):递归和栈(附代码) 深入介绍了递归。我们将探索 分而治之(divide and conquer,D&C) —— 一种著名的递归式问题解决方法。只能解决一种问题的算法毕竟用处有限,而D&C提供了解决问题的思路,是另一个可供使用的工具。面对新问题时,你不再束手无策,而是自问:“使用分而治之能解决吗?”

D&C好像很不错,那我们就一直用这个方法好了,然而它并不那么容易掌握,先来看示例。

1)例子一

假设你是农场主,有一小块土地(其实面积也不小了,168 x 64 = 10752 m 2 m^2 m2)。

你要将这块地均匀地分成方块,且分出的方块要尽可能大。一共有下面三种方法:

显然,上面的分法都不符合要求。那么如何将一块地均匀地分成方块,并确保分出的方块是最大的呢?

答案是使用D&C策略!D&C算法是递归的。使用D&C解决问题的过程包括两个步骤:

  1. 找出基线条件,这种条件必须尽可能简单。
  2. 不断将问题分解(或者说缩小规模),直到符合基线条件。

下面就来使用D&C来找出前述问题的解决方案,可你能使用的最大方块有多大呢?

首先,找出基线条件。《算法图解》学习笔记(三):递归和栈(附代码) 中我们提到过,基线条件很重要!!!

最容易处理的情况是,一条边的长度是另一条边的整数倍。

如果一边长25 m,另一边长50 m,那么可使用的最大方块为 25 m × 25 m。换言之,可以将这块地分成两个这样的方块。

现在需要找出 递归条件,这正是D&C的用武之地。

定义:所谓 分而治之 就是把一个复杂的算法问题按一定的“分解”方法分为等价的规模较小的若干部分,然后逐个解决,分别找出各部分的解,把各部分的解组成整个问题的解,这种朴素的思想来源于人们生活与工作的经验,也完全适合于技术领域。诸如软件的体系结构设计、模块化设计都是分而治之的具体表现。

根据D&C的定义,每次递归调用都必须缩小问题的规模。如何缩小前述问题的规模呢?我们首先找出这块地可容纳的最大方块。

你可以从这块地中划出两个640 m × 640 m的方块,同时余下一小块地。现在是顿悟时刻:何不对余下的那一小块地使用相同的算法呢?

最初要划分的土地尺寸为1680 m × 640 m,而现在要划分的土地更小,为640 m × 400 m。适用于这小块地的最大方块,也是适用于整块地的最大方块。换言之,你将均匀划分1680 m × 640 m土地的问题,简化成了均匀划分640 m × 400 m土地的问题!

下面再次使用同样的算法。对于640 m × 400 m的土地,可从中划出的最大方块为400 m × 400 m。

这将余下一块更小的土地,其尺寸为400 m × 240 m。

你可从这块土地中划出最大的方块,余下一块更小的土地,其尺寸为240 m × 160 m。

接下来,从这块土地中划出最大的方块,余下一块更小的土地。

余下的这块土地满足基线条件,因为160 是80 的整数倍。将这块土地分成两个方块后,将不会余下任何土地!

因此,对于最初的那片土地,适用的最大方块为80 m × 80 m。

这里重申一下D&C的工作原理:

  1. 找出简单的基线条件;
  2. 确定如何缩小问题的规模,使其符合基线条件。

D&C并非可用于解决问题的算法,而是一种解决问题的思路。

2)例子二

我们再来看一个例子。给定一个数字数组:

你需要将这些数字相加,并返回结果。使用循环很容易完成这种任务。

def sum(arr):total = 0for x in arr:total += xreturn totalprint(sum([1, 2, 3, 4]))


但如何使用递归函数来完成这种任务呢?

第一步:找出基线条件。最简单的数组什么样呢?请想想这个问题,再接着往下读。如果数组不包含任何元素或只包含一个元素,计算总和将非常容易。

因此这就是基线条件。

第二步:每次递归调用都必须离空数组更近一步。如何缩小问题的规模呢?下面是一种办法。

这与下面的版本等效。

这两个版本的结果都为12,但在第二个版本中,给函数sum传递的数组更短。换言之,这缩小了问题的规模!

函数sum的工作原理类似于下面这样。

这个函数的运行过程如下。

别忘了,递归记录了状态。

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

二、快速排序

快速排序是一种常用的排序算法,比选择排序快得多。例如,C语言标准库中的函数qsort实现的就是快速排序。快速排序也使用了D&C。

快速排序的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

下面来使用快速排序对数组进行排序。对排序算法来说,最简单的数组什么样呢?就是根本不需要排序的数组。

因此,基线条件为数组为空或只包含一个元素。在这种情况下,只需原样返回数组——根本就不用排序。

def quicksort(array):if len(array) < 2:return array

我们来看看更长的数组。对包含两个元素的数组进行排序也很容易。

包含三个元素的数组呢?

别忘了,你要使用D&C,因此需要将数组分解,直到满足基线条件。下面介绍快速排序的工作原理。首先,从数组中选择一个元素,这个元素被称为基准值(pivot)。

稍后再介绍如何选择合适的基准值。我们暂时将数组的第一个元素用作基准值。

接下来,找出比基准值小的元素以及比基准值大的元素。

这被称为 分区(partitioning)。现在你有

  • 一个由所有小于基准值的数字组成的子数组;
  • 基准值;
  • 一个由所有大于基准值的数组组成的子数组。

这里只是进行了分区,得到的两个子数组是无序的。但如果这两个数组是有序的,对整个数组进行排序将非常容易。

如果子数组是有序的,就可以像下面这样合并得到一个有序的数组:左边的数组 + 基准值 + 右边的数组

在这里,就是 [10, 15] + [33] + [],结果为有序数组[10, 15, 33]。

如何对子数组进行排序呢?对于包含两个元素的数组(左边的子数组)以及空数组(右边的子数组),快速排序知道如何将它们排序,因此只要对这两个子数组进行快速排序,再合并结果,就能得到一个有序数组!

不管将哪个元素用作基准值,这都管用。假设你将15用作基准值。

这个子数组都只有一个元素,而你知道如何对这些数组进行排序。现在你就知道如何对包含三个元素的数组进行排序了,步骤如下:

  1. 选择基准值。
  2. 将数组分成两个子数组:小于基准值的元素和大于基准值的元素。
  3. 对这两个子数组进行快速排序。

包含四个元素的数组呢?

假设你也将33用作基准值。

左边的子数组包含三个元素,而你知道如何对包含三个元素的数组进行排序:对其递归地调用快速排序。

因此你能够对包含四个元素的数组进行排序。如果能够对包含四个元素的数组进行排序,就能对包含五个元素的数组进行排序。为什么呢?假设有下面这样一个包含五个元素的数组。

根据选择的基准值,对这个数组进行分区的各种可能方式如下。

注意,这些子数组包含的元素数都在0~4内,而你已经知道如何使用快速排序对包含0~4个元素的数组进行排序!因此,不管如何选择基准值,你都可对划分得到的两个子数组递归地进行快速排序。

例如,假设你将3用作基准值,可对得到的子数组进行快速排序。

将子数组排序后,将它们合并,得到一个有序数组。即便你将5用作基准值,这也可行。

将任何元素用作基准值都可行,因此你能够对包含五个元素的数组进行排序。同理,你能够对包含六个元素的数组进行排序,以此类推。

归纳证明
刚才我们使用的就是归纳证明!归纳证明是一种证明算法行之有效的方式,它分两步:基线条件和归纳条件。是不是有点似曾相识的感觉?例如,假设我要证明我能爬到梯子的最上面。递归条件是这样的:如果我站在一个横档上,就能将脚放到下一个横档上。换言之,如果我站在第二个横档上,就能爬到第三个横档。这就是归纳条件。而基线条件是这样的,即我已经站在第一个横档上。因此,通过每次爬一个横档,我就能爬到梯子最顶端。

对于快速排序,可使用类似的推理。在基线条件中,证明这种算法对空数组或包含一个元素的数组管用。在归纳条件中,证明如果快速排序对包含一个元素的数组管用,对包含两个元素的数组也将管用;如果它对包含两个元素的数组管用,对包含三个元素的数组也将管用,以此类推。因此可以说,快速排序对任何长度的数组都管用。

下面是快速排序的代码。

python版本代码如下:

def quicksort(array):if len(array) < 2:#基本情况下,具有0或1个元素的数组是已经“排序”的return arrayelse:#递归情况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)print(quicksort([10, 5, 2, 3]))


c++版本代码如下:

#include <iostream>
#include <vector>using std::cout;
using std::endl;template <typename T>
std::vector<T> quicksort(const std::vector<T>& arr) {// base case, arrays with 0 or 1 element are already "sorted"if (arr.size() < 2) return arr;// recursive caseconst T* pivot = &arr.front() + arr.size() / 2 - 1; // set the pivot somewhere in the middlestd::vector<T> less;  // vector to store all the elements less than the pivotstd::vector<T> greater;  // vector to store all the elements greater than the pivotfor (const T* item = &arr.front(); item <= &arr.back(); item++) {if (item == pivot) continue; // skip pivot elementif (*item <= *pivot) less.push_back(*item);else greater.push_back(*item);}std::vector<T> sorted_less = quicksort(less);std::vector<T> sorted_greater = quicksort(greater);// concatenate less part, pivot and greater partsorted_less.push_back(*pivot);sorted_less.insert(sorted_less.end(), sorted_greater.begin(), sorted_greater.end());return sorted_less;
}int main() {std::vector<int> arr = {10, 5, 2, 3};std::vector<int> sorted = quicksort(arr);for (int num : sorted) {cout << num << " ";}cout << endl;
}

三、再谈大O 表示法

快速排序的独特之处在于,其速度取决于选择的基准值。在讨论快速排序的运行时间前,我们再来看看最常见的大O运行时间。

上述图表中的时间是基于每秒执行10次操作计算得到的。这些数据并不准确,这里提供它们只是想让你对这些运行时间的差别有大致认识。实际上,计算机每秒执行的操作远不止10次。

对于每种运行时间,本书还列出了相关的算法。来看看第2章介绍的选择排序,《算法图解》学习笔记(二):选择排序(附代码),其运行时间为O(n2),速度非常慢。

还有一种名为 合并排序(merge sort) 的排序算法,其运行时间为O(n log n),比选择排序快得多!快速排序的情况比较棘手,在最糟情况下,其运行时间为O(n2)。与选择排序一样慢!但这是最糟情况。在平均情况下,快速排序的运行时间为O(n log n)。你可能会有如下疑问。

  • 这里说的最糟情况和平均情况是什么意思呢?
  • 若快速排序在平均情况下的运行时间为O(n log n),而合并排序的运行时间总是O(n log n),为何不使用合并排序?它不是更快吗?

带着疑问,接着看下去,,,

1)比较合并排序和快速排序

假设有下面这样打印列表中每个元素的简单函数。

def print_items(list):for item in list:print(item)

这个函数遍历列表中的每个元素并将其打印出来。它迭代整个列表一次,因此运行时间为O(n)。现在假设你对这个函数进行修改,使其在打印每个元素前都休眠1秒钟。

from time import sleep
def print_items2(list):for item in list:sleep(1)print(item)

它在打印每个元素前都暂停1秒钟。假设你使用这两个函数来打印一个包含5个元素的列表。

这两个函数都迭代整个列表一次,因此它们的运行时间都为O(n)。你认为哪个函数的速度更快呢?我认为 print_items 要快得多,因为它没有在每次打印元素前都暂停1秒钟。因此,虽然使用大O表示法表示时,这两个函数的速度相同,但实际上 print_items 的速度更快。在大O表示法O(n)中,n实际上指的是这样的。

c 是算法所需的固定时间量,被称为常量。例如,print_ items 所需的时间可能是10毫秒 * n,而 print_items2 所需的时间为1秒 * n

通常不考虑这个常量,因为如果两种算法的大O运行时间不同,这种常量将无关紧要。就拿二分查找简单查找来举例说明。假设这两种算法的运行时间包含如下常量。

你可能认为,简单查找的常量为10毫秒,而二分查找的常量为1秒,因此简单查找的速度要快得多。现在假设你要在包含40亿个元素的列表中查找,所需时间将如下。

正如你看到的,二分查找的速度还是快得多,常量根本没有什么影响。

但有时候,常量的影响可能很大,对快速查找合并查找来说就是如此。快速查找的常量比合并查找小,因此如果它们的运行时间都为O(n log n),快速查找的速度将更快。实际上,快速查找的速度确实更快,因为相对于遇上最糟情况,它遇上平均情况的可能性要大得多。

此时你可能会问,何为平均情况,何为最糟情况呢?

2)平均情况和最糟情况

快速排序的性能高度依赖于你选择的基准值。假设你总是将第一个元素用作基准值,且要处理的数组是有序的。由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序。

注意,数组并没有被分成两半,相反,其中一个子数组始终为空,这导致调用栈非常长。现在假设你总是将中间的元素用作基准值,在这种情况下,调用栈如下。

调用栈短得多!因为你每次都将数组分成两半,所以不需要那么多递归调用。你很快就到达了基线条件,因此调用栈短得多。

第一个示例展示的是最糟情况,而第二个示例展示的是最佳情况。在最糟情况下,栈长为O(n),而在最佳情况下,栈长为O(log n)。

现在来看看栈的第一层。你将一个元素用作基准值,并将其他的元素划分到两个子数组中。这涉及数组中的全部8个元素,因此该操作的时间为O(n)。在调用栈的第一层,涉及全部8个元素,但实际上,在调用栈的每层都涉及O(n)个元素。

即便以不同的方式划分数组,每次也将涉及O(n)个元素。

因此,完成每层所需的时间都为O(n)。

在这个示例中,层数为O(log n)(用技术术语说,调用栈的高度为O(log n)),而每层需要的时间为O(n)。因此整个算法需要的时间为O(n) * O(log n) = O(n log n)。这就是最佳情况。

在最糟情况下,有O(n)层,因此该算法的运行时间为O(n) * O(n) = O(n2)。

知道吗?这里要告诉你的是,最佳情况也是平均情况。只要你每次都随机地选择一个数组元素作为基准值,快速排序的平均运行时间就将为O(n log n)。快速排序是最快的排序算法之一,也是D&C典范。

四、总结

  • D&C将问题逐步分解。使用D&C处理列表时,基线条件很可能是空数组或只包含一个元素的数组。
  • 实现快速排序时,请随机地选择用作基准值的元素。快速排序的平均运行时间为O(n log n)。
  • 大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在。
  • 比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时,O(log n)的速度比O(n)快得多。

参考文章

  • 《算法图解》
  • 百度百科——分而治之方法

《算法图解》学习笔记(四):分而治之和快速排序(附代码)相关推荐

  1. 算法图解学习笔记02:递归和栈

    计算机内存原理 要说递归和栈的问题,首先就要说下计算机内存的基本原理.简单理解计算机内存原理可以将一台电脑看作超市的存包柜,每个柜子都有柜号(即计算机中的地址,如0x000000f).当需要将数据存储 ...

  2. 算法图解学习笔记01:二分查找大O表示法

    二分查找 二分查找又称折半查找,其输入的必须是有序的元素列表.二分查找的基本思想是将n个元素分成大致相等的两部分,取a[n/2]与x做比较,如果x=a[n/2],则找到x,算法中止:如果x<a[ ...

  3. 算法导论学习笔记 第7章 快速排序

    对于包含n个数的输入数组来说,快速排序是一种时间复杂度为O(n^2)的排序算法.虽然最环情况的复杂度高,但是快速排序通常是实际应用排序中最好的选择,因为快排的平均性能非常好:它的期望复杂度是O(nlg ...

  4. 算法图解——学习笔记

    文章目录 算法简介 **第二章 选择排序** 数组与链表 排序算法 算法简介 算法:一组完成任务的指令,任何片段都可以视为算法. 第一章 算法集合: 算法种类 定义 二分法 一种查询方法,通过将查找特 ...

  5. 算法图解学习笔记01之二分查找

    不知道可以学到第几章,就不立flag了,容易倒 你要学会的不是写算法而是何时何地用何算法 package 第一章; //数组有序,要求快速查找出数据 public class BinarySearch ...

  6. Acwing算法基础课学习笔记(四)--数据结构之单链表双链表模拟栈模拟队列单调栈单调队列KMP

    单链表 算法题中最常考的单链表就是邻接表(用来存储图和数),比如最短路问题,最小生成树问题,最大流问题.双链表用于优化某些问题. 利用数组来表达单链表:存储值和指针的两个数组利用下标进行关联. 需要注 ...

  7. 算法图解学习笔记02之选择排序

    package 第二章;import java.util.Arrays;//找出列表中最小(大)的,放入新列表(O(n)),再次找 public class SelectionSort {public ...

  8. 算法训练营学习笔记1

    算法训练营学习笔记 贪心算法 心算法总是做出当前最好的选择,期望通过局部最优选择得到全局最优的解决方案.从问题的初始解开始,一步歩地做出当前最好的选择,逐步逼近问题的目标,尽可能得到最优解: 贪心本质 ...

  9. 计算机视觉算法——Transformer学习笔记

    算机视觉算法--Transformer学习笔记 计算机视觉算法--Transformer学习笔记 1. Vision Transformer 1.1 网络结构 1.2 关键知识点 1.2.1 Self ...

  10. 吴恩达《机器学习》学习笔记四——单变量线性回归(梯度下降法)代码

    吴恩达<机器学习>学习笔记四--单变量线性回归(梯度下降法)代码 一.问题介绍 二.解决过程及代码讲解 三.函数解释 1. pandas.read_csv()函数 2. DataFrame ...

最新文章

  1. (chap1 网络基础知识)通信类型
  2. QImage 与 cv::Mat 之间的相互转换
  3. Ifvisible.js – 判断网页中的用户是闲置还是活动状态
  4. Spring-----多环境中加载资源配置文件
  5. 解读NoSQL最新现状和趋势:云NoSQL数据库将成重要增长引擎
  6. mysql封装增删改查_jdbc封装一行代码实现增删改查
  7. python结构体数组传出接收c动态库_使用Python向C语言的链接库传递数组、结构体、指针类型的数据...
  8. 算法与数据结构 第3章 高级排序算法下 学习笔记
  9. GPS(全球定位系统)
  10. 攻防演习防御体系构建之第一篇之介绍和防守的四个阶段
  11. 广东四大姓氏,排名第三的是李姓,你的姓氏排名第几?
  12. java手机振动软件_Android实现手机震动效果
  13. linux 调整cpu使用率,linux下限制CPU使用率的3种方法
  14. 艾永亮:刘昊然盲盒、大白兔润唇膏,95后推动超级产品IP的发展
  15. java套接字创建失败_Linux的文件描述符个数限制导致创建文件(或socket)失败的问题...
  16. 肖特基二极管的作用与识别方法
  17. 股票接口level2的短线动力指标公式源码是什么?
  18. 无人车最新破绽:3D 打印障碍轻松骗过 9 成算法
  19. 第六次作业--结队编程第二次
  20. 高质量股票数据_10个网站免费下载高质量的股票视频

热门文章

  1. 【推荐】【分享】老公PK老婆
  2. 合肥城市POI数据综合运用研究——功能区识别
  3. SQL怎么查询一个时间包含在另外一个时间段内?
  4. 【Airtest】实现UI自动化测试(一)
  5. python爬取视频的工具_Python爬取视频(其实是一篇福利)
  6. 2021 年度总结——平安喜乐
  7. Puzzle Game HihoCoder - 1634
  8. 有哪些期货交易理念(期货交易的概念及主要特征)
  9. Window同一芯片,禁用串口序列号,固定端口号9091和9008
  10. 【Linux基础】vim配置及插件安装管理