算法实现 - 快速排序(Quick Sort) - 理解版

文章目录

  • 算法介绍
  • 算法分析
    • 核心思想
    • 三个版本运行过程
      • 挖坑法
      • Hoare 原版
      • 前后指针法
  • 算法稳定性和复杂度

算法介绍

快速排序是一种高效的排序算法,由英国计算机科学家C. A. R. Hoare在1960年提出,是对冒泡排序算法的一种改进。

该算法采用 分治策略,其基本思想:
选择一个基准元素(pivot),通过基准元素将待排序的序列分成两部分,一部分包含所有小于基准元素的元素,另一部分包含所有大于基准元素的元素,然后递归地对这两部分进行快速排序,最终使整个数组成为一个有序的序列。

算法分析

核心思想

  1. 选择基准:从数组中选择一个元素作为基准(pivot)。通常可以选择第一个元素、最后一个元素、随机元素或中间元素。
  2. 分区操作
    • 遍历数组,将小于基准的元素移动到基准左侧,大于基准的元素移动到右侧。这样基准元素就会被放置到其正确的排序位置。
    • 此步骤会让数组分成了两部分:左侧(小于基准)和右侧(大于基准)。
  3. 递归排序
    • 对于基准位置左侧和右侧的两个子数组,分别递归地运用快速排序。
    • 递归结束的条件是子数组的长度小于或等于 1,此时它们已经有序。
  4. 合并结果:合并已经排序好的子数组与基准,得到一个完整的有序数组。

三个版本运行过程

以数列 a[] = {40, 20, 30, 60, 10, 50} 作为分析案例;

挖坑法

在这里插入图片描述

分析:

x 取索引为0 的最左值 为 40,即x = 40。

  • 从"右 --> 左"查找小于x的数: 找到满足条件的数a[j]=10,此时 j=4,i=0;然后将a[j]赋值a[i],即将 a[4] 赋值给 a[0];接着从左往右遍历。
  • 从"左 --> 右"查找大于x的数: 找到满足条件的数a[i]=60,此时 i=3,j=4;然后将a[i]赋值a[j],即将 a[3] 赋值给 a[4];接着从右往左遍历。
  • 从"右 --> 左"查找小于x的数: 没有找到满足条件的数。当i>=j时,停止查找;然后将x赋值给a[i],即将 40 赋值给 a[3]。此趟遍历结束!

代码:

#include <stdio.h>/*** Quick sort by C Lang.** params:*   arr -- the data array*   left -- the first index of arr*   right -- the last index of arr**/
void quickSort(int arr[], int left, int right)
{// the arr is empty or left one elementif (left >= right){return;}// init dataint pivot = arr[left];int i = left, j = right;while (i < j){// right -> left: find the first element less than pivot// notice: when arr[j] meet the condition `arr[j] < pivot`, it will break the while loop, indicate that current element is less than pivot.while (i < j && arr[j] > pivot)j--;if (i < j){arr[i++] = arr[j];}// left -> right: find the first element great than pivotwhile (i < j && arr[i] < pivot)i++;if (i < j){arr[j--] = arr[i];}arr[i] = pivot; // The element has benn correctly placed in its corresponding positionquickSort(arr, left, i - 1);  // Recursive invoke in the left sub arrayquickSort(arr, i + 1, right); // Recursive invoke in the right sub array}
}/*** show array*/
void printArray(int array[], int n)
{for (size_t i = 0; i < n; i++){printf("%d ", array[i]);}printf("\n\n");
}int main()
{int arr[] = {40, 20, 30, 60, 10, 50};// calculate the length of a arrayint n = sizeof(arr) / sizeof(arr[0]);printf("before sort: \n");printArray(arr, n);// The third param must be `n -1`quickSort(arr, 0, n - 1);printf("after sort: \n");printArray(arr, n);return 0;
}

代码详见:GitHub 快速排序法 挖坑版

Hoare 原版

