【数据结构】快速排序非递归算法及其改进

在学数据结构中排序这一章节的时候,有一道有关快速排序的作业题描述如下:

按下述要求编写快速排序的非递归算法:
定义一个栈(或队列),把整个序列的上、下界入栈(或队列)。当栈(或队列)非空时进行如下操作:
(1)取栈顶(或队头)元素作为序列的上、下界,在区间的头部、中间、尾部取关键字居中的元素作为中枢元素,进行一趟快速排序;
(2)在一趟排序过程中,如果子表已有序(没有发生元素交换),则该子序列排序结束,否则先对划分出的长度较短的子表进行排序,且将另一子表的上、下界入栈(或队列)保存;
(3)若待排序区间中数据元素数小于等于3,则不再进行分割,而是直接进行比较完成排序。
完成算法实现后,用大数据量进行测试,同原有快速排序算法在时间上进行对比分析。

这道题本质上是在优化快速排序。

1.用栈代替递归

函数的递归本身就利用了堆栈,用栈改写递归算法为非递归是常用思路。我们可以将序列的上下界入栈,排序时再取栈顶元素并出栈,本题中要求先对划分出的长度较短的子表进行排序,那么我们就先入栈较长的子表,将较短的子表后入栈以便下次循环中优先出栈。(部分)代码如下:

SeqStack<int> s;
while( !s.isEmpty())  //栈非空且未排好序{//得到某分区的左右边界int r;s.getTop(r);s.pop();int l;s.getTop(l);s.pop();boundary = Part(a, l, r, Flag);  //boundary为枢轴元素所在位置if( boundary - l < r - boundary)    ///如果左分区比右分区短{if( boundary + 1 < r ) //判断右分区是否存在{s.push(boundary + 1);s.push(r);}if( boundary - 1 > l ) //判断左分区是否存在{//将左分区端点后入栈以便优先排序s.push(l);s.push(boundary - 1);}}else                                       //如果右分区比左分区短{if( boundary - 1 > l ) //判断左分区是否存在{s.push(l);s.push(boundary - 1);}if( boundary + 1 < r ) //判断右分区是否存在{///将右分区端点后入栈以便优先排序s.push(boundary + 1);s.push(r);}}}

2.中枢元素的取法

一般书本上的快速排序是取第一个待排序的元素作为中枢元素,而为了优化快速排序算法,如果能够尽量将中枢元素取在整个待排序数组的中位数附近,那么两个子表的长度就会接近,这样一来就减少了时间复杂度,优化了快速排序算法,本题中采取的方法是:在区间的头部、中间、尾部取关键字居中的元素作为中枢元素。其实我们也可以平均取五个点、七个点来找居中的元素。(头中尾取中值)代码如下:

int Mid(int first, int mid, int last)
//求头,中,尾中关键字居中的元素
{if ((first>=mid&&first<=last)||(first<=mid&&first>=last))return first;else if ((mid>=first&&mid<=last)||(mid<=first&&mid>=last))return mid;elsereturn last;
}

注意,要在Partition函数中交换首元素和中枢元素的位置:

int pivot = Mid(low, (low+high)/2, high);//选择区间的头部、中间、尾部取关键字居中的元素作为中枢元素Swap(elem[pivot], elem[low]);  //保持代码的统一性

3.在一趟排序中,如果子表已有序,则该子序列排序结束

这一点便于理解,代码实现上可以定义一个bool类型的标记,来判断Partition中是否发生过元素交换,如果没有交换bool赋true。

4.待排区间中元素数<=3,则不再进行分割,则直接比较排序

快速排序是适用于大数据量的排序方式,在数据量比较小的时候,使用插入排序或者选择排序较好,所以很多实用的排序算法使用快排+插排的方式排序。本题中要求:若待排序区间中数据元素数小于等于3,则不再进行分割,而是直接进行比较完成排序,代码如下:

