【排序算法】快速排序(C语言)

2023-11-10

【排序算法】—— 快速排序

一、快速排序的单趟排序

​ 快速排序的单趟排序是以一个数作为基准值,实现将数组中比基准数小的数放在基准值的左侧,比基准值大的数放在基准值的右侧。

​ 我们共有3种实现方法。

1. 霍尔法

​ 霍尔是最初发现快速排序的人,它使用的单趟排序算法被称为霍尔法。

​ 用key标记基准值的下标(这里是数组第一个元素下标),利用两个指针leftright分别指向待排数组的最左侧和最右侧,right指针找比key基准值小的数,left找比key基准值大的数,找到后将两个数交换位置,同时实现大数右移和小数左移,直到leftright相遇则排序完成,此时将key基准值的下标返回给函数调用者就完成了单趟排序。

  1. left记录左下标,从左向右遍历,right记录右下标,从右向左遍历,以第一个数作为基准值key

霍尔法快速排序1

  1. right先出发,寻找比key小的值,若是找到则停下来

霍尔法快速排序2

  1. 然后left再出发,寻找比key大的值,若是找到则停下来,与right的值进行交换

霍尔法快速排序3

霍尔法快速排序4

  1. 接着right寻找比key小的值,找到后left找比key大的值,直到left遇到right,此时leftright指向同一个数

霍尔法快速排序5

  1. leftright指向的数与key进行交换,则单趟排序就完成了,最后将基准值的下标返回给函数调用者

霍尔法快速排序6

如何保证相遇位置比key:因为right先走

  1. right停下时,leftright相遇,由于right找比key小的值,所以此时right的位置一定比key小
  2. left停下时,rightleft进行交换,交换后left指向的值比key小,此时right遇到left的位置一定比key小

代码实现

int PartSort(int* arr, int left, int right)
{
    int key = left;		//使用key保存基准值下标
    while (left < right)
    {
        while (left < right && arr[key] <= arr[right])	//只找小的,等于要过滤,找前判断right有没有走过
        {
            right--;
        }
        
        while (left < right && arr[key] >= arr[left])
        {
            left++;
        }
        
        if (left < right)	//没有相遇时左右交换
        {
            Swap(&arr[left], &arr[right]);
        }
    }
    
    Swap(&arr[key], &arr[left]);	//交换基准值和相遇位置的值,并返回基准值的下标
    return left;
}

2. 挖坑法

​ 挖坑法是将key基准值用变量单独保存,然后将key的位置空出来形成一个坑,leftright指针分别从左右两端向中心遍历,此时left先指向这个坑,right找比key小的数,找到后将该数直接放进坑里,并将自己空出来的位置设置为坑,left找比key大的数,找到后将该数放进坑里,并将现在空出来的位置设置为坑,一直遍历,直到leftright相遇,相遇位置一定为坑(leftright必定有一个指向坑),此时将key基准值放进坑内,并返回基准值下标完成单趟排序

  1. 先定义变量key,存储数组第一个数作为key,此时left指向的位置就是坑

挖坑法快速排序1

  1. right开始找小,找到后停止,将right位置的数放进坑里,此时right位置作为新的坑

挖坑法快速排序2

  1. left继续行动,找到比key大的数停止,并将值放进坑里,此时left位置作为新坑

挖坑法快速排序3

  1. right接着找,依次循环,直到leftright相遇

挖坑法快速排序4

  1. key放入相遇时的坑里,排序完毕,将key值下标返回

挖坑法快速排序5

代码实现

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

3. 前后指针

​ 将数组第一个元素作为key基准值,定义前指针prev指向第一个数,后指针cur指向第二个数,由cur挨个遍历数组中的数据,cur寻找比key基准值小的数,prevcur找到小的数时才加一,并与cur位置交换数值,保证数组中较小的数在prev指针之前,而cur找到大的数时接着往前走,prev依然守着较小的数的末尾,依次类推直到cur完全遍历完数组,所以利用如此机制保证prev之前的值一定小于key基准值,而prevcur之间的一定大于基准值(小的都被交换到prev处了),最后将prev(小于基准值)处与key位置的数据交换,将基准值下标返回则完成单趟排序

  1. 开始时prev指向第一个数,cur指向prev的下一位,此时cur位置的数比key基准值小,所以prev加一后与cur位置的数交换,由于此时prev+1 == cur,所以交换后没有变化