分析:

  • 选择基准值:快速排序的第一步是选择一个基准值(pivot)。通常可以选择数组的第一个元素,但为了提高排序效率,有时也会选择随机元素或使用其他策略。
  • 划分过程:使用两个指针,左指针和右指针,分别从数组的两端开始向中间移动。
  • 具体步骤:
    1. 左指针移动:左指针从左向右移动,直到找到一个大于或等于基准值的元素。
    2. 右指针移动:右指针从右向左移动,直到找到一个小于或等于基准值的元素。
    3. 交换元素:如果左指针指向的元素大于基准值,且右指针指向的元素小于基准值,则交换这两个元素。
    4. 继续移动指针:继续移动左右指针,直到它们相遇。
    5. 交换基准值:最后,将基准值与右指针指向的元素交换,确保基准值左侧的所有元素都不大于基准值,右侧的所有元素都不小于基准值。

代码:

#include <stdio.h>/*** swap two number*/
void swap(int *a, int *b)
{int temp = *a;*a = *b;*b = temp;
}/*** Quick sort by C Lang.** params:*   arr -- the data array*   left -- the first index of arr*   right -- the last index of arr**/
int partition(int arr[], int left, int right)
{// init dataint pivotIndex = left;int i = left, j = right;while (i < j){// right -> left: find the first element less than pivotwhile (i < j && arr[j] > arr[pivotIndex]){j--;}// left -> right: find the first element great than pivotwhile (i < j && arr[i] < arr[pivotIndex]){i++;}// after upper two loop, indicate that it is able to swapif (i < j)swap(&arr[i], &arr[j]);}// Swap the pivot with the element pointed by the right pointerswap(&arr[pivotIndex], &arr[j]);return j;
}void quickSort(int arr[], int left, int right)
{if (left < right){int pivotIndex = partition(arr, left, right);quickSort(arr, left, pivotIndex - 1);quickSort(arr, pivotIndex + 1, right);}
}/*** show array*/
void printArray(int array[], int n)
{for (size_t i = 0; i < n; i++){printf("%d ", array[i]);}printf("\n\n");
}int main()
{int arr[] = {40, 20, 30, 60, 10, 50};// calculate the length of a arrayint n = sizeof(arr) / sizeof(arr[0]);printf("before sort: \n");printArray(arr, n);// The third param must be `n -1`quickSort(arr, 0, n - 1);printf("after sort: \n");printArray(arr, n);return 0;
}

代码详见:GitHub 快速排序法 hoare版

前后指针法

前后指针法(也称为荷兰国旗问题解法)是快速排序中的一种高效实现方式。在这个方法中,通常将数组划分为三个部分:

  • 小于基准值的元素
  • 等于基准值的元素
  • 大于基准值的元素。

分析:

  • 变量初始化:

    • key 是基准元素的索引,初始为 left。
    • prev 是逻辑分区点之前的最后一个小于基准的元素的索引,开始等于 left。
    • cur 是当前遍历的元素索引,从 left + 1 开始。
  • 分区逻辑:

    • 使用 while 循环遍历数组,从 cur = left + 1 到 right。
    • 如果 a[cur] < a[key],则 prev 增加(指向下一个元素),并交换 a[prev] 和 a[cur],确保小于基准的元素移动到前面。
    • 即使 prev 和 cur 相等,也进行交换,这一步是为了简化逻辑,即不需要特判 prev 和 cur 相等的情况。实际上在交换时什么也不会变。
  • 基准交换:
    循环结束后,将基准值与 a[prev] 交换,把基准放置在正确的位置。

代码

#include <stdio.h>/*** swap two number*/
void swap(int* a, int* b) {int temp = *a;*a = *b;*b = temp;
}/*** Quick sort by C Lang.** params:*   arr -- the data array*   left -- the first index of arr*   right -- the last index of arr**/
int partition(int arr[], int left, int right) {int pivotIndex = left;int prev = left;int cur = left + 1;while (cur <= right) {if (arr[cur] < arr[pivotIndex] && ++prev != cur) {swap(&arr[prev], &arr[cur]);}cur++;}swap(&arr[prev], &arr[pivotIndex]);return prev;
}void quickSort(int arr[], int left, int right) {if (left < right) {int pivot_index = partition(arr, left, right);quickSort(arr, left, pivot_index - 1);quickSort(arr, pivot_index + 1, right);}
}void print_array(int arr[], int size) {for (int i = 0; i < size; i++) {printf("%d ", arr[i]);}printf("\n");
}int main() {int arr[] = {30, 40, 60, 10, 20, 50};int n = sizeof(arr) / sizeof(arr[0]);printf("Before sorting:\n");print_array(arr, n);quickSort(arr, 0, n - 1);printf("After sorting:\n");print_array(arr, n);return 0;
}

