[数据结构 -- 手撕排序算法第六篇] 递归实现快速排序(集霍尔版本,挖坑法,前后指针法为一篇的实现方法,很能打)

目录

1、常见的排序算法

1.1 交换排序基本思想

2、快速排序的实现方法

2.1 基本思想

3 hoare(霍尔)版本

3.1 实现思路

3.2 思路图解

3.3 为什么实现思路的步骤2、3不能交换

3.4 hoare版本代码实现

3.5 hoare版本代码测试

4、挖坑法

4.1 实现思路

4.2 思路图解

4.3 挖坑法代码实现

4.4 挖坑法代码测试

5、前后指针版本

5.1 实现思路

5.2 思路图解

5.3 前后指针法代码实现

5.4 前后指针法代码测试

6、时间复杂度分析

6.1 最好情况

6.2 最坏情况

7、优化快速排序

7.1 选 key 优化

7.2 小区间优化


1、常见的排序算法

1.1 交换排序基本思想

冒泡排序属于交换排序之一,我们先来了解以下冒泡排序思想。

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

2、快速排序的实现方法

递归实现与二叉树的前序遍历很相似,将区间划分为左右两半部分的常见方式有三种:1.hoare版本;2.挖坑法;3.左右指针法。

2.1 基本思想

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

所以,在后面很重要的一点就是,左边走找小,右边走找大。

3 hoare(霍尔)版本

3.1 实现思路

我们规定排升序,排序数组名称为a,基准值 key,基准值下标 keyi,左 left,右 right。

1.选出一个key,key可以是需要排序的数组中任意一个元素,我们本篇文章选key为a[left]

2.从数组右端(尾部)往左走,当a[right] < key(a[keyi]),right停下来;

3.然后从数组左端(首部)往右走,当a[left] > key(a[keyi]),left停下来;

4.交换a[left], a[right];

5.循环步骤2、3、4,当 left与right 走到相同位置时,交换此位置的元素与 key,key 的位置就确定了。

6.此时 key 就将数组分为了左右两个区间,我们对左右两个区间分别使用前5步,然后再次确定了左右区间的key,递归左区间,再递归右区间,就实现了排序。

注意:步骤2、3的顺序不能换。至于为什么,我们思路图解后再说。

3.2 思路图解

我们的图解就是按照实现思路来画的。

3.3 为什么实现思路的步骤2、3不能交换

我们可以看到,思路图解里,当最后找到 3 时候是 R 先走,R先遇到 3,如果是 L 先走,左找大,L 遇见 3 不会停下来,会直接走到R的位置,R 位置是 9,这时候相遇,就交换,一旦交换,左区间就不是全部小于 key(6)了,这就会出现错误。因此,key 如果选在左边,右边先走。

Q:hoare版本我们是理解了,如果key选在右边,那是不是左边先走呢?

A:答案是一定的。这里其实就是 L遇到R 还是 R遇到L 的问题。我们依然可以用上面的解法来想这问题,当L遇到R 时,说明 R 已经停下,R 停下就是 a[right]<key,这时 L遇到R,相遇位置就是小于 key 的位置,交换(key,a[left]),这时最后一个小于 key 的元素就放在了最左边,而 key 就到了确定的位置,不用再调了,以key 为中点的左区间全部小于 key,右区间全大于 key;

key在右边,当 R遇到L 时,说明 L 已经停下,L 停下就是 a[left]>key,这时 R遇到L,相遇位置就是大于 key 的位置,交换(a[left],key),这时最后一个大于 key 的元素就放在了最右边,而 key 就到了确定的位置,不用再调了,以 key 为中点的左区间全部小于 key,右区间全大于 key。

所以如果 key 选在左边,就先走右边;key 选在右边,就先走左边。

3.4 hoare版本代码实现

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{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 left, int right)
{if (left >= right)return;int keyi = PartSort1(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}
 

3.5 hoare版本代码测试

void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
void Print(int* a, int n)
{for (int i = 0; i < n; i++){printf("%d ", a[i]);}printf("\n");
}//快速排序递归实现//快速排序hoare版本
int PartSort1(int* a, int left, int right)
{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[left], &a[keyi]);return left;
}
void QuickSort(int* a, int left, int right)
{if (left >= right)return;int keyi = PartSort1(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}
void test()
{int a[] = { 6,3,2,1,5,7,9 };QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);Print(&a, sizeof(a) / sizeof(int));
}
int main()
{test();return 0;
}