前后指针快速排序1

  1. cur找到比key大的数,此时cur接着遍历,prev坚守本地

前后指针快速排序2

  1. cur再次找到小的后,将prev右移一位,并与cur交换位置

前后指针快速排序3

  1. 重复上述动作,直到遍历完整个数组

前后指针快速排序4

  1. cur遍历完数组后,将prevkey交换数值,完成排序,并将key下标返回

前后指针快速排序5

代码实现

int PartSort(int* arr, int left, int right)
{
    int key = left;
    int prev = left;
    int cur = left + 1;
    
    while (cur <= right)
    {
        //arr[cur]小于基准值就交换
        if (arr[cur] <= arr[key] && ++prev != cur)	//这里做了优化:如果prev+1等于cur则不用交换,该语句顺便将prev加一
        {
            Swap(&arr[cur], &arr[prev]);
        }
        cur++;
    }
    
    Swap(&arr[key], &arr[prev]);
    return prev;
}

二、快速排序

​ 快速排序是以一个数为基准,将数组分为两个子序列,左子序列放比基准数小的,右子序列放比基准数大的数,然后再将子序列以以上方式同样分割,直到数组有序

1. 排序步骤

  1. 将下列数组以6为基准,将比6小的数放在数组右侧,比6大的数放在数组左侧,得到如下数组

快速排序1

快速排序2

  1. 以6为分界线,将6以前的数据作为一个数组,以3为分界线,将比3小的数放在数组左边,比3大的数放在数组右边
  2. 以6为分界线,将6以后的数据作为一个数组,以7为分界线,将比7小的数放在数组左边,比7大的数放在数组右边

快速排序3

快速排序4

  1. 再继续重复上述操作,数组就排好顺序了

快速排序5

快速排序6

2. 排序完整步骤图

快速排序

3. 快速排序代码

3.1 递归实现

​ 快速排序使用递归的方式调用单趟排序,每次调用后都以基准值为界,将数组分为2个子序列,继续排序

void QuickSort(int* arr, int begin, int end)
{
    if (begin >= end)
    {
        return;
    }
    
    int key = PartSort(arr, begin, end);	//单趟排序并获取基准值
    QuickSort(arr, begin, key-1);	//排左序列
    QuickSort(arr, key+1, end);		//排右序列
}

3.2 非递归实现

​ 非递归要使用栈存储每一次分割后的数组下标区间,以数组下标区间传入单趟排序实现对递归思想的模拟,具体步骤看注释

注意:栈的顺序是先进后出,获取区间以先右后左顺序时,就要以先左再右压栈

//栈的接口
void StackInit(Stack** pps);			 //初始化
void StackDestroy(Stack** pps);			 //销毁
void StackPush(Stack* ps, STDataType x);  //入栈
void StackPop(Stack* ps);				//出栈
STDataType StackTop(Stack* ps);			 //获取栈顶元素
_Bool StackEmpty(Stack* ps);			 //判空
void QuickSort(int* arr, int begin, int end)
{
    //创建栈并压入数组区间
	Stack *ps = NULL;
    StackInit(&ps);
    StackPush(ps, begin);
    StackPush(ps, end);
    
    while (!StackEmpty(ps))
    {
        //从栈中获取左右区间
        int right = StackTop(ps);
        StackPop(ps);
        int left = StackTop(ps);
        StackPop(ps);
        
        //判断左右区间是否合理,若不合理则跳过本次循环
        if (left >= right)
        {
            continue;
        }
        
        //执行单趟排序并获取基准值下标
        int key = PartSort(arr, left, right);
        
        //将基准值分割的两个区间压入栈中
        StackPush(ps, left);
        StackPush(ps, key-1);
        
        StackPush(ps, key+1);
        StackPush(ps, right);
    }
    StackDestroy(&ps);
}

三、选择基准数key

1. 为什么要选择基准数key

​ 在我们选择基准值时,都是以数组中第一个数作为基准值进行排序,这样写的好处是非常方便且易懂,但是也有个大问题。

