十大排序 —— 快速排序

十大排序 —— 快速排序

  • 快速排序
  • 一些坑
  • 快速排序的性能
      • 优点:
      • 缺点:
      • 性能优化:

我们今天来看看十大排序中很出名的一个算法——快速排序

快速排序

快速排序(Quick Sort)是一种经典的、高效的排序算法,属于比较排序算法的一种。它的基本思想是通过分治法(Divide and Conquer)将待排序的序列分割成两个子序列,然后递归地对子序列进行排序,最后将子序列的结果合并起来得到最终的排序结果。

快速排序的具体步骤如下:

  1. 选择基准值(Pivot):从待排序序列中选择一个元素作为基准值。通常情况下,可以选择序列的第一个元素、最后一个元素或者随机选择一个元素作为基准值。
  2. 分区(Partition):将待排序序列按照基准值进行分区,将小于基准值的元素放在基准值的左边,将大于基准值的元素放在基准值的右边,基准值自身则被放在合适的位置。这一步骤可以使用双指针法来实现,也就是从序列两端分别向中间移动指针,将元素进行交换,直到两个指针相遇。
  3. 递归排序:对分区后得到的左右两个子序列分别进行递归排序。
  4. 合并结果:将左右两个已经排序好的子序列合并起来,得到最终的排序结果。

快速排序的平均时间复杂度为O(nlogn),其中n为待排序序列的长度。它是一种原地排序算法,不需要额外的辅助空间,因此空间复杂度为O(logn)。快速排序是一种非稳定排序算法,即相同元素的相对位置在排序后可能会改变。

我们一步一步来看:

我们这里有一个数组:
在这里插入图片描述
在这里插入图片描述
我们传入一个区间,我这里一般会用begin和end:
在这里插入图片描述
这步要注意,要不后面会踩坑,然后我们对传入的参数进行二次命名,命名为left,right:
在这里插入图片描述
我们left下标里的数为基准:
在这里插入图片描述

我们先从右边走,一开始,a[right] = -1比12小,但是却在12的右边,这个时候right下标停住:
在这里插入图片描述
这个时候开始往left往右边走,这个时候有个问题a[left]的值是12,我们的标准值也是12,我们该如何处理?

答案是我们直接放过,标准值我们直接最后处理,放到合适的位置上:
在这里插入图片描述
交换:
在这里插入图片描述
依次类推:
在这里插入图片描述
在这里插入图片描述
之后,right和left会走到一起:
在这里插入图片描述
这个时候处理标准值:直接交换stander和left:
在这里插入图片描述
这个时候,我们看12左边,全是比12小的,12右边全是比12大的:
在这里插入图片描述
这个时候,用同样的方法处理左区间和右区间:
在这里插入图片描述