代码详见:GitHub 快速排序法 前后指针版

算法稳定性和复杂度

稳定性

这一点是因为在分区过程中,元素的相对顺序可能会改变。

具体来说,当我们交换元素以确保小于基准的元素在前,大于基准的在后时,相同元素的相对位置可能改变。

例如,如果两个相等的元素分别在基准的两侧,交换后它们的位置关系就被打破了。

举个例子:

示例数组:[ 3 a 3_a 3a, 1, 2, 3 b 3_b 3b]
这里的 3 a 3_a 3a, 3 b 3_b 3b 是两个不同位置的相同元素(相同的值但用下标区分,用于说明其在数组中的初始位置)。
根据实现可能不交换,但假设实现中等于基准的情况下它也移动,这里交换 3 b 3_b 3b 3 a 3_a 3a得到数组 [ 3 b 3_b 3b, 1, 2, 3 a 3_a 3a],顺序已经发生了变更。

时间复杂度

平均情况O(nlogn)

基本操作
快速排序的基本操作是划分和递归调用。

操作次数

  • 划分操作:每次划分操作将数组分为两部分。在平均情况下,我们假设每次划分都能将数组大致分为两个相等的部分。这意味着每次划分操作都会遍历整个数组一次,操作次数为 n。
  • 递归操作:由于每次划分都将数组分为两个大致相等的部分,所以每次递归调用处理的数组大小大约是前一次的一半。这个过程会一直持续到子数组的大小为1,此时不需要再进行划分。

对于大小为 n 的数组,快速排序的递归关系可以表示为:
T ( n ) = 2 T ( n 2 ) + n T(n) = 2T\left(\frac{n}{2}\right) + n T(n)=2T(2n)+n

  • T(n)是对大小为 n 的数组进行排序所需的总操作次数。
  • T( n 2 \frac{n}{2} 2n)表示对两个大小为 n 2 \frac{n}{2} 2n的子数组进行排序所需的操作次数。
  • n 是当前划分操作所需的总比较次数。

大O表示法
这种形式可以用主定理求解,符合主定理的标准形式,所以复杂度为:O(n log n)

分治算法主定理,点击看我的另外一篇文章

最差情况O( n 2 n^2 n2)

基本操作
快速排序的基本操作是划分和递归调用。

操作次数

在最坏情况下,每次划分操作只能将数组分成一个大小为 n−1 的部分和一个大小为 0 的部分(例如,当数组已经有序或每次都选择最大或最小元素作为基准时)。在这种情况下,递归关系式变为:

T ( n ) = T ( n − 1 ) + n T(n) = T(n - 1) + n T(n)=T(n1)+n

  • T(n - 1)是对大小为 n−1 的子数组进行排序所需的操作次数。
  • n 是当前划分操作所需的总比较次数。

上述等价于
T ( n ) = n + ( n − 1 ) + ( n − 2 ) + ( n − 3 ) + . . . + 2 + 1 = ( n + 1 ) n 2 = n 2 2 + n 2 T(n)= n + (n-1) + (n-2) + (n-3) + ... + 2 + 1 = \frac{(n + 1)n}{2} = \frac{n^2}{2} + \frac{n}{2} T(n)=n+(n1)+(n2)+(n3)+...+2+1=2(n+1)n=2n2+2n

大O表示法
上述用大O表示法为:O( n 2 n^2 n2)

空间复杂度