​ 如果基准值是数组中最大或最小的数值,则快速排序的递归深度会非常深,排序效率会很低。若是一个有序数组使用快速排序,则递归深度为n,单趟排序也为n,时间复杂度为 O ( n 2 ) O(n^2) O(n2)

快速排序小基准值图

基准数的选择有3种方法:

2. 随机选一个

​ 在数组中随机选择一个数作为基数,每次都选到最大或最小的概率很小,但是有概率会选到最大或最小值

#include <stdlib.h>
#include <time.h>
int GetRandIndex(int* arr, int left, int right)
{
    srand((size_t)time(NULL));	//初始化随机种子
    return rand() % (right-left+1) +left;
}

3. 选中间位置

​ 选取左右下标的中间下标为基准值,针对有序数组能达到最大效率,但是无序数组仍可能选到最大或最小值

int GetMidIndex(int* arr, int left, int right)
{
    return (left + right) / 2;
}

4. 三值取中

​ 在leftright、和中间下标的值中选取一个折中值,基准值不可能为最大值或最小值

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

注意:在我们选择好基准值后,为了保证原来的单趟排序保持原有状态,我们会将选好的基准数与数组中第一个数交换位置,然后使用第一个数作为基准值排序

int PartSort(int* arr, int left, int right)
{
    //获取基准值,并与left交换位置
    int key = GetMidIndex(arr, left, right);
    Swap(&arr[key], &arr[left]);
    key = left;
    
    //单趟排序算法
    ...
}

四、完整代码

1. 快速排序的完整代码

​ 这里是三值取中选择的基准值,挖坑法实现的单趟排序,和递归实现的快速排序,如果想使用其他方法,请自由组合(单趟排序之前别忘记交换基准数与第一个数的值)

//交换变量
void Swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

//三值取中
int GetMidIndex(int* arr, int left, int right)
{
    int mid = (left + right) / 2;
    
	if (arr[left] < arr[right])
	{
		if (arr[mid] < arr[left])
		{
			return left;
		}
		else if (arr[mid] <arr[right])
		{
			return mid;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (arr[mid] < arr[right])
		{
			return right;
		}
		else if (arr[mid] < arr[left])
		{
			return mid;
		}
		else
		{
			return left;
		}
	}
}

//挖坑法单趟排序
int PartSort(int* arr, int left, int right)
{
    //获取基准值,并与left交换位置
    int key = GetMidIndex(arr, left, right);
    Swap(&arr[key], &arr[left]);
    
    int key = arr[left];
    int hole = left;
    
    while (left < right)
    {
        while (left < right && arr[right] >= key)
        {
            right--;
        }
        arr[hole] = arr[right];
        hole = right;
        
        while (left < right && arr[left] <= key)
        {
            left++;
        }
        arr[hole] = arr[left];
        hole = left;
    }
    
    arr[hole] = key;
    return hole;
}

//快速排序递归实现
void QuickSort(int* arr, int begin, int end)
{
    if (begin >= end)
    {
        return;
    }
    
    int key = PartSort(arr, begin, end);	//单趟排序并获取基准值
    QuickSort(arr, begin, key-1);	//排左序列
    QuickSort(arr, key+1, end);		//排右序列
}

2. 快速排序的优化

​ 快速排序时以递归形式(非递归也是用栈模拟递归方法)对分好大小的两个子序列进行单趟排序,若是递归到较深处时,待排数组较短,此时再使用递归方式对短数组进行快速排序就会导致效率下降,所以优化的方式就是设置一个数组排序的长度下限,若是数组长度到达下限以下,则不再调用快速排序,而是调用插入排序。

​ 为什么快速排序递归到深处时效率会下降

快速排序的递归类似于二叉树的形式,深度越深待排数组的长度越短,但是数量也越多,调用函数的次数就越多,开辟函数栈帧的消耗越大,导致效率下降

​ 为什么待排数组较短时调用的是插入排序

  1. 冒泡排序、选择排序、插入排序的时间复杂度都是 O ( n 2 ) O(n^2) O(n2)
    1. 冒泡排序每一次单趟排序都会执行n-1次,一共执行n-1次单趟排序,若是做出优化,单趟排序已经有序,则停止执行
    2. 选择排序每一次单趟排序都会执行n次,一共执行n/2次单趟排序,无论数组是否有序执行次数都不变
    3. 插入排序每一次单趟排序都只向前遍历到最大值之后,一共执行n次,若数组有序,则总共只执行n次,最差情况下每次都只是从i遍历到0下标,综合考虑是执行次数最优的
  2. 希尔排序的时间复杂度是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),但是对于越大的数组效率越高,数组越小效率越接近 O ( n 2 ) O(n^2) O(n2)
  3. 堆排序的时间复杂度也是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),但是在数量很多且长度很短的数组下,建大量的堆显然不会让效率高出多少
  4. 桶排序(基数排序)和归并排序暂时不考虑,我还没学