void JustSort(int a[], int left, int right)
//数组元素小于等于3时的直接比较排序
{if(right-left==1)         //序列只有两个元素时{if(a[left] > a[right]){Swap(a[left], a[right]);}}else                      //三个元素时{if(a[left] > a[left+1])Swap(a[left], a[left+1]);if(a[left+1] > a[right])Swap(a[left+1], a[right]);if(a[left] > a[left+1])Swap(a[left], a[left+1]);}
}

应用以上优化方法后,用50000个随机元素的数组进行排序,与书本上的递归快排进行时间比较,如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可见有一定的优化效果。

全部代码如下:

#ifndef __ExQUICKSORT_H__
#define __ExQUICKSORT_H__
#include "SeqStack.h"int Mid(int first, int mid, int last)
///求头,中,尾中关键字居中的元素
{if ((first>=mid&&first<=last)||(first<=mid&&first>=last))return first;else if ((mid>=first&&mid<=last)||(mid<=first&&mid>=last))return mid;elsereturn last;
}int Part(int elem[], int low, int high, int &flag)  ///参数flag用以判断是否已经有序,以便结束排序
//原快速排序算法中的划分部分,写成函数方便循环调用
{int pivot = Mid(low, (low+high)/2, high);//选择区间的头部、中间、尾部取关键字居中的元素作为中枢元素Swap(elem[pivot], elem[low]);   /// 交换枢轴元素和首元素的位置,保持代码的统一性int e = elem[low];				// 取枢轴元素int i = low, j = high;while (i < j){while (i < j && elem[j] >= e)	// 使j右边的元素不小于枢轴元素j--;if (i < j){elem[i++] = elem[j];flag = 1;}while (i < j && elem[i] <= e)	// 使i左边的元素不大于枢轴元素i++;if (i < j){elem[j--] = elem[i];flag = 1;}}elem[i] = e;return i;                           //返回枢轴元素位置
}void JustSort(int a[], int left, int right)
///数组元素小于等于3时的直接比较排序
{if(right-left==1)         //序列只有两个元素时{if(a[left] > a[right]){Swap(a[left], a[right]);}}else                      //三个元素时{if(a[left] > a[left+1])Swap(a[left], a[left+1]);if(a[left+1] > a[right])Swap(a[left+1], a[right]);if(a[left] > a[left+1])Swap(a[left], a[left+1]);}
}template <class ElemType>
void ExQuickSort(ElemType a[], int left, int right)
// 操作结果:对数组elem[low .. high]中的元素进行快速排序
{int Flag = 0;             ///标记,用来判断是否已经排好序(未发生过交换)SeqStack<int> s;if( left<right ){if(right-left < 3)   //如果序列小于等于3个元素{JustSort(a, left, right);return ;}int boundary = Part(a, left, right, Flag);   //划分后的中枢所在位置if(Flag == 0)return ;if( boundary - left < right - boundary)    ///如果左分区比右分区短{if( boundary + 1 < right ) //判断右分区是否存在{s.push(boundary + 1);s.push(right);}if( boundary - 1 > left ) //判断左分区是否存在{///将左分区端点后入栈以便优先排序s.push(left);s.push(boundary - 1);}}else                                       ///如果右分区比左分区短{if( boundary - 1 > left ) //判断左分区是否存在{s.push(left);s.push(boundary - 1);}if( boundary + 1 < right ) //判断右分区是否存在{///将右分区端点后入栈以便优先排序s.push(boundary + 1);s.push(right);}}while( !s.isEmpty())  //栈非空且未排好序{//得到某分区的左右边界int r;s.getTop(r);s.pop();int l;s.getTop(l);s.pop();boundary = Part(a, l, r, Flag);  //boundary为枢轴元素所在位置if(Flag == 0)        ///如果已经有序,结束排序。return ;if(right-left < 3)   //如果序列小于等于3个元素{JustSort(a, left, right);return ;}if( boundary - l < r - boundary)    ///如果左分区比右分区短{if( boundary + 1 < r ) //判断右分区是否存在{s.push(boundary + 1);s.push(r);}if( boundary - 1 > l ) //判断左分区是否存在{///将左分区端点后入栈以便优先排序s.push(l);s.push(boundary - 1);}}else                                       ///如果右分区比左分区短{if( boundary - 1 > l ) //判断左分区是否存在{s.push(l);s.push(boundary - 1);}if( boundary + 1 < r ) //判断右分区是否存在{///将右分区端点后入栈以便优先排序s.push(boundary + 1);s.push(r);}}}}
}#endif // __ExQUICKSORT_H__

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384578.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构】对快速排序原理的理解(图解,通俗易懂)

