快速排序及归并排序的实现与排序的稳定性

目录

快速排序

一. 快速排序递归的实现方法

1. 左右指针法

步骤思路

为什么要让end先走?

2. 挖坑法

步骤思路

3. 前后指针法

步骤思路

二. 快速排序的时间和空间复杂度

1. 时间复杂度

2. 空间复杂度

三. 快速排序的优化方法

1. 三数取中优化

2. 小区间优化

四. 使用栈来实现非递归快排

步骤思路

归并排序

​编辑

一. 归并排序的递归实现

步骤思路

二. 时间复杂度与空间复杂度

1. 时间复杂度

2. 空间复杂度

三. 非递归实现归并排序

步骤思路

排序算法的稳定性


快速排序

一. 快速排序递归的实现方法

1. 左右指针法
步骤思路

(假设排升序)将数组a最左边的下标用begin记录下来,最右边用end记录下来,定义一个key为begin或end

(假设key定义为begin)end向左查找找到<a[key]的数停下begin再向右查找找到>a[key]的值停下,此时将begin指向的值与end指向的值交换,以此类推直到end的值<=begin,将此时的a[key]与begin与end相遇坐标的值交换,我们发现此时的a[key],左边的值都比其小,右边的值都比其大,那就说明key所指向的值在数组中已经排好位置了

如以下代码,即完成了单趟

		int key = left;int begin = left, end = right;while (begin < end){while (a[end] >= a[key] && begin < end){end--;}while (a[begin] <= a[key] && begin < end){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);

我们在end和begin寻找比a[key]大或小的值的时候不要忘记也要判断循环成立的条件

既然key已经在数组排好位置,我们接下来递归就不需要加上key了,只需要递归key的左右区间即可,直到递归的区间左边与右边相等即只有一个数

完整代码如下

void QuickSort1(int* a, int left,int right)
{if (left >= right)return;int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);int key = left;int begin = left, end = right;while (begin < end){while (a[end] >= a[key] && begin < end){end--;}while (a[begin] <= a[key] && begin < end){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);QuickSort1(a, left, begin - 1);QuickSort1(a, begin + 1, right);
}
为什么要让end先走?

左边做key右边先走,可以保证相遇位置比key小
相遇场景分析

begin遇end:end先走,停下来,end停下条件是遇到比key小的值,end停下来的位置一定比key小,begin没有找到大的遇到end停下了
end遇begin:end先走,找小,没有找到比key更小的,直接跟begin相遇了。begin停留的位置是上一轮交换的位置(即,上一轮交换,把比key小的值,换到begin的位置了)
同样道理让右边做key,左边先走,可以保证相遇位置比key要大  

2. 挖坑法

步骤思路

(假设排升序,给数组a)将最左边的值定义key存储起来,最左边的下标用bigen记录,最右边的下标用end记录,定义pivot记录为最左边的下标,即将最左边视为坑位

然后end向左寻找比key小的值放到pivot所指向的位置即坑位中,并将这个地方(end所找到的)视作新的坑(更新pivot的值)。

begin向右寻找比key大的值,放到坑位中,并将这个地方视作新的坑(更新pivot的值)

重复以上步骤直到end<=begin

然后将key填进pivot中,再通过递归,即可完成排序

由于与左右指针法类似就不写单趟,直接上完整代码

void QuickSort2(int* a, int left, int right)
{if (left >= right)return;int key = a[left];int begin = left, end = right;int pivot = left;while (begin < end){while (a[end] >= key && begin < end){end--;}a[pivot]=a[end];pivot = end;while (a[begin] <= key && begin < end){begin++;}a[pivot] = a[begin];pivot = begin;}a[pivot] = key;QuickSort2(a, left, pivot - 1);QuickSort2(a, pivot + 1, right);
}
3. 前后指针法

步骤思路

(假设排升序)定义key为数组最左边的下标,并定义,prev=key与after=key+1

after在找到比key指向的值小的值时,prev++,并将after指向的值与现在的prev(即prev++后的值)交换

以此往复,直到after>数组的值

然后将prev所指向的值与key所指向的值交换

代码如下

我们要注意,当prev++后的值==after就会发生与自身交换

完成一次后,效果依然是a[key]左区间的值比其小,右区间的值比其大

	int key = left;int prev = left, after = left + 1;while (after<=right){while (a[after] < a[key]&&++prev!=after){Swap(&a[prev], &a[after]);}after++;}Swap(&a[prev], &a[key]);

递归是和上面两种方法同样的道理

完整代码如下

void QuickSort3(int* a,int left,int right)
{if (left >= right)return;int key = left;int prev = left, after = left + 1;while (after<=right){while (a[after] < a[key]&&++prev!=after){Swap(&a[prev], &a[after]);}after++;}Swap(&a[prev], &a[key]);QuickSort3(a, left, prev - 1);QuickSort3(a, prev + 1, right);
}

二. 快速排序的时间和空间复杂度

1. 时间复杂度

①最好情况

每次的划分都使得划分后的子序列长度大致相等,一般在数据已经部分有序或者随机分布的情况下发生。此时时间复杂度为O(Nlog₂N)

②最坏情况

待排序序列有序的情况下,每一次划分的两个区间都有一个为0,此时快速排序的时间复杂度退化为O(N²)

③平均情况

实际应用中快速排序的平均情况大概会接近于最好情况,因为待排序序列通常不是有序的,我们还可以通过三数取中来优化,减少最坏情况的可能性,所以快速排序的时间复杂度为O(Nlog₂N)

2. 空间复杂度

由于需要递归调用,相当于求递归树的深度,

①最坏情况

当数组接近有序时,递归深度很深,空间复杂度为O(N)

②最好情况

当数组无序时,递归树基本相当与完全二叉树,空间复杂度为O(log₂N)

③平均情况

实际应用中,平均情况大概会接近最好情况,同样可以用三数取中优化

所以快速排序空间复杂的为O(log₂N)

三. 快速排序的优化方法

1. 三数取中优化

为了让每次左右区间长度接近,我们可以使用三数取中,即最左边最右边与中间的值取不大也不小的一个值并返回

int GetMid(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])//上面if条件不成立可得a[right]<a[mid]return right;else//又可得 a[left] > a[right]return left;}else//a[left]>=a[mid]{if (a[mid] > a[right])return mid;else if (a[left]<  a[right])//上面if条件不成立可得a[right]>a[mid]return left;else//又可得 a[left] < a[right]return right;}}

