当前位置: 首页 > news >正文

【数据结构与算法】:快速排序和归并排序的非递归实现

1. 递归实现的缺陷

在以前的文章中我们把快速排序和归并排序的递归实现方式进行了介绍,但是在校招面试和在企业的日常开发过程中,仅掌握递归方法是不够的,因为递归也有它的缺陷。

我们知道在函数调用过程中会在内存中建立栈帧,栈帧的建立是会消耗空间的。而递归最致命的缺陷就是:在极端情况下,当栈帧的深度太深时,栈空间不够用,就会导致栈溢出!

1.1 栈溢出的例子
可以举一个简单的例子来证明存在栈溢出的情况。
比如:我们用递归实现 1 + 2 + 3 + …… + n 的求和。

#define _CRT_SECURE_NO_WARNINGS #include <stdio.h>int func(int n)
{return n == 1 ? 1 : n + func(n - 1);
}int main()
{int n = 0;scanf("%d", &n);int sum = func(n);printf("%d\n", sum);return 0;
}

当输入的 n = 10000 时 ,调试结果如下:

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/656443118d0e4b79a8993831e158fa7d.png

2. 递归改非递归的实现方式

通常来说,递归改非递归有两种方式:
1. 直接改成循环(迭代)
2. 借助数据结构的栈模拟

3. 快速排序的非递归 — 使用栈

1.首先先来观察快排的递归实现(三种方法均可,这里用的"前后"指针法 ):

在这里插入图片描述
通过观察我们发现,每次递归调用传过去的是一个数组一个区间,数组不用多说,这个区间就是我们的突破点。
也就是说我们要想一个方法来拿到每左右子区间,再对它们分别进行排序,这样才能模拟出递归的过程。那该如何做呢?借助数据结构的栈。

2.非递归的代码实现:

注意:由于C语言没有栈函数的库,所以这里使用的栈要提前准备好,实现栈的过程这里不再介绍。若想了解请前往我的主页。


void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}void QuickSortNonR(int* arr, int n)
{ST st;StackInit(&st);//先入右,后入左StackPush(&st, n - 1);StackPush(&st, 0);while (!StackEmpty(&st)){//先出左边界int left = StackTop(&st);StackPop(&st);//后出右边界int right = StackTop(&st);StackPop(&st);int keyi = left;int prev = left;//关键字默认左边第一个元素int cur = left + 1;while (cur <= right){//++prev != cur是指当cur和prev重合时不用多于的交换if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[cur], &arr[prev]);}cur++;}Swap(&arr[keyi], &arr[prev]);keyi = prev;if (keyi + 1 < right){StackPush(&st, right);StackPush(&st, keyi + 1);}if (left < keyi - 1){StackPush(&st, keyi - 1);StackPush(&st, left);}}StackDestory(&st);
}

排序结果如下:

在这里插入图片描述

3.步骤总结:

3.1.首先要先把数组 最右端最左端 入栈,这样就有了一个初始区间,然后开始循环。

3.2.取出栈顶的两个数据,分别赋给 leftright注意在这之后要pop掉取出的数据

3.3.再使用前后指针法走完一趟后就得到了keyi

3.4.然后数组就被 keyIndex 分成了两个子区间,分别是:
左区间:[left,keyi -1]
右区间:[keyi +1,right]

3.5.由于我们一般是先排左区间,再排右区间,根据栈的后进先出特性,所以要先入右区间

分别将左区间和右区间入栈,注意这里要判断
keyi + 1 < right
left < keyi - 1
否则会出现数组访问越界或是死循环的情况。

3.6.循环结束后,销毁栈。

4.总结一下

最后我们要知道的是,快排的非递归并不会使性能受到破坏,它的时间复杂度也是O(N*logN),它的效率依旧极高。使用非递归的主要原因就是防止溢出。

4. 归并排序的非递归 — 使用循环

首先我们知道归并排序的思想是将两组有序的数据合成一组有序的数据。

1. 循环步骤如下:

1.1 那么我们会这样想,当第一组只有1个数据,第二组也只有1个数据时,我们认为这两个数是有序的,把它们一一归并到一个临时数组中,此时临时数组里的2个数据就有序了。

1.2.当第一组有2个有序数据,第二组也有2个有序数据时,把它们两两归并到一个临时数组中此时临时数组里的4个数据就有序了。

1.3.当第一组有3个有序数据,第二组也有3个有序数据时,把它们三三归并到一个临时数组中,此时临时数组里的6个数据就有序了。

1.4 重复上述步骤……直到全部数据有序为止。

2.循环图解如下:

我们假设 gap 为每一组的数据个数,这里最难的是如何控制每一组的范围,即那个区间的边界

假设循环的初始值从 i = 0 开始,则两组的范围可以分别表示为:

第一组:[ i , i + gap - 1 ]
第二组:[ i + gap , i + 2gap - 1 ]

每组1个数据,一一归,归完后临时数组里的2个数据就有序了;
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/388cbe63cedd45ed9a8c8a013bd8a29e.png

