数据结构——排序算法之快速排序

 

  个人主页:日刷百题

系列专栏〖C/C++小游戏〗〖Linux〗〖数据结构〗 〖C语言〗

🌎欢迎各位点赞👍+收藏⭐️+留言📝 

前言:

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。
基本思想:
任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

递归实现方式常见有三种,区别于单趟思想,性能差别不大,下面我们看下快排递归实现。

一、快速排序的递归实现

1.1   Hoare排序

1.1.1  单趟目的

 左子序列中所有元素均小于基准值key,右子序列中所有元素均大于基准值key。

1.1.2   动图解析

单趟思路:

(1)首先记录下keyi位置为最左边位置,然后left和right分别从数组两端开始往中间走。
(2)right先开始向中间行动,如果right处的值小于keyi处的值,则停止等待left走。
(3)left开始行动,当left找到比keyi处小的值时,left和right处的值进行交换。
(4)当两个位置相遇时,将相遇位置的值与keyi处的值进行交换。
 

该排序有一个需要注意的点是:必须左边先走找小

因为左边先走,必定相遇时位置对应的值小于keyi位置值,保证最后这俩个位置交换,相遇位置即是keyi位置对应值最终位置。

解析:

(1)右边先走,假设left遇到right,最后相遇情况是right找到了小于keyi位置的值,left没有找到大于keyi位置值,所以相遇位置值小于keyi位置值。

(2)右边先走,假设right遇到left,最后相遇情况是left找到大,right找到小,left与right互换,left位置对应值小于keyi位置值,right继续找小,与left相遇,所以相遇位置值小于keyi位置值。

 1.1.3  代码实现

解析:

该代码将单趟写在子函数中,这样使得整个代码层次更加清晰,也便于理解。可以发现我们对单趟中keyi做了优化,因为keyi的位置,是影响快速排序效率的重大因素。因此我们采用了三数取中的方法解决选keyi不合适的问题。即知道这组无序数列的首和尾后,我们只需要在首,中,尾这三个数据中,选择一个排在中间的数据作为基准值(keyi),进行快速排序,即可进一步提高快速排序的效率。

后面2种单趟也做这样的优化,后面就不过多介绍。

