DS:八大排序之堆排序、冒泡排序、快速排序

                                                 创作不易,友友们给个三连吧!! 

一、堆排序

堆排序已经在博主关于堆的实现过程中详细的讲过了,大家可以直接去看,很详细,这边不介绍了

DS:二叉树的顺序结构及堆的实现-CSDN博客

直接上代码:

void AdjustDown(int* a, int n, int parent)//升序要建大堆
{int child = parent * 2 + 1;//假设左孩子比右孩子大while (child < n){//如果假设错误,就认错if (child + 1 < n && a[child] < a[child + 1])child++;//如果孩子大于父亲,交换if (a[child] > a[parent]){Swap(&a[child], &a[parent]);//交换完后,让原来的孩子变成父亲,然后再去找新的孩子parent = child;child = parent * 2 + 1;}elsebreak;}
}void HeapSort(int* a, int n)
{//向下调整建堆for (int i = (n - 1 - 1) / 2; i >= 0; i--)AdjustDown(a, n, i);//大堆,向下调整int end = n - 1;while (end >= 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);end--;}
}

建堆的时间复杂度是o(N),排序的时间复杂度是(N*logN)所以堆排序的总时间复杂度是N*logN

二、冒泡排序

2.1 思想

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

2.2 冒泡排序的实现

方法:从前往后,逐一比较相邻元素,前面的大于或小于后面的则进行交换,每一轮将当前轮最大或最小的元素冒泡到当前论的最后。重复上述过程最后就是有序的。

该排序比较简单,不画图了,不懂得可以看看博主之前写的文章有介绍:

C语言:深入理解指针(2)-CSDN博客

代码实现:

void BubbleSort(int* a, int n)
{for (int i = 0; i < n - 1; i++)//每一趟拍好一个,最后排完n-1个,最后一个数就没必要比了{for (int j = 0; j < n - i - 1; j++)//第一趟需要比较的n-1次,第二次需要比较n-2次……//所以结束条件跟着i变化{if (a[j] > a[j + 1])Swap(&a[j], &a[j + 1]);}}
}

2.3 冒泡排序的改进

如果比较的过程中,比到中间的时候就有序了,但是我们并不知道,这个时候一直到后面进行的比较都是无效的,所以为了优化这个情况,我们设置一个标志,如果一趟排序中没有发生交换,说明后面都有序了,这个时候就停止冒泡排序!!

void BubbleSort(int* a, int n)
{for (int i = 0; i < n - 1; i++)//每一趟拍好一个,最后排完n-1个,最后一个数就没必要比了{int flag = 1;//假设有序for (int j = 0; j < n - i - 1; j++)//第一趟需要比较的n-1次,第二次需要比较n-2次……//所以结束条件跟着i变化{if (a[j] > a[j + 1]){Swap(&a[j], &a[j + 1]);flag = 0;//如果没有发生交换,说明不符合有序}}if (flag == 1)break;}
}

2.4 复杂度分析

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

空间复杂度:O(1)

三、快速排序

3.1 思想

         快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法

其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

       根据区间按照基准值划分的过程,有最早的hoare版本,包括后来深入研究之后的挖坑法、前后指针法、快排法、非递归法……以下博主会用图文的方式和大家一起学习不同方法的实现!!并在这个过程中理解为什么说这中排序方法是二叉树结构。

3.2 hoare快排实现

hoare大佬最初的思想是:

1、在数组中选取一个key值,一般是选择第一个元素或者最后一个元素(我们主要这边主要研究第一个元素做key值)

2、设置两个指针,一个指针right先从往右往左找比key小的值,找到之后,另一个指针left再从左往右走找比key大的值,然后都找到后就交换两边的值,重复上述过程,最后当两个指针相遇的时候,再将相遇点的值与key点的值交换,此时key的左边都是比key小的值,key的右边都是比key大的值,这样就完成了一次选基准值的排序。

3、完成一次找基准值的排序后,使数组有序的问题通过基准值key转化成使key左边和右边都有序的问题,也就是这个key为基准,再分割成两块区间,再分别用上述方法找各自区间的key值,继续重复之前的步骤(递归),直到区间不能被分割,此时的数组就实现了有序。

以上的说法可能大家还不太能理解,我们通过画图来理解hoare大佬的思想。

