在《算法导论》一书中,插入排序作为一个例子是第一个出现在该书中的算法。

插入排序:

对于少量元素的排序,它是一个有效的算法。

插入排序的工作方式像许多人排序一手扑克牌。开始时,我们手中牌为空,我们每次从牌堆中取出一张牌并将其放入正确的位置。为了找到一张牌的正确位置,我们从左到右将它与手中已有的每张牌进行比较。

将其伪代码过程命名为 INSERTION-SORT,参数是一个数组A,具体如下:

INSERTION-SORT(A):

  for j = 2 to A.length

    key = A[j]   // Insert A[j] into the second sequence A[1..j - 1]. 

    i = j - 1       // A[i]、A[j]

    while i > 0 and A[i] > key

      A[i + 1] = A[i]

      i = i - 1

    A[i + 1] = key

伪代码解释:下标 j 指出正被插入到手中的牌,在for循环的每次迭代开始,A[j]将数组A分为了两部分,一部分是A[1..j - 1]的子数组构成当前排序好的手中的牌,剩余的子数组A[j + 1..n]对应仍在桌上的牌堆。

我在刚看到插入排序的工作方式后,也写了一下伪代码。采用牌堆模拟的方法,但在具体的实现过程,我用一个数组表示牌堆,用一个空vector表示手中的空手牌,看了书中的伪代码才直到原来只用一个数组就行,减少了近一半的内存占用。这也算是这段伪代码的一个亮点。其实仔细想想,这种对空间复杂度的优化不算少见,非常典型的例子就是背包问题的一维、二维数组实现。背包问题在进行动态规划时,我们使用一个矩阵来记录它的状态,通过不断更新矩阵进行状态转移,由于我们只需要最后一行矩阵,而且每次更新矩阵的一行时只有矩阵上一行的值确定,故可采用二维数组优化,两个数组不断交换更新即可。对于01背包问题,可以更近一步使用一维数组(由于01背包的状态转移方程指示出,当一维数组的值更新时一定由该数组前面的值给出,我们每次从后向前更新数组即可动态更新整个矩阵)。

所以在以后,执行对数组的一些操作前可以想一想是否可以利用数组自身的状态更新自身,减少不必要的内存消耗。

 


该书在之后介绍了一个非常重要的概念:循环不变式

循环不变式主要用来证明算法的正确性。关于循环不变式,我们必须证明三条性质:

初始化:循环的第一次迭代之前,它为真。

保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真。(即对这一次迭代循环不变式保持为真)

终止:循环终止时,不变式为我们提供了一个有用的性质,该性质有助于证明算法的正确性。

类似于数学归纳法,为证明某条性质成立,需要证明一个基本情况和一个归纳步。

对插入排序而言,

初始化:第一次循环迭代之前,此时 j=2,子数组A[1..j - 1]仅有单个元素A[1]组成,已排序,循环不变式为真;

保持:非形式化地(指理解描述,未用数学符号严格表示),对某次迭代而言,for循环体的第4~7行语句将A[j - 1]、A[j - 2]、A[j - 3]等向右移动一个位置,直到找到A[j]的适当位置,第8行语句将A[j]的值插入该位置。这时子数组A[1..j]中的元素组成已排好序,在该次迭代中,循环不变式保持为真。

终止:导致for循环终止的条件是j > A.length。每次for迭代 j 增加一,那么循环结束时必有 j=n+1。在循环不变式的表述中用将 j 用 n+1 替换,我们有,子数组A[1..n]中的元素已排好序,子数组A[1..n]就是整个数组,因此我们推断整个数组已排好序。因此算法正确。

 


以插入排序算法为例进行进一步分析,本书给出了“输入规模”、“运行时间”两个重要概念。

输入规模:输入规模依赖于研究的问题,如插入排序算法,最自然的量度是输入数据的项数,如果是两个整数相乘,输入规模的最佳量度是用通常的二进制记号表示输入所需的总位数。但有时需要用两个数而不是一个数来描述输入规模可能更合适,最典型的情况就是算法输入为一张图时,此时输入规模可以用图中的顶点数和边数来描述。

运行时间:一个算法在特定输入上的运行时间是指执行的基本操作数和步数。我们目前若假设,执行每行伪代码需要常量时间,即我们假定第 i 行的每次执行需要时间 ci (c是常量)。

对插入排序算法而言,外层循环次数定为 n ,内层循环次数定为 tj,算法运行时间是执行每条语句的运行时间之和,则有:

    T(n) = c1n + c2(n - 1) + c4(n  - 1) + c5j=2tj + c6j=2(tj - 1) + c7j=2(tj - 1) + c8(n - 1)    (注:c3 = 0)

若输入数组已经排好序,则出现最佳情况,这时:

    T(n) = (c1 + c2 + c4 + c5 + c8) n - (c2 + c4 + c5 + c8)

我们可以把该运行时间表示为an + b,其中常量 a 和 b 依赖于语句代价ci。因此它是 n 的线性函数。

若输入数组开始时为反向排序,则导致最坏情况,此时∑j=2tj = ∑j=2j = n(n + 1) / 2 - 1、∑j=2(j - 1) = n(n - 1) / 2。