快速排序是一种递归算法。每次递归调用时,程序会在栈上保存当前调用的状态信息:

  • 平均情况下:
    • 快速排序以较好的概率实现了平均分区,递归调用的深度大约是树的高度,即 O(logn)。
    • 每次递归在栈上保存的信息量是常数级别的,例如数组的起始和结束索引、基准元素信息等。
  • 最坏情况下:
    • 如果每次选择的基准是最小或最大元素,导致极端不平衡的分区,递归深度达到 O(n)。
    • 这种情况会在已经排序的数组或逆序数组上发生。

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

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

相关文章

算法【Java】—— 动态规划之斐波那契数列模型

动态规划 动态规划的思路一共有五个步骤&#xff1a; 状态表示&#xff1a;由经验和题目要求得出&#xff0c;这个确实有点抽象&#xff0c;下面的题目会带大家慢慢感受状态标识状态转移方程初始化&#xff1a;避免越界访问 dp 表&#xff0c;所以在进行填表之前我们要预先填…

SpringBoot学生请假系统:从零到一的构建过程

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

json-server的使用(根据json数据一键生成接口)

一.使用目的 在前端开发初期&#xff0c;后端 API 可能还未完成&#xff0c;json-server 可以快速创建模拟的 RESTful API&#xff0c;帮助前端开发者进行开发和测试。 二.安装 npm install json-server //局部安装npm i json-server -g //全局安装 三.使用教程 1.准备一…

【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型

一、介绍 车辆车型识别&#xff0c;使用Python作为主要编程语言&#xff0c;通过收集多种车辆车型图像数据集&#xff0c;然后基于TensorFlow搭建卷积网络算法模型&#xff0c;并对数据集进行训练&#xff0c;最后得到一个识别精度较高的模型文件。再基于Django搭建web网页端操…

【Redis】浅析Redis大Key

目录 1、什么是Redis大Key 2、大 Key 是怎么产生的 3、大 Key 导致的问题 4、如何快速找到 Redis 大 Key 5、大 Key 优化策略 6、总结 我们在使用 Redis 的过程中&#xff0c;如果未能及时发现并处理 Big keys&#xff08;下文称为“大Key”&#xff09;&#xff0c;可能…

Rocky DEM tutorial3_Vibrating Screen_振荡筛

tutorial3_Vibrating Screen_振荡筛 文章目录 tutorial3_Vibrating Screen_振荡筛0. 目的1. 模型介绍2. 模型设置2.1 Physics设置2.2 导入几何2.3 创建一个进口的几何面2.4 定义运动 Motion frame2.5 材料设置&#xff0c;保持默认即可2.6 设置材料间的相互作用 materials inte…

小林渗透入门:burpsuite+proxifier抓取小程序流量

目录 前提&#xff1a; 代理&#xff1a; proxifier&#xff1a; 步骤&#xff1a; bp证书安装 bp设置代理端口&#xff1a; proxifier设置规则&#xff1a; proxifier应用规则&#xff1a; 结果&#xff1a; 前提&#xff1a; 在介绍这两个工具具体实现方法之前&#xff0…

阿里云-防火墙设置不当导致ssh无法连接

今天学网络编程的时候&#xff0c;看见有陌生ip连接&#xff0c;所以打开了防火墙禁止除本机之外的其他ip连接&#xff1a; 但是当我再次用ssh的时候&#xff0c;连不上了才发现大事不妙。 折腾了半天&#xff0c;发现阿里云上可以在线向服务器发送命令&#xff0c;所以赶紧把2…

深度学习基础(2024-11-02更新到图像尺寸变换 与 裁剪)

1. 名词解释 FFN FFN &#xff1a; Feedforward Neural Network&#xff0c;前馈神经网络馈神经网络是一种基本的神经网络架构&#xff0c;也称为多层感知器&#xff08;Multilayer Perceptron&#xff0c;MLP&#xff09;FFN 一般主要是包括多个全连接层(FC)的网络&#xff…

【初阶数据结构篇】链式结构二叉树(二叉链)的实现(感受递归暴力美学)

文章目录 须知 &#x1f4ac; 欢迎讨论&#xff1a;如果你在学习过程中有任何问题或想法&#xff0c;欢迎在评论区留言&#xff0c;我们一起交流学习。你的支持是我继续创作的动力&#xff01; &#x1f44d; 点赞、收藏与分享&#xff1a;觉得这篇文章对你有帮助吗&#xff1…