//快速排序递归的优化实现
void QuickSort(int* arr, int begin, int end)
{
    if (begin >= end)
    {
        return;
    }
    
    //数组长度小于等于8时,调用插入排序
    if (end-begin+1 <= 8)
    {
        InsertSort(arr, end-begin+1);
        return;
    }
    
    int key = PartSort(arr, begin, end);	//单趟排序并获取基准值
    QuickSort(arr, begin, key-1);	//排左序列
    QuickSort(arr, key+1, end);		//排右序列
}

​ 需要优化后的完整代码将以上代码直接替换前面的快速排序代码,并将插入排序代码复制到快速排序代码之前

//插入排序
void InsertSort(int* arr, int size)
{
	int end = 0;
	int i = 0;
	for (i = 0; i < size-1; i++)
	{
		end = i;
		int temp = arr[end + 1];

		while (end >= 0)
		{
			if (temp < arr[end])
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = temp;
	}
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【排序算法】快速排序(C语言) 的相关文章

随机推荐

  • 如何在 Ubuntu 14.04 LTS 上设置 Apache 虚拟主机

    介绍 Apache Web 服务器是在互联网上提供 Web 内容的最流行的方式 它占互联网上所有活跃网站的一半以上 并且非常强大和灵活 Apache 将其功能和组件分解为可以独立定制和配置的单独单元 描述单个站点或域的基本单位称为virtu
  • 如何使用多站点设置多个 WordPress 站点

    关于多个 WordPress 安装 2010 年 WordPress 发布了流行的内容管理平台 3 0 版本 在该版本中包含的众多改进中 WordPress 社区将 WordPress MU 合并到了主要的 WordPress 配置中 自更
  • Java 9 功能与示例

    Java 9 是一个主要版本 它为我们开发人员带来了很多功能 在本文中 我们将详细研究 Java 9 功能 Java 10 已发布 有关 Java 10 版本的完整概述 请访问Java 10 特性 Java 9 特性 Some of the
  • 如何在 CentOS 7 服务器上添加和删除用户

    介绍 当您第一次开始使用新的 Linux 服务器时 添加和删除用户通常是您需要做的第一件事 在本指南中 您将学习如何创建用户帐户 分配sudo权限 并删除 CentOS 7 服务器上的用户 先决条件 要完成本教程 您将需要 一台 CentO
  • 如何在 Ubuntu 22.04 上设置私有 Docker 注册表

    作者选择了免费开源基金接受捐赠作为为捐款而写程序 介绍 Docker 注册表是一个管理 Docker 容器镜像存储和交付的应用程序 注册表集中容器映像并减少开发人员的构建时间 Docker 镜像通过虚拟化保证相同的运行时环境 但构建镜像可能
  • 如何在 Ubuntu 16.04 上将 Nginx Web 根移动到新位置

    介绍 在 Ubuntu 上 默认情况下 Nginx Web 服务器将其文档存储在 var www html 它通常与操作系统的其余部分一起位于根文件系统上 但有时 将文档根移动到另一个位置会很有帮助 例如单独安装的文件系统 例如 如果您从同
  • 如何使用BackupPC在Ubuntu 12.04 VPS上创建备份服务器

    Status 已弃用 本文介绍不再受支持的 Ubuntu 版本 如果您当前运行的服务器运行 Ubuntu 12 04 我们强烈建议您升级或迁移到受支持的 Ubuntu 版本 升级到Ubuntu 14 04 从 Ubuntu 14 04 升级
  • 如何设置 Nginx 负载均衡

    关于负载均衡 负载平衡是一种有用的机制 可以在多个功能强大的虚拟专用服务器之间分配传入流量 通过将处理机制分配给多台计算机 可以为应用程序提供冗余 确保容错和提高稳定性 用于负载平衡的循环算法将访问者发送到一组 IP 中的一个 在最基本的层
  • 如何在 Java 中使用运算符

    作者选择了免费开源基金接受捐赠作为为捐款而写程序 介绍 An operator是一个或多个符号的组合 比如著名的算术运算符减号 并加上 或更高级的instanceof 当您对值或变量应用运算符时 您会得到运算结果 此类操作是编程的基础 因为
  • 如何在 Ubuntu 14.04 服务器上安装 Node.js

    介绍 Node js 是一个用于服务器端编程的 Javascript 平台 允许用户快速构建网络应用程序 通过在前端和后端都利用 Javascript 开发可以更加一致并在同一系统内进行设计 在本指南中 我们将向您展示如何在 Ubuntu
  • mysql如何显示ddl_Mysql DDL语句之视图

    Mysql 视图是一个虚拟表 内容由 select 查询语句定义 同真实的表数据一致 但是视图并不在数据库中以存储的数据值形式存在 试图引用自定义查询表的字段 并且在引用试图时动态生成 对其所引用的基础表来说 Mysql 视图的作用类似于筛
  • 【计算机视觉

    文章目录 一 ModaNet 二 SKU110K 三 SceneNet 四 VT5000 五 Washington RGB D 六 Argoverse HD 七 CADC Canadian Adverse Driving Condition
  • 小技巧——宝塔面板重启、重置命令

    1 Centos 安装脚本 yum install y wget wget O install sh http download bt cn install install sh sh install sh 2 Ubuntu Deepin
  • Qt示例5:用Qt画一个漂亮预警仪表

    以下是用Qt实现漂亮预警仪表的步骤和代码 创建一个Qt项目 并添加一个主窗口 在主窗口中添加QGraphicsView控件 用于绘制预警仪表 创建一个QGraphicsScene对象 并将其设置为QGraphicsView的场景 QGrap
  • emmx用xmind打开_XMind思维导图:专注扩展延伸和梳理,让你事半功倍!

    更多精彩软件 请关注我们 今日新闻 现如今 思维导图被普遍运用在各行各业 充当着重要的角色 但你会发现这些导图绝大多数是通过电脑软件绘制的 随着移动互联网的高速发展 实际情况告诉我们需要一款手机版的思维导图软件 便于我们在手机上就能自由整理
  • mmdetection踩坑1~docker内RuntimeError: DataLoader worker (pid 1727) is killed by signal: Bus errer

    今天在docker内使用mmdetection做训练时 workers per gpu参数设置为0时 可以正常训练 但修改配置文件中workers per gpu 2参数后 开始训练 程序报错 网上查资料显示 是因为docker的共享内存不
  • 初始化列表

    在构造函数后面 属性 值 参数 属性 值 参数 define CRT SECURE ND WARNINGS include
  • 1.4最流行的NoSQL——Redis

    本文比较重要的概念 NoSQL 及它的优点 Redis 及它的优点 NoSQL Not Only SQL NoSQL 在互联网中作用很大 可以在很大程度上提高互联网系统的性能 具备一定持久层的功能 也可以作为一种缓存工具 注释 Redis缓
  • 重学JavaScript 第二天

    数据类型 js数据类型整体分为两大类 1 基本数据类型 2 引用数据类型 1 数据类型 数字类型 number JavaScript 中的正数 负数 小数等 统一称为 数字类型 注意 JS 是弱数据类型 变量到底属于那种类型 只有赋值之后
  • 【排序算法】快速排序(C语言)

    排序算法 快速排序 目录 一 快速排序的单趟排序 1 霍尔法 2 挖坑法 3 前后指针 二 快速排序 1 排序步骤 2 排序完整步骤图 3 快速排序代码 3 1 递归实现 3 2 非递归实现 三 选择基准数key 1 为什么要选择基准数ke