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

[排序]hoare快速排序

今天我们继续来讲排序部分,顾名思义,快速排序是一种特别高效的排序方法,在C语言中qsort函数,底层便是用快排所实现的,快排适用于各个项目中,特别的实用,下面我们就由浅入深的全面刨析快速排序。事先声明,快速排序有不同的版本,今天我们讲的是hoare的版本

目录

快排的定义

hoare快排的具体实现

快排的时间复杂度

优化快速排序

三数取中

小区间优化

相遇位置比key小的问题


快排的定义

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

hoare快排的具体实现

我们先看一下排序的动态图

快排的思想与其他的排序不同,其他排序的基本思想是将最大或者最小的数找出来,放到某一个位置,而在快排中,是将一个数排到有序的位置,然后将其左右分割。

快排会有key,left,right三个变量,key就是当前排序的数的下标,left就是左端,right就是右端

我们先看一下单趟排序的逻辑

注意:左右寻找比key位置大或小的数时,必须从key的另一侧开始移动不然会出现排序错误的问题,这个问题我们之后会具体讲到

那么我们用代码实现一下单趟排序的逻辑

void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}
void quicksort(int a[],int left,int right)
{int key = left;//我们假设key位于left的位置int begin = left;int end = right; //用begin和end记录left和right的位置//我们之后要用left和right的值,进行分割区间递归,所以不能改变其值while (begin < end){//右边找小while (begin<end&&a[end] >= a[key]){end--;}//左边找大while (begin<end&&a[begin] <= a[key]){begin++;}swap(&a[begin], &a[end]);}swap(&a[begin], &a[key]);
}

既然单趟排序的逻辑我们已经清楚了,那么我们下一步就该进行多次单趟排序的逻辑,这样我们就能完成快排的逻辑

我们这里先用递归的思想进行实现,看下面的逻辑图

以上便是快排多次进行单词排序的逻辑,即快速排序的全部实现逻辑

下面我们用代码进行实现

void quicksort(int a[],int left,int right)
{if (left >= right){return;}int key = left;//我们假设key位于left的位置int begin = left;int end = right; //用begin和end记录left和right的位置//我们之后要用left和right的值,进行分割区间递归,所以不能改变其值while (begin < end){//右边找小while (begin<end&&a[end] >= a[key]){end--;}//左边找大while (begin<end&&a[begin] <= a[key]){begin++;}swap(&a[begin], &a[end]);}swap(&a[begin], &a[key]);key = begin;//[left,key-1] key [key+1,right]quicksort(a, left, key - 1);//左区间递归排序quicksort(a, key+1, right);//右区间递归排序
}

这里我们还要理解一下,递归终止条件

left >= right

以上即快速排序的基本实现

快排的时间复杂度

我们都知道判断一个排序效率的方法就是比较其时间复杂度

那么快排的时间复杂度是多少呢?

如果从代码的角度看,这个时间复杂度是非常难以计算的

我们先来看快排的递归层数,我们根据上面的逻辑图,可以大致的发现,快排是将数组类似分为二叉树的结构

因此递归的层数为logN层,而在单趟排序中end和begin从两边开始走直到相遇一共走了N步

从这个角度看快排的时间复杂度为  O(NlogN)

因此快排是和堆排序,希尔排序位于同一赛道的排序算法,都是极其高效的算法

排序十万个数(单位毫秒ms)

排序一百万个数 (单位毫秒ms)

 排序五百万个数 (单位毫秒ms)

可以看到当前的快排,并没有想象中那么快,甚至在数多的情况下和堆排序以及希尔排序,还显得效率较低。
而且在排有序数组的情况下,不要说一百万个数,在十万个数有序数组中,会发生一个大问题

1,效率变低

2.由于递归层次太深,每次递归都要建立新的栈帧,这就会可能导致栈溢出的问题 

我们来分析一下问题,之前在正常情况下,时间复杂度为N*logN的前提是每次都是二分递归,即key位置的数都是接近中间的值,此时当二分递归时,递归的深度就是logN,但如果按上面有序情况下,递归的深度是N,这就是上面问题的来源

因此我们现在的快排还是有明显的缺陷

优化快速排序

那么我们如何解决这个问题呢?

避免有序情况下,效率退化