2024年第六届全球校园人工智能算法精英大赛——【算法挑战赛】钢材表面缺陷检测与分割 比赛复盘

引言 钢材表面缺陷检测在钢铁生产中是确保质量的关键环节&#xff0c;传统的人工检测方式难以满足大 规模工业生产的需求。近年来&#xff0c;基于深度学习的缺陷检测方法因其高效性和准确性受到广泛关 注。然而&#xff0c;现有的深度学习模型如U-Net虽具备较好的分割性能&am…

【网络】自定义协议——序列化和反序列化

> 作者&#xff1a;დ旧言~ > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;了解什么是序列化和分序列&#xff0c;并且自己能手撕网络版的计算器。 > 毒鸡汤&#xff1a;有些事情&#xff0c;总是不明白&#xff0c;所以我不…

Darknet 连接教程

本篇文章仅供学习&#xff0c;严禁用于非法用途。 1&#xff0c;前言&#xff1a; 首先明确一点&#xff0c;Darknet真没那么神奇&#xff0c;虽然有些技术文章的确很有水平&#xff0c;对于前端学习&#xff0c;软件开发以及PHP和一些服务器端维护都有许多文章&#xff0c;但…

Windows密码的网络认证---基于挑战响应认证的NTLM协议

一&#xff0c;网络认证NTLM协议简介 在平时的测试中&#xff0c;经常会碰到处于工作组的计算机&#xff0c;处于工作组的计算机之间是无法建立一个可信的信托机构的&#xff0c;只能是点对点进行信息的传输。 举个例子就是&#xff0c;主机A想要访问主机B上的资源&#xff0c;…

北斗有源终端|智能5G单北斗终端|单兵|单北斗|手持机

在当今科技日新月异的时代&#xff0c;智能设备的创新与升级速度令人目不暇接。其中&#xff0c;智能5G终端作为连接数字世界的桥梁&#xff0c;正逐步渗透到我们生活的方方面面。今天&#xff0c;让我们聚焦于一款集尖端科技与实用功能于一身的智能5G设备——QM-L5智能5G单北斗…

如何对数据库的表字段加密解密处理?

对于表格数据的加密处理&#xff0c;通常涉及到对数据库中存储的数据进行加密&#xff0c;以保护敏感信息。 Java示例&#xff08;使用AES算法加密数据库表数据&#xff09; 首先&#xff0c;你需要一个数据库连接&#xff0c;这里假设你使用的是JDBC连接MySQL数据库。以下是…

【AI+教育】一些记录@2024.11.04

一、尝新 今天尝试了使用九章随时问&#xff0c;起因是看到快刀青衣的AI产品好用榜&#xff0c;里面这么介绍九章随时问&#xff1a;「它不是像其他产品那样&#xff0c;直接给你出答案。而是跟你语音对话&#xff0c;你会感觉更像是有一位老师坐在你的旁边&#xff0c;一步步…

DNS域名解析实验

准备工作 [rootlocalhost ~]# setenforce 0 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# mount /dev/sr0 /mnt [rootlocalhost ~]# dnf install bind -y DNS正向解析&#xff1a; 对主配置文件进行修改 [rootlocalhost ~]# vim /etc/named.conf 正向解析…

Jmeter参数化的4种方法 你get了吗?

1. 用Jmeter中的函数获取参数值 __Random&#xff0c;__threadNum&#xff0c;__CSVRead&#xff0c;__StringFromFile&#xff0c;具体调用方法如下&#xff1a; KaTeX parse error: Expected group after _ at position 2: {_̲_Random(,,)}&#xff0c;&#xff0c;KaTeX p…

C语言 运算符

时间&#xff1a;2024.11.4 一、学习内容 1、算数运算符&#xff08;加、减、乘、除法和取余&#xff09; 通用细节&#xff1a; 1.整数计算&#xff0c;结果一定是一个整数 2.小数计算&#xff0c;结果一定是一个小数 3.整数和小数计算&#xff0c;结果一定是一…