递归形式

递归形式是算法中常用到的一种构造思路。递归允许函数或过程对自身进行调用,是对算法中重复过程的高度概括,从本质层面进行刻画,避免算法书写中过多的嵌套循环和分支语法。因此,是对算法结构很大的简化。

递归形式实际可以看做一个函数表达式:

f ( n ) = G ( f ( g ( n ) ) ) f(n)=G(f(g(n)))f(n)=G(f(g(n))),即f ( n ) f(n)f(n)可以通过一个f ( g ( n ) ) f(g(n))f(g(n))的表达式间接推出。当然,如果递归式可解,则最后也能将f ( n ) f(n)f(n)直接用n表示出来。

如:f ( n ) = f ( n − 1 ) + n f(n)=f(n-1)+nf(n)=f(n−1)+n (1)

f ( n ) = f ( n 2 ) + n f(n)=f(\frac{n}{2})+nf(n)=f(2n​)+n (2)

f ( n ) = f ( n 1 ) + f ( n 2 ) + . . . + f ( n k ) f(n)=f(n_1)+f(n_2)+...+f(n_k)f(n)=f(n1​)+f(n2​)+...+f(nk​) (3)

可以想到,如果运行时间可以用这样的递归表达式,那么求解f ( n ) f(n)f(n)可能会相对简单。

递归形式分为线性递归、二分递归、多分支递归等。

在上面举例中,(1)式即为线性递归(该递归关系实际上描述了求和过程);(2)式即为二分递归(后面会看到,该关系实际描述了合并排序过程)。(3)式即为多分支回归,即每次分成的子问题规模不一定相同。将这些递归形式应用于算法中,就形成了遍历、减治、分治等算法策略。此处重点讨论分治法,并且仅关注分治法应用于排序问题中的一种经典算法:合并排序。

合并排序

基本思想:分治法

分治法是把一个规模较大的问题分成若干个规模较小的问题,并递归的对这些问题进行求解,当问题规模足够小时,可以直接求解。最终将这些规模小问题的求解结果递归的合并,建立原问题的解。

使用分治法的意义之一是,这样容易求出算法的时间复杂度,有很多方法可以套用。可以达成这样目标的一个前提是,1.运行时间可以用一个递归式表示;2.分解到最后,规模足够小的子问题应当是常数时间就可以求解的,否则还是没法简化时间复杂度的计算。

在排序过程中,主要分成两个步骤:首先,将n个数用二分递归分解,直到每个子问题规模为1。再将这些子问题递归的合并,合并就是一个排序的过程。

下面以一个排序问题举例:对A=(3,2,4,6,1,5,7,8)用合并排序从小到大排列。

分解过程

这是一个规模为8的排序问题。首先将问题不断二分直到8个规模为1的子问题。因为每步操作的结构类似,因此这几步操作可以用一个递归函数merge-sort表示:merge-sort(A,p,r),其中p为每步分解操作中子序列头的位置,r表示子序列尾的位置。如何保证若干次操作后子序列规模能降到1呢?这就需要对头和尾数字的位置进行比较。其操作过程是:若p

回到上述例子,分解之后,A=({3},{2},{4},{6},{1},{5},{7},{8})

merge过程

当子问题变成单元素时,就开始调用merge过程,即两个已经排好序的子序列合并,在合并的过程中就要排序。因此,在每一次merge前,都有左右两个子序列已经排好序(这里和插入排序中,当选择新元素判断插入序列位置时,已有序列已经排好序这一思想有共通之处)。

此时思路仍可以类比抓牌。现在有两堆牌(A,B)。每堆都已经从小到大排列,小的放上面,大的放下面。先比较A和B堆中最上面的牌(A1,B1)。若A1>B1,则把B1拿出来放在第三堆(C)中,令为C1。接着,把留下的A1继续跟B2比,若A1还大则重复上述操作,若A1小则把A1取出,令为C2,而换B2和A堆中剩下元素比较。最终,某一堆的数字可能被全部取出,而另一堆还没取完,则此时直接把另一堆的数字追加到C堆后面即可。