//Hoare快排
int GetMid(int* a, int begin, int end)
{int mid = (begin + end) / 2;if (a[begin] > a[end]){if (a[end] > a[mid]){return end;}else{if (a[begin] > a[mid]){return mid;}else{return begin;}}}else//(a[begin]<= a[end]){if (a[begin] > a[mid]){return begin;}else{if (a[end] > a[mid]){return mid;}else{return end;}}}
}
void swap(int* x, int* y)
{int z = *x;*x = *y;*y = z;
}
int  _QuickSort_Hoare(int* a, int begin, int end)
{int mid = GetMid(a,begin, end);swap(&a[begin], &a[mid]);int keyi = begin;int left = begin;int right = end;while (left < right){//右边找小while (left < right && a[right] >= a[keyi]){right--;}//左边找大while (left < right && a[left] <= a[keyi]){left++;}swap(&a[left], &a[right]);}swap(&a[keyi], &a[left]);return left;}
void  QuickSort_Hoare(int* a, int begin, int end)
{if (begin >= end){return;}int keyi= _QuickSort_Hoare(a, begin, end);//单趟//递归  [begin,keyi-1] keyi,[keyi+1,end]QuickSort_Hoare(a, begin, keyi - 1);QuickSort_Hoare(a, keyi+1, end);}

1.2  挖坑法 

1.2.1  单趟目的

 左子序列中所有元素均小于基准值key,右子序列中所有元素均大于基准值key。

1.2.2  动图解析

单趟思路:

(1)将begin处的值放到key中,将其置为坑位(pit)
(2)right找到比key小的值后将值放入坑位,然后将此处置为新的坑。
  (3)  left找到比key大的值后将值放入坑位,然后将此处置为新的坑。
  (4)当left与right相遇的时候,将key放入到坑位中。

 1.2.3  代码实现 

int GetMid(int* a, int begin, int end)
{int mid = (begin + end) / 2;if (a[begin] > a[end]){if (a[end] > a[mid]){return end;}else{if (a[begin] > a[mid]){return mid;}else{return begin;}}}else//(a[begin]<= a[end]){if (a[begin] > a[mid]){return begin;}else{if (a[end] > a[mid]){return mid;}else{return end;}}}
}
void swap(int* x, int* y)
{int z = *x;*x = *y;*y = z;
}
int  _QuickSort_Pit(int* a, int begin, int end)
{int mid = GetMid(a, begin, end);swap(&a[begin], &a[mid]);int pit = begin;int  key = a[begin];int left = begin;int right = end;while (left < right){while (left < right && a[right] >= key){right--;}a[pit] = a[right];pit = right;while(left < right&& a[left] <= key){left++;}a[pit] = a[left];pit = left;}a[left] = key;return left;}
void  QuickSort_Pit(int* a, int begin, int end)
{if (begin >= end){return;}int keyi = _QuickSort_Pit(a, begin, end);//[begin,keyi-1],keyi,[keyi+1,end]QuickSort_Pit(a, begin, keyi - 1);QuickSort_Pit(a, keyi + 1, end);}

1.3 双指针法

1.3.1  单趟目的

 左子序列中所有元素均小于基准值key,右子序列中所有元素均大于基准值key。

1.3.2  动图解析

单趟思路:

(1)cur位于begin+1的位置,prev位于begin位置,keyi先存放begin处的值。
(2)如果cur处的值大于key处的值,cur++.
(3)如果cur处的值小于等于key处的值,cur处的值,则与prev+1处的值进行交换。
(4)当循环结束时,将prev处的值与keyi的值相交换,返回prev

1.3.3  代码实现

int GetMid(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] > a[end])
{if (a[end] > a[mid]){return end;}else{if (a[begin] > a[mid]){return mid;}else{return begin;}}
}
else//(a[begin]<= a[end])
{if (a[begin] > a[mid]){return begin;}else{if (a[end] > a[mid]){return mid;}else{return end;}}
}
}
void swap(int* x, int* y)
{int z = *x;*x = *y;*y = z;
}
int  _QuickSort_Pointer(int* a, int begin, int end)
{int mid = GetMid(a, begin, end);swap(&a[begin], &a[mid]);int key = begin;int prev= begin;int cur = prev + 1;while (cur <= end){if (a[cur] > a[key]){cur++;}else{prev++;swap(&a[prev], &a[cur]);cur++;}}swap(&a[key], &a[prev]);return prev;}
void  QuickSort_Pointer(int* a, int begin, int end)
{if (begin >= end){return;}int keyi = _QuickSort_Pointer(a, begin, end);//[begin,keyi-1],keyi,[keyi+1,end]QuickSort_Pointer(a, begin, keyi - 1);QuickSort_Pointer(a, keyi + 1, end);}

二、快速排序的优化

2.1  三数取中法选key

这个方法提升效率比较显著,上面已经排序均用该方法优化。

2.2  递归到小的子区间,使用插入排序

由于快速排序是递归进行的,当递归到最后三层时,此时数组中的值其实已经接近有序,而且这段区间再递归会极大占用栈(函数栈帧开辟的地方)的空间,最后三层的递归次数占总递归次数的百分之90,所以在区间数据量小于10,我们就不进行递归快速排序了,转而使用插入排序。

 

int GetMid(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] > a[end])
{if (a[end] > a[mid]){return end;}else{if (a[begin] > a[mid]){return mid;}else{return begin;}}
}
else//(a[begin]<= a[end])
{if (a[begin] > a[mid]){return begin;}else{if (a[end] > a[mid]){return mid;}else{return end;}}
}
}
void swap(int* x, int* y)
{int z = *x;*x = *y;*y = z;
}
int  _QuickSort_Pointer(int* a, int begin, int end)
{int mid = GetMid(a, begin, end);swap(&a[begin], &a[mid]);int key = begin;int prev= begin;int cur = prev + 1;while (cur <= end){if (a[cur] > a[key]){cur++;}else{prev++;swap(&a[prev], &a[cur]);cur++;}}swap(&a[key], &a[prev]);return prev;}
void  QuickSort_Pointer(int* a, int begin, int end)
{if (begin >= end){return;}if(end-begin+1>10)
{
int keyi = _QuickSort_Pointer(a, begin, end);//[begin,keyi-1],keyi,[keyi+1,end]QuickSort_Pointer(a, begin, keyi - 1);QuickSort_Pointer(a, keyi + 1, end);
}
else
{
InsertSort(a + begin, end - begin + 1);
}}

三、快速排序的非递归实现

递归改为非递归,一般2种方法:

1、递归转化为非递归可以写成循环,比如斐波那契数列

2、递归转化为非递归可以写成栈,比如现在的快排

递归使用的空间是栈空间,所以容易出现栈溢出的情况,我们将快速排序改为非递归版本,这样空间的开辟就在堆上了,这样也就解决了这个问题。

快速排序的非递归与递归思想相同,非递归使用栈来模拟递归的实现,思路如下:

(1)入栈一定要保证先入左再入右。
(2)取出两次栈顶的元素,然后进行单趟排序

(3)将区间分为[left , keyi - 1] ,keyi ,[ keyi +  1 , right ] 进行右、左入栈。若区间不存在或为1个值则不入栈。
(4)循环2、3步骤直到栈为空。
 

代码实现:

int GetMid(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] > a[end])
{if (a[end] > a[mid]){return end;}else{if (a[begin] > a[mid]){return mid;}else{return begin;}}
}
else//(a[begin]<= a[end])
{if (a[begin] > a[mid]){return begin;}else{if (a[end] > a[mid]){return mid;}else{return end;}}
}
}
void swap(int* x, int* y)
{int z = *x;*x = *y;*y = z;
}
int  _QuickSort_Pointer(int* a, int begin, int end)
{int mid = GetMid(a, begin, end);swap(&a[begin], &a[mid]);int key = begin;int prev= begin;int cur = prev + 1;while (cur <= end){if (a[cur] > a[key]){cur++;}else{prev++;swap(&a[prev], &a[cur]);cur++;}}swap(&a[key], &a[prev]);return prev;}typedef int DateType;
typedef struct Stack
{DateType* a;int top;int capacity;
}Stack;
//初始化和销毁栈
void InitStack(Stack* ps)
{assert(ps);ps->a = NULL;ps->top = ps->capacity = 0;
}
void DestoryStack(Stack* ps)
{assert(ps);free(ps->a);ps->a = NULL;ps->top = 0;ps->capacity = 0;
}//出栈和入栈
void StackPush(Stack* ps, DateType x)
{assert(ps);if (ps->top == ps->capacity){int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;DateType* tmp = (DateType*)realloc(ps->a, sizeof(DateType) * newcapacity);if (tmp == NULL){perror("realloc fail:");return;}ps->a = tmp;ps->capacity = newcapacity;}ps->a[ps->top] = x;ps->top++;
}
void StackPop(Stack* ps)
{assert(ps);assert(ps->top > 0);ps->top--;
}//栈的有效个数和栈顶元素
int StackSize(Stack* ps)
{assert(ps);return ps->top;
}
DateType StackTop(Stack* ps)
{assert(ps);assert(ps->top > 0);return   ps->a[ps->top - 1];
}
//判空
bool IsEmptyStack(Stack* ps)
{assert(ps);return ps->top == 0;
}
void  QuickSort_Non_r(int* a, int begin, int end)
{Stack tmp;InitStack(&tmp);StackPush(&tmp,end);StackPush(&tmp, begin);while (!IsEmptyStack(&tmp)){int left = StackTop(&tmp);StackPop(&tmp);int right = StackTop(&tmp);StackPop(&tmp);int keyi = _QuickSort_Pointer(a, left, right);if (keyi+1 <right){StackPush(&tmp,right);StackPush(&tmp,keyi+1);}if (left < keyi - 1){StackPush(&tmp, keyi-1);StackPush(&tmp,left);}}DestoryStack(&tmp);}

 总结:本篇文章总结了快速排序的递归及非递归俩大种方式。

