C语言快速排序,以及注意点。

2023-11-06

快速排序尤其适用于对大数据的排序,它的高速和高效无愧于“快速”两个字。虽然说它是“最常用”的,可对于初学者而言,用它的人却非常少。因为虽然很快,但它也是逻辑最复杂、最难理解的算法,因为快速排序要用到递归和函数调用。

快速排序所采用的思想是分治的思想。所谓分治,就是指以一个数为基准,将序列中的其他数往它两边“扔”。以从小到大排序为例,比它小的都“扔”到它的左边,比它大的都“扔”到它的右边,然后左右两边再分别重复这个操作,不停地分,直至分到每一个分区的基准数的左边或者右边都只剩一个数为止。这时排序也就完成了。

所以快速排序的核心思想就是将小的往左“扔”,将大的往右“扔”,只要能实现这个,就与快速排序的思想吻合。从初学者的角度,“小的往左扔大的往右扔”首先能想到的往往是小的往前插,大的往后插。这确实是一个思路,但我们知道,数组是不擅长插入的。这种思路虽然能吻合快速排序的思想,但实现起来就不是“快速”排序,而是“慢速”排序了。所以这种方法是不可行的。于是就有了下面的“舞动算法”。

“舞动算法”的思想是用交换取代插入,大大提高了排序的速度。下面首先详细地讲解一下数组快速排序的过程。

假设序列中有 n 个数,将这 n 个数放到数组 A 中。“舞动算法”中一趟快速排序的算法是:

  1. 设置两个变量 i、j,排序开始的时候:i=0,j=n–1。
  2. 以数组第一个元素为关键数据,赋给变量 key,即 key=A[0]。
  3. 从 j 开始向前搜索,即由后开始向前搜索(j--),找到第一个小于 key 的值 A[j],将 A[j] 和 A[i] 互换。
  4. 然后再从 i 开始向后搜索,即由前开始向后搜索(++i),找到第一个大于 key 的 A[i],将 A[i] 和 A[j] 互换。
  5. 重复第 3、4 步,直到 i=j。此时就能确保序列中所有元素都与 key 比较过了,且 key 的左边全部是比 key 小的,key 的右边全部是比 key 大的。


第一轮比较后序列就以 key 为中心分成了左右两部分,然后分别对左右两部分分别递归执行上面几个步骤,直到排序结束。

下面列举一个简单的例子,比如对如下数组 a 中的元素使用快速排序实现从小到大排序:

35  12  37  -58  54  76  22

1) 首先分别定义 low 和 high 用于存储数组第一个元素的下标和最后一个元素的下标,即 low=0,high=6。

2) 然后定义 key 用于存放基准数,理论上该基准数可以取序列中的任何一个数。此处就取数组的第一个元素,即把 a[low] 赋给 key。

3) 然后 key 和 a[high] 比较,即 35 和 22 比较,35>22,则它们互换位置:

22  12  37  -58  54  76  35

4) 然后 low++==1,key 和 a[low] 比较,即 35 和 12 比较,12<35,则不用互换位置;继续 low++==2,然后 key 和 a[low] 比较,即 35 和 37 比较,37>35,则它们互换位置:

22  12  35  -58  54  76  37

5) 然后 high--==5,key 和 a[high] 比较,即 35 和 76 比较,35<76,则不用互换位置;继续 high--==4,然后 key 和 a[high] 比较,即 35 和 54 比较,35<54,则不用互换位置;继续 high--==3,然后 key 和 a[high] 比较,即 35 和 -58 比较,35>–58,则它们互换位置:

22  12  -58  35  54  76  37

6) 然后 low++==3,此时 low==high,第一轮比较结束。从最后得到的序列可以看出,35 左边的都比 35 小,35 右边的都比 35 大。这样就以 35 为中心,把原序列分成了左右两个部分。接下来只需要分别对左右两个部分分别重复上述操作就行了。

对于人类而言,这个过程确实比前面的排序算法复杂。但对于计算机而言,这个过程却没那么复杂。下面来写一个程序:

#include <stdio.h>
void Swap(int *, int *); //函数声明, 交换两个变量的值
void QuickSort(int *, int, int); //函数声明, 快速排序

int main(void)
{
    int i; //循环变量
    int a[] = {900, 2, -58, 3, 34, 5, 76, 7, 32, 4, 43, 9, 1, 56, 8,-70, 635, -234, 532, 543, 2500};
    QuickSort(a, 0, 20); /*引用起来很简单, 0为第一个元素的下标, 20为最后一个元素的下标*/
    printf("最终排序结果为:\n");
    
    for (i=0; i<21; ++i)
    {
        printf("%d ", a[i]);
    }
    printf("\n");
    
    return 0;
}