将以上两个式子代进最初的T(n)函数中,可得:

T(n) = (c5/2 + c6/2 + c7/2) n2 + (c1 + c2 + c4 + c5/2 - c6/2 - c7/2 + c8)n - (c2 + c4 + c5 + c8)

我们可以把该最坏情况的运行时间表示为 an2+bn+c ,其中a、b、c依赖于语句代价ci。因此它是 n 的二次函数。

通过上述对插入排序算法的分析,应该要认识到对一个算法运行时间的分析应该要清晰而细致,认真分析每一个指令,判断是否是常量时间,判断循环执行该指令的次数。然后在熟练的基础上对一些简单指令可以只做简略处理。此外还应当补充,在分析一个算法的平均情况的时候,可能要对所谓“平均”输入使用随机化算法,它做出一些随机的选择,以允许进行概率分析并产生某个期望的运行时间。


插入排序使用了增量方法,接下来介绍一种重要的算法设计思想:分治法

分治法:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后在合并这些子问题的解来建立原问题的解。

分治模式在每层递归时都有三个步骤:

分解:分解原问题为若干子问题,,这些子问题是原问题的规模较小的实例。

解决:解决这些子问题,递归地求解各子问题。然而,若子问题的规模足够小,则直接求解。

合并:合并这些子问题的解成原问题的解。

归并排序算法完全遵循分治模式:

分解:分解待排序的 n 个元素的序列成各具 n/2 个元素的两个子序列。

解决:使用归并排序递归地排序两个子序列。

合并:合并两个已排序的子序列以产生已排序的答案。

归并排序算法的关键是“合并”步骤。我们将其定义为 MERGE(A, p, q, r) 来完成合并。其中A是一个数组,p、q、r是数组下标(p ≤ q < r),三个下标将数组分为左右两个数组,A[p..q]和A[q + 1..r]。

我们假设桌上有两堆输入牌,每堆都已排好序,最小的牌在顶上。每次我们取出两堆牌的顶上两张牌中较小的一张,将此牌移除并将其放置到输出堆。为避免在每个基本步骤必须检查是否有堆为空,《算法导论》给出了一种处理这种情况的方法:哨兵牌。在每个堆的底部放置一张哨兵牌,它是一个特殊的值,这里我们使用 ∞ 作为哨兵值,结果每当显露一张值为 ∞ 的牌,它不可能为较小的牌,除非两个堆已显露出其哨兵牌。但一旦发生这种情况,我们可以通过 for 循环控制循环次数为 r - p + 1,因为我们事先知道刚好有 r - p + 1张牌将被放置到输出堆上,所以一旦执行 r - p + 1个基本步骤,算法就刚好停止。

伪代码如下:

MERGE(A, p, q, r)

  n1 = q - p + 1

  n2 = r - q

  let L[1..n1 + 1] and R[1..n2 + 1] be new arrays

  for i = 1 to n1

    L[i] = A[p + i - 1]

  for j = 1 to n2

    R[j] = A[q + j]

  L[n1 + 1] = ∞

  R[n2 + 1] = ∞

  i  =1

  j = 1

  for k = p to r

    if L[i] <= R[j]

      A[k] = L[i]

      i = i + 1

    else

      A[k] = R[j]

      j = j + 1

算法导论》用循环不变式对该算法的正确性进行了稍显复杂的证明,但非形式化地理解该算法的正确性并不太难,故此处略去。

现在我们可以把过程 MERGE 作为归并排序算法中的一个子程序来用。MERGE-SORT(A, p, r) 排序数组A[p..r]中的元素。若 p>=r,则该子数组最多一个元素,此时已排好顺序。否则,分解步骤。伪代码如下:

MERGE-SORT(A, p, r)

  if p < r

    q = (p + r) / 2

    MERGE-SORT(A, p, q)

    MERGE-SORT(A, q + 1, r)

    MERGE(A, p, q, r)  

《算法导论》在给出了归并排序算法的伪代码后,对归并排序算法进行了一定的分析,初步引入了递归式递归树。关于分治策略的严格数学形式,即递归式和递归树的一些数学描述将在《算法导论》第四章-分治策略 一章里讨论。

(¬_¬) 好像到这里篇幅不是很多,那就来些闲话做结束语吧:

到目前为止,已经看了《算法导论》里的两个算法,这两个算法给我的感觉都写得非常精简,没有赘余。在刷题过程中我经常会苦思一些程序细节,苦于没有较好的方法处理这些细节,只能多写一些代码讨论情况,处理细节。这样就导致了代码量增大,而多出的指令既可能增加程序运行时间,又可能让思路混乱,出bug概率也大......

~额,看来我的问题有点大呢......