学习数据结构时&#xff0c;书本上直接给出了快速排序的过程以及代码&#xff0c;对其原理解释的不够详细&#xff0c;琢磨代码后&#xff0c;发现其原理其实十分简单&#xff0c;简述如下&#xff1a; &#xff08;1&#xff09;在待排序列中找一个“中枢元素”&#xff08;书…

【离散数学】图论基础知识

文章目录1 图的基本概念2 图的连通性3 图的矩阵表示4 几种特殊的图4.1 二部图4.2 欧拉图4.3 哈密顿图4.4 平面图5 无向树6 生成树1 图的基本概念 无向图&#xff1a; 简而言之&#xff0c;边不带方向的图就是无向图。 有向图&#xff1a; 简而言之&#xff0c;边带方向的图就…

【运筹与优化】单纯形法解线性规划问题(matlab实现)

文章目录单纯形法步骤&#xff1a;1.将线性规划问题化为标准形式2.列出单纯形表3.进行最优性检验4.从一个基可行解转换到另一个目标值更大的基可行解&#xff0c;列出新的单纯形表5.重复3、4直到计算结束为止举例单纯形法matlab实现单纯形法是一种解线性规划问题的算法&#xf…

【Linux系统编程学习】 GCC编译器

此为牛客网Linux C课程1.2&1.3的课程笔记。 0. 简介 1. gcc和g的安装 sudo apt install gcc g2. gcc常用参数选项 3. gcc工作流程 首先是预处理器对源代码进行预处理&#xff08;后缀名.i&#xff09;&#xff0c;主要做以下事情&#xff1a; 把头文件加入到源代码当中删…

Spring5底层原理之BeanFactory与ApplicationContext

目录 BeanFactory与ApplicationContext BeanFactory ApplicationContext 容器实现 BeanFactory实现 ApplicationContext实现 ClassPathXmlApplicationContext的实现 AnnotationConfigApplicationContext的实现 AnnotationConfigServletWebServerApplicationContext的实…

【Linux系统编程学习】 静态库的制作与使用

此为牛客网Linux C课程 1.4&1.5 的课程笔记。 0. 关于静态库与动态库 库就是封装好的、可服用的代码&#xff0c;而静态和动态是指链接。 这节课讲的是静态库&#xff0c;是指在链接阶段&#xff0c;会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中&…

【Linux系统编程学习】 动态库的制作与使用

此为牛客网Linux C课程1.6&1.7 的课程笔记。 1. 动态库命名规则 2. 动态库的制作 第一步&#xff0c;用gcc编译生成.o目标文件&#xff0c;注意要用-fpic参数生成与位置无关的代码&#xff1b; 第二步&#xff0c;用gcc的-shared参数生成动态库。 涉及到的两个参数之前学过…

【Linux系统编程学习】 静态库与动态库的对比与总结

此为牛客网Linux C课程 1.9 的课程笔记。 1. 前几节课知识总结 程序编译成为可执行文件的过程&#xff1a; 静态库制作过程&#xff1a; 动态库制作过程&#xff1a; 2. 静态库的优缺点&#xff1a; 3. 动态库的优缺点&#xff1a; 更多可参考&#xff1a;吴秦&#xff1…

【Linux系统编程学习】 Makefile简单入门

此为牛客网Linux C课程1.10&1.11&1.12 的课程笔记。 0. Makefile介绍 1. Makefile文件命名与规则 示例&#xff1a; 使用vim编写如下名为Makefile的文件&#xff1a; app:sub.o add.o mult.o div.o main.ogcc sub.o add.o mult.o div.o main.o -o appsub.o:sub.cgcc …

【Linux系统编程学习】 GDB调试器的简单使用