void Swap(int *p, int *q)
{
    int buf;
    buf = *p;
    *p = *q;
    *q = buf;
    return;
}

void QuickSort(int *a, int low, int high)
{
    int i = low;
    int j = high;
    int key = a[low];
    if (low >= high) //如果low >= high说明排序结束了
    {
        return ;
    }
    
    while (low < high) //该while循环结束一次表示比较了一轮
    {
        while (low < high && key <= a[high])
        {
            --high; //向前寻找
        }
        if (key > a[high])
        {
            Swap(&a[low], &a[high]);
            ++low;
        }
        while (low < high && key >= a[low])
        {
            ++low; //向后寻找
        }
        if (key < a[low])
        {
            Swap(&a[low], &a[high]);
            --high;
        }
    }
    QuickSort(a, i, low-1); //用同样的方式对分出来的左边的部分进行同上的做法
    QuickSort(a, low+1, j); //用同样的方式对分出来的右边的部分进行同上的做法
}

输出结果是:
最终排序结果为:
-234 -70 -58 1 2 3 4 5 7 8 9 32 34 43 56 76 532 543 635 900 2500

这个程序就是按上面讲的过程写的。实际上还可以对这个程序进行优化。在快速排序算法中,每轮比较有且仅有一个 key 值,但是 key 值的位置是不断变化的,只有比较完一轮后 key 值的位置才固定。上面这个程序中每次执行 swap 时实际上交换的是 key 和 a[low] 或 key 和 a[high],而 key 的位置每次都是不一样的。所以既然 key 的位置是“动来动去”的,所以就不必将它赋到数组中了,等最后一轮比较结束后,它的位置“不动”了,再将它赋到数组中。

比如,数组 a 中元素为:3142。如果按从小到大排序,那么 key=3,按上面这个程序就是 3 和 2 互换。2 赋给 a[0] 是必需的,但 key 就没有必要赋给 a[3] 了。但你可以想象 key 是在 a[3] 这个位置,这个很重要。即此时序列变成 2142(key)。

然后 key 和 1 比较,不用换;key 和 4 比较,将 4 赋给 a[3],然后想象 key 在 4 的位置,即此时序列变成 214(key)4。此时 key 左边全是比 key 小的,key 的右边全是比 key 大的。这时 key 的位置就固定了,再将它赋到数组中,即 2134。

#include <stdio.h>
void QuickSort(int *, int, int); /*现在只需要定义一个函数, 不用交换还省了一个函数, 减少了代码量*/

int main(void)
{
    int i; //循环变量
    int a[] = {900, 2, -58, 3, 34, 5, 76, 7, 32, 4, 43, 9, 1, 56, 8,-70, 635, -234, 532, 543, 2500};
    QuickSort(a, 0, 20); /*引用起来很简单, 0为第一个元素的下标, 20为最后一个元素的下标*/
    printf("最终排序结果为:\n");
    
    for (i=0; i<21; ++i)
    {
        printf("%d ", a[i]);
    }
    printf("\n");
    return 0;
}

void QuickSort(int *a, int low, int high)
{
    int i = low;
    int j = high;
    int key = a[low];
    if (low >= high) //如果low >= high说明排序结束了
    {
        return ;
    }
    while (low < high) //该while循环结束一次表示比较了一轮
    {
        while (low < high && key <= a[high])
        {
            --high; //向前寻找
        }
        if (key > a[high])
        {
            a[low] = a[high]; //直接赋值, 不用交换
            ++low;
        }
        while (low < high && key >= a[low])
        {
            ++low; //向后寻找
        }
        if (key < a[low])
        {
            a[high] = a[low]; //直接赋值, 不用交换
            --high;
        }
    }
    a[low] = key; //查找完一轮后key值归位, 不用比较一次就互换一次。此时key值将序列分成左右两部分
    QuickSort(a, i, low-1); //用同样的方式对分出来的左边的部分进行同上的做法
    QuickSort(a, low+1, j); //用同样的方式对分出来的右边的部分进行同上的做法
}

 

输出结果是:
最终排序结果为:
-234 -70 -58 1 2 3 4 5 7 8 9 32 34 43 56 76 532 543 635 900 2500

总结

快速排序的基本思想是通过一趟快速排序,将要排序的数据分割成独立的两部分,其中一部分的所有数据比另外一部分的所有数据都要小,然后再按此方法递归地对这两部分数据分别进行快速排序。如此一直进行下去,直到排序完成。

快速排序实际上是冒泡排序的一种改进,是冒泡排序的升级版。这种改进就体现在根据分割序列的基准数,跨越式地进行交换。正是由于这种跨越式,使得元素每次移动的间距变大了,而不像冒泡排序那样一格一格地“爬”。快速排序是一次跨多格,所以总的移动次数就比冒泡排序少很多。