4、挖坑法

4.1 实现思路

我们规定排升序,排序数组名称为a,基准值 key,左 left,右 right。

1.选出一个key,key可以是需要排序的数组中任意一个元素,我们依然选key为a[left],将a[left]交给key保存起来,被选为key的元素位置就是坑位

2.从数组右端(尾部)往左走,当a[right] < key,right停下来,将a[right]放到坑位中,坑位就换为下标为 right 的位置,此时right不动;

3.然后从数组左端(首部)往右走,当a[left] > key,left停下来,将a[left]放到坑位中,坑位就换为下标为 left 的位置,此时left不动;

4.循环步骤2、3,当 left与right 走到相同位置时,这个位置就是最后一个坑位,将key放到这个坑位中,key的最终位置就确定下来了,后面就不用再调整了。

5.此时 key 就将数组分为了左右两个区间,我们对左右两个区间分别使用前4步,然后再次确定了左右区间的key,递归左区间,再递归右区间,就实现了排序。

挖坑法与hoare版本的区别:

1.挖坑法不需要去想最后的 L与R 相遇的位置的元素要小于key,因为最终相遇位置是坑位,直接将 key 放入坑位就好了;

2.不用去想为什么选左边为 key 要右边先走,选右边为 key 要左边先走,当选左边为 key 时,因为需要填左边的坑,所以肯定是右边先走。左边找小,右边找大来理解这句话更合适。

4.2 思路图解

4.3 挖坑法代码实现

// 快速排序挖坑法
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 left, int right)
{if (left >= right)return;int keyi = PartSort2(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

4.4 挖坑法代码测试

void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
void Print(int* a, int n)
{for (int i = 0; i < n; i++){printf("%d ", a[i]);}printf("\n");
}
// 快速排序挖坑法
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 left, int right)
{if (left >= right)return;int keyi = PartSort2(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}
void test()
{int a[] = { 6,3,2,1,5,7,9 };QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);Print(&a, sizeof(a) / sizeof(int));
}
int main()
{test();return 0;
}

5、前后指针版本

5.1 实现思路

我们规定排升序,排序数组名称为a,基准值 key

1.选出一个key,key可以是需要排序的数组中任意一个元素,我们依然选key为a[left];

2.定义一个prev指针,和一个cur指针,初始化 prev 指向数组首部位置,cur 指向 prev 的下一个位置。cur先走,cur 找小于 key 的元素,找到之后停下来,让 prev++,然后交换 (a[cur], a[prev])。交换完继续往后走,cur 找的值不小于 key,cur继续往后走,找到后让 prev++,交换 (a[cur], a[prev]),不断重复此步骤;

3.当cur走完整个数组的时候,交换(a[left], a[prev]),这时key的最终位置就确定下来了。key 将数组分为左右两个子区间,左子区间小于 key,右子区间大于 key;

4.左右子区间继续重复前 3 步骤,递归下去就实现了数组的排序。

5.2 思路图解

这里不断交换其实是将小于 key 的值一直在往前抛,把大于 key 的值往后抛,cur与prev 之间的值其实就是大于 key 的所有值,不断交换就实现了最终以 key 划分的左右区间,左区间小于 key,右区间大于 key。

5.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 != cur){Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[keyi], &a[prev]);return prev;
}
void QuickSort(int* a, int left, int right)
{if (left >= right)return;int keyi = PartSort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

5.4 前后指针法代码测试

// 快速排序前后指针法
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 != cur){Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[keyi], &a[prev]);return prev;
}
void QuickSort(int* a, int left, int right)
{if (left >= right)return;int keyi = PartSort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}
void test()
{int a[] = { 6,3,2,1,5,7,9 };QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);Print(&a, sizeof(a) / sizeof(int));
}
int main()
{test();return 0;
}