可以看出,这样操作就不是插入排序那样的原地排序了,而是每层递归都要新生成一个空序列以存放每层排好序的元素。当然,为了节省空间复杂度,这个储存空间需要尽快释放。

总的来说,计算顺序是先对原序列递归分解→ \to→直到子序列为单元素→ \to→对子序列递归合并+排序,直到序列总长度等于原问题规模。

将解这个问题的过程写成merge-sort(n),问题规模为n。下面讨论如何将这个问题的运算时间T ( n ) T(n)T(n)用递归式表示,这里仍然是基于RAM模型的假设。解这个问题分为哪几个步骤呢?

首先,需要找到这个问题的中间元素,分解为两个子问题。而因为这一步只要计算中间的索引即可,其运算时间与问题规模无关,是常数c 1 c_1c1​;其次,需要对分成的两个子问题分别调用该过程merge-sort(n/2)。求解这两个子问题的时间即为T ( n 2 ) T(\frac{n}{2})T(2n​)。最后,当子问题全部解完之后,需要对这两个子问题合并,合并的merge(n)过程需要c 2 n c_2nc2​n运行时间。因为合并是将每个堆最上面的元素进行比较,若要合并成n个元素,则一共最多要比较n次(回想抓牌过程),每次比较只要常数时间c 2 c_2c2​。由于c 2 n + c 1 c_2n+c_1c2​n+c1​仍然是n的一个线性函数,可以表示为Θ ( n ) \Theta(n)Θ(n),因此得到运行时间的递归表达式:

T ( n ) = T ( n 2 ) + Θ ( n ) T(n)=T(\frac{n}{2})+\Theta(n)T(n)=T(2n​)+Θ(n)

伪代码

首先看merge-sort伪代码,即假设两侧序列都已经排好序,如何对这两段序列合并排序。

有一个需要注意的构造技巧。排序总体应当分为两个阶段:1.所有堆中的元素均非空时,这样只要一直比较两堆中最上面的元素即可;2.当某堆中的元素被取完时,这样直接把非空的那堆追加到排好序的序列后面即可。因此,在选取某堆最上面元素的时候,需要先判断该堆元素是否为空。一种自然的想法是把这堆元素循环计数,看个数是否为0。但这样的话,每轮合并都要把所有元素循环一遍很费时间。因此,简化的操作是每轮合并后对每堆序列的最下层追加一个一定不属于该序列的“哨兵”,因此只要某轮轮合并排序的时候发现了“哨兵”,说明该堆已空。

“哨兵”如何选择?首先,需要排序的元素里一定不存在∞ \infty∞,并且,利用∞ \infty∞比所有数大的性质也可以进一步简化代码,直接与两堆数字比较的过程融合,不需要单独写一行代码判断下一个数是否是哨兵。

以下是merge过程伪代码:

MERGE(A, p, q, r)

1 n1 ← q - p + 1

2 n2 ← r - q

3 create arrays L[1 ‥ n1 + 1] and R[1 ‥ n2 + 1]

4 for i ← 1 to n1

5 do L[i] ← A[p + i - 1]

6 for j ← 1 to n2

7 do R[j] ← A[q + j] //将A分成两堆,用这两堆的比较更新A序列

8 L[n1 + 1] ← ∞ //新堆尾部插入哨兵

9 R[n2 + 1] ← ∞

10 i ← 1

11 j ← 1

12 for k ← p to r

13 do if L[i] ≤ R[j] //两个新堆最上面的元素比较。这里可以合并遇到∞的情况,是对代码很大的简化

14 then A[k] ← L[i]

15 i ← i + 1

16 else A[k] ← R[j]

17 j ← j + 1

> 引自清华计算机系武永卫老师课件

以下是合并排序merge-sort整个过程的伪代码:

merge-sort(A,p,r)

if p

