0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

以非递归的形式来写快速排序

算法与数据结构 来源:码农的荒岛求生 作者:陆小风 2022-11-08 17:01 次阅读

今天给大家讲解一道非常有趣的算法面试题,以非递归的形式来写快速排序

其实这也可以衍生出更多同类问题,非递归二叉树的前序、中序、后序遍历等等,这些问题的背后的思想是一致的,那就是用栈来手动模拟递归调用

道理很简单有没有,一句话就能说清楚,但问题是你真的理解了吗?该怎样用栈来手动模拟递归调用呢?你的大脑在面对这个问题时有一个清晰的思路吗?

别着急,我们先从最简单的快排开始。

快排,quick sort

快速排序想必大家都知道,我们以数组中的某个数字为基准,通常是数组的第一个或者最后一个(当然也可以是其它选择方式),这里假设以数组的最后一个元素为基准:

7091ae22-5f3e-11ed-8abf-dac502259ad0.png

然后将数组中小于该基准的数字放在左边、将大于该数字的放在右边:

70afb958-5f3e-11ed-8abf-dac502259ad0.png

经过这一次处理后base就被放到了最终的位置上并得到了两个子数组:base左边的数组和base右边的数组,以同样的方式处理这两个子数组即可。

用代码表示就是这样:

voidquick_sort(vector&arr,intb,inte){
if(b>=e)return;
inti=b-1;
for(intk=b;k< e; k++)
        if (arr[k] < arr[e])
            swap(&arr[++i], &arr[k]);
    swap(&arr[++i], &arr[e]);
    
    quick_sort(arr, b, i - 1);
    quick_sort(arr, i + 1, e);
}

其中参数中的b和e表示begin和end,也就是范围

可以看到,最终使用递归的方式编写的代码非常简洁,也很容易理解,递归是计算机科学中一个极其重要的概念,递归对于理解编译原理、编程语言、分而治之算法思想、排序以及动态规划等等有重要的意义

递归版本很简单有没有,如果让你用非递归的方式来实现呢

非递归手写快速排序

想一想这个问题!如果你真正理解递归的话那么就应该能写出来。

我们再来看看这个递归写法。

首先会得到一个问题quick_sort(arr, b, e),我们利用base进行一次划分后得到两个子问题:

quick_sort(arr, b, i - 1)

quick_sort(arr, i + 1, e)

在递归版本中这两个子问题的状态(所谓的状态就是要解决哪个子问题,这里用参数中的begin和end来界定)是随着函数的调用自动保存在栈帧中的,而我们需要用栈这种数据结构来模拟这个过程。

接下来,我们用变量task来表示要处理的子问题,也就是说入栈出栈的都是task,task可以这样定义:

pair

表示要对哪一段数组进行排序,因此使用了pair来记录这段数组的开始和结尾。

由于需要使用栈来追踪问题的解决顺序,因此我们最终这样定义栈:

stack>tasks;

一切准备就绪,是时候创建些任务了,任务的起源是什么呢?很简单,就是数组本身:

intsize=arr.size();
tasks.push(pair(0,size-1));

接下来就是最重要的部分了:

while(!tasks.empty()){
//取出栈顶元素
//处理
//是否有新的子任务需要push到栈中
}

整体的框架就是这样,接下来的三个问题就是:

取出栈顶元素

处理

是否有新的子任务需要push到栈中,如果有则push到栈中

第一个问题很简单,没什么可说的;第二个问题是说我们该怎样处理一个子问题,其实也很简单,就是用base将数组划分为两个子数组。

第三个问题是重点,我们该怎么知道接下来是否有新的子任务需要push到栈中呢?

想一想这个问题。。。

如果用base对数组进行划分后发现数组已经是有序的那么就没有必要创建子任务了,因为当前的数组已经有序了嘛!否则我们就需要创建子任务。

因此我们必须知道对数组进行划分后数组是不是已经排好序。

基于上述讨论,我们可以这样实现划分函数partition:

intpartition(vector&arr,intb,inte,bool*sorted){
if(b>e||b==e)return-1;

inti=b-1;
for(intj=b;j< e; j++) {
    if (arr[j] < arr[e]) {
      *sorted = false;
      swap(arr[++i],arr[j]);
    }
  }
  swap(arr[++i], arr[e]);

  return i;
}