6、时间复杂度分析

6.1 最好情况

上面的三种情况下,最好情况时间复杂度是O(N* logN)

每次 key 被排到区间的中间位置,像二叉树一样要递归 logN 次,每一次的子区间排序的时间复杂度是O(N),所以最好的情况就是O(N * logN)。

6.2 最坏情况

当数组有序的时候排序,无论 key 选最左边还是最右边,时间复杂度都是O(N^2)。

7、优化快速排序

快速排序的优化有两种思想:

1.我们对选key法可以进行优化;

2.递归到小的子区间,我们可以考虑使用插入排序,也称小区间优化。

7.1 选 key 优化

选 key 优化主要是针对数组有序,或者是接近有序。

对选 key 的优化我们有两种思路:

1.随机选 key;

2.三数取中选 key。(拿出left, mid, right,在下标为这三个位置的数中选出一个中间值作为 key)。

第一种思路是不可控的,所以第二种选 key 的思路才是最合适的。

下面是三数取中的优化代码:

int GetMidIndex(int* a, int left, int right)
{int mid = (left + right) / 2;if (a[left] < a[mid]){if (a[mid] < a[right])return mid;else if (a[left] < a[right])return right;elsereturn left;}else //a[left] > a[mid]{if (a[mid] > a[right])return mid;else if (a[left] > a[right])return right;elsereturn 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 != cur){Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;return keyi;
}

我们取到中后,将该数字与 a[left]交换,依旧用之前的前后指针法的思路是没有问题的。霍尔版本与挖坑法是一样的优化方法。

如果我们不做三数取中的优化,当数组是有序或者接近有序的时候,时间复杂度会是最坏情况,O(N^2)。经过三数取中后,如果数组是有序的,时间复杂度仍是O(N * logN)。

7.2 小区间优化

在递归的时候,我们之前画的图中不难看到,在不断的划分的时候,到后面划分的越来越多了,当数据量特别大的时候,对栈的消耗会很大,会造成栈溢出的风险。因此,当划分到一定的程度,我们不再划分,直接选择插入排序。一般的情况下,当我们的子区间数据个数为10的时候,我们就不再递归了,直接就用插入排序。

实现代码:

// 插入排序
//时间复杂度(最坏):O(N^2) -- 逆序
//时间复杂度(最好):O(N) -- 顺序
void InsertSort(int* a, int n)
{for (int i = 0; i < n - 1; i++){int end = i;int tmp = a[i + 1];while (end >= 0){if (a[end] > tmp){a[end + 1] = a[end];end--;}else{break;}}a[end + 1] = tmp;}
}
int GetMidIndex(int* a, int left, int right)
{int mid = (left + right) / 2;if (a[left] < a[mid]){if (a[mid] < a[right])return mid;else if (a[left] < a[right])return right;elsereturn left;}else //a[left] > a[mid]{if (a[mid] > a[right])return mid;else if (a[left] > a[right])return right;elsereturn left;}
}
// 快速排序前后指针法
//[left, right]
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 != cur){Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;return keyi;
}
void QuickSort(int* a, int left, int right)
{//子区间只有一个值,或者子区间不存在的时候递归结束if (left >= right)return;//小区间优化if (right - left + 1 < 10){InsertSort(a + left, right - left + 1);}int keyi = PartSort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

这两种优化的方式在时间空间两个方面都有一定程度的提升,但快速排序的本质没有改变,优化只是在原有的思想上锦上添花。

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

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

相关文章

51单片机LCD12864温度曲线显示温度温控系统加热制冷控制

实践制作DIY- GC00155-温度曲线显示温度温控系统 一、功能说明&#xff1a; 基于51单片机设计-温度曲线显示温度温控系统 二、功能说明&#xff1a; STC89C52单片机LCD12864显示器DS18B20温度传感器1个红色LED灯模拟加热1个绿色LED灯模拟制冷4个按键&#xff08;温度下限加减键…

js 实现导入导出功能(FileSave.js)

点击导出按钮弹出文件框导出文件&#xff0c;点击导入选择文件获取文件数据&#xff0c;以下我保存的是json。 html&#xff1a; <div class"layui-inline"> <div class"layui-btn-group"><button type"button" class"la…

Docker 数据卷

1、什么是数据卷 通过镜像创建一个容器。容器一旦被销毁&#xff0c;则容器内的数据将一并被删除。但有些情况下&#xff0c;通过服务器上传的图片出会丢失。容器中的数据不是持久化状态的。这个时候可以通过数据卷来解决这个问题。 数据卷是一个可供一个或多个容器使用的特殊目…

GAMES101 OpenCV环境安装

文章目录 Opencv 库编译Step 1.下载源码Step 2. 使用CMake编译Step3. 解决CMake 过程中的报错错误1&#xff1a; 错误的Python版本:错误1 解决办法 错误2&#xff1a;下载ippicv_2020_win_ia32_20191018_general.zip失败错误2 解决办法 错误3&#xff1a;ffmpeg相关文件下载失败…

时间序列的季节性:3种模式及8种建模方法

分析和处理季节性是时间序列分析中的一个关键工作&#xff0c;在本文中我们将描述三种类型的季节性以及常见的8种建模方法。 什么是季节性? 季节性是构成时间序列的关键因素之一&#xff0c;是指在一段时间内以相似强度重复的系统运动。 季节变化可以由各种因素引起&#xf…

C++——map和set(multimap和multiset)

目录 1.关联式容器 2.键值对 3.树形结构的关联式容器 3.1 set 3.1.1 set的介绍 3.1.2 set的使用 3.2 multiset 3.2.1 multiset的介绍 3.2.2 multiset的使用 3.3 map 3.3.1 map的介绍 3.3.2 map的使用 3.4 multimap 3.4.1 multimap的介绍 3.4.2 multimap的使用 …

(五)「消息队列」之 RabbitMQ 主题(使用 .NET 客户端)

0、引言 先决条件 本教程假设 RabbitMQ 已安装并且正在 本地主机 的标准端口&#xff08;5672&#xff09;上运行。如果您使用了不同的主机、端口或凭证&#xff0c;则要求调整连接设置。 获取帮助 如果您在阅读本教程时遇到问题&#xff0c;可以通过邮件列表或者 RabbitMQ 社区…

简单工厂模式(java)

目录 结构 案例 类图 代码实现 简单咖啡工厂类 咖啡店类 咖啡类 具体咖啡类 简单工厂方法的优缺点 优点 缺点 结构 简单工厂包含如下角色&#xff1a; 抽象产品 &#xff1a;定义了产品的规范&#xff0c;描述了产品的主要特性和功能。具体产品 &#xff1a;实现或者…

Spring Boot Helper插件免费版的下载

在使用idea的社区版的时候&#xff0c;我们创建springboot项目就要下载springboot helper插件&#xff0c;下载是很简单&#xff0c;但是呢&#xff0c;有一个问题就是&#xff1a; 可以看到它是收费的&#xff0c;要想免费创建spring boot项目那么我们第一可以采用网页版创建&…

Java线程池

目录 线程池 一、Java构建线程的方法 二、线程池的7个参数 三、线程池的执行流程 四、线程池属性标识 4.1.核心属性 4.2.线程池状态变化 五、线程池的 execute 方法执行 六、Worker的封装 七、线程执行的后续处理 线程池 线程池ThreadPoolExecutor源码剖析 // Execu…

B071-项目实战-用户模块--手机注册 管理员登录

目录 完成注册功能后端开发完成UserControllerUserServiceImplLogininfoMapper 前端页面完成绑定数据绑定事件准备登录页 管理员登录1需求分析登录设计页面设计表设计流程设计所需技术 员工新增级联操作登录信息EmployeeServiceImplShopServiceImpl 管理员登录2前端页面后端接口…

MacOS使用USB接口与IPhone进行Socket通信

演示效果如下: 开源地址: GitHub - rsms/peertalk: iOS and Mac Cocoa library for communicating over USB 克隆源码: git clone https://github.com/rsms/peertalk.git 克隆后打开peertalk然后启动xcode工程 先启动MacOS服务端工程,再启动iOS客户端工程 客户端 服务端