但是快速排序也有一个问题,就是递归深度的问题。调用函数要消耗资源,递归需要系统堆栈,所以递归的空间消耗要比非递归的空间消耗大很多。而且如果递归太深的话会导致堆栈溢出,系统会“撑”不住。快速排序递归的深度取决于基准数的选择,比如下面这个序列:

5  1  9  3  7  4  8  6  2

5 正好处于 1~9 的中间,选择 5 作基准数可以平衡两边的递归深度。可如果是:

1  5  9  3  7  4  8  6  2

选择 1 作为基准数,那么递归深度就全部都加到右边了。如果右边有几万个数的话则系统直接就崩溃了。所以需要对递归深度进行优化。怎么优化呢?就是任意取三个数,一般是取序列的第一个数、中间数和最后一个数,然后选择这三个数中大小排在中间的那个数作为基准数,这样起码能确保获取的基准数不是两个极端。

前面讲过,快速排序一般用于大数据排序,即数据的个数很多的时候(不是指数的值很大)。如果是小规模的排序,就用前面讲的几种简单排序方式就行了,不必使用快速排序。

四种排序算法的比较

冒泡排序是最慢的排序算法。在实际运用中它是效率最低的算法。它通过一趟又一趟地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升。

插入排序通过将序列中的值插入一个已经排好序的序列中,直到该序列结束。插入排序是对冒泡排序的改进。它比冒泡排序快两倍。一般不用在数据的值大于 1000 的场合,或数据的个数超过 200 的序列。

选择排序在实际应用中处于与冒泡排序基本相同的地位。它们只是排序算法发展的初级阶段,在实际中使用较少。但是它们最好理解。

快速排序是大规模递归的算法,它比大部分排序算法都要快。一般用于数据个数比较多的情况。尽管可以在某些特殊的情况下写出比快速排序快的算法,但是就通常情况而言,没有比它更快的了。快速排序是递归的,对于内存非常有限的机器来说,它不是一个好的选择。



 

稳定性:假定在待排序的序列中存在多个相同的值,若经过排序后,这些值的相对次序保持不变,即在原序列中 ri=rj,且 ri 在 rj 之前,而在排序后的序列中 ri 仍在 rj 之前,则称这种排序算法是稳定的,否则称为不稳定的。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言快速排序,以及注意点。 的相关文章