这其实和开始递归版本中quick_sort函数里的划分部分代码没什么区别,变化的部分仅在于我们将一次划分后base所在的下标以及判断一次划分后数组是否有序记录在参数sorted中

一次划分后如果sorted的值为true也就是数组已经有序那么我们无需再创建新的子问题,一次划分后我们得到两个新的更小的子问题,即:

boolsorted=true;
intp=partition(arr,top.first,top.second,&sorted);

if(sorted){
continue;
}else{
tasks.push(pair(p+1,top.second));
tasks.push(pair(top.first,p-1));
}

所有问题分析完毕,完整的代码为:

voidquick_sort(vector&arr){
intsize=arr.size();
if(size==0||size==1)return;
stack>tasks;
tasks.push(pair(0,size-1));

intb=0;

while(!tasks.empty()){
autotop=tasks.top();
tasks.pop();

boolsorted=true;
intp=partition(arr,top.first,top.second,&sorted);

if(sorted){
continue;
}else{
tasks.push(pair(p+1,top.second));
tasks.push(pair(top.first,p-1));
}
}
}

运行一下,it works like magic,有没有!

这段代码是怎样运行的?

No,其实一点都不magic,接下来我们仔细看看这段代码是怎么运行的。

假设当前栈顶元素为(2,9),我们获取栈顶元素,并将其从中pop掉:

70f63d60-5f3e-11ed-8abf-dac502259ad0.png

此时我们要对数组下标2到9的元素进行排序,把末尾的base作为基准进行划分:

71187c36-5f3e-11ed-8abf-dac502259ad0.png

假设划分后base放到了下标为5的位置,这样我们得到了两个子问题(2,3)以及(4,9):

713b77c2-5f3e-11ed-8abf-dac502259ad0.png

由于经过base的划分后我们判断出该数组不是有序的(partition函数中sorted参数的作用),因此我们需要将两个子问题(2,3)以及(4,9)放到栈中:

715f5ade-5f3e-11ed-8abf-dac502259ad0.png

就这样,我们解决了子任务(2,9),并得到了两个更小的子问题(2,3)以及(4,9),接着while循环继续从栈中弹出任务并重复上述过程,当栈为空时我们一定能确信数据已经有序了。

这个过程“完全”模拟了上述递归函数的调用,这里之所以加了引号,是因为我们的迭代快排版本进行了一点点小小的优化,这个优化是什么呢?

尾递归

依然假设递归调用到函数quick_sort(2,9),此时的函数栈帧为:

717f73e6-5f3e-11ed-8abf-dac502259ad0.png

基于base划分后依然得到:

713b77c2-5f3e-11ed-8abf-dac502259ad0.png

根据递归版本的quick_sort实现接着我们需要调用quick_sort(2,3),此时的栈帧为:

71bb9722-5f3e-11ed-8abf-dac502259ad0.png

看到非递归版本与递归版本的不同了吧:

728a3cbc-5f3e-11ed-8abf-dac502259ad0.png

在非递归版本下,对处理子任务(2,9)时会将该任务从栈中pop出来,而递归版本则不会pop出quick_sort(2,9)的栈帧,函数quick_sort(2,3)执行完后还会再次回到函数quick_sort(2,9),然后接着调用函数quick_sort(4,9)。

而之所以非递归实现可以提前将子任务(2,9)从栈中弹出是因为递归版本下所有递归调用都位于函数的末尾,这就是所谓的“尾递归”。

尾递归是一种比较常见的现象,二叉树的前序遍历递归实现也是这样:

voidtree_travel(Tree*t){
if(t){
print(t->value);
tree_travel(t->left);
tree_travel(t->right);
}
}

你可以使用和本文一样的套路将上述递归代码转为非递归代码,但是如果是二叉树的中序遍历或者后序遍历呢?

voidtree_travel(Tree*t){
if(t){
tree_travel(t->left);
print(t->value);
tree_travel(t->right);
}
}

此时,本文中讲解的套路就失效了,因此我们需要一种更加通用的方法将此类非尾递归代码转为递归代码,这种通用的方法是什么呢





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 算法
    +关注

    关注

    23

    文章

    4587

    浏览量

    92478