希望大家阅读完可以有所收获,同时也感谢各位铁汁们的支持。文章有任何问题可以在评论区留言,百题一定会认真阅读!

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

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

相关文章

设计模式-工厂方法模式

一 设计模式-工厂方法模式 工厂方法模式&#xff08;Factory Method Pattern&#xff09;是一种常用的类创建型设计模式&#xff0c;它属于对象的创建型模式&#xff0c;主要用来封装对象的创建过程。在该模式中&#xff0c;一个抽象工厂定义了一个接口用于创建产品对象&#x…

C++ 具名要求-全库范围的概念 - (Swappable) - (ValueSwappable)

此页面中列出的具名要求&#xff0c;是 C 标准的规范性文本中使用的具名要求&#xff0c;用于定义标准库的期待。 某些具名要求在 C20 中正在以概念语言特性进行形式化。在那之前&#xff0c;确保以满足这些要求的模板实参实例化标准库模板是程序员的重担。若不这么做&#xf…

opencv-4.8.0编译及使用

1 编译 opencv的编译总体来说比较简单&#xff0c;但必须记住一点&#xff1a;opencv的版本必须和opencv_contrib的版本保持一致。例如opencv使用4.8.0&#xff0c;opencv_contrib也必须使用4.8.0。 进入opencv和opencv_contrib的github页面后&#xff0c;默认看到的是git分支&…