将返回值接收并将其指向位置与最左边的值交换,代码如下

		if (left >= right)return;int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);int key = left;
2. 小区间优化

当快速排序要排的数据很长时,越递归到后面区间越小递归的层数越多,我们可以考虑,当要递归区间小于10的时候用别的排序来代替,这样就可以省去80%到90%的递归

代码如下

void QuickSort1(int* a, int left,int right)
{if ( (right-left+1)<10)//小区间优化{InsertSort(a+left, right - left + 1);//a+left 有可能是后半段区间//减少递归层数}else{if (left >= right)return;int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);int key = left;int begin = left, end = right;while (begin < end){while (a[end] >= a[key] && begin < end){end--;}while (a[begin] <= a[key] && begin < end){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);QuickSort1(a, left, begin - 1);QuickSort1(a, begin + 1, right);}
}

四. 使用栈来实现非递归快排

栈的实现可以看一下我以前的博客

栈的实现详解-CSDN博客

步骤思路

初始化栈后,将数组的最右边与最左边分别放入栈(即将一个区间放入栈中)

进入循环(当栈为空时循环结束),用begin和begin1接收栈顶端的值,再删除栈的值,再用end和end1接收栈顶端的值,再删除栈的值,使用左右指针法(挖坑法,前后指针法皆可)(用begin与end来寻找值,begin1与end1不变)进行一趟排序,

如果right1>=begin+1 就往栈里存 right1(当前排序区间的最右边) 和 begin+1 反之不存

如果left1<=begin-1 就往栈里存  begin-1 和 left1(当前排序区间的最左边)  反之不存

最后不要忘记销毁栈

代码如下