我们可以改变key的选取,如果我们每次都选取最左侧值为key或者最右侧值为key,就会导致上面递归过深的问题,所以我们不能固定选key。

1.随机选key

随机数选key虽然能够解决问题,但是还是有些不靠谱,毕竟是随机的

2.三数取中

最左边,最右边,中间,选取不是最大的和最小的作key

为了保证代码的逻辑不发生变化,即还从最左端的为key,我们就将三数取中的值与最左边的值进行交换,再执行代码逻辑。

三数取中

三数取中是取大小是中间的值,然后完成最好的情况就是二分的情况,即效率最高的情况

运用分支语句进行两两比较返回中间值,直接放代码,逻辑比较简单,不作解释

int GetMid(int* a, int left, int right)
{int mid = (left + right) / 2;//left mid rightif (a[mid] > a[left]){if (a[mid] < a[right])return mid;else if (a[left] > a[right])return left;elsereturn right;}else{if (a[mid] > a[right])return mid;else if (a[right] > a[left])return left;elsereturn right;}
}

那么我们的快排中需要将交换left和三数取中mid的位置,即加上两行代码,我们其他的逻辑不发生变化

代码如下

void quicksort(int a[],int left,int right)
{if (left >= right){return;}//三数取中int mid = GetMid(a, left, right);swap(&a[mid], &a[left]);  int key = left;//我们假设key位于left的位置int begin = left;int end = right; //用begin和end记录left和right的位置//我们之后要用left和right的值,进行分割区间递归,所以不能改变其值while (begin < end){//右边找小while (begin<end&&a[end] >= a[key]){end--;}//左边找大while (begin<end&&a[begin] <= a[key]){begin++;}swap(&a[begin], &a[end]);}swap(&a[begin], &a[key]);key = begin;//[left,key-1] key [key+1,right]quicksort(a, left, key - 1);quicksort(a, key+1, right);}

在优化后,我们再来比较一下快排的效率

 可以发现,在三数取中后,快排效率也有了优化,而且避免了在有序情况下,递归过深的问题

小区间优化

我们的快排虽然有了优化,但是还有一点缺陷,描述如下图所示

而我们小区间优化,只需要加一个判断语句,对数据个数进行判断,若小于10就用其他的排序方法,大于10就正常递归排序

那么我们选用其他的排序方法要用哪个比较好呢?

我们有插入,堆排序,选择,冒泡,希尔排序,归并排序

我们可以一一进行比较与排除

希尔排序不适用于小数据的排序,堆排序虽然可以,但是我们想一下,没有必要为10个数再单独进行建堆,不然就得不偿失了;归并也是利用递归,没有必要。

那么我们就剩下了冒泡,选择,插入

而在之前的文章中,我们分析过,冒泡和选择排序是远远不如插入排序的效率的

那么我们就选择插入排序

在快排的底层中,小区间优化也是使用的插入排序,这就是插入排序的实际应用

代码如下

	//小区间优化,不再递归分割排序,减少递归次数if ((right - left + 1) < 10){InsertSort(a + left, right - left - 1);}

以上便是优化快排的全部实现

下面放上优化过快排代码

int GetMid(int* a, int left, int right)
{int mid = (left + right) / 2;//left mid rightif (a[mid] > a[left]){if (a[mid] < a[right])return mid;else if (a[left] > a[right])return left;elsereturn right;}else{if (a[mid] > a[right])return mid;else if (a[right] > a[left])return left;elsereturn right;}
}
void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}
void quicksort(int a[],int left,int right)
{if (left >= right){return;}//小区间优化,不再递归分割排序,减少递归次数if ((right - left + 1) < 10){InsertSort(a + left, right - left - 1);}else{//三数取中int mid = GetMid(a, left, right);swap(&a[mid], &a[left]);  int key = left;//我们假设key位于left的位置int begin = left;int end = right; //用begin和end记录left和right的位置//我们之后要用left和right的值,进行分割区间递归,所以不能改变其值while (begin < end){	//右边找小while (begin<end&&a[end] >= a[key]){end--;}//左边找大while (begin<end&&a[begin] <= a[key]){begin++;}swap(&a[begin], &a[end]);}swap(&a[begin], &a[key]);key = begin;//[left,key-1] key [key+1,right]quicksort(a, left, key - 1);quicksort(a, key+1, right);}
}
int main()
{int a[] = { 6,1,2,7,9,3,4,5,10,8 };int sz = sizeof(a) / sizeof(a[0]);quicksort(a, 0, sz - 1);for (int i = 0; i < sz - 1; i++){printf("%d ", a[i]);}return 0;
}

