快速排序(C语言实现)

news2024/11/19 16:20:53

目录

基本概念

Hoare版本

动图演示

思路

 代码实现:

性能分析

取Key优化

三数取中法选择基准(Median-of-Three Partitioning)

实现步骤

代码实现

挖坑法 

基本步骤

动图

示例说明

代码实现

前后指针法

动图示范

思路 

代码实现:

迭代实现的快速排序

代码:

复杂度分析

时间复杂度

空间复杂度

稳定性与适用场景

      稳定性:

     适用场景:


基本概念

快速排序(Quick Sort)是一种高效的排序算法,由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

对于如何按照基准值将待排序列分为两子序列,常见的方式有:
 1、Hoare版本
 2、挖坑法
 3、前后指针法

下面将对这三个方法进行一一讲解。

Hoare版本

动图演示

思路

Hoare版本的单趟排序的基本步骤如下:
 1、选出一个key,一般是最左边或是最右边的。
 2、定义一个L和一个R,L从左向右走,R从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要R先走;若选择最右边的数据作为key,则需要L先走)。
 3、在走的过程中,若R遇到小于key的数,则停下,L开始走,直到L遇到一个大于key的数时,将L和R的内容交换,R再次开始走,如此进行下去,直到L和R最终相遇,此时将相遇点的内容与key交换即可。(选取最左边的值作为key)

 经过一次单趟排序,最终使得key左边的数据全部都小于key,key右边的数据全部都大于key。

 然后我们在将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,因为这种序列可以认为是有序的。

 代码实现:

int PartSort_Hoare(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]);
	}
	int meeti = left;
	Swap(&a[meeti], &a[keyi]);
	return meeti;
}

void QuickSort(int* a,int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort_Hoare(a, begin, end-1);
	QuickSort(a, begin, keyi);
	QuickSort(a, keyi + 1, end);


}

 这里一定要注意边界的处理,PartSort_Hoare函数中的[left,right]为闭区间,传参的时候也应该注意,递归的时候要也分清边界。

代码测试:

性能分析

  • 时间复杂度:平均时间复杂度为O(n log n),但最坏情况下的时间复杂度为O(n^2)(当数组已经是有序或逆序时)。
  • 空间复杂度:快速排序的空间复杂度主要是递归引起的栈空间使用,最优为O(log n),最坏为O(n)。

 快速排序是一种非常高效的排序算法,在实际应用中非常广泛。虽然其最坏情况下的时间复杂度不如某些排序算法(如归并排序),但其平均性能非常出色,并且在实际应用中经常可以通过一些策略(如随机选择基准、三数取中等)来避免最坏情况的发生。

所以我们接下来对它进行一下优化

取Key优化

三数取中法选择基准(Median-of-Three Partitioning)

在快速排序中,选择一个合适的基准值至关重要,因为它直接影响到排序的效率。如果基准值选择不当,特别是在数据已经部分有序或包含大量重复元素的情况下,可能会导致排序过程退化为冒泡排序,从而增加排序的时间复杂度。三数取中法通过比较并选取三个位置上的元素的中位数作为基准值,试图在一定程度上缓解这个问题。

实现步骤

  1. 选择三个元素:从数组的开始(mid)、中间(mid = (left + right) / 2)和结束(right)位置选择三个元素。
  2. 比较并排序:将这三个元素进行比较,并按照从小到大的顺序进行排序(如果需要的话,可以交换它们在数组中的位置)。
  3. 选择中位数:将排序后的中间元素作为基准值(keyi)。
  4. 进行分区:以基准值为界,将数组分为两个子数组,一个包含所有小于基准值的元素,另一个包含所有大于基准值的元素。
  5. 递归排序:对这两个子数组递归地应用快速排序算法。

优点

  • 减少最坏情况的发生:通过选择中位数作为基准值,减少了排序过程中最坏情况(即时间复杂度退化到O(n^2))的出现概率。
  • 提高排序效率:在处理包含大量重复元素或接近有序的数据集时,三数取中法能够显著提高排序的效率

