数据结构-快速排序“人红是非多”?看我见招拆招

目录

1.快速排序

Hoare版本:

挖坑法:

前后指针版本:

快速排序的时间复杂度

2.快速排序的优化

三数取中法选key

随机数选key

三路划分法

3. 非递归实现快速排序


1.快速排序

快速排序一共有三种版本:Hoare版本、挖坑法、前后指针版本

Hoare版本:

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

也就是说如果我们要排一个升序,我们可以在待排数据中选择一个值key,把大于该值的数据放在该值的右边,小于该值的数据放在该值的左边,然后在左边的数据中同样选择一个值,重复以上步骤,同时,在右边的数据中选择一个值,重复以上步骤,直到key的左边和右边都是有序的,此时所有数据都有序了。

过程如图所示,是一个递归的过程:

下面我们先来实现一趟的排序:

可以选左边第一个数据为key,然后从右边先开始遍历,当右边找到小于key的值时,停下来,当左边遍历找到大于key的值时,也停下来,然后交换左右两边的数据,最后当左右相遇的时候,把key交换到相遇位置,这就保证了小于key的数据落入key左边,大于key的数据落在key右边。

上代码:

int PartSort(int* a, int left, int right)
{int key = a[left];int keyi = left;while (left < right){while (left < right && a[right] >= key){right--;}while (left < right && a[left] <= key){left++;}Swap(&a[left], &a[right]);}Swap(&a[keyi], &a[left]);return left;
}

我们单趟排完之后应该如下图所示:

下面来解释一下代码中的循环判断条件:

最外层循环:left<right,左右相遇时就停止。

内层循环:left < right && a[right] >= key,这段代码解决了两个可能存在的问题:

1.  死循环问题

当待排数据如下面所示就可能造成死循环:

所以a[right] >= key时继续遍历。

2. 越界问题

如果a[right] >= key时继续遍历,下面极端情况可能导致越界:

所以我们在内层循环中还要判断一下 left<right。

这就是单趟排序的代码了,我们要实现对所有数据的升序,递归调用就行了,当完成一趟排序时,返回相遇位置,然后对相遇位置的左边和右边数据继续重复进行以上操作。这有些类似于二叉树的递归问题。

代码如下:

//交换函数
Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
//快速排序
int PartSort(int* a, int left, int right)
{int key = a[left];int keyi = left;while (left < right){while (left < right && a[right] >= key){right--;}while (left < right && a[left] <= key){left++;}Swap(&a[left], &a[right]);}Swap(&a[keyi], &a[left]);return left;
}void QuickSort(int* a, int begin, int end)
{if (begin >= end){return;}int keyi = PartSort(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1,end);
}Print(int* a, int n)
{for (int i = 0; i < n; i++){printf("%d ", a[i]);}printf("\n");
}
int main()
{int a[] = { 9,7,5,2,4,7,1,6,0,8 };QuickSort(a, 0, sizeof(a) / sizeof(int) - 1);Print(a, sizeof(a) / sizeof(int));return 0;
}

整个排序过程如下图:

注意下面这段代码的作用是:当区间只有一个值或者出现区间不存在的情况的时候就返回

 if (begin >= end)
    {
        return;
    }

不知道大家有没有注意到一个情况,我们在选择key为左边的数据时,先让右边开始遍历,这是为什么呢?

首先,我们选左边的数据为key,那最终相遇位置的数就一定要比key的值小,这样交换后才能保证key的左边的值都比它小,右边的值都比它大,那我们如何保证相遇位置的值一定就比key小呢?

先给结论:

1. 左边做key,右边先走;保证了相遇位置的值比key小。

2. 右边做key,左边先走;保证了相遇位置的值比key大。

下面我们来论证一下:

结论2的论证同上。

这就是Hoare版本,但是通过上文的学习,这种版本存在的坑太多,下面我们来学一种方法避坑。

挖坑法:

挖坑法的单趟排序过程如图所示:

先选左边的一个数据,把它作为坑,并保存它的值,然后继续右边遍历找到比key小的值就停下,挖走这个值填坑,挖走后形成新的坑,左边遍历找比key大的值就停下,挖走这个值填坑......最后左右相遇,把保存的key值填坑。

代码实现如下:

//挖坑法
int PartSort2(int* a, int left, int right)
{int key = a[left];int hole = left;while (left < right){while (left < right && a[right] >= key){right--;}a[hole] = a[right];hole = right;while (left < right && a[left] <= key){left++;}a[hole] = a[left];hole = left;}a[hole] = key;return hole;
}void QuickSort(int* a, int begin, int end)
{if (begin >= end){return;}int keyi = PartSort2(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);
}
Print(int* a, int n)
{for (int i = 0; i < n; i++){printf("%d ", a[i]);}printf("\n");
}
int main()
{int a[] = { 9,7,5,2,4,7,1,6,0,8 };QuickSort(a, 0, sizeof(a) / sizeof(int) - 1);Print(a, sizeof(a) / sizeof(int));return 0;
}

前后指针版本:

前后指针版本单趟排序过程如下图所示:

我们可以看到,cur在找小,如果a[cur]<key,prev++,然后交换a[prev]和a[cur],如果a[cur]>key,prev不动,整个过程中cur一直不停的往后走,直到cur越界就结束了,此时再交换key和a[prev]。

代码如下:

//前后指针版本
int PartSort3(int* a, int left, int right)
{int prev = left;int cur = left + 1;int keyi = left;while (cur <= right){if (a[cur] <= a[keyi]){prev++;Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;return keyi;
}
void QuickSort(int* a, int begin, int end)
{if (begin >= end){return;}int keyi = PartSort3(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);
}
Print(int* a, int n)
{for (int i = 0; i < n; i++){printf("%d ", a[i]);}printf("\n");
}
int main()
{int a[] = {6,1,2,7,9,3,4,5,10,8};QuickSort(a, 0, sizeof(a) / sizeof(int) - 1);Print(a, sizeof(a) / sizeof(int));return 0;
}

快速排序的时间复杂度

时间复杂度(最好):O(N*logN)。

时间复杂度(最坏):O(N^2)。

什么时候最好呢?

当每次选的key恰好是中位数时,每次都把数据分成两份,每次减少一半的运算量,相当于二分法:

什么时候最坏呢?

当待排数据本来就是有序的时候,每次选key,选的都是最小的值,此时就相当于等差数列:

那我们选key有两种方案:

1. 随机数取key。

2. 三数取中法选key。

这样可以保证不会是最坏的情况。

2.快速排序的优化

三数取中法选key

三数取中法就是,把左边、右边和中间的三个数相比较,取出其中的中位数,把它作为key,这样就可以提高快速排序的效率。

代码如下:

//三数取中法选key
int GetMidIndex(int* a, int left, int right)
{int mid = (left + right) / 2;if (a[mid] < a[left]){if (a[mid] > a[right]){return mid;}else if (a[right] > a[left]){return left;}else{return right;}}else{if (a[mid] < a[right]){return mid;}else if (a[right] > a[left]){return right;}else{return left;}}
}
//快速排序
//Hoare版本
int PartSort(int* a, int left, int right)
{int midi = GetMidIndex(a, left, right);Swap(&a[left], &a[midi]);int keyi = left;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(int* a, int begin, int end)
{if (begin >= end){return;}int keyi = PartSort(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);
}

以上就是三数取中法对快速排序的优化了,下面我们来看一道题,看看我们的快速排序能不能通过?

题目链接:力扣(LeetCode)

结果呢?

超出时间限制了,这其实是力扣针对快速排序三数取中专门设计的一个测试用例,他故意把左边、右边和中间的值都设的很小,这样即使你三数取中,选出的key依旧很小,接近我们上文说的最坏情况,所以会超出时间限制,那我们不玩三数取中能不能过呢?

结果很明显,还是过不了,这次他直接给了个有序的测试用例,这就直接是我们上文中所说的最坏情况了,那怎么办呢?别急,我们还有一招:

随机数选key

随机数取key的意思是,我们保证左右的数据位置不变,中间数据的位置取一个随机数,这样我们三数取中得到的key也是随机的数据,这样力扣就针对不到我们了。

代码如下:

void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}//三数取中法选key
int GetMidIndex(int* a, int left, int right)
{//随机数取keyint mid=left+(rand()%(right-left));if (a[mid] < a[left]){if (a[mid] > a[right]){return mid;}else if (a[right] > a[left]){return left;}else{return right;}}else{if (a[mid] < a[right]){return mid;}else if (a[right] > a[left]){return right;}else{return left;}}
}
//前后指针版本
int PartSort3(int* a, int left, int right)
{int midi = GetMidIndex(a, left, right);Swap(&a[left], &a[midi]);int prev = left;int cur = left + 1;int keyi = left;while (cur <= right){if (a[cur] <= a[keyi]){prev++;Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;return keyi;
}
void QuickSort(int* a, int begin, int end)
{if (begin >= end){return;}int keyi = PartSort3(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);
}int* sortArray(int* nums, int numsSize, int* returnSize) {srand(time(0));QuickSort(nums,0,numsSize-1);*returnSize=numsSize;return nums;
}

int mid=left+(rand()%(right-left));

表示中间位置取随机位置,为了防止随机数越界,我们用它取余(right-left)。

但是这就结束了吗?还是太天真了,力扣预判了你的预判,不信再运行一下:

这次它给的数据全部相同,那不管我们怎么取key值都是取的最小的,这就又相当于最坏的情况,可见这道题为了针对快速排序是费尽了心思,那我们就没办法了吗?

当然不是,我们还有终极一招,

三路划分法

何谓三路划分呢?我们之前的快速排序是把大于等于key的放在右边,小于等于key的放在左边,相当于待排数据分为两份,而三路划分的意思是把小于key的放在左边,大于key的放在右边,等于key的放在中间,如图所示:

这种方法就是把等于key的收拢在中间位置,当我们递归子区间的时候,只递归小于和大于的区间,这样当待排数据中有重复数据时,可以大大提高效率,尤其是上述测试用例,收拢之后,左右子区间直接就没有值了,都不用再递归。

下图就是三路划分的思想:

我们可以演示一下三路划分的过程:

可以看到,三路划分的本质就是:

1. 和key相等的值都被收拢到中间

2. 小的被甩到左边,大的被甩到右边。

代码如下:

//三数取中法选key
int GetMidIndex(int* a, int left, int right)
{//随机数取keyint mid=left+(rand()%(right-left));if (a[mid] < a[left]){if (a[mid] > a[right]){return mid;}else if (a[right] > a[left]){return left;}else{return right;}}else{if (a[mid] < a[right]){return mid;}else if (a[right] > a[left]){return right;}else{return left;}}
}void QuickSort(int* a, int begin, int end)
{if (begin >= end){return;}//三数取中int midi = GetMidIndex(a, begin, end);Swap(&a[begin], &a[midi]);int left=begin;int right=end;int cur=left+1;int key=a[left];//三路划分while (cur  <= right){if (a[cur] < key){Swap(&a[left], &a[cur]);left++;cur++;}else if (a[cur] > key){Swap(&a[right], &a[cur]);right--;}else{cur++;}}//递归QuickSort(a, begin, left - 1);QuickSort(a, right + 1, end);
}int* sortArray(int* nums, int numsSize, int* returnSize) {srand(time(0));QuickSort(nums,0,numsSize-1);*returnSize=numsSize;return nums;
}

到这,这道题就用了三种优化方式了,而且三种方式缺一不可,那能不能解决问题呢?

当然可以啦,如果没有上述优化方式,用快排做这道题会很坑,不是快排不快,而是“人红是非多”啊,快排在这道题上被针对的体无完肤,反而堆排、希尔排序等还能通过。

3. 非递归实现快速排序

 我们前文讲的递归方式,实际上递归过程处理的是左右子区间,现在我们不能用递归,那要如何处理左右子区间呢?

其实可以用栈实现,每次从栈中拿出一段区间,单趟分割处理,然后让左右子区间入栈

代码如下(栈部分的代码可以拷贝前面章节的,这里只给核心代码):

//前后指针版本
int PartSort3(int* a, int left, int right)
{int prev = left;int cur = left + 1;int keyi = left;while (cur <= right){if (a[cur] <= a[keyi]){prev++;Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;return keyi;
}
//非递归方式实现快排
void QuickSortNonR(int* a, int begin, int end)
{ST st;STInit(&st);STPush(&st, end);STPush(&st, begin);while (!STEmpty(&st)){int left = STTop(&st);STPop(&st);int right = STTop(&st);STPop(&st);int keyi = PartSort3(a, left, right);if (keyi + 1 < right){STPush(&st, right);STPush(&st, keyi + 1);}if (left < keyi-1){STPush(&st, keyi-1);STPush(&st, left);}}STDestroy(&st);
}

好了,以上就是快速排序,下节继续学习归并排序,

未完待续。。。 

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

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

相关文章

腾讯云轻量数据库开箱测评,1核1G轻量数据库测试

腾讯云轻量数据库1核1G开箱测评&#xff0c;轻量数据库服务采用腾讯云自研的新一代云原生数据库TDSQL-C&#xff0c;轻量数据库兼100%兼容MySQL数据库&#xff0c;实现超百万级 QPS 的高吞吐&#xff0c;128TB海量分布式智能存储&#xff0c;虽然轻量数据库为单节点架构&#x…

kettle spoon连接MySQL8.0数据库报错解决方法

kettle 连接 mysql 8.0报错&#xff0c;显示无法连接到数据库服务 错误连接数据库 [11] : org.pentaho.di.core.exception.KettleDatabaseException: Error occurred while trying to connect to the databaseError connecting to database: (using class org.gjt.mm.mysql.D…

斯坦福NLP课程来了

生成式AI&#xff0c;尤其是以ChatGPT为首的大语言模型正在改变人们的生活方式&#xff0c;我想一定有小伙伴想加入NLP这个行列。 微软重磅发布4个适合初学者的机器学习资料 我在前一篇文章中分享了微软人工智能初学者课程&#xff0c;其中的【生成式AI】非常适合初学者&…

利用 Gem5 模拟器创建一个简单的配置脚本——翻译自官网

文章目录 创建简单的配置脚本gem5 配置脚本关于模拟对象的插话 创建配置文件全系统与系统调用模拟 运行Gem5 创建简单的配置脚本 本章教程将指导你如何为 gem5 设置一个简单的模拟脚本&#xff0c;并首次运行 gem5。我们假定你已完成本教程第一章的学习&#xff0c;并已成功创…

重生奇迹mu格斗怎么加点

1.力量加点 力量是格斗家的主要属性之一&#xff0c;它可以增加你的攻击力和物理伤害。因此&#xff0c;对于格斗家来说&#xff0c;力量加点是非常重要的。建议在前期将大部分的加点放在力量上&#xff0c;这样可以让你更快地杀死怪物&#xff0c;提高升级速度。 2.敏捷加点…

Java 环境其他下载2

1 Eclipse Temurin Latest Releases | Adoptium Eclipse Temurin 是由基于 OpenJDK 的开源 Java SE 产生的构建版本。Temurin 适用于 广泛的平台 以及诸多 Java SE 版本。以下列出了推荐用于生产的最新版本&#xff0c;并且定期由 Adoptium 社区发布更新和支持。迁移帮助、容器…

不是说人工智能是风口吗,那为什么工作还那么难找?

最近确实有很多媒体、机构渲染人工智能可以拿高薪&#xff0c;这在行业内也是事实&#xff0c;但前提是你有足够的竞争力&#xff0c;真的懂人工智能。 首先&#xff0c;人工智能岗位技能要求高&#xff0c;人工智能是一个涵盖了多个学科领域的综合性学科&#xff0c;包括数学、…

虹科分享 | 平衡速度与优先级:为多样化的实时需求打造嵌入式网络(3)——CAN与CANopen的实时能力与局限性

在回顾了选择具有实时能力的嵌入式通信系统的基本要求之后&#xff0c;我们现在将更详细地探讨CAN和CANopen的实时能力和局限性。 控制器局域网(CAN)协议是各个行业众多应用的基础&#xff0c;每个应用都有其独特的实时需求。CANopen和J1939等著名示例强调了该协议的多种适应性…

「浙江科聪新品发布」新品发布潜伏顶升式移动机器人专用控制器

聚焦专用车型 最小专用控制器 控制器只占整机5%&#xff0c;纵向出线方式&#xff0c;占比更小 更易插拔 整体解决方案 更具价格优势 提供整体解决方案&#xff0c;配套各类型产品设备及车体厂家 打造持久稳定使用 坚持工业级品质 采用车规级接口&#xff0c;不用其它类不可…

蓄势・前行 | 博格华纳的新能源+战略布局

10月11日-13日&#xff0c;AWC 2023深圳国际新能源及智能网汽车全产业博览会在深圳国际会展中心盛大开幕&#xff0c;此次展会现场共计接待专业观众3W余名&#xff0c;同期举办20场次主题论坛峰会&#xff0c;不仅汇聚众多中外汽车产业巨擘&#xff0c;更是一场涵盖智能驾驶、新…

(内部资料)收下这几个人工智能学习秘籍!

秘籍一&#xff1a;练好基本功 学习基础知识&#xff1a;人工智能涉及多个学科领域&#xff0c;包括数学、计算机科学、统计学等。因此&#xff0c;学习基础知识是非常重要的。您可以通过学习线性代数、概率论和微积分等数学基础知识&#xff0c;以及掌握Python编程语言和常用…

String类常用方法总结

目录 一.简单认识String 二.String对象的比较 1.equals 内部实现原理&#xff1a; 2.compareTo 3.compareToIgnoreCase 三.字符串查找 示例&#xff1a; 四.字符串与其他类型转化 1.数值和字符串相互转换 2.大小写相互转化 3.字符串转数组 4.格式化转化 五.字符串…

振南技术干货集:制冷设备大型IoT监测项目研发纪实(1)

注解目录 1.制冷设备的监测迫在眉睫 1.1 冷食的利润贡献 1.2 冷设监测系统的困难 &#xff08;制冷设备对于便利店为何如何重要&#xff1f;了解一下你所不知道的便利店和新零售行业。关于电力线载波通信的论战。&#xff09; 2、电路设计 2.1 防护电路 2.1.1 强电防护 …

企业该怎么选择IP证书

IP证书是一种数字证书&#xff0c;它由权威的数字证书颁发机构&#xff08;CA&#xff09;颁发&#xff0c;部署在只有公网IP地址的站点上&#xff0c;用于在网络中验证身份和保护信息安全。IP证书可以在各种场景下保护网站的信息安全&#xff0c;比如网站vip登录&#xff0c;线…

创作一款表情包生成微信小程序:功能详解与用户体验优化

一、引言 在当今社交媒体充斥着各种趣味表情包的时代&#xff0c;表情包生成工具成为许多用户创作和分享创意的热门选择。为了迎合这一趋势&#xff0c;我设计并开发了一款功能丰富、用户友好的微信小程序&#xff0c;旨在让用户能够轻松而有趣地创作个性化的表情包。这篇博客将…

【Spring总结】注解开发

本篇讲的内容主要是基于Spring v2.5的注解来完成bean的定义 之前都是使用纯配置的方式来定义的bean 文章目录 前言1. Spring v2.5 注解开发定义bean第一步&#xff1a;在需要定义的类上写上注解Component第二步&#xff1a;在Spring Config中定义扫描包第三步&#xff1a;主方法…

C# Onnx 百度PaddleSeg发布的实时人像抠图PP-MattingV2

目录 效果 模型信息 项目 代码 下载 效果 图片源自网络侵删 模型信息 Inputs ------------------------- name&#xff1a;img tensor&#xff1a;Float[1, 3, 480, 640] --------------------------------------------------------------- Outputs -----------------…

5.3 Windows驱动开发:内核取应用层模块基址

在上一篇文章《内核取ntoskrnl模块基地址》中我们通过调用内核API函数获取到了内核进程ntoskrnl.exe的基址&#xff0c;当在某些场景中&#xff0c;我们不仅需要得到内核的基地址&#xff0c;也需要得到特定进程内某个模块的基地址&#xff0c;显然上篇文章中的方法是做不到的&…

C语言矩阵乘积(ZZULIOJ1127:矩阵乘积)

题目描述 计算两个矩阵A和B的乘积。 输入第一行三个正整数m、p和n&#xff0c;0<m,n,p<10&#xff0c;表示矩阵A是m行p列&#xff0c;矩阵B是p行n列&#xff1b;接下来的m行是矩阵A的内容&#xff0c;每行p个整数&#xff0c;用空格隔开&#xff1b;最后的p行是矩阵B的内…

深兰科技多款大模型技术产品登上新闻联播!

11月20日晚&#xff0c;新闻联播报道了2023中国5G工业互联网大会&#xff0c;深兰科技metamind、汉境大型城市智能体空间等大模型技术和产品在众多参展产品中脱颖而出&#xff0c;被重点播报。 2023中国5G工业互联网大会 本届大会由工信部和湖北省人民政府联合主办&#xff0c;…