void StackQuickSort(int* a, int left, int right)
{ST s;StackInit(&s);StackPush(&s, right);StackPush(&s, left);while (!StackEmpty(&s)){int begin = StackTop(&s);int left1 = begin;StackPop(&s);int end = StackTop(&s);int right1= end;StackPop(&s);int key = begin;//int mid = GetMid(a, begin, end);//Swap(&a[mid], &a[begin]);while (begin < end){while (a[end] >= a[key] && begin < end){end--;}while (a[begin] <= a[key] && begin < end){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);if(right1>=begin+1){StackPush(&s,right1);StackPush(&s, begin + 1);}if(left1<=begin-1){StackPush(&s, begin - 1);StackPush(&s, left1);}}StackDestroy(&s);
}

归并排序

一. 归并排序的递归实现

步骤思路

malloc一个临时数组进入子函数(创建子函数递归会更方便),进行递归,子函数利用分治思想一直递归直到left>=right 开始执行下面操作

k赋初值为当前区间最左边begin1 , end1来记录左数组最左边和最右边,定义begin2 ,end2 来记录右数组的最左边和最右边,将两个数组从头比较,较小的赋值给临时数组,直到有一方赋完值,再将没赋完值的数组给临时数组赋值。最后给要排序数组left到right赋值为临时数组left到right

代码如下

//递归
void _MergeSort(int* a,int* tmp, int left, int right)
{if(left>=right){return;}int mid = (left + right) / 2;//如果[left,mid][mid+1,right]有序就可以归并了_MergeSort(a,tmp, left, mid);_MergeSort(a,tmp, mid + 1, right);int begin1 = left;int end1 = mid;int begin2 = mid + 1;int end2 = right;int k=left;while (begin1 <= end1&&begin2<=end2){if(a[begin1]<a[begin2]){tmp[k++] = a[begin1++];}else{tmp[k++] = a[begin2++];}}while (begin1 <= end1){tmp[k++] = a[begin1++];}while (begin2 <= end2){ tmp[k++] = a[begin2++];}for (int i = left; i <= right; i++){a[i] = tmp[i];}}void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}//_MergeSort(a, tmp, 0, n - 1);_MergeSort2(a,tmp,  n);free(tmp); tmp = NULL;
}

二. 时间复杂度与空间复杂度

1. 时间复杂度

归并排序的时间复杂度是稳定的,不受输入数组的初始顺序影响

 将数组分成两个子数组的时间复杂度为O(1),递归对子数组进行排序,假设每个子数组长度为n

则两个子数组排序的总时间复杂度为O(NlogN),将两个有序数组合并为一个有序数组时间复杂度为O(N),所以归并排序时间复杂度为O(NlogN)

2. 空间复杂度

调用栈所需要的额外空间为O(logN),因为我们需要一个额外数组来存储数据所以又额外消耗O(N)的空间,我们将较小的O(logN)忽略可以得到归并排序的空间复杂度为O(N)

三. 非递归实现归并排序

步骤思路

开辟动态空间后定义一个数gap=1来控制区间(gap相当于每组数据个数),(每一次gap*2,使每次区间扩大)gap<数组长度

设计一个for循环i+=gap*=2

每次分两组[i][i+gap-1]和[i+gap][i+2*gap-1]  (i每次+=正好跳过这些数据)

将两个区间的值比较放入新开辟的数组,再拷贝到原数组

代码如下

//非递归
void _MergeSort2(int* a,int* tmp,int n)
{int gap = 1;while(gap<n){for (int i = 0; i < n; i += 2 * gap){int begin1 = i;int end1 = i + gap - 1;;int begin2 = i + gap;int end2 = i + 2 * gap - 1;int k = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[k++] = a[begin1++];}else{tmp[k++] = a[begin2++];}}while (begin1 <= end1){tmp[k++] = a[begin1++];}while (begin2 <= end2){tmp[k++] = a[begin2++];}//memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));for (int j = i; j < k; j++){a[j] = tmp[j];}}gap *= 2;}
}

但是我们发现,这样如果会发生越界的现象

一共三种可能

1. [begin1,end1][begin2,end2]  end2越界
2. [begin1,end1][begin2,end2]  begin2,end2越界
3. [begin1,end1][begin2,end2]  end1,begin2,end2越界

 第2,3种我们可以直接不递归了,因为后面区间的不存在前面区间的在上一次已经递归好了,

第一种呢我们需要把区间(即end)给修正一下

修正代码如下

//非递归
void _MergeSort2(int* a,int* tmp,int n)
{int gap = 1;while(gap<n){for (int i = 0; i < n; i += 2 * gap){int begin1 = i;int end1 = i + gap - 1;;int begin2 = i + gap;int end2 = i + 2 * gap - 1;int k = i;if (begin2 >= n)//第二种情况,第二组不存在,不需要归并break;if (end2 >= n)//第一种情况,需要修正一下end2 = n - 1;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[k++] = a[begin1++];}else{tmp[k++] = a[begin2++];}}while (begin1 <= end1){tmp[k++] = a[begin1++];}while (begin2 <= end2){tmp[k++] = a[begin2++];}//memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));for (int j = i; j < k; j++){a[j] = tmp[j];}}gap *= 2;}
}

排序算法的稳定性

假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的相对次序保持不变

原序列中 r[i]=r[j],且r[i]在r[j]之前而在排序后的序列中r[i]仍在r[j]前,则称这种排序算法是稳定的,否则是不稳定的

冒泡选择稳定
选择排序不稳定***只会考虑自身,假如找到最小值1下标为3,将其与下标为0(假设此处为6)处交换若下标为1处也是6,就改变了
直接插入排序稳定
希尔排序不稳定(分组)预排序时相同的值可能分到不同的组
堆排序不稳定建堆时可能就乱了
归并排序稳定当两个数相等,让第一个下来就是稳定的(可以控制)
快速排序不稳定end先找到 j 和begin交换了,在找到 i 和bigin交换,显然改变了

这篇文章就到这里了,感谢大家阅读

(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤ 

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

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

相关文章

实验丰富、原创改进!|多策略改进蜣螂优化算法(MATLAB)

本文内容来源于本人公众号&#xff1a;KAU的云实验台&#xff0c;更新内容&#xff1a;智能优化算法及其改进应用。 本文核心内容&#xff1a; 新颖的多策略改进蜣螂优化算法 对比算法包括&#xff1a;高引用/新发布/经典/其他DBO变体&#xff08;共11种&#xff09; 实验设计…

SpringBoot结合ip2region实现博客评论显示IP属地

你好呀&#xff0c;我是小邹。 在现代的Web应用中&#xff0c;特别是博客和论坛类网站&#xff0c;为用户提供地理定位服务&#xff08;如显示用户所在地理位置&#xff09;可以极大地增强用户体验。本文将详细探讨如何使用Java和相关技术栈来实现在博客评论中显示用户的地址信…

Java实验3

实验内容 学生信息管理系统 学生成绩表Student(Sno 字符串&#xff0c;长度9, Sname 字符串&#xff0c;长度10, Class 字符串&#xff0c;长度10, Age 整型, Sex 字符串&#xff0c;长度2) 实现如下功能&#xff1a; A&#xff0e;输入若干个学生的信息到Student表&#x…

【NetTopologySuite类库】合并所有几何的包围盒AABB

流程示意图 示例代码 using GeoAPI.Geometries; using Microsoft.VisualStudio.TestTools.UnitTesting; using NetTopologySuite.Geometries; using NetTopologySuite.IO; using System.Collections.Generic; using System.Linq;namespace Test472 {[TestClass]public class T…

深度解析:电商订单API及其技术实现

随着电子商务的发展&#xff0c;实体企业开拓电商渠道的越来越多&#xff0c;原有的管理系统都需要增加电商业务管理功能&#xff0c;其中&#xff0c;对电商订单的管理是每一个电商商家都需要的功能&#xff0c;所以对于开发者来说&#xff0c;了解电商API是什么是非常重要的&…

第100+16步 ChatGPT学习:R实现Xgboost分类

基于R 4.2.2版本演示 一、写在前面 有不少大佬问做机器学习分类能不能用R语言&#xff0c;不想学Python咯。 答曰&#xff1a;可&#xff01;用GPT或者Kimi转一下就得了呗。 加上最近也没啥内容写了&#xff0c;就帮各位搬运一下吧。 二、R代码实现Xgboost分类 &#xff08…

C++动态内存的管理

今天来分享C动态内存管理相关知识&#xff0c;闲言勿谈&#xff0c;直接上干货。 1. 动态内存的开辟和销毁(new和delete) (1)前置知识&#xff1a;我们知道c语言有malloc和calloc和realloc三个函数可以进行动态的开辟内存&#xff0c;那么它们有什么区别呢&#xff1f;首先是…

PLC_博图系列☞基本指令”TONR:时间累加器“

PLC_博图系列☞基本指令”TONR&#xff1a;时间累加器“ 文章目录 PLC_博图系列☞基本指令”TONR&#xff1a;时间累加器“背景介绍TONR&#xff1a; 时间累加器说明参数脉冲时序图示例 关键字&#xff1a; PLC、 西门子、 博图、 Siemens 、 TONR 背景介绍 这是一篇关于P…

昇思25天学习打卡营第24天|基于MindSpore的Diffusion扩散模型

Diffusion扩散模型 本文基于Hugging Face&#xff1a;The Annotated Diffusion Model一文翻译迁移而来&#xff0c;同时参考了由浅入深了解Diffusion Model一文。 关于扩散模型&#xff08;Diffusion Models&#xff09;有很多种理解&#xff0c;本文的介绍是基于denoising di…

基础动态规划题目基础动态规划题目

目录 题目1&#xff1a; P1216 [USACO1.5] [IOI1994]数字三角形 Number Triangles 代码示例&#xff1a; 题目2&#xff1a; Common Subsequence 代码示例 题目3 &#xff1a;最长上升子序列 最长不下降子序列 最长上升子序列oj答案 题目1&#xff1a; P1216 [USACO1.5]…

SQL面试题练习 —— 查询每个用户最大连续登录天数

目录 1 题目2 建表语句3 题解 1 题目 查询每个用户最大连续登录天数 样例数据如下 login_log&#xff1a; 2 建表语句 --建表语句 create table if not exists login_log (user_id int comment 用户id,login_time date comment 登录时间 ); --数据插入 INSERT overwrit…

Matlab进阶绘图第63期—带标记线的三维填充折线图

三维填充折线图是在三维折线图的基础上&#xff0c;对其与XOY平面之间的部分进行颜色填充&#xff0c;从而能够更好地刻画细节变化。 而带标记线的三维填充折线图是在其基础上&#xff0c;添加X相同的一条或多条标记线&#xff0c;以用于进一步讨论分析。 由于Matlab中未收录…

飞睿智能UWB Tag蓝牙防丢器标签,宠物安全新升级,5cm精准定位测距不迷路

宠物早已成为许多家庭不可或缺的一员&#xff0c;它们用无条件的爱温暖着我们的心房&#xff0c;陪伴我们度过每一个平凡而温馨的日子。然而&#xff0c;随着宠物活动范围的扩大和外界环境的复杂多变&#xff0c;宠物走失的风险也随之增加。每一次出门遛弯&#xff0c;都像是心…

【学术会议征稿】第六届光电材料与器件国际学术会议(ICOMD 2024)

第六届光电材料与器件国际学术会议&#xff08;ICOMD 2024&#xff09; 2024 6th International Conference on Optoelectronic Materials and Devices 第六届光电材料与器件国际学术会议&#xff08;ICOMD 2024&#xff09;将于2024年11月1-3日在中国重庆召开。 大会面向基…

Windows与Ubuntu安装ffmpeg

文章目录 前言ffmpeg的简介安装ffmpegWindows下载设置环境变量 Ubuntu 总结 前言 FFmpeg是一款非常强大的开源音视频处理工具&#xff0c;它包含了众多的音视频编解码库&#xff0c;可以用于音视频的采集、编解码、转码、流化、过滤和播放等复杂的处理。在Windows系统上安装FF…

【Android14 ShellTransitions】(七)Transition就绪

Transition.onTransactionReady的内容比较长&#xff0c;我们挑重点的部分逐段分析&#xff08;跳过的地方并非不重要&#xff0c;而是我柿子挑软的捏&#xff09;。 1 窗口绘制状态的流转以及显示SurfaceControl 注意我们这里的SurfaceControl特指的是WindowSurfaceControll…

Flink底层原理解析:案例解析(第37天)

系列文章目录 一、flink架构 二、Flink底层原理解析 三、Flink应用场景解析 四、fink入门案例解析 文章目录 系列文章目录前言一、flink架构1. 作业管理器&#xff08;JobManager&#xff09;2. 资源管理器&#xff08;ResourceManager&#xff09;3. 任务管理器&#xff08;Ta…

JavaScript统计字符串中出现次数最多的字符

思路&#xff1a;使用charAt()方法可以通过for循环来依次遍历出字符串中的字符 将遍历出来的字符作为一个空对象的属性 如果该对象中该属性没有值则对其赋值为1 如果该对象中已经有值了则在原基础上加1 最后通过for in循环比较对象中个属性值大大小然后将其打印出来 <!DOCT…

liunx面试题目

如何看当前Linux系统有几颗物理CPU和每颗CPU的核数&#xff1f; 查看物理cup&#xff1a; cat /proc/cpuinfo|grep -c ‘physical id’ 查看每颗cup核数 cat /proc/cpuinfo|grep -c ‘processor’ 若希望自动实现软件包的更新&#xff0c;可以使用yum-cron并启动该服务 yum -y …

C++ std::lock_guard和 std::unique_lock

二者都是 C 标准库中用于管理互斥锁&#xff08;mutex&#xff09;的 RAII&#xff08;Resource Acquisition Is Initialization&#xff09;机制的类。这些类可以确保互斥锁在构造时被获取&#xff0c;在析构时被释放&#xff0c;从而避免死锁和资源泄漏问题。不过&#xff0c…