q = (p+r)/2 //注意这里q是下取整,因此最后总能循环到p>=q

merge-sort(A,p,q)

merge-sort(A,q+1,r)

MERGE(A,p,q,r)

python代码实现

以下是merge过程python代码的实现:

A = [1,4,6,2,4,5,7]

def MERGE(A, p, q, r): ##其中,r为原序列末位数的索引,p为原序列首位数的索引,q为中间某个数的索引(q左右两侧的数已经顺序排列)

L = A[p:q+1]

R = A[q+1:r+1]

L.append(float("inf"))

R.append(float("inf"))

i = 0

j = 0

for k in range(p,r+1):

if L[i]<=R[j]:

A[k] = L[i]

i = i+1

else:

A[k] = R[j]

j = j+1

return A

MERGE(A, 0, 2, 6)

输出结果是一个已经排好序的序列:

>>> [1, 2, 4, 4, 5, 6, 7]

以下是整个合并排序merge-sort过程的代码实现(需要调用前面定义的MERGE函数):

def merge_sort(A, p, r):

if p < r:

q = math.floor((p+r)/2)

merge_sort(A,p,q)

merge_sort(A,q+1,r)

MERGE(A, p, q, r)

return A

A = [1,6,4,2,5,4,7]

merge_sort(A, 0, 6)

>>> [1, 2, 4, 4, 5, 6, 7]

用递归树猜测时间复杂度

如何确定合并排序算法的时间复杂度?虽然用主定理可以直接确定,但这里还是从递归树的角度给出猜测。因为对于一些无法用主定理直接确定的递归算法,还是需要将递归树和代换法结合确定复杂度。用递归树可以提出猜想,用代换法则可以给出该猜想结果的数学证明。

如图所示,是一个递归树的结构。问题总规模为n。这里为了简化问题,令n = 2 k n = 2^kn=2k。令从上到下为第1,2,…m层。用递归树求解总的运算时间,需要每层都看。这也是和写递归式的不同之处。递归式只要看其中任意一层即可。可以从上往下看。第1层的运算时间是从第1-2层的分解加第2-1层的合并,前面提到过,分解与合并时间之和是c 1 + c 2 n c_1+c_2nc1​+c2​n,因为低次项在算渐进界时不重要,可以直接简化为c n cncn。第2层每个问题规模n/2,分解与合并时间之和均为c n 2 \frac{cn}{2}2cn​,因此,第2层总时间也为c n cncn。这样递推可得,所有m层每层运行时间均为c n cncn。如何计算总层数?从n = 2 k n = 2^kn=2k个元素二分降到1个元素,需要降k = l g n k=lgnk=lgn次,因此总层数为m = l g n + 1 m=lgn+1m=lgn+1,故有总运行时间T ( n ) = c n l g n + c n T(n)=cnlgn+cnT(n)=cnlgn+cn

我之前文章提到过,函数增长率只看高阶,因此,猜测该算法运行时间的渐进确界为n l g n nlgnnlgn。

用代换法可以证明,这一结论成立。则其时间复杂度即为Θ ( n l g n ) \Theta(nlgn)Θ(nlgn)。