NAS搭建NextCloud集成OnlyOffice

1、安装NextCloud&#xff08;如果总是中断就换个镜像源&#xff09; 2、创建容器 如果需要穿透选HOST 端口必须80 读写必须开 3、启动容器并配置&#xff0c;看图。 启动看日志&#xff0c;等启动完成再访问。首次启动大约5-10分钟左右。 成功后&#xff0c;我们正常进行安装…

学习selenium+python使用 XPath 表达式来实现找到目标元素时智能封装等待,执行测试代码启动Chrome浏览器后,地址栏只显示data;

背景 学习使用 XPath 表达式来实现找到目标元素时智能封装等待执行测试代码启动Chrome浏览器后&#xff0c;地址栏只显示data&#xff1b; 代码如下 import unittest from selenium import webdriver from selenium.common.exceptions import NoSuchElementException from …

6.2 声音编辑工具GoldWave5简介(5)

6.2.4录制声音 利用Windows自带的“录音机”录制声音时&#xff0c;只能录制最大时长为1分钟的声音&#xff0c;而利用GoldWave5&#xff0c;可以录制时长长达277小时以上的声音&#xff0c;而且&#xff0c;录制完成后&#xff0c;还可以很方便地对声音进行处理、转换等操作。…

由jar包冲突导致的logback日志不输出

最近接手一个厂商移交的项目&#xff0c;发现后管子系统不打印日志。 项目使用的logback 本地断点调试发现logback-classic jar冲突导致 打出的war中没有 相关的jar 解决方法&#xff1a; 去除pom 文件中多余的 logback-classic 应用&#xff0c;只保留最新版本的。 重新打…

记录用python封装的第一个小程序

前言 我要封装的是前段时间复现的一个视频融合拼接的程序&#xff0c;现在我打算将他封装成exe程序&#xff0c;我在这里只记录一下我封装的过程&#xff0c;使用的是pyinstaller&#xff0c;具体的封装知识我就不多说了&#xff0c;可以参考我另一篇博客&#xff1a;将Python…

NLP技术在搜索推荐场景中的应用

NLP技术在搜索推荐中的应用非常广泛&#xff0c;例如在搜索广告的CTR预估模型中&#xff0c;NLP技术可以从语义角度提取一些对CTR预测有效的信息&#xff1b;在搜索场景中&#xff0c;也经常需要使用NLP技术确定展现的物料与搜索query的相关性&#xff0c;过滤掉相关性较差的物…

HashMap 为什么线程不安全?

如果你现在需要准备面试&#xff0c;可以关注我的公众号&#xff1a;”Tom聊架构“&#xff0c;回复暗号&#xff1a;”578“&#xff0c;领取一份我整理的50W字面试宝典&#xff0c;可以帮助你提高80%的面试通过率&#xff0c;价值很高&#xff01;&#xff01; JDK1.7 及之前…