```cpp
/**
 * 快速排序函数
 * @param array 待排序的整型数组指针
 * @param begin 数组的起始索引
 * @param end 数组的结束索引
 */
void quick_sort_part(int *array, int begin, int end) {
    int left = begin; // 初始化左指针
    int right = end;  // 初始化右指针

    // 当左指针小于右指针时继续排序
    if (left >= right) {
        return;
    }

    int stander = left; // 选择数组起始位置的元素作为基准值

    // 外层循环:直到左右指针相遇
    while (left < right) {
        // 移动右指针,直到找到一个小于等于基准值的元素或右指针到达左指针
        while (right > left && array[stander] <= array[right]) {
            right--;
        }

        // 移动左指针,直到找到一个大于等于基准值的元素或左指针到达右指针
        while (left < right && array[stander] >= array[left]) {
            left++;
        }

        // 如果左右指针未相遇,则交换这两个元素的位置
        if (left < right) {
            swap(array[left], array[right]);
        }
    }

    // 将基准值放到最终位置(左右指针相遇的位置)
    swap(array[stander], array[left]);

    // 对基准值左边的子数组进行快速排序
    quick_sort_part(array, begin, left - 1);

    // 对基准值右边的子数组进行快速排序
    quick_sort_part(array, left + 1, end);
}

这段注释解释了每个关键步骤的目的和逻辑,有助于理解快速排序算法的实现细节。

一些坑

如果有些同志第一次接触这个算法,可能会写出这样的代码:

	//快速排序
void quick_sort_part(int* array, int left, int right)
{

	if (left >= right)
	{
		return;
	}

	int stander = left;

	while (left < right)
	{
		while (right > left && array[stander] <= array[right])
		{
			right--;
		}

		while (left < right && array[stander] >= array[left])
		{
			left++;
		}

		if (left < right)
		{
			swap(array[left], array[right]);
		}
	}

	swap(array[stander], array[left]);

	quick_sort_part(array, 0, left - 1);

	quick_sort_part(array, left + 1, right);
}

然后发现:
在这里插入图片描述
咋排序还只排一半呀?右半为啥不排了?

仔细,想想这份代码我们是直接拿来就对right大张旗鼓修改:
在这里插入图片描述
所以执行到quick_sort_part(array, left + 1, right)时,right被污染了,自然不会排了,正确的做法是备份一份,这就是为什么我取了两个不同的别名:right,end:
在这里插入图片描述

快速排序的性能

快速排序是一种非常高效的排序算法,在平均和最好的情况下具有O(n log n)的时间复杂度,其中n是数组中的元素数量。这意味着对于大规模数据集,它的性能通常优于O(n^2)的排序算法,如冒泡排序或选择排序。

优点:

  1. 效率高:在大多数实际情况下,快速排序都非常快,因为它的内部循环可以在大多数架构上高效执行。
  2. 原地排序:除了递归调用栈外,快速排序不需要额外的存储空间,是一种原地排序算法。
  3. cache友好:由于其分区操作,快速排序往往能很好地利用CPU缓存,提高执行效率。

缺点:

  1. 最坏情况性能:在最坏的情况下,即输入数组已经是正序或逆序时,快速排序退化为O(n^2)的时间复杂度。但这种情况可以通过随机选取或“三数取中”等策略来避免。
  2. 非稳定排序:快速排序不是稳定的排序算法,即相等的元素可能会因为排序而改变它们的原始相对位置。
  3. 递归开销:快速排序使用递归来排序子数组,当深度很大时,递归调用栈可能会消耗大量内存,特别是对于极大数组。针对此,可以采用尾递归优化或者迭代的方式来减少递归开销。

性能优化:

  • 尾递归优化:在某些递归调用中,可以直接修改参数后调用自身,避免栈的额外增长。
  • 小数组时切换到插入排序:对于小数组,快速排序的优势不明显,此时可以切换到插入排序,因为插入排序在小数组上的效率更高。
  • 三数取中法:选择基准时,从首、中、尾三个元素中选择中位数作为基准,可以有效避免最坏情况的发生。
  • 非递归实现:使用栈来模拟递归调用,减少递归深度,防止栈溢出。

这里实现一下三数取中:

#include<algorithm> // 包含 std::swap 函数

// 三数取中法选择基准值索引
int GetIndex(int* a, int left, int right) {
    int mid = (left - right) / 2 + right; // 计算中间索引
    // 比较三个元素的大小,返回中间值的索引
    if (a[mid] < a[left]) {
        if (a[right] < a[mid]) {
            return mid;
        } else if (a[left] < a[right]) {
            return left;
        } else {
            return right;
        }
    } else {
        if (a[left] < a[mid]) {
            return mid;
        } else if (a[right] < a[left]) {
            return right;
        } else {
            return left;
        }
    }
}

// 快速排序的分区函数
void quick_sort_part(int *array, int begin, int end) {
    int left = begin;
    int right = end;

    if (left >= right) {
        return; // 如果左边界大于等于右边界,直接返回
    }

    int pivot = GetIndex(array, left, right); // 使用三数取中法选择基准值索引
    std::swap(array[pivot], array[left]); // 将基准值与左边界交换,确保基准值在正确的位置

    while (left< right) {
        while (right > left && array[left] <= array[right]) {
            right--; // 从右向左找到第一个小于基准值的元素
        }

        while (left< right && array[left] >= array[right]) {
            left++; // 从左向右找到第一个大于基准值的元素
        }

        if (left< right) {
            std::swap(array[left], array[right]); // 交换这两个元素
        }
    }

    std::swap(array[left], array[begin]); // 将基准值放到正确的位置

    quick_sort_part(array, begin, left - 1); // 对左侧子数组递归排序
    quick_sort_part(array, left + 1, end); // 对右侧子数组递归排序
}

如果嫌弃空间复杂度太高,我们还可以用迭代的方式实现:

// 分区函数,选取最后一个元素作为基准,将数组分为两部分,返回基准元素的最终位置
int partition(int* array, int left, int right) {
    // 选择数组最右侧的元素作为基准值
    int pivot = array[right];
    // i 初始化为左侧索引减一,用于记录比基准值小的元素的最终位置
    int i = left - 1;

    // 遍历数组,从左侧开始到基准值的前一个位置
    for (int j = left; j < right; ++j) {
        // 如果当前元素不大于基准值(包括等于),则交换到左侧区域,并移动i指针
        if (array[j] <= pivot) {
            ++i;
            swap(array[i], array[j]); // 交换元素位置
        }
    }

    // 将基准值放到中间,i+1的位置即为基准值的正确位置
    swap(array[i + 1], array[right]);
    // 返回基准值的索引位置
    return i + 1;
}

// 非递归实现快速排序的函数
void quick_sort_iterative(int* array, int size) {
    // 如果数组长度小于等于1,无需排序,直接返回
    if (size <= 1) {
        return;
    }

    // 使用栈来模拟递归调用的函数栈
    stack<int> stack;

    // 初始时,将整个数组的左右边界压入栈
    stack.push(0);
    stack.push(size - 1);

    // 当栈非空时,持续进行排序
    while (!stack.empty()) {
        // 先弹出右边界,再弹出左边界
        int end = stack.top(); stack.pop();
        int begin = stack.top(); stack.pop();

        // 对当前区间进行分区,并获取基准元素的索引
        int pivotIndex = partition(array, begin, end);

        // 如果基准值左侧还有未排序的元素,则将左侧区间压入栈
        if (pivotIndex - 1 > begin) {
            stack.push(begin);
            stack.push(pivotIndex - 1);
        }

        // 如果基准值右侧还有未排序的元素,则将右侧区间压入栈
        if (pivotIndex + 1 < end) {
            stack.push(pivotIndex + 1);
            stack.push(end);
        }
    }
}

int main() {
    int array[] = { 12,34,14,7,2,100,4,5,1,1000,888,0,221,2,-1 };
    int size = sizeof(array) / sizeof(array[0]);

    quick_sort_iterative(array, size);

    // Print sorted array
    for (int i = 0; i < size; ++i) {
        cout << array[i] << " ";
    }
    cout << endl;

    return 0;
}

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

centos8stream 编译安装 php-rabbit-mq模块

官方GitHub&#xff1a;https://github.com/php-amqp/php-amqp 环境依赖安装 dnf install cmake make -y 1.安装rabbitmq-c cd /usr/local/src/ wget https://github.com/alanxz/rabbitmq-c/archive/refs/tags/v0.14.0.tar.gz tar xvf v0.14.0.tar.gz cd rabbitmq-c-0.14.0/…

Linux下多线程的相关概念

&#x1f916;个人主页&#xff1a;晚风相伴-CSDN博客 &#x1f496;如果觉得内容对你有帮助的话&#xff0c;还请给博主一键三连&#xff08;点赞&#x1f49c;、收藏&#x1f9e1;、关注&#x1f49a;&#xff09;吧 &#x1f64f;如果内容有误或者有写的不好的地方的话&…

php反序列化入门

一&#xff0c;php面向对象。 1.面向对象&#xff1a; 以“对象”伪中心的编程思想&#xff0c;把要解决的问题分解成对象&#xff0c;简单理解为套用模版&#xff0c;注重结果。 2.面向过程&#xff1a; 以“整体事件”为中心的编程思想&#xff0c;把解决问题的步骤分析出…

实时监控电脑屏幕的软件是什么?三款超受欢迎的电脑监控软件

实时监控电脑屏幕的软件在现代企业管理中扮演着至关重要的角色&#xff0c;它们不仅帮助管理者实时监控员工的工作状态&#xff0c;提高工作效率&#xff0c;还通过数据分析和报告功能&#xff0c;为企业提供了优化管理流程和决策支持的依据。以下将介绍几款市面上广泛使用的实…

Redis过期策略数据淘汰策略

过期策略 一、设置过期时间 redis有四种命令可以用于设置键的生存时间和过期时间&#xff1a; EXPIRE : 将键的生存时间设为 ttl 秒 PEXPIRE :将键的生存时间设为 ttl 毫秒 EXPIREAT :将键的过期时间设为 timestamp 所指定的秒数时间戳 PEXPIREAT : 将键的过期时间设为 times…

GNU Radio创建qt time plot python OOT块

文章目录 前言一、创建自定义的 OOT 块1、安装相应依赖2、创建 OOT 块3、修改相关4、编译及安装 OOT 块 二、测试1、grc 图2、运行结果 三、资源自取 前言 官方提供的绘制时域波形的 block 名字叫做 QT GUI Time Sink&#xff0c;其底层实现是用 C 写的&#xff0c;但是我发现…

virtualbox中ubuntu22.04网络配置

第一&#xff1a;添加两个网卡&#xff0c;网卡1是NAT方式&#xff0c;网卡2是仅主机模式&#xff08;两个顺序不能颠倒&#xff09; 第二步&#xff1a;启动ifconfig查看网络

『 Linux 』文件系统

文章目录 磁盘构造磁盘抽象化 磁盘的寻址方式磁盘控制器磁盘数据传输文件系统Inode数据块(Data Blocks)超级块(SuperBlock)块组描述符(Group Descriptor) 磁盘构造 磁盘内部构造由磁头臂,磁头,主轴,盘片,盘面,磁道,柱面,扇区构成; 磁头臂&#xff1a;控制磁头的移动,可以精确地…

Exce 两列一组对齐呈现,缺失补 0

Excel 里有 多 组数据&#xff0c;每组 2 列&#xff0c;每组长度不同。第 1 列是编号&#xff0c;列之间的编号有重复。 ABCDEFGH1Mass10Mass11Mass12Mass132802200581309088146532802225938133306824779282975598142002482273148413154988335698822331305832720485110460842…

go解析yaml

go解析yaml文件关键就是结构体的创建 初学go tag字段要和yaml文件中的key对应起来&#xff0c;每个层级都要创建对应的结构体&#xff0c;有点烦 package configimport ("gopkg.in/yaml.v3""os" )type Config struct {MysqlConfig MysqlConfig yaml:&q…

Spring Boot 开发 -- 过滤器与拦截器详解

引言 在Web开发中&#xff0c;经常需要对请求进行预处理或在响应后进行后处理&#xff0c;Spring Boot提供了过滤器和拦截器两种机制来实现这一需求。虽然它们都可以用来处理HTTP请求和响应&#xff0c;但在使用场景、执行顺序和配置方式上存在明显的差异。本文将详细讲解Spri…

【UML用户指南】-01-UML基本元素的介绍(一)

1、UML的词汇表 &#xff08;1&#xff09;事物&#xff1b; &#xff08;2&#xff09;关系&#xff1b; &#xff08;3&#xff09;图。 事物是对模型中首要成分的抽象&#xff1b;关系把事物结合在一起&#xff1b;图聚集了相关的事物。 注&#xff1a;事物也称为元素 2…

东芝机械人电池低报警解除与机器人多旋转数据清零

今天启动一台设备&#xff0c;触摸屏一直显示机器人报警&#xff08;翻译过后为电池电量低&#xff09;&#xff0c;更换电池后关机重启后也不能消除&#xff0c;所以打开示教器&#xff0c;下面就来说说怎么解决此项问题&#xff08;可以参考官方发的手册&#xff0c;已手册为…

携程梁建章:持续投资创新与AI,开启旅游行业未来增长

5月30至31日&#xff0c;携程集团在上海和张家界举办Envision 2024全球合作伙伴大会&#xff0c;邀请超50个国家和地区的1600余名外籍旅游业嘉宾与会&#xff0c;共同探讨中国跨境旅游市场发展机遇&#xff0c;讲好中国故事。 携程国际业务增速迅猛&#xff0c;创新与AI解锁未…

IntelliJ IDEA / Android Studio 方法显示Git提交人

显示方法&#xff1a; 设置 > 编辑器 > 嵌入提示 > Code Vision > 代码作者&#xff08;勾选&#xff09; IntelliJ IDEA Android Studio

css-表头筛选的特定样式

背景 饿了么的表头筛选样式比较简单&#xff0c;如图1&#xff0c;产品觉得不够醒目&#xff08;觉得用户可能不知道这是筛选&#xff0c;我表示不理解&#xff09; 要求改进筛选的样式&#xff0c;达到图2的效果&#xff0c;主要是状态列&#xff0c;既希望这列的宽度固定&a…

git应用最佳实践

插&#xff1a; AI时代&#xff0c;程序员或多或少要了解些人工智能&#xff0c;前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家(前言 – 人工智能教程 ) 坚持不懈&#xff0c;越努力越幸运&#xff0c;大家…

Linux内网中安装jdk1.8详细教程

本章教程,主要介绍如何在内网环境中配置JDK1.8环境变量 一、下载Linux版压缩包 下载地址:https://www.oracle.com/java/technologies/downloads/#java8 下载完成之后,通过XFTP等工具,将安装包上传到内网服务器 二、安装配置步骤 1、解压压缩包 tar -zxvf /usr/local/jdk-…

jpeg编码学习

正点原子stm32教程提到过jpeg解码库libjpeg&#xff0c;但是没有提到jpeg编码&#xff0c;我也好奇jpeg编码怎么实现&#xff0c;用代码怎么生成jpeg文件的。所以最近学习了jpeg编码&#xff0c;在这里做记录。 参考文章 jpeg图片格式详解 https://blog.csdn.net/yun_hen/art…

【嵌入式DIY实例】-OLED显示网络时钟

OLED显示网络时钟 文章目录 OLED显示网络时钟1、硬件准备与接线2、代码实现在上一个ESP8266 NodeMCU文章中,我们用DS3231 RTC芯片和SSD1306 OLED制作了一个简单的实时时钟,时间和日期显示在SSD1306屏幕上,并且可以通过两个按钮进行设置。 在本中,我们将使用ESP 8266 NodeMC…