原文标题:字节一面:非递归手写快速排序

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    FPGA排序-冒泡排序介绍

    排序算法是图像处理中经常使用一种算法,常见的排序算法有插入排序、希尔排序、选择排序、冒泡排序、归
    发表于 07-17 10:12 1047次阅读
    FPGA<b class='flag-5'>排序</b>-冒泡<b class='flag-5'>排序</b>介绍

    matlab实现快速排序法(原创)

    使用快速排序法进行排序,本以为很简单就可以实现,但搜索了一下help文档,只有C中的qsort存在,况且调用比较麻烦,其实在数据结构中,快速排序
    发表于 08-15 11:33

    matlab快速排序算法实现

    只有C中的qsort存在,调用比较麻烦,其实在数据结构中,快速排序法是经典排序之一,上网搜了一下简介,把对应的VC程序改了一下,做成了下面的matlab代码:%快速
    发表于 02-29 15:58

    快速排序

    // 快速排序package algorithmsimport "fmt"// 第一种写法func quickSort(values []int, left, right int
    发表于 10-17 19:05

    简述计算机排序

    不用交换,低指针指向的小于枢轴的元素不用交换。直到高低指针指向小于等于或者大于等于的元素后直接交换元素。一趟过后,在低子分区和高子分区中继续进行递归快速排序方式。(一种优化方法是三位选中,第二种是在最后高低分区中,high –
    发表于 12-26 23:07

    《C Primer Plus》读书笔记——递归

    必须包含可以终止递归调用的语句(如if)。尾递归最简单的递归形式。把递归调用语句放在函数结尾(return语句之前)。举个栗子: 计算n的阶
    发表于 02-05 20:06

    数组快速排序及索引vi

    分享一个数组快速排序及索引的vi,lv里面的某些集成功能vi还是比较好用的,善于调用的话可以节约大家的编写时间。
    发表于 05-06 09:11

    C#实现快速排序

    快速排序法是对冒泡排序的一种改进。它的基本思想是,通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字
    发表于 08-09 17:57 16次下载

    快速排序是一种交换排序

    快速排序在每次分割的过程中,需要 1 个空间存储基准值。而快速排序的大概需要 Nlog2N次的分割处理,所以占用空间也是 Nlog2N 个。
    的头像 发表于 07-27 14:49 2859次阅读
    <b class='flag-5'>快速</b><b class='flag-5'>排序</b>是一种交换<b class='flag-5'>排序</b>

    C语言排序快速排序的技巧

    快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,
    的头像 发表于 07-29 15:14 2435次阅读
    C语言<b class='flag-5'>排序</b>中<b class='flag-5'>快速</b><b class='flag-5'>排序</b>的技巧

    所有递归代码都可以转为递归代码

    之所以所有的递归都能转为迭代算法是因为递归借助函数调用,函数调用本身就是基于调用栈这种结构实现的,只不过这一切都是自动完成的,我们当然也可以用代码手动模拟出来。
    的头像 发表于 04-19 15:02 2016次阅读

    递归代码都转为递归可以吗

    之所以所有的递归都能转为迭代算法是因为递归借助函数调用,函数调用本身就是基于调用栈这种结构实现的,只不过这一切都是自动完成的,我们当然也可以用代码手动模拟出来。
    的头像 发表于 02-17 14:35 707次阅读
    <b class='flag-5'>递归</b>代码都转为<b class='flag-5'>非</b><b class='flag-5'>递归</b>可以吗

    2分钟看懂快速排序的算法

    之前有同学提出想要复习一下排序算法,那我们今天就挑一个难度中等的,快速排序
    的头像 发表于 02-25 09:32 763次阅读

    排序算法有哪些

    1. 归并排序递归版) 归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治策略,即分为两步:分与治。 分:先递归
    的头像 发表于 10-11 15:49 570次阅读
    <b class='flag-5'>排序</b>算法有哪些

    递归神经网络结构形式主要分为

    递归神经网络(Recurrent Neural Networks,简称RNN)是一种具有时间序列处理能力的神经网络,其结构形式多样,可以根据不同的需求进行选择和设计。本文将介绍递归神经网络的几种主要
    的头像 发表于 07-05 09:32 428次阅读