我们通过上图可以理解为什么说快速排序是一种二叉树结构的排序!!这个过程和二叉树的前序遍历非常相似。既然我们知道了hoare大佬具体想怎么干,我们就明白了这个快排是用递归去实现的,当然现在问题的关键就是我们如果进行基准排序去找到key值对应的位置,下面我们来分析一趟基准排序的过程

       以上就是完成一次基准排序的过程,大家会发现此时恰好key的左边是比key小的,key的右边都是比key大的,其实这并不是巧合,我们后面会分析,这边我们先实现一下一次基准排序的代码!

基准排序代码实现:

int PartSort1(int* a, int left, int right)//hoare基准排序
{int keyi = left;while (left < right){//右先找比key大的while (left < right&&a[right] >= a[keyi])right--;//左找比key小的while (left < right && a[left] <= a[keyi])left++;//找到后,就交换Swap(&a[left], &a[right]);}//此时相遇了,把相遇的位置和keyi的位置交换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);
}

思考以下问题:

1、怎么判断递归的结束条件呢??

2、hoare大佬是怎么确定每次相遇的时候,相遇点的位置的值一定比key的值小呢??

3、left可以从第二个位置开始走吗?

通过上述的分析,相信大家对hoare大佬的思想已经理解了!!

3.3 挖坑法快排实现

         hoare大佬的思想真的是超凡脱俗,非常厉害,但是也有很多不容易理解的地方,后面就有人进行了改进,发明了挖坑版本的快排,虽然名字叫挖坑,但是比hoare会好理解很多,下面我们来进行挖坑版本的分析!

发明挖坑版本大佬的思想是:

1、先把keyi对应的值记住,然后在这个位置挖坑,right找大,找到了就停下来,将停下来的位置的值去填坑,然后right的位置变成新坑,left找小,找到后就把对应的值填坑,然后left成为新坑,以此类推下去,直到left和right相遇,将之前记住keyi的值填入坑内,这个时候就完成了一次基准排序。

2、除了基准排序的方法和hoare大佬不同,其余地方都是一样的,也是用递归去解决。

 通过这个方法,我们来实现对应的基准排序算法

int PartSort2(int* a, int left, int right)//挖坑基准排序
{int key = a[left];//记住key的值int hole = left;//开始挖坑while (left < right){//右先找比key大的while (left < right && a[right] >= key)right--;//找到后,填坑,然后挖新坑a[hole] = a[right];hole = right;//左找比key小的while (left < right && a[left] <= key)left++;//找到后,填坑,然后挖新坑a[hole] = a[left];hole = left;}//此时相遇了,把key值放在坑里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);
}

我们发现挖坑和填坑的思想特别好理解!!然后接下去就是前后指针版本快排 !!

3.4 前后指针法快排实现

发明前后指针版本大佬的思想是:

1、设置前指针prev,指向首元素,后指针cur指向第二个元素,如果cur开始找小,如果没找到小,就一直往前走,如果找到小的了,就先停下来,然后prev往前走一步,在和cur交换值,然后cur接着走……重复上述步骤,最后cur走出数组返回后,循环终止!此时prev指向的位置和keyi的位置交换。

 

下面我们来实现前后指针快排的基准排序

int PartSort3(int* a, int left, int right) //前后指针基准排序
{int prev = left;int cur = left + 1;int keyi = left;while (cur <= right)//cur走出数组循环停止{//cur一直在走,如果遇到比keyi小的,就停下来等perv走一步后交换,再接着走if (a[cur] < a[keyi]){++prev;Swap(&a[prev], &a[cur]);}cur++;}//cur出去后,prev的值和keyi交换Swap(&a[keyi], &a[prev]);return prev;
}

 因为prev和cur指向一个位置的时候,其实没有交换的必要,所以可以将上面代码的优化一下

int PartSort3(int* a, int left, int right) //前后指针基准排序
{int prev = left;int cur = left + 1;int keyi = left;while (cur <= right)//cur走出数组循环停止{//cur一直在走,如果遇到比keyi小的,就停下来等perv走一步后交换,再接着走if (a[cur] < a[keyi]&&++prev!=cur)Swap(&a[prev], &a[cur]);cur++;}//cur出去后,prev的值和keyi交换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);
}

3.5 快排的复杂度分析

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

 如果使用了三数取中的优化方法(后面会介绍),那么快排就不可能出现O(N^2)的情况,所以最后时间复杂度是O(logN*N)

空间复杂度:O(logN)

         因为递归是要开销栈帧的,空间可以重复利用而时间不可重复利用。所以这里至多递归到他的深度即h = logN,所以他的空间复杂度是O(logN)

3.6 快排的优化

快排优化的过程源于这道OJ题

力扣:排序数组

看到这题,你可能会很开心:这不就是一个排序吗?我把最厉害的快排用上肯定能过的!!

结果是这样的——

为什么呢??因为力扣的测试用例设置了一个有序的数组来针对快排(复杂度分析过原因了),本质上就是因为有序的时候前面的keyi太小了!!所以我们设置一个改进方法——三数取中,解决有序的情况

3.6.1 三数取中——针对有序

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[right] < a[left])return left;elsereturn right;}else//a[left] >a[mid]{if (a[mid] > a[right])return mid;else if (a[right] > a[left])return left;elsereturn right;}
}

