引:2019年408中数据结构一道考察快速排序的选择题

答案:D

定位:这道题在考察快速排序一趟的概念。注意,基本的冒泡,插入,选择排序的一趟概念很容易理解,

接下来我们要讨论的是递归排序算法中(本文以快排和归并排序)讨论“趟”是否有意义。(先说结论,有,但并不像基本排序算法里那么简单,或者说,像基本算法里那么有普适性,归并排序只有非递归实现才有讨论趟的必要性,而快速排序更为复杂一些

思路:

回想教材(《数据结构》严)里对一趟的定义

算法描述:

可见第一张图的过程实际上是递归形式实现的快排第一次调用Partition函数产生的结果。

然而,我们知道,如果第一次选中的pivot处在了待排序元素最终结果中的中间位置。那么接下来的处理也是递归进行的,

如图(a)所示,在第一次调用Patition函数后,元素49被放到了最终位置,之后对49左侧位置元素调用Qsort进行处理,

之后,关键关键的一步来了。

对 49左侧位置元素(27 48 13)调用Qsort处理时,首先调用Partition函数选出Pivot 27(默认第一个元素作pivot),

接下来呢?是立刻返回并去处理49右边的元素再调用一次Partition吗?并不是,这显然不是递归函数执行过程,

正确的执行过程是在对(27 48 13)这个左子表调用Partition后变为(13 27 48)接下来会继续对27左侧子表调用Qsort进行处理。

什么意思?等递归函数处理到 49右侧位置元素 时,其左侧元素都已经排好序了!

我们再看题干描述  的概念:对尚未确定最终位置的所有元素进行一遍处理称为一趟。

发现问题了吗?如果是递归实现的快排,按题干的的意思,所有尚未确定最终位置元素进行一遍处理,只有第一次选中的元素处在最终位置的边缘,

才能保证有第二趟的概念,为什么?很简单,如果第一次选中的元素恰好在最终位置中间部分,比如教材里这个例子,那么严格意义上讲,这种情况下它的执行过程只有两趟(递归实现的快排)!

最新更改:上面我的结论有问题,如果第一次pivot选在了中间,也是有第二趟概念的。以2019年408选择题为例:
D选项中12和32排在了最终位置,那么假设这是两趟的结果吧,那得满足什么条件?
或者说D选项怎么改才算正确?
答:2,5,12,28,16,32,72,60(只是其中一种可能)
没错,把12左侧元素改为顺序即为可能出现的情况,也就是对应的快排代码中递归的顺序是先对pivot左侧递归调用Qsort,左侧都已经有序了,
再对右侧进行一次递归调用Qsort,首先会调用Partition,那么刚进行完这次Partition后,得到的结果即为“第二趟”。

注意这里的 趟 是题目定义的趟,所有未确定最终位置元素进行一遍处理。

(这里注意区分,我们平时讲的快速排序最坏情况下(初始情况完全有序),它的递归次数会达到最高(不是这里题目中的趟数))

简单反思下这个题纠结在哪了?快速排序我们平时记得多是Partition函数会使待排序数列产生一个位于最终位置的元素。

平时的教材中多是这样描述

就容易先入为主地认为1之后立刻执行2或者1,2同时进行(实际上的确有并行快速排序算法),

然而我们熟悉的多是递归形式实现的快速排序算法,PS:我又去查了下非递归实现的快排,多是用栈模拟..(把栈模拟出来,这难道就不算递归吗?)

这时另外一些稀奇古怪的问题冒出来了:所有的递归都可以改成非递归吗?手动压栈这种写法就不算递归了吗?(深坑,暂时不多做探讨,之所以会有这个问题是因为我看到了递归和非递归归并算法的实现)

即同样的,也有类似问题,问归并排序的“第二趟”处理结果类似问题

对序列25,57,48,37,12,82,75,29进行二路归并排序,第二趟归并后的结果为()。
A.25,57,37,48,12,82,29,75
B.25,37,48,57,12,29,75,82
C.12,25,29,37,48,57,75,82
D.25,57,48,37,12,82,75,29

然而,我们平时写的也多是递归形式的,

//递归形式
void Msort(int a[], int l, int r)
{mid = (l + r) / 2Msort(a,l,mid);Msort(a,mid+1,r);merge(a,l,r,mid);  //合并两个子表
}

还是和快排相似的问题:递归形式的归并排序有“趟”的概念吗?(如果按照和上一道题定义的趟的概念,是没有的!没有!)

那题就错了吗?不是,因为归并排序可以不借助栈,由循环结构实现。

非递归形式的归并排序思想是以2^k递增的间隔来划分待排序序列

void non_recur_msort(int arr[], int temp[], int left_end, int right_end)
{for(int i = left_end; i <= right_end; i = 2*i){//i 是划分长度,以2^k速度递增,for(int j = left_end; j <= right_end-i; j += 2*i)//j用来迭代处理每个划分长度下,待排序序列划分得到的子表merge(arr, temp, j, j+i-1, Min(j + 2*i - 1 , right_end)); //子表长度为i,合并的两个子表左子表起始位置为j,mid为 j+i-1,右子表终止位置为j+2*i-1,}
}

有些人会纠结奇数个元素怎么被处理的,看两张图,第一张是递归归并排序过程,第二张是非递归归并排序过程

第一张图——递归实现的归并排序是自顶向下划分(也就是划分时由大到小,再把小规模逐步求解),之后自底向上合并。

第二张图——非递归实现的归并排序的划分是由小到大(注意对比着上一张图看)

写到这,又回到了之前问题,快速排序的非递归形式会不会有类似(归并排序非递归方式)的实现?

似乎好多所谓的非递归只是手动实现了栈操作,然而我不确定这算不算是非递归。还有需要想到的应该是有没有并行快速排序的实现?

因为如果是并行实现的快排,那么同时调用Partition函数也就可以解释了

反思总结

像一些最基本的排序如插入排序,冒泡排序,选择排序的实现。在讨论一趟概念的时候并不需要考虑这么多。Why?

因为这些最基本的算法思想是迭代,什么是迭代?是逐步求得结果并更新,一趟的概念较契合:每迭代处理一次所有未排序元素,就会求解出一个未排序元素最终位置。

而快排和归并接触到的写法多是递归形式的,利用了分治的思想,既然涉及分治,就无法避免划分和求解问题的顺序问题。

出现这个问题的根源是我们用递归方式去思考问题划分问题时是正向进行的,而实际运算处理是自底向上(递归划分到最底层再向上求解)的,这一点很容易混淆。

那说了半天遇到这种问题怎么搞?

如果是快速排序问第二趟,那么只有第一趟选中的元素位于边缘,才能有第二趟的存在,第k趟以此类推,或者换句话说,快速排序如果运行产生了第2,3,4,5趟,那一定是出现了初始状态导致了最坏情况的问题(也就是初始状态基本有序,导致递归趟数最大),这种情况下,递归趟数和排序趟数是一样的。

如果是归并排序第二趟,那么默认是在讨论非递归形式的归并排序(注意不是把栈写出来就算了..从本质上讲你把栈写出来并不能算是把递归算法转化成了非递归算法)

总而言之,趟是个非常鸡肋的概念,国外教材中暂时没有见过类似于趟描述,进一步说,这个概念纯粹是造出来出题玩的,无聊至极

后记:

去stackoverflow上找了下non-recursion quicksort without a stack,没想到用英文搜问题抓到了我疑惑的实质:

非尾递归的递归转化成iterative(迭代)算法是有代价的,那就是格外的数据结构(栈),原理涉及计算理论里图灵完备性(然而我仍然纠结non-recursive这个概念,不过这玩意用了这么久总不可能所有人都错了吧..基本现在一提非递归就都在说用栈模拟...

StackOverflow上有一个人回复类似问题的角度值得记下来:

因为快速排序的思想是divide and conquer,因此在你不需要用到其他partition时候,必然需要额外的数据结构保存那些partition,因此是无法通过不添加额外数据结构来实现快速排序的

快速排序和归并排序中一趟的理解(递归和非递归)相关推荐

  1. C#实现(递归和非递归)快速排序和简单排序

    C#实现(递归和非递归)快速排序和简单排序 本人因为最近工作用到了一些排序算法,就把几个简单的排序算法,想冒泡排序,选择排序,插入排序,奇偶排序和快速排序等整理了出来,代码用C#代码实现,并且通过了测 ...

  2. 二叉树的中序遍历(递归和非递归版本)

    难易程度:★★ 重要性:★★★★★ 树结构是面试中的考察的重点,而树的遍历又是树结构的基础.中序遍历的非递归版本要求重点理解掌握. /*** 非递归版本的中序遍历* node指向待处理的节点,在中序遍 ...

  3. 算法之快速排序(递归和非递归)

    快速排序的两种实现方式.递归和非递归 1 package com.ebiz.sort; 2 3 import java.text.SimpleDateFormat; 4 import java.uti ...

  4. 二叉树的先中后序递归和非递归遍历(数据结构作业)

    一.设计思想 我创建二叉树是用的先序创建,其中用'#'代表空节点. 1.递归先序遍历 (1)如果当前节点为空节点(用'#'代表空节点),结束当前函数 (2)打印当前节点 (2)递归当前节点的左子树 ( ...

  5. C++实现二叉树 前、中、后序遍历(递归与非递归)非递归实现过程最简洁版本

    本文并非我所写,是复制的该链接中的内容: 最近学习二叉树,想编程实现递归和非递归的实现方式: 递归的方式就不说了,因为大家的递归程序都一样:但是对于非递归的实现方式, 根据这几天的查阅资料已看到差不多 ...

  6. 二叉树,二叉树的归先序遍历,中序遍历,后序遍历,递归和非递归实现

    二叉树,二叉树的归先序遍历,中序遍历,后序遍历,递归和非递归实现 提示:今天开始,系列二叉树的重磅基础知识和大厂高频面试题就要出炉了,咱们慢慢捋清楚! 文章目录 二叉树,二叉树的归先序遍历,中序遍历, ...

  7. (伪代码)树的前中后遍历和层次遍历算法实现(考研适用,递归和非递归)

    文章目录 前言 一.递归实现树的遍历 二.非递归实现 层次遍历 总结 前言 2022考研初试结束,总结了一些考研中基本常用算法.这篇主要是关于树的前中后遍历,递归实现和非递归实现两种,现在很多自命题在 ...

  8. python快速排序递归与非递归

    快速排序递归与非递归python 写在前面 快速排序的递归函数 快排的切分函数 快排的非递归函数 完整的源代码 写在前面 众所周知,快速排序相对于选择排序,插入排序,冒泡排序等初级排序有着天然的优势. ...

  9. 分别用递归和非递归方式实现二叉树先序、中序和后序遍历(java实现)

    分别用递归和非递归方式实现二叉树先序.中序和后序遍历 用递归和非递归方式,分别按照二叉树先序.中序和后序打印所有的节点.我们约定:先序遍历顺序 为根.左.右;中序遍历顺序为左.根.右;后序遍历顺序为左 ...

最新文章

  1. 定向输出命令_网络工程师之linux重定向命令和管道命令详解
  2. CAN笔记(20) 过程数据对象
  3. Feature event receviers
  4. ubuntu 安装 theano
  5. Facebook语音助手Aloha细节曝光,它的logo竟然是一座小火山?
  6. Android中activity的生命周期
  7. 计算机主板复位电路的组成,电脑主板复位电路工作原理
  8. 怎么把PDF文件拆分,PDF拆分软件怎么操作
  9. 第三十四篇-Palette(调色板)的使用
  10. 偏微分方程的特征线法
  11. 快递跨界电商是在“走弯路”
  12. 做SEO,如何分析竞争对手网站-趣味seo
  13. 25 欧拉积分: (伽马)函数、(贝塔)函数
  14. 中小游戏研发怎么靠发展游戏代理杀出一条血路
  15. 关于人工智能(一) 诞生与发展
  16. JavaScript如何实现多线程?
  17. 行政管理专业如何选择合适的毕业论文题目?
  18. Oracle 10g数据库概述
  19. Linux平台下Java调用C函数
  20. 第一章 计算机系统漫游

热门文章

  1. 咖说 | 激励层:区块链生态建设的驱动力量
  2. html认识时间游戏,认识时间教学设计
  3. qc35 说明书_教你Bose QC35耳机的使用方法
  4. 漫步有感 | 让自己温和一点
  5. Java常用的几种JSON解析工具
  6. PostgreSQL数据库如何查询表的主键
  7. Deepin下在线安装和使用ClamAV
  8. 卸载 金山毒霸 的方法
  9. C语言编程题(基础)
  10. MySQL三表查询(学生表、课程表、成绩表)查询出语文成绩比数学成绩高的学生信息