随机推荐

  • 消息队列原理及选型

    什么是消息队列 消息队列 Message Queue 是一种进程间通信或同一进程的不同线程间的通信方式 什么时候需要消息队列 异步处理 例如短信通知 终端状态推送 App推送 用户注册等 有些业务不想也不需要立即处理消息 消息队列提供了异步
  • Redis主从复制架构和Sentinel哨兵机制

    一 redis主从复制原理 redis主从同步策略 slave刚加入集群会触发一次全量同步 全量复制 全量同步之后 进行增量复制 slave优先是增量同步 如果增量同步失败会尝试从master节点进行全量复制 全量复制 slave初始化阶段
  • 华为OD机试真题- 数值同化-2023年OD统一考试(B卷)

    题目描述 存在一个m n的二维数组 其成员取值范围为0 1 2 其中值为1的元素具备同化特性 每经过1S 将上下左右值为0的元素同化为1 而值为2的元素 免疫同化 将数组所有成员随机初始化为0或2 再将矩阵的 0 0 元素修改成1 在经过足
  • IDEA连接SSH以及上传项目文件到指定远程服务器文件夹

    一 IDEA连接SSH 确认你的密码输对了 还好我保存了我的远程服务器的密码 原先我一直以为我输对了 导致一直报错Auth fail 点击ok 连接上了 二 将项目文件传输到 远程服务器的指定文件夹 name随意起个名字 1 是你的本地项目
  • 什么叫异步通信?同步通信与异步通信的区别是什么?

    异步通信 又称为起止式异步通信 数据帧与数据帧之间没有固定时间间隔约定 可以是不定时长的 空闲位 异步通信是在内部约定好时钟 芯片设计设定好的时钟 用起始位开头 中间包含数据位后面随效验位和停止位的格式 我们称之为 帧 整个数据帧的位组成是
  • 【云服务器】阿里云服务器部署web项目前的准备(安装Nginx,jdk,Tomcat,MySQL)

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 目录 前言 一 首先得有一台云服务器 白嫖党可以去看看鱼皮的视频或者百度搜索如何白嫖阿里云服务器 二 安装nginx反向代理服务器 二 Jdk和Tomcat的安装 1 jdk
  • 集团服务器 系统,开创集团云服务器

    开创集团云服务器 内容精选 换一换 华为云帮助中心 为用户提供产品简介 价格说明 购买指南 用户指南 API参考 最佳实践 常见问题 视频帮助等技术文档 帮助您快速上手使用华为云服务 本节操作介绍通过华为云APP连接Linux实例的操作步骤
  • umi搭建的react项目——如何mock数据

    umi搭建的react项目 如何mock数据 1 本地配置打开 本地配置umirc local ts中的mock配置打开 export default defineConfig mock 2 配置接口 在项目配置接口的文件中添加接口 例如
  • eclipse 设置系统变量和运行参数

    在开发时 有时候可能需要根据不同的环境设置不同的系统参数 我们都知道 在使用java jar命令时可以使用 D参数来设置运行时的系统变量 同样 在Eclipse中运行java程序时 我们怎么设置该系统变量呢 另外 如果我们的程序需要输入运行
  • mysql 游标

    who 游标是什么 游标 cursor 官方定义 是系统为用户开通的一个数据缓冲区 存放sql执行结果 每个游标区都有一个名字 用户可以通过sql语句逐一从游标中获取记录 并赋值给变量 交由主语言进一步处理 个人理解 感觉游标和指针相似 指
  • 手写函数柯里化(Curry)--原理剖析

    函数柯理化的作用 前端使用柯里化的用途主要就是简化代码结构 提高系统的维护性 一个方法 只有一个参数 强制了功能的单一性 很自然就做到了功能内聚 降低耦合 函数柯理化的优点 降低代码的重复 提高代码的适用性 在后面实现应用部分 ajax 会
  • webpack打包速度优化

    优化WebPack打包速度 在开发过程中 WebPack的打包速度是一个非常重要的考虑因素 随着项目规模的增长 打包时间也会越来越长 影响开发效率和用户体验 本文将循序渐进地介绍一些优化WebPack打包速度的方法 先分析打包瓶颈 然后逐步
  • 超分(Super-Resolution, SR )

    知乎有一篇帖子总结了几篇超分的论文 写的很好 https zhuanlan zhihu com p 25532538 utm source tuicool utm medium referral 下面针对上面没有提到的论文进行补充 6 PR
  • 数据库学习笔记(2)——workbench和SQL语言

    1 workbench简介 登录客户端的两种方法 在cmd中 只能通过sql语句控制数据库 workbench其实就是一种图形化数据库管理工具 在workbench中既可以通过sql语句控制数据库 也可以通过图形化界面控制数据库 通过wor
  • ubuntu pytorch 深度学习环境配置

    目录 一 Ubuntu20 04下用ppa源安装NVIDIA显卡驱动 1 先查询适用自己电脑型号的英伟达驱动版本 官网查选 官方 GeForce 驱动程序 NVIDIA 2 禁用默认驱动 nouveau 3 打开 软件与更新 点击 附加驱动
  • 物理磁盘的四种使用方式

    一 物理磁盘整个直接使用 把整个物理磁盘直接格式话成文件系统 然后mount 二 通过分区使用 把整个物理磁盘通过fdisk dev sdx这样分区 通过格式化各个分区来使用磁盘 三 通过逻辑卷使用 可以把整个物理磁盘作为一个物理卷pvcr
  • 电子科技大学软件工程期末复习笔记(一):概论

    目录 前言 重点一览 软件的定义 软件的特点 软件的双重作用 软件危机 软件工程的概念 软件工程的目标与原则 软件工程的一些误解 本章小结 前言 2022年底疫情彻底放开 开始自愿返乡 大面积传染开始 在校生几乎无一幸免 因为自愿返乡后只能
  • The Code is successfully generatd...使用stm32cude生成工程时报错

    找了一下午的方法 在此总结 1 路径问题 路径不能包含中文以及空格字符 如E 举例 测试 以及路径不能太深 本人未测试 不是我的情况 2 金山等即时翻译软件未关闭 论坛上看到 本人未测试 3 版本问题 本人测试通过 有的人说是jdk版本太高
  • 2019年底总结

    一年很快 又到改写总结的时候了 回顾这一年 2019年办成了很多的事情 在此借用这句 忆往昔 年少轻狂时 俱远矣 看今日 而立之年始 继拼之 表达下吧 看看2018年的计划 发现大部分自己都在不自不觉中做了 时事 经济领域 用平时的碎片时间
  • C语言快速排序,以及注意点。

    快速排序尤其适用于对大数据的排序 它的高速和高效无愧于 快速 两个字 虽然说它是 最常用 的 可对于初学者而言 用它的人却非常少 因为虽然很快 但它也是逻辑最复杂 最难理解的算法 因为快速排序要用到递归和函数调用 快速排序所采用的思想是分治