代码实现

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

 注意:当大小居中的数不在序列的最左或是最右端时,我们不是就以居中数的位置作为key的位置,而是将key的值与最左端的值进行交换,这样key就还是位于最左端了,所写代码就无需改变,而只需在单趟排序代码开头加上以下两句代码即可:

	int keyi = GetMidIndex(a, begin, end);//获取大小居中的数的下标
	Swap(&a[begin], &a[keyi]);//将该数与序列最左端的数据交换
	//以下代码保持不变

挖坑法 

快排挖坑法(也称为挖坑填补法)是快速排序算法的一种实现方式,其思路主要基于分治策略。以下是快排挖坑法的基本思路:

基本步骤

  1. 选择基准元素
    • 通常选择数组的第一个元素作为基准元素(key),但也可以采用更复杂的策略,这里我们用三数取中法,来选取一个更合适的基准元素。
  2. 挖坑
    • 将基准元素保存起来(通常保存在一个临时变量中),并将基准元素所在的位置视为一个“坑”。
  3. 填坑
    • 从数组的右端开始向左遍历,找到第一个比基准元素小的元素,将其填入“坑”中,并将这个位置视为新的“坑”。
    • 然后,从数组的左端开始向右遍历,找到第一个比基准元素大的元素,将其填入新的“坑”中,并将这个位置再次视为新的“坑”。
    • 重复上述过程,直到左右指针相遇,此时左右指针所指的位置即为基准元素的最终位置。
  4. 递归排序
    • 将基准元素填入最终的“坑”中后,基准元素左边的所有元素都比它小,右边的所有元素都比它大。
    • 接着,对基准元素左边和右边的子数组递归地进行上述操作,直到整个数组有序。

动图

示例说明

假设有一个数组 [6, 1, 4, 7, 9, 2, 5, 6, 7, 3, 10],选择第一个元素 6 作为基准元素。

  1. 挖坑:将 6 保存在临时变量中,数组变为 [_, 1, 4, 7, 9, 2, 5, 6, 7, 3, 10],其中 _ 表示“坑”。

  2. 填坑:

    • 从右向左找到第一个比 6 小的元素 3,填入“坑”中,数组变为 [3, 1, 4, 7, 9, 2, 5, 6, 7, _, 10]
    • 从左向右找到第一个比 6 大的元素 7,填入新的“坑”中,数组变为 [3, 1, 4, _, 9, 2, 5, 6, 7, 7, 10]
    • 重复此过程,直到左右指针相遇,最终将基准元素 6 填入相遇的位置,数组变为 [3, 1, 4, 5, 6, 2, 9, _, 7, 7, 10]
  3. 递归排序:对基准元素 6 左边和右边的子数组 [3, 1, 4, 5] 和 [2, 9, 7, 7, 10] 分别进行快速排序。

代码实现