封装后,我们前面三种方法的基准值排序都要加上以下代码,这样就可以确保keyi的值是中间值了

int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);

你可能觉得这样子应该能过了 ,那我们再测试一下——

结果你发现,又被针对了,有序的情况解决了,又面临着有大量重复数据的排序问题,我们来分析一下:

你会发现,重复其实跟有序的情况是一样的,每次right指针都会走到keyi的位置,这样子时间复杂度也变成O(N^2)了。所以为了解决重复的问题,我们有了三路划分的方法—— 

3.6.2 三路划分——针对重复

因为每次递归需要返回两个边界才行,所以没办法单独封装一个partsort函数 

代码实现: 

void QuickSort2(int* a, int left, int right)
{if (left >= right)return;int mid = GetMidIndex(a, left, right);Swap(&a[mid], &a[left]);int phead = left;int pcur = left + 1;int ptail = right;int key = a[left];while (pcur <= ptail){if (a[pcur] < key){Swap(&a[pcur], &a[phead]);pcur++;phead++;}else if (a[pcur] > key){Swap(&a[pcur], &a[ptail]);ptail--;}elsepcur++;}QuickSort2(a, left, phead - 1);QuickSort2(a, ptail+1,right);
}

结果我们发现,还是过不了,那究竟是哪里出问题了呢?? 

因为里面有一组测试用例针对了三数取中,也就是他把偏大和偏小的数都安排在恰好的位置,所以我们为了针对这种情况,将三数取中的其中一个数改成随机数,接下来我们对三数取中再优化

3.6.3 三数取中再优化

将三数取中的其中一个数的下标变成随机下标:

同时要记得在主函数加上时间种子

int GetMidIndex2(int* a, int left, int right)
{int mid = left + (rand() % (right - left));if (a[left] < a[mid]){if (a[mid] < a[right])return mid;else if (a[right] < a[left])return left;elsereturn right;}else//a[left] >a[mid]{if (a[mid] > a[right])return mid;else if (a[right] > a[left])return left;elsereturn right;}
}
void QuickSort2(int* a, int left, int right)
{if (left >= right)return;int mid = GetMidIndex2(a, left, right);Swap(&a[mid], &a[left]);int phead = left;int pcur = left + 1;int ptail = right;int key = a[left];while (pcur <= ptail){if (a[pcur] < key){Swap(&a[pcur], &a[phead]);pcur++;phead++;}else if (a[pcur] > key){Swap(&a[pcur], &a[ptail]);ptail--;}elsepcur++;}QuickSort2(a, left, phead - 1);QuickSort2(a, ptail+1,right);
}

终于过了!!要重点理解快排的优化思想!! 

3.7 非递归法快排实现

3.7.1 栈实现

我们发现无论是hoare、挖坑还是前后指针,本质区别就是基准排序方法不同,但最后还是用递归去解决的,递归虽好但是有些时候如果数据太大的话还是有可能造成栈空间不够的情况,因此我们应该研究一下如果非递归实现!!——利用栈!

利用栈实现非递归快排的大佬的思想是:

利用栈来存储基准排序需要处理的区间,每次从栈拿出需要处理的区间,再将分割的区间放进栈中,利用栈后进先出的特点,每次都会先处理左边的区间再处理右边的区间,模拟了递归的过程。

我们画个图来理解吧!!(类似二叉树的前序遍历)

 非递归快排代码实现:

需要包含栈的相关代码