函数指针和回调函数 以及指针函数

函数指针&#xff08;Function Pointer&#xff09;&#xff1a; 定义&#xff1a; 函数指针是指向函数的指针&#xff0c;它存储了函数的地址。函数的二制制代码存放在内存四区中的代码段&#xff0c;函数的地址它在内存中的开始地址。如果把函数的地址作为参数&#xff0c;就…

力扣2182.构造限制重复的字符串

思路&#xff1a;先记录每个字符的出现次数&#xff0c;构建一个新字符串&#xff0c;从尾取字符&#xff0c;每取一个该字符个数-1&#xff0c;若该字符已经取到有repeatLimit个&#xff0c;则递归取次大的字符&#xff0c;并对应字符个数-1&#xff0c;若没有次大字符了&…

Elasticsearch基础篇(七):分片大小修改和路由分配规则

Elasticsearch基础篇(七)&#xff1a;分片大小修改和路由分配规则1. 分片1.1 主分片&#xff08;Primary Shard&#xff09;1.2 副本分片&#xff08;Replica Shard&#xff09;1.3 分片路由&#xff08;Routing Shard&#xff09; 2. 分片分配的基本策略3. 分片写入验证3.1 数…

2024年前端最新面试题-vue3(持续更新中)

文章目录 前言正文什么是 MVVC什么是 MVVM什么是 SPA什么是SFC为什么 data 选项是一个函数Vue 组件通讯&#xff08;传值&#xff09;有哪些方式Vue 的生命周期方法有哪些如何理解 Vue 的单项数据流如何理解 Vue 的双向数据绑定Vue3的响应式原理是什么介绍一下 Vue 的虚拟 DOM介…

设计模式-- 3.适配器模式

适配器模式 将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 角色和职责 请求者&#xff08;client&#xff09;&#xff1a;客户端角色,需要使用适配器的对象&#xff0c;不需要关心适配器内部的实现&#xff0c;…

IEEE论文LaTeX模板解析(十一)| 尾页栏目均衡(Last Page Column Equalization)

本文收录于专栏&#xff1a;IEEE论文LaTeX模板解析&#xff0c;本专栏将会围绕IEEE论文LaTeX模板解析持续更新。欢迎点赞收藏关注&#xff01; 文章目录 IEEE 在最后一页平衡了各栏的长度。这种平衡是粗略的&#xff0c;因为参考文献或 IEEE 传记条目通常不会断开&#xff0c;…

Origin 或 Referer 的关系和区别

Origin 或 Referer 的关系和区别 Origin 和 Referer 都可以服务端用来做来源验证&#xff0c;来防止 csrf 攻击&#xff0c;都是浏览器自动带在请求头的但是&#xff0c;可以通过 Referrer Policy 来禁止请求携带 referer&#xff0c;【请求头增加字段 Referrer-Policy: no-ref…

TF-IDF(词频-逆文档频率)

文章目录 高频词只能说明词汇在评论中出现的频率高&#xff0c;但并不能说明这个词汇的重要性。利用关键词提取可以弥补这一不足&#xff0c;关键词提取是一种自动化的文本处理技术&#xff0c;它可以从一篇文章中自动抽取出最能代表文章主题和内容的若干个词语或短语。通常情况…

常用的检测数据异常值方式,以及异常数据如何处理!!

清除数据异常值 1.箱线图检测数据异常值方法2.3σ原则检测数据异常值方法3. 异常数据处理方式&#xff1a;总结&#xff08;小白看看就行&#xff09; 1.箱线图检测数据异常值方法 箱线图检测&#xff1a;箱线图是一种常用的异常值检测方法&#xff0c;它以数据的分位数为基础…

常见的加密算法

加密算法 AES 高级加密标准(AES,Advanced Encryption Standard)为最常见的对称加密算法(微信小程序加密传输就是用这个加密算法的)。对称加密算法也就是加密和解密用相同的密钥&#xff0c;具体的加密流程如下图&#xff1a; RSA RSA 加密算法是一种典型的非对称加密算法&am…