int PartSort_Hole(int* a, int left, int right)
{
	int keyi = GetMidIndex(a, left, right);
	Swap(&a[keyi], &a[left]);
	int tmp = a[keyi];
	int hole = left;
	while (left < right)
	{
		while(left < right&&a[right] >= tmp)
		{
			--right;
		}
		a[hole] = a[right];
		hole = right;//新的坑在右边
		while (left < right&&a[left]<=tmp)
		{
			++left;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = tmp;
	return hole;
}

void QuickSort(int* a,int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort_Hole(a, begin, end-1);
	QuickSort(a, begin, keyi);
	QuickSort(a, keyi + 1, end);


}

前后指针法

动图示范

思路 

这里我们依然用(三数取中法)获得key,前指针定义为prev = left,后指针定义为cur = left+1,由于我们上传的是闭区间,所以循环是cur<=right,越界循环即截止。

从动图中我们能看到,cur一直在向后++,找小于key位置上的值,如果小于,和prev位置的值交换,然后prev++。

当结束循环,把key与prev的值进行交换。

代码实现:

int PartSort_Double_p(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[mid], &a[left]);
	int cur = left + 1;
	int prev = left;
	int keyi = left;

	while (cur <= right)//闭区间
	{
		//代码优化
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		++cur;

		//if (a[cur] < a[keyi])//当cur = prev的时候没有必要交换
		//{
		//	++prev;
		//	Swap(&a[prev], &a[cur]);
		//}
		//cur++;
	}
	int meeti = prev;//cur越界时,prev的位置
	Swap(&a[keyi], &a[meeti]);//交换key和prev指针指向的内容
	return meeti;//返回key的当前位置
}

void QuickSort(int* a,int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort_Double_p(a, begin, end-1);
	QuickSort(a, begin, keyi);
	QuickSort(a, keyi + 1, end);


}

 

迭代实现的快速排序

迭代实现的快速排序通常使用栈(stack)来模拟递归调用栈。这种方法可以避免递归调用带来的栈溢出风险,尤其是在处理大数据集时。

代码:

void QuickSortNonR(int* a, int begin, int end)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);
	
	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);

		int left = StackTop(&st);
		StackPop(&st);
		if (left >= right)
		{
			continue;
		}
		int keyi = PartSort_double(a, left, right);

		StackPush(&st, keyi+1);
		StackPush(&st, right);

		StackPush(&st, left);
		StackPush(&st, keyi - 1);
	}
	StackDestroy(&st);
}

复杂度分析

时间复杂度

快速排序的时间复杂度依赖于基准元素(pivot)的选择方式以及数据的初始状态。

  1. 最佳情况:当每次分区操作都能将数组均匀地划分为两个子数组时,即两个子数组的大小都接近n/2,此时的时间复杂度最优。在这种情况下,递归树的高度为log2(n),每一层的时间复杂度为O(n)(因为需要遍历整个数组进行分区),所以总的时间复杂度为O(n) * log2(n) = O(nlogn)。

  2. 平均情况:在实际应用中,由于基准元素的选择通常是随机的或采用某种策略(如三数取中法),分区操作大致能将数组均匀地划分为两部分。因此,平均情况下的时间复杂度也是O(nlogn)。

  3. 最坏情况:当输入的数组已经有序(或接近有序)时,每次分区操作都会得到一个大小为0的子数组和一个大小为n-1的子数组,递归树退化为一个线性结构。此时,时间复杂度为O(n) + O(n-1) + ... + O(1) = O(n^2)。然而,通过采用随机选择基准元素或使用优化策略(如三数取中法),可以避免最坏情况的发生,使时间复杂度保持在O(nlogn)。

空间复杂度

快速排序的空间复杂度主要取决于递归栈的深度以及算法中使用的辅助空间。

  1. 递归栈:快速排序是递归的,需要使用递归栈来保存中间状态。在最好情况下,递归栈的深度为O(logn)(与递归树的高度相同)。在最坏情况下,递归栈的深度可能达到O(n)(当输入数组已经有序时)。

  2. 辅助空间:除了递归栈外,快速排序还需要额外的空间来保存分区操作的临时数据(如pivot元素的索引或临时数组)。然而,这部分空间通常是O(1)的,因为只需要几个变量来保存临时数据。如果考虑使用额外数组来存储分区结果,则空间复杂度会增加到O(n)。但通常情况下,快速排序采用原地排序(in-place sorting),即直接在原数组上进行排序,不需要额外的存储空间。

综上所述,快速排序的平均时间复杂度为O(nlogn),最坏情况下为O(n^2),但可以通过优化策略避免。空间复杂度方面,主要取决于递归栈的深度,通常为O(logn)到O(n),但在原地排序时额外空间复杂度为O(1)。

稳定性与适用场景

      稳定性

          快速排序是一种不稳定的排序算法。因为快速排序在分区过程中可能会改变相等元素的相对            顺序。

     适用场景


           快速排序在大多数情况下表现优异,特别适用于大规模数据的排序。由于它的平均时间复               杂 度为O(nlogn),且实现简单、效率高,因此在实际应用中非常广泛。然而,在处理小规                模数据或已经接近有序的数据时,快速排序的性能可能不如其他排序算法(如插入序)。

 

 

 

 

 

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

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