算法导论 - 基础知识 - 算法基础(插入排序&归并排序)的更多相关文章

  1. 数据结构和算法(Golang实现)(10)基础知识-算法复杂度主方法

    算法复杂度主方法 有时候,我们要评估一个算法的复杂度,但是算法被分散为几个递归的子问题,这样评估起来很难,有一个数学公式可以很快地评估出来. 一.复杂度主方法 主方法,也可以叫主定理.对于那些用分治法 ...

  2. 数据结构和算法(Golang实现)(9)基础知识-算法复杂度及渐进符号

    算法复杂度及渐进符号 一.算法复杂度 首先每个程序运行过程中,都要占用一定的计算机资源,比如内存,磁盘等,这些是空间,计算过程中需要判断,循环执行某些逻辑,周而反复,这些是时间. 那么一个算法有多好, ...

  3. Linux基础知识与基础命令

    Linux基础知识与基础命令 系统目录 Linux只有一个根目录,没有盘符的概念,文件目录是一个倒立的树形结构. 常用的目录功能 bin 与程序相关的文件 boot 与系统启动相关 cdrom 与Li ...

  4. java线程基础知识----线程基础知识

    不知道从什么时候开始,学习知识变成了一个短期记忆的过程,总是容易忘记自己当初学懂的知识(fuck!),不知道是自己没有经常使用还是当初理解的不够深入.今天准备再对java的线程进行一下系统的学习,希望 ...

  5. day63:Linux:nginx基础知识&nginx基础模块

    目录 1.nginx基础知识 1.1 什么是nginx 1.2 nginx应用场景 1.3 nginx组成结构 1.4 nginx安装部署 1.5 nginx目录结构 1.6 nginx配置文件 1. ...

  6. 这些C++基础知识的基础知识你都学会了吗?

    一.C++基础知识 新的数据类型 C语言中的数据类型  C++中新的数据类型 思考:新的数据类型有什么好处?请看下面的代码:  可以见得:新的类型使整个程序更加简洁,程序变得易读易懂!这个就是bool ...

  7. 算法导论(第三版)Exercises2.3(归并排序、二分查找、计算集合中是否有和为X的2个元素)

    2.3-1: 3 9 26 38 41 49 52 59 3 26 41 52   9 38 49 57 3 41   52 26   38 57   9 49 3   41  52  26  38  ...

  8. Ceph基础知识和基础架构认识

    1  Ceph基础介绍 Ceph是一个可靠地.自动重均衡.自动恢复的分布式存储系统,根据场景划分可以将Ceph分为三大块,分别是对象存储.块设备存储和文件系统服务.在虚拟化领域里,比较常用到的是Cep ...

  9. Ceph 基础知识和基础架构认识

    1  Ceph基础介绍 Ceph是一个可靠地.自动重均衡.自动恢复的分布式存储系统,根据场景划分可以将Ceph分为三大块,分别是对象存储.块设备存储和文件系统服务.在虚拟化领域里,比较常用到的是Cep ...

随机推荐

  1. MapReduce中一次reduce方法的调用中key的值不断变化

    简单一句话总结就是:ReduceContextImpl类的RawKeyValueIterator input迭代器对象里面存储中着key-value对的元素, 以及一个只存储value的迭代器,然后每 ...

  2. SyntaxError: keyword can't be an expression

    创建字典对象时: D1=dict('name'='Bob','age'=20,'score'=90) SyntaxError: keyword can't be an expression 解决方法: ...

  3. Chrome:F12开发者模式下console不打印信息

    控制台不打印信息的解决方法 你要看看你是否在之前进行过查找关键字操作,操作之后忘记删去这个关键字,导致console中只会留下对于该关键字的查询结果.

  4. [BOI2019][第K大问题][暴力剪枝]D2T1 Olympiads

    目录 题意 输入格式 输出格式 样例 Input Output 数据范围 时间限制 思路 代码 题意 有\(N\)个人,现在你要从中选出\(K\)个人出来,然后让这\(K\)个人一起参加\(K\)场比 ...

  5. 压力测试工具——jmeter

    Jmeter:这是一个绿色的工具,但是它需要依赖与jdk 8的环境,所以在安装的时候需要安装jdk8. 下载地址: 链接:https://pan.baidu.com/s/1pGj1hAqJBBoSHf ...

  6. k8s命令行web代理神器gotty

    目录 介绍 安装 使用示例 -p 指定端口 -c 指定账号密码 -w 支持tty交互 --permit-arguments 支持get参数传参 --random-url 生成随机地址 --reconn ...

  7. CF587F&CF547E题解

    这两道题好像啊 贡献一种使用SAM和ACAM草两道题的方法 下面假装有 \(O(\sum |S|=m)=O(n)\). 你看看,这CF换过多少个出题人啦?换汤不换药啦!其实这两道题是同一个人出的 CF ...

  8. 可移植的python环境

    创建可移植的python环境 工作时使用的系统不联网,而且自带的python环境库不完整,每次干活都心累,所以想要做一个可移植的精简版的python环境. 开始前的准备: Ubuntu18.04 py ...

  9. k8s原来这么简单(二)安装k8s1.23集群

    官方文档:安装 kubeadm 安装条件 多台Linux机器 CentOS7 2G以上RAM,2个以上CPU 集群网络互通,可访问外网 关闭防火墙,关闭swap分区 准备安装环境 node IP k8 ...

  10. ActiveMQ-模块代码-02

    模块模式 p2p模式 生产者 ConfigBeanQueue package com.producerp2p.producerp2p; import org.apache.activemq.comma ...