此为牛客网Linux C课程 1.13&1.14&1.15&1.16 的课程笔记。 0. GDB简介 1. 准备工作 想要使用gdb调试&#xff0c;首先需要用gcc的-g参数生成可执行文件&#xff0c;这样才能在可执行文件中加入源代码信息以便调试&#xff0c;但是注意这并不是将源文件嵌入到可执行…

【Linux系统编程学习】C库IO函数与系统IO函数的关系

此为黑马Linux课程笔记。 1. C标准IO函数工作流程 如图&#xff0c;以C库函数的fopen为例&#xff0c;其返回类型是FILE类型的指针&#xff0c;FILE类型包含很多内容&#xff0c;主要包含三个内容&#xff1a;文件描述符、文件读写指针的位置和I/O缓冲区的地址。 文件描述符&…

【Linux系统编程学习】 文件描述符

此为牛客网Linux C课程1.19课程笔记。 1. 文件描述符表 如图&#xff0c;我们知道每个进程都有其虚拟地址空间&#xff08;0~4G&#xff09;&#xff0c;其中3 ~ 4G部分为内核区。进程的进程控制块保存就在内核区&#xff0c;而PCB中维护一个打开文件描述符表&#xff0c;每个…

【Linux系统编程学习】Linux系统IO函数(open、read、write、lseek)

此为牛客网Linux C课程1.20课程笔记。 1.open函数 open函数有两种&#xff0c;分别是打开一个已经存在的文件和创建并打开一个不存在的文件。 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>// 打开一个已经存在的文件 int open(const…

【Linux系统编程学习】Linux进程控制原语(fork、exec函数族、wait)

此为牛客Linux C和黑马Linux系统编程课程笔记。 1. fork函数 1.1 fork创建单个子进程 #include<unistd.h> pid_t fork(void);作用&#xff1a;创建一个子进程。 pid_t类型表示进程ID&#xff0c;但为了表示-1&#xff0c;它是有符号整型。(0不是有效进程ID&#xff0…

【Linux系统编程学习】匿名管道pipe与有名管道fifo

此为牛客Linux C和黑马Linux系统编程课程笔记。 0. 关于进程通信 Linux环境下&#xff0c;进程地址空间相互独立&#xff0c;每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到&#xff0c;所以进程和进程之间不能相互访问&#xff0c;要交换…

【Linux系统编程学习】信号、信号集以其相关函数

此为牛客Linux C和黑马Linux系统编程课程笔记。 文章目录0. 信号的概念1. Linux信号一览表2. 信号相关函数3. kill函数4. raise函数5. abort函数6. alarm函数7. setitimer函数8. signal函数9. 信号集10. 自定义信号集相关函数11. sigprocmask函数12. sigpending函数13. sigacti…

【Linux系统编程学习】父进程捕获SIGCHLD信号以处理僵尸进程

配合之前说过的sigaction函数和waitpid函数&#xff0c;我们可以解决子进程变成僵尸进程的问题。 先看如下示例程序&#xff1a; #include <sys/time.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> …

【Linux系统编程学习】Linux线程控制原语

此为牛客Linux C课程笔记。 0. 关于线程 注意&#xff1a;LWP号和线程id不同&#xff0c; LWP号是CPU分配时间片的依据&#xff0c;线程id是用于在进程内部区分线程的。 1. 线程与进程的区别 对于进程来说&#xff0c;相同的地址(同一个虚拟地址)在不同的进程中&#xff0c;反…

【Linux网络编程学习】预备知识(网络字节序、IP地址转换函数、sockaddr数据结构)

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 网络字节序 我们已经知道&#xff0c;内存中的多字节数据相对于内存地址有大端和小端之分。 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分&#xff0c;那么如何定义网络数…

【Linux网络编程学习】socket API(socket、bind、listen、accept、connect)及简单应用

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 什么是socket 所谓 socket&#xff08;套接字&#xff09;&#xff0c;就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。 一个套接字就是网络上进程通信的一端&#xff0c;提供了应用层进程利用网络协议交换…