相关文章

Linux操作系统中docker

1、docker概述 1、什么是docker Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中&#xff0c;然后发布到任何流行的 Linux或Windows&#xff08;对于windows不是太友好&#xff09;操作系统的机器上&#xff0c;也可以…

Amazon EC2:权限设置指南,构建安全的云环境

在数字化转型的浪潮中&#xff0c;企业纷纷将业务迁移到云端&#xff0c;以提高灵活性和效率。Amazon Elastic Compute Cloud&#xff08;EC2&#xff09;作为 AWS 的核心服务之一&#xff0c;为企业提供了一个强大的云计算平台。然而&#xff0c;随着云环境的复杂性增加&#…

DHCP 中继器

在实际应用中可能会遇到一个比较大的物理网络中存在多个ip子网&#xff0c;而每个ip子网的主机都需要DHCP服务器来动态分配ip地址&#xff0c;实现的方法有两种: 第一种是在每一个子网中设置DHCP服务器&#xff0c;将其分别为每个子网分配ip地址&#xff0c;但此方法会增加开销…

【Hadoop】【vim编辑器】【~/.bashrc 文件】如何编辑

1. 进入 vim 编辑器 在终端中输入以下命令&#xff1a; vim ~/.bashrc 2. 进入插入模式 打开文件后&#xff0c;你将处于普通模式。在普通模式下&#xff0c;你不能直接编辑文本。 要进入插入模式&#xff0c;请按下 i 键。这时&#xff0c;你应该会看到屏幕底部出现 -- 插…

优化java中 HashMap 的容量](capacity值)

我们很多人都知道&#xff0c;分配比我们所需更多的内存可能会对应用程序的性能产生负面影响。因此&#xff0c;使用带有容量的构造函数创建列表可能会产生很大的不同。 但是&#xff0c;使用Maps时&#xff0c;这个优化步骤可能不是那么简单。在本文中&#xff0c;我们将学习…

鸿蒙OpenHarmony【小型系统基础内核(互斥锁)】子系统开发

互斥锁 基本概念 互斥锁又称互斥型信号量&#xff0c;用于实现对共享资源的独占式处理。当有任务持有时&#xff0c;这个任务获得该互斥锁的所有权。当该任务释放它时&#xff0c;任务失去该互斥锁的所有权。当一个任务持有互斥锁时&#xff0c;其他任务将不能再持有该互斥锁…

Redis 主从复制的实现过程

Redis 主从复制的实现过程 1. 初始同步请求2. 快照生成与发送3. 从服务器载入数据4. 增量同步5. 持续同步与部分重同步 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; Redis 的主从复制是一个高效的数据同步机制&#xff0c;主要步骤为以下…

5分钟内不能重复发送验证码!

文章目录 引言校验5分钟内不能重复发送验证码生成验证内容保存验证码到缓存获取缓存验证内容验证短信验证码是否正确数据模型see also引言 防止被恶意攻击,使用需要限制用户获取验证码的频率,例如5分钟内不能重复发送验证码!获取验证码接口,也必须进行签名校验。 使用Red…

单片机STM32 外部中断线的使用笔记

一、STM32外部中断线问题小结 1.1 不同的端口同一PIN 在STM32中&#xff0c;不同的端口&#xff08;如PA、PB、PC等&#xff09;上的相同PIN号&#xff08;如PA1、PB1、PC1&#xff09;可以共用一个外部中断线&#xff08;EXTI_Line&#xff09;。这意味着&#xff0c;虽然这些…

[系列]相关的知识点关联

系列 独立&不相关不相关&正交协方差&互相关相关系数协方差&相关系数余弦系数&内积余弦系数&相关系数滤波&卷积卷积&互相关互相关&内积互相关&归一化互相关

Jenkins入门:从搭建到部署第一个Springboot项目(踩坑记录)