每组2个数据,二二归,归完后临时数组里的4个数据就有序了;
在这里插入图片描述
每组4个数据,四四归,归完后临时数组里的8个数据就有序了;
在这里插入图片描述

2. 非递归的代码实现:

这里有两种边界修正问题需要特别注意:
(1)在归并过程中右半区间可能不存在
这时直接跳出循环,把剩下的那一个数据直接拷贝进数组即可。
比如在一一归并时:
在这里插入图片描述

(2)在归并过程中右半区间可能算多了

此时要把右半区间的右边界修改为数组最后一个元素的下标。
在这里插入图片描述


//归并排序的非递归
void MergeSortNonR(int* a, int sz)
{int* tmp = (int*)malloc(sizeof(int) *sz);int gap = 1; // 每组数据个数while (gap < sz){for (int i = 0; i < sz; i += 2 * gap){// [i, i+gap-1] [i+gap,i+2*gap-1]int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;// 归并过程中右半区间可能就不存在if (begin2 >= sz)break;// 归并过程中右半区间算多了, 修正一下if (end2 >= sz){end2 = sz - 1;}//归并的过程int index = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[index++] = a[begin1++];}else{tmp[index++] = a[begin2++];}}while (begin1 <= end1){tmp[index++] = a[begin1++];}while (begin2 <= end2){tmp[index++] = a[begin2++];}// 归一部分,拷一部分,没归的就不拷for (int j = i; j <= end2; ++j){a[j] = tmp[j];}}gap *= 2;}free(tmp);
}

排序结果如下:

在这里插入图片描述

3.总结一下

归并排序的非递归的时间复杂度也是O(N*logN),它与递归的性能也差不多。

相关文章:

  • 微软卡内基梅隆大学:无外部干预,GPT4等大语言模型难以自主探索
  • 设计模式|状态机模式(State Machine Pattern)
  • C++的stack和queue类(一):适配器模式、双端队列与优先级队列
  • ARM、X86、RISC-V三分天下
  • Android 14.0 SystemUI修改状态栏电池图标样式为横屏显示
  • Sybase ASE中的char(N)的坑以及与PostgreSQL的对比
  • 【机器学习】决策树(Decision Tree,DT)算法介绍:原理与案例实现
  • 如何使用Python中的logging模块进行日志记录?
  • 301永久重定向与302临时重定向的正确运用
  • 适用于 Windows 10 的 10 大免费数据恢复软件
  • Go语言中测试和性能
  • 速盾:服务器有cdn 带宽上限建议多少
  • HBase详解(4)
  • 云计算存在的安全隐患
  • PyQt PySide6零基础入门与项目实战视频教程
  • IndexedDB
  • Linux编程学习笔记 | Linux多线程学习[2] - 线程的同步
  • node.js
  • Python学习笔记 字符串拼接
  • React Transition Group -- Transition 组件
  • SpringCloud(第 039 篇)链接Mysql数据库,通过JpaRepository编写数据库访问
  • Unix命令
  • windows下mongoDB的环境配置
  • Yeoman_Bower_Grunt
  • 编写高质量JavaScript代码之并发
  • 解决iview多表头动态更改列元素发生的错误
  • 世界编程语言排行榜2008年06月(ActionScript 挺进20强)
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 新版博客前端前瞻
  • 一些基于React、Vue、Node.js、MongoDB技术栈的实践项目
  • 用jquery写贪吃蛇
  • 鱼骨图 - 如何绘制?
  • 怎么将电脑中的声音录制成WAV格式
  • 【运维趟坑回忆录 开篇】初入初创, 一脸懵
  • AI算硅基生命吗,为什么?
  • SAP CRM里Lead通过工作流自动创建Opportunity的原理讲解 ...
  • 交换综合实验一
  • #if和#ifdef区别
  • #laravel 通过手动安装依赖PHPExcel#
  • #QT(串口助手-界面)
  • (31)对象的克隆
  • (4) PIVOT 和 UPIVOT 的使用
  • (C#)Windows Shell 外壳编程系列4 - 上下文菜单(iContextMenu)(二)嵌入菜单和执行命令...
  • (Matlab)基于蝙蝠算法实现电力系统经济调度
  • (vue)页面文件上传获取:action地址
  • (八)Docker网络跨主机通讯vxlan和vlan
  • (附源码)python旅游推荐系统 毕业设计 250623
  • (三)Hyperledger Fabric 1.1安装部署-chaincode测试
  • (四)Linux Shell编程——输入输出重定向
  • (原)本想说脏话,奈何已放下
  • (转载)虚幻引擎3--【UnrealScript教程】章节一:20.location和rotation
  • (轉貼) 蒼井そら挑戰筋肉擂台 (Misc)
  • (轉貼) 寄發紅帖基本原則(教育部禮儀司頒布) (雜項)
  • ******IT公司面试题汇总+优秀技术博客汇总
  • **CI中自动类加载的用法总结