pythonsort函数时间复杂度_合并排序算法——时间复杂度详解和python代码实现相关推荐

  1. 排序算法python实现_合并排序算法– Java,C和Python实现

    排序算法python实现 Merge sort is one of the most efficient sorting algorithms. It works on the principle o ...

  2. KNN算法原理详解及python代码实现

    KNN算法 算法原理 对数据的要求 算法的优缺点 算法需要注意的点 算法实现(python) 算法原理 计算待测样本与train_data的距离d并保存数组中 对d进行排序,取d最近的k个样本 统计样 ...

  3. Dijkstra 路径规划算法原理详解及 Python 代码实现

    荷兰数学家 E.W.Dijkstra 于 1959 年提出了 Dijkstra 算法,它是一种适用于 非负权值 网络的 单源最短路径算法,同时也是目前求解最短路径问题的理论上最完备.应用最广的经典算法 ...

  4. ADMM,ISTA,FISTA算法步骤详解,MATLAB代码,求解LASSO优化问题

    ADMM,ISTA,FISTA算法步骤详解,MATLAB代码,求解LASSO优化问题 原创文章!转载需注明来源:©️ Sylvan Ding's Blog ❤️ 实验目的 了解 ADMM, ISTA, ...

  5. kmeans算法详解和python代码实现

    kmeans算法详解和python代码实现 kmeans算法 无监督学习和监督学习 监督学习: 是通过已知类别的样本分类器的参数,来达到所要求性能的过程 简单来说,就是让计算机去学习我们已经创建好了的 ...

  6. 编辑距离算法详解和python代码

    编辑距离(Levenshtein Distance)算法详解和python代码 最近做NLP用到了编辑距离,网上学习了很多,看到很多博客写的有问题,这里做一个编辑距离的算法介绍,步骤和多种python ...

  7. c语言合并排序算法_合并排序算法

    c语言合并排序算法 Merge Sort follows the rule of Divide and Conquer to sort a given set of numbers/elements, ...

  8. c++数据结构中 顺序队列的队首队尾_数据结构与算法—队列详解

    前言 栈和队列是一对好兄弟,前面我们介绍过数据结构与算法-栈详解,那么栈的机制相对简单,后入先出,就像进入一个狭小的山洞,山洞只有一个出口,只能后进先出(在外面的先出去).而队列就好比是一个隧道,后面 ...

  9. Dijkstra算法图文详解和C++代码

    文章目录 1 Dijkstra算法基本原理 2 算法过程图解1(有向图) 3 算法过程图解2(无向图) 4 C++代码 4.1 案例1代码 4.2 案例2邻接矩阵定义 4.3 案例2代码Dijkstr ...

最新文章

  1. 右键菜单打开文件所在文件夹的插件EasyExplore
  2. c#打包工具支持bat_程序打包工具
  3. python7.1处理异常
  4. 全国计算机等级考试和职称考试题库,全国职称计算机等级考试题库及答案
  5. Apollo进阶课程㉖丨Apollo规划技术详解——Understand More on the MP Difficulty
  6. Android安全笔记-Android签名文件及初略架构
  7. python,文件md5校验
  8. 大数据环境下数据质量指标体系
  9. 极智开发 | UmiJS 快速上手
  10. 流程图中的实线_流程图符号_流程图中的带箭头的线段代表什么?
  11. 计算机不用鼠标怎么移动,如何在不使用鼠标的情况下拖动文件
  12. matlab光学教程,基于MATLAB的物理光学仿真
  13. oeasy教您玩转vim - 52 - # 正则查找
  14. FPGA(3)--VHDL及原理图--4位全加器
  15. thinkpad重装系统不引导_thinkpad系统重装教程
  16. Glide学习(二)—缓存策略
  17. 机器学习:公式推导与代码实现全书代码!
  18. 功放与喇叭的匹配原则
  19. urllib和urllib2的区别和使用
  20. 数仓工具—Hive集成篇之Kafka(03)

热门文章

  1. 【剑指offer】面试题57 - II:和为s的连续正数序列(Java)
  2. psycopg2.errors.UndefinedTable: relation “xxxx“ does not exist
  3. 体育测试数据绘图软件,原创健身运动体育测试数据统计app界面
  4. 鱼c论坛python课后作业_三日速成python?打工人,小心钱包,别当韭菜
  5. python中def main是什么意思_关于python:为什么使用def main()?
  6. 【每日SQL打卡】​​​​​​​​​​​​​​​DAY 8丨平面上的最近距离【难度中等】
  7. SpringBoot 自带工具类~FileCopyUtils
  8. 微服务架构设计模式~为应用程序定义微服务架构
  9. python正方形阴影面积计算_利用Python求阴影部分的面积实例代码
  10. matlab 建立ctruct,扩频通信系统及MATLAB仿真1