相遇位置比key小的问题

之前我们遗留了一个小问题,就是怎么保证eft和right相遇位置的值一定比key位置小,这样交换后,会让key的左右两边分为比key大的和比key小的,如果相遇位置比key要大的话,那就让数据排序毁了。

那么如何保证相遇位置比key小呢?

先说结论,就是我们上面所说的

当左边作key时,就让右边先走,可以保证相遇位置比key小

以下即解释:

以上是便是hoare排序相关问题

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 为什么多数大数据治理项目都是失败的?Gartner调查失败率超过90%
  • Vue2父传子
  • JNI回调用中不同线程的env无法找到正确的kotlin的class
  • Vite 常用插件配置:自动导入+自动注册组件+动态创建图标+设置组件名
  • C 语言基础概念总结
  • 在没有源程序的情况时,如何通过控制鼠标按钮控制电脑exe程序?
  • Android小技巧:利用动态代理自动切换线程(续)
  • wodpress设置固定链接的方式和好处【SEO优化】
  • Qt遇到qt自身组件找不到
  • Firefox扩展程序和Java通信
  • C# Task.WaitAll 的用法
  • BGP选路之Local Preference
  • table car vs. table cars:数据库命名用单数还是复数?
  • OpenCV图像滤波(1)双边滤波函数bilateralFilter的使用
  • 小规模的LLMS
  • crontab执行失败的多种原因
  • Django 博客开发教程 8 - 博客文章详情页
  • exports和module.exports
  • Idea+maven+scala构建包并在spark on yarn 运行
  • iOS帅气加载动画、通知视图、红包助手、引导页、导航栏、朋友圈、小游戏等效果源码...
  • JAVA并发编程--1.基础概念
  • Linux编程学习笔记 | Linux多线程学习[2] - 线程的同步
  • Linux中的硬链接与软链接
  • PHP 程序员也能做的 Java 开发 30分钟使用 netty 轻松打造一个高性能 websocket 服务...
  • Spring框架之我见(三)——IOC、AOP
  • 猫头鹰的深夜翻译:Java 2D Graphics, 简单的仿射变换
  • 巧用 TypeScript (一)
  • 入口文件开始,分析Vue源码实现
  • 思维导图—你不知道的JavaScript中卷
  • 一份游戏开发学习路线
  • 一起来学SpringBoot | 第十篇:使用Spring Cache集成Redis
  • 最近的计划
  • media数据库操作,可以进行增删改查,实现回收站,隐私照片功能 SharedPreferences存储地址:
  • AI算硅基生命吗,为什么?
  • Java总结 - String - 这篇请使劲喷我
  • 微龛半导体获数千万Pre-A轮融资,投资方为国中创投 ...
  • ​​​​​​​​​​​​​​汽车网络信息安全分析方法论
  • ​马来语翻译中文去哪比较好?
  • #{} 和 ${}区别
  • #《AI中文版》V3 第 1 章 概述
  • #NOIP 2014#day.2 T1 无限网络发射器选址
  • $ is not function   和JQUERY 命名 冲突的解说 Jquer问题 (
  • $.extend({},旧的,新的);合并对象,后面的覆盖前面的
  • (17)Hive ——MR任务的map与reduce个数由什么决定?
  • (4)logging(日志模块)
  • (9)目标检测_SSD的原理
  • (Arcgis)Python编程批量将HDF5文件转换为TIFF格式并应用地理转换和投影信息
  • (保姆级教程)Mysql中索引、触发器、存储过程、存储函数的概念、作用,以及如何使用索引、存储过程,代码操作演示
  • (二)c52学习之旅-简单了解单片机
  • (附源码)springboot金融新闻信息服务系统 毕业设计651450
  • (附源码)计算机毕业设计ssm电影分享网站
  • (附源码)计算机毕业设计SSM疫情下的学生出入管理系统
  • (六)c52学习之旅-独立按键
  • (论文阅读笔记)Network planning with deep reinforcement learning
  • (三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)