本文讲述在虚拟机环境下(模拟服务器)&#xff0c;使用docker方式搭建jenkins&#xff0c;并部署一个简单的Springboot项目。仅记录关键步骤和遇到的坑&#xff0c;后续再进行细节补充。 一、环境准备和基础工具安装 1. 环境 系统环境为本机vmware创建的Ubuntu24.04。 2. yum…

Unity 3D UGUI 系统一口气讲完!(^U^)ノ~YO

UGUI Canvas 画布 Canvas画布是摆放所有 UI 元素的区域&#xff0c;在场景中创建的所有控件都会自动变为 Canvas游戏对象的子对象。 若场景中没有画布&#xff0c;在创建控件时会自动创建画布。 不论是你主动创建还是被动创建&#xff0c;系统都会自动创建一个名为 EventSys…

使用Python解决数据分析中的相关性分析

目录 1.相关系数基础1.1 使用Pandas计算皮尔逊相关系数1.2 计算物品A与其他物品的相关系数1.3 用户间的相关系数1.4 获取相关系数矩阵 2. 相似度计算的Python实现2.1 欧式距离2.2 余弦相似度2.3 皮尔逊相关系数的手动实现 3. 总结 在数据分析中&#xff0c;相关系数是衡量两个变…

应用案例 | HK-MSR数据记录仪如何计算滑雪时膝盖上的应力?

计算滑雪时膝盖上的应力 阿尔卑斯山高山滑雪运动员在滑雪时对膝盖产生的压力有多大&#xff1f;Thea Waldleben&#xff0c;现任瑞士青年速降赛冠军&#xff0c;在她的 "Maturaarbeit"&#xff08;考试项目&#xff09;中回答了这个问题。通过使用HK-MSR数据记录仪&…

Python OpenCV精讲系列 - 边缘检测深入理解(十三)

&#x1f496;&#x1f496;⚡️⚡️专栏&#xff1a;Python OpenCV精讲⚡️⚡️&#x1f496;&#x1f496; 本专栏聚焦于Python结合OpenCV库进行计算机视觉开发的专业教程。通过系统化的课程设计&#xff0c;从基础概念入手&#xff0c;逐步深入到图像处理、特征检测、物体识…

地质工程专业职称申报条件详细解读

一、初级&#xff08;助理&#xff09;地质工程工程师评审条件&#xff1a; 1、理工类或者地质工程类专业毕业 2、专科毕业满3年或本科毕业满1年 3、研究生毕业&#xff0c;从事本专业技术工作&#xff0c;当年内考核认定 二、中级地质工程工程师评审条件&#xff1a; 1、理工…

解决远程连接AlpineLinux Mysql/MariaDB 无法连接的问题

&#x1f525;博客介绍&#xff1a; EvLast &#x1f3a5;系列专栏&#xff1a; << C项目>> <<数据结构与算法>> << 算法入门>> &#x1f3a5; 当前专栏:<< C项目>> 专题 : 解决开发中的日常Bug &#x1f44d;&#x1f44…

服务端的 Session 详解

0x01&#xff1a;Session 简介 Session 是在 Cookie 的基础上发展的&#xff0c;其主要功能和 Cookie 一样&#xff0c;都是为了解决 HTTP 无状态的痛点&#xff0c;和 Cookie 不同的是&#xff0c;它是将大部分数据存储在了服务端&#xff0c;而只给用户一个 SESSID&#xff…

django学习入门系列之第十点《A 案例: 员工管理系统21》

文章目录 16 Ajax(订单案例)16.9 编辑想要去数据库中获取数据时:对象/字典将具体内容的错误信息传入到前端&#xff08;Ajax&#xff09;将数据库数据传入前端&#xff08;Ajax&#xff09;清空新建订单的数据 16.10 编辑后保存 往期回顾 16 Ajax(订单案例) 16.9 编辑 点击编辑…

TCN预测 | MATLAB实现TCN时间卷积神经网络多输入单输出回归预测

TCN预测 | MATLAB实现TCN时间卷积神经网络多输入单输出回归预测 目录 TCN预测 | MATLAB实现TCN时间卷积神经网络多输入单输出回归预测预测效果基本介绍模型描述程序设计参考资料预测效果