void QuickSortNonR1(int* a, int left, int right)
{Stack st;StackInit(&st);//把区间压进去,一定要先压右区间!!StackPush(&st, right);StackPush(&st, left);while (!StackEmpty(&st)){//将数据弹出来进行进准计算int left = StackTop(&st);StackPop(&st);int right= StackTop(&st);StackPop(&st);//进行基准计算int keyi = PartSort3(a, left, right);//分割区间(left keyi-1)keyi(keyi+1,right)//如果对应的区间还能分割,就继续压,要注意要先压后面在压前面if (keyi + 1 < right){StackPush(&st, right);StackPush(&st, keyi+1);}if (keyi - 1 > left){StackPush(&st, keyi-1);StackPush(&st,left);}}//会一直到栈为空,此时就拍好序了!!StackDestory(&st);
}

3.7.2 队列实现

使用队列也是可以的!!就有点类似二叉树的层序遍历

代码实现:

 需要包含队列的一些文件

void QuickSortNonR2(int* a, int left, int right)
{Queue q;QueueInit(&q);QueuePush(&q, left);QueuePush(&q, right);while (!QueueEmpty(&q)){int left = QueueFront(&q);QueuePop(&q);int right = QueueFront(&q);QueuePop(&q);int keyi = PartSort3(a, left, right);if (keyi - 1 > left){QueuePush(&q, left);QueuePush(&q, keyi-1);}if (keyi + 1 <right){QueuePush(&q, keyi +1);QueuePush(&q, right);}}QueueDestory(&q);
}

 

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

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

相关文章

算法||实现典型数据结构的查找、添加和删除数据 并分析其时间和空间复杂度

实现典型数据结构的查找、添加和删除数据 并分析其时间和空间复杂度 线性结构&#xff1a; 数组&#xff1a;是一种线性表数据结构&#xff0c;它用一组连续的内存空间&#xff0c;来存储一组具有相同类型的数据。 查找数据 &#xff1a;随机访问 流程图 /** 查询元素下标…

QML | 属性特性(property)

一、属性特性 属性是对象的一个特性,可以分配一个静态的值,也可以绑定一个动态表达式。属性的值可以被其他对象读取。一般而言,属性的值也可以被其他对象修改,除非显式声明不允许这么做,也就是声明为只读属性。 1.定义属性特性 属性可以在C++中通过注册一个类的Q_PROPERT…

anaconda安装路径默认在D盘,但安装环境的envs路径跑到C盘,修改为D盘

安装的anaconda环境&#xff0c;路径是在anaconda安装目录下的envs中&#xff08;D:\APPFile\Anaconda3\envs&#xff09;&#xff0c;然而&#xff0c;这次创建的却是在 C:\Users\xxx.conda\envs 中。 首先&#xff0c;找到用户目录下的.condarc文件&#xff08;C:\Users\use…

力扣题目训练(12)

2024年2月5日力扣题目训练 2024年2月5日力扣题目训练476. 数字的补数482. 密钥格式化485. 最大连续 1 的个数148. 排序链表164. 最大间距 2024年2月5日力扣题目训练 2024年2月5日第十二天编程训练&#xff0c;今天主要是进行一些题训练&#xff0c;包括简单题3道、中等题2道和…

项目第一次git commit后如何撤销

问题描述&#xff1a; # 1. 新建gitcode目录&#xff0c;然后在目录下 git init# 2. 用idea打开目录后&#xff0c;新建.gitignore文件后 git add .git commit -m "init project"git log# 3. 就出现如下图情况目的&#xff1a;向撤销该次代码提交 # 仅撤销 git com…

Stable Diffusion教程——常用插件安装与测试(一)

前言 随着Stable Diffusion不断演进&#xff0c;越来越多的开发者开始涉足插件开发。尽管网络上存在大量教程&#xff0c;但它们通常零散分布&#xff0c;逐个学习和查找非常耗时&#xff0c;使人感觉每天都在劳累思考。这里总结了Stable Diffusion常用的插件安装与测试方法。…

表的操作【mysql数据库】

目录 一、创建表 二、查看表 三、修改表 改表名&#xff1a; 新增一列&#xff1a; 修改某列的属性&#xff1a; 删除某列&#xff1a; 改列名 四、删除表 一、创建表 二、查看表 desc&#xff1a;查看表的详细信息 查看建表时的详细信息&#xff1a; 三、修改表 改表…

20-k8s中pod的调度-nodeSelector节点选择器

一、概念 我们先创建一个普通的deploy资源&#xff0c;设置为10个副本 [rootk8s231 dns]# cat deploy.yaml apiVersion: apps/v1 kind: Deployment metadata: name: dm01 spec: replicas: 10 selector: matchLabels: k8s: k8s template: metadata: …

红队打靶练习:IMF: 1

目录 信息收集 1、arp 2、nmap 3、nikto 目录探测 gobuster dirsearch WEB 信息收集 get flag1 get flag2 get flag3 SQL注入 漏洞探测 脱库 get flag4 文件上传 反弹shell 提权 get flag5 get flag6 信息收集 1、arp ┌──(root㉿ru)-[~/kali] └─# a…

计算机设计大赛 深度学习中文汉字识别

文章目录 0 前言1 数据集合2 网络构建3 模型训练4 模型性能评估5 文字预测6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习中文汉字识别 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xf…

Vue练习4:插槽

预览 <Layout><template #left><div class"left">左边栏区域&#xff0c;宽度适应内容&#xff0c;溢出隐藏</div></template><template #main><div class"main">中间区域</div></template><te…

17.Qt 单选框相关操作

目录 前言&#xff1a; 技能&#xff1a; 内容&#xff1a; 1. 布置界面 2.管理 3.槽函数 参考&#xff1a; 前言&#xff1a; 组合选择只能选择一个的单选框实现&#xff0c;以及管理组合单选框 技能&#xff1a; <QRadioButton> <QButtonGroup> 内容&…

Rust 学习笔记 - 注释全解

前言 和其他编程语言一样&#xff0c;Rust 也提供了代码注释的功能&#xff0c;注释用于解释代码的作用和目的&#xff0c;帮助开发者理解代码的行为&#xff0c;编译器在编译时会忽略它们。 单行注释 单行注释以两个斜杠 (//) 开始&#xff0c;只影响它们后面直到行末的内容…

基于PPNSA+扰动算子的车间调度最优化matlab仿真,可以任意调整工件数和机器数,输出甘特图

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于PPNSA扰动算子的车间调度最优化matlab仿真,可以任意调整工件数和机器数,输出甘特图和优化收敛曲线。 2.测试软件版本以及运行结果展示 MATLAB2022a版本运行…

【JavaScript】面试手写题精讲之数组(上)

专题缘由 该专题主要是讲解我们在面试的时候碰到一些JS的手写题, 确实这种手写题还是比较恶心的。有些时候好不容易把题目写出来了&#xff0c;突然面试官冷不丁来一句有没有更优的解法&#xff0c;直接让我们僵在原地。为了解决兄弟们的这些困扰&#xff0c;这个专题于是就诞…

贪心算法之合并区间

“任世界多宽广&#xff0c;停泊在这港口~” 区间问题&#xff0c;涉及到最多的就是 取交集 和 并集的概念。我们使用C排序算法后&#xff0c;其默认规则就是按照 “左排序”进行的。因而&#xff0c;我们实质上注意的是每一个区间的 右端点&#xff0c;根据题目要求&#xff…

蓝桥杯:C++模运算、快速幂

模运算 模运算是大数运算中的常用操作。如果一个数太大&#xff0c;无法直接输出&#xff0c;或者不需要直接输出&#xff0c;则可以对它取模&#xff0c;缩小数值再输出。取模可以防止溢出&#xff0c;这是常见的操作。 模是英文mod的音译&#xff0c;取模实际上是求余。 取…

树莓派:使用mdadm为重要数据做RAID 1保护

树莓派作为个人服务器可玩性还是有点的。说到服务器&#xff0c;在企业的生成环境中为了保护数据&#xff0c;基本上都会用到RAID技术。比如&#xff0c;服务器两块小容量但高性能的盘做个RAID-1按装操作系统&#xff0c;余下的大容量中性能磁盘做个RAID-5或者RAID-6存放数据。…

网络原理(HTTP篇)

网络原理HTTP 前言HTTPHTTP的工作流程抓包工具抓取HTTP报文HTTP报文格式 请求报文具体细节首行URLURL的基本格式URL encode 方法 报头(header)HostContent-Length 和 Content-TypeUser-Agent&#xff08;UA&#xff09;RefererCookie&#xff08;重要&#xff09; 前言 如图&a…

HAL/LL/STD STM32 U8g2库 +I2C SSD1306/sh1106 WouoUI磁贴案例

HAL/LL/STD STM32 U8g2库 I2C SSD1306/sh1106 WouoUI磁贴案例 &#x1f4cd;基于STM32F103C8T6 LL库驱动版本&#xff1a;https://gitee.com/chcsx/platform-test/tree/master/MDK-ARM&#x1f3ac;视频演示&#xff1a; WouoUI移植磁贴案例&#xff0c;新增确认弹窗 &#x1f…