news 2026/2/28 16:36:57

数据结构——六十、快速排序(王道408)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据结构——六十、快速排序(王道408)

文章目录

  • 前言
  • 一.思想
  • 二.具体例子
  • 三.代码实现
    • 1.具体代码
    • 2.代码运行过程
  • 四.算法效率分析
    • 1.时间复杂度
      • 1.基本分析
      • 2.进一步分析
      • 3.总结
    • 2.空间复杂度
    • 3.稳定性
    • 4.适用性
  • 五.快速排序的优化
    • 1.思路
    • 2.随机选取基准代码实现
    • 3.三数取中法代码实现
  • 六.知识回顾与重要考点
  • 结语

前言

快速排序是一种基于交换的排序算法,通过分治策略将待排序序列划分为两部分。算法首先选取基准元素(通常为首元素),使用双指针low和high从序列两端向中间扫描,确保low指针左侧元素均小于基准,high指针右侧元素均大于等于基准。通过不断交换元素位置,最终将基准元素放置到正确位置,完成一次划分。然后递归地对左右子序列重复上述过程,直至所有元素有序。示例中展示了以49为基准的一次完整划分过程,通过双指针交替移动和元素交换实现排序。该算法平均时间复杂度为O(nlogn),是一种高效的排序方法。

基于“交换”的排序:根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置

一.思想

  • 算法思想:在待排序表L [ 1... n ] L[1 ... n]L[1...n]中任取一个元素pivot \text{pivot}pivot作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分L [ 1... k − 1 ] L[1 ... k-1]L[1...k1]L [ k + 1... n ] L[k+1 ... n]L[k+1...n],使得L [ 1... k − 1 ] L[1 ... k-1]L[1...k1]中的所有元素小于pivot, L [ k + 1... n ] \text{pivot},L[k+1 ... n]pivotL[k+1...n]中的所有元素大于等于pivot \text{pivot}pivot,则pivot \text{pivot}pivot放在了其最终位置L(k)上,这个过程称为一次“划分”。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。

二.具体例子

  • 增序
  1. 我们用low和high两个指针分别指向此时我们要处理的这个序列的头和尾两个位置
  2. 然后我们选择low所指向的这个元素让它作为所谓的基准元素(枢轴元素)
  3. 接下来我们会让low和high这两个指针开始往中间移动,把所有的这些带排序的元素都给扫描一遍,在整个扫描的过程当中,我们需要保证high所指的这个指针的右边都是大于等于当前的基准元素49,而low指针它的左边,我们要保证都是小于49的元素
    1. 由于此时low所指的这个位置是空的,所以我们先让high向左移动,那high当前所指的元素49 ≥ 49,所以下标为7的这个元素并不需要移动,然后high–
    2. 此时high所指的这个元素是要小于49的,由于所有小于49的元素都应该放到low指针它所指的位置的左边,所以我们可以把27这个元素,让它移动到low所指的这个位置
    3. 现在high所指的这个位置空出来了,那接下来我们让low指针往右移动,此时low所指的位置是要小于49的,所以不需要把它移到右边,接下来让low指针右移
    4. 当前low指向的这个元素同样小于49,不需要把它移到右边,接下来让low指针右移
    5. 当前low指向的这个元素已经大于了49,因此将其移动到high所指的位置
    6. 接下来low所指的这个位置空了,因此我们看high指针,此时 high所指的元素是要大于等于49的,所以不需要移动,因此让high指针左移
    7. 当前high所指的元素小于49,因此将49移动到low指向的位置
    8. 由于high指向空,因此接下来切换回low指针,13<49,因此low指针右移
    9. 此时low所指元素为97>49,因此需要将97右移到high所指向的位置
    10. 现在又切换回high指针,97>49,因此直接将high左移即可
    11. 此时high指向元素为76≥49,所以high–
    12. 当low和high碰到一起的时候,就说明其实我们已经把所有的这些带排序的元素,都给扫了一遍,所有比基准元素49更小的元素,都把它放到了low指针所指位置的左边,而所有比49这个基准元素更大的或者相等的元素,我们统统把它放在了high指针所指的右半部分,因此49一定放在low和high相遇的这个位置
    13. 此时49这个元素的最终位置已确定
  4. 对已经确定位置的元素49的左右这两个子表,再一次的用同样的方法进行划分
    1. 左子表,依旧用low和high分别指向表的首元素和尾元素
    2. 选中最前边的一个元素27作为基准元素,后面也是和之前一样的步骤
    3. low和high相遇的位置就是基准元素27的插入位置
    4. 接下来0~2这个子表又再一次的被我们刚才选中的数轴元素,划分为了左右两个部分,那由于我们剩余的左右两个部分都只有一个元素,所以显然这两个部分我们不需要再进行别的处理
    5. 也就是说0~2这个子表当中,所有这些元素的最终位置,我们此时都已经确定了
    6. 右子表,和之前一样的操作,不做赘述
    7. 对于4~7这个子表,通过基准元素76,再次的把它划分成了更小的两个部分,对于左半部分的处理思路是一样的,不做赘述
    8. 显然,65和97这两个元素的最终位置也被确定了
  5. 这就是最终结果

三.代码实现

1.具体代码

//用第一个元素将待排序序列分成左右两个部分intPartition(intA[],intlow,inthigh){intpivot=A[low];//第一个元素作为枢轴while(low<high&&A[high]>=pivot)--high;A[low]=A[high];//比枢轴小的元素移动到左端while(low<high&&A[low]<=pivot)++low;A[high]=A[low];//比枢轴大的元素移动到右端}A[[low]=pivot;//枢轴元素存放到最终位置returnlow;}
93//快速排序94voidQuickSort(intA[],intlow,inthigh){95if(low&high){//递归跳出的条件96intpivotpos=partition(A,low,high);//划分97QuickSort(A,low,pivotpos-1);//划分左子表98QuickSort(A,pivotpos+1,high);//划分右子表99}100}

2.代码运行过程

  1. 刚开始会传入整个数组a的值,同时指明最左边的元素和最右边的元素,下标应该是0~7这个范围
  2. 此时满足if条件low小于high,因此进入if条件进行处理if(low< high),接下来会调用Partition这个函数int pivotpos= partition(A, low, high);,这个函数的功能就是进行一次划分,我们要把a这个数组的low到high内的元素进行一个划分,所以接下来在函数调用栈当中会压入和Partition这个函数相关的一些的信息,同时记录QuickSort函数执行到了第几行,此时是执行到了96行(#96)
  3. 此时我们传入的low = 0和high = 7值给Partition函数,此时用临时变量pivot(基准/枢轴)存放low指针所指向的这个元素作为基准int pivot=A[Low];
  4. 此时外层while循环判断low < highwhile(low<high)
    1. 因此进入第一个内层while循环,第一个内层while循环会判断low是否小于high且此时high指向的元素是否大于等于基准,如果是,则直接左移high
      1. 很显然,这层while循环做的事情是让high指针不断地左移,直到找到一个比枢轴元素更小的元素为止while(low<high&&A[high]>==pivot)--high;
    2. 此时发现high所指元素比pivot更小,因此将high所指元素覆盖到low所指元素A[low]=A[high];
    3. 接下来就是进入第二个内层循环,第二个内层while循环会判断low是否小于high且此时low指向的元素是否小于基准,如果是,则直接右移low
      1. 很显然,这层while循环做的事情是让low指针不断地右移,直到找到一个比枢轴元素更大的元素为止while(low<high&&A[low]<=pivot)++low;
    4. 此时发现low所指元素比pivot更大,因此将low所指元素覆盖到high所指元素A[high]=A[low];
  5. 此时发现low仍小于high,因此继续重复上面的操作,结果如下
  6. 此时由于low = high,因此A[high]=A[low];或者A[low]=A[high];不再起作用,并且可以跳出外层循环,将pivot的值复制到low和high一起指向的位置A[low]=pivot;
  7. 最终返回这个low的值作为新的边界分割原表,同时将第一层QuickSor函数pivotpos = 3,函数代码执行到97的信息更新到函数调用栈int pivotpos=partition(A, low, high);
  8. 查看函数调用栈中之前是执行到了96行,因此此时我们需要执行第97行,划分左子表(low = 0 ~ pivotpos - 1 = 2)QuickSort(A, low, pivotpos-1);,此时将low = 0,high等于2,的QuickSort函数信息压入栈中
  9. 处理过程和之前的一层QuickSort函数处理一致,不多赘述
  10. 同时将第二层函数pivotpos = 1,函数代码执行到96的信息更新到函数调用栈
  11. 查看函数调用栈中最顶层函数的执行行数为96行,因此接下来执行97行,也就是划分左子表
  12. 由于low等于high,说明此时这个子表当中其实只剩余一个元素,不满足if的条件if(low<high),因此可以直接返回执行第二层QuickSort,通过查看这一层的信息得知之前是执行到了第97行,因此此时执行第98行,也就是划分右子表QuickSort(A,pivotpos+1,high);

  13. 由于low = high,因此直接回到第二层,发现第二层执行到了第98行,后面已经没有可执行语句,因此返回第一层
  14. 此时发现第一层函数执行到了第97行,因此接下来执行第98行QuickSort(A,pivotpos+1,high);
  15. 后面的步骤也差不多,因此不再赘述,图片过程如下
  16. 接着更新第二层函数的函数调用栈
  17. 处理左子表
  18. 更新第三层函数的函数调用栈
  19. 接下来不再赘述,结果如下

四.算法效率分析

1.时间复杂度

1.基本分析

  • 初始序列:由于Partition函数是两个指针不断往中间移动,因此其时间复杂度不会超过O(n)
  • 第一层QuickSort处理后:由于Partition函数是两个指针不断往中间移动,因此其时间复杂度不会超过O(n)
  • 第二层QuickSort处理后:同样不会超过O(n)
  • 第三层QuickSort处理后:同样不会超过O(n)
  • 第四层QuickSort处理后,得到最终序列
  • 可以看出:每一层的QuickSort只需要处理剩余的待排序元素,时间复杂度不超过O(n)
  • 因此:时间复杂度 = O ( n ∗ 递归层数 ) 时间复杂度=O(n*递归层数)时间复杂度=O(n递归层数)

2.进一步分析

  • 递归可以画成如下图进行表示
  • 把n个元素组织成二叉树,二叉树的层数就是递归调用的层数
  • 由于n 个结点的二叉树
    最小高度 = ⌊ log ⁡ 2 n ⌋ + 1 最小高度 = \lfloor\log_{2}n\rfloor +1最小高度=log2n+1
    最大高度 = n 最大高度 = n最大高度=n
  • 因此:
    最好时间复杂度 = O ( n ∗ ⌊ log ⁡ 2 n ⌋ + 1 ) → O ( n l o g 2 n ) 最好时间复杂度=O(n*\lfloor\log_{2}n\rfloor +1)\rightarrow O(nlog_2n)最好时间复杂度=O(nlog2n+1)O(nlog2n)
    最坏时间复杂度 : O ( n 2 ) 最坏时间复杂度:O(n^2)最坏时间复杂度:O(n2)

3.总结

  • 最好时间复杂度:O ( n l o g 2 n ) O(nlog_2n)O(nlog2n)
  • 平均时间复杂度=O ( n l o g 2 n ) O(nlog_2n)O(nlog2n)
  • 最坏时间复杂度:O ( n 2 ) O(n^2)O(n2)

快速排序是所有内部排序算法中平均性能最优的排序算法

2.空间复杂度

  • 空间复杂度 = O ( 递归层数 ) 空间复杂度=O(递归层数)空间复杂度=O(递归层数)
  • 因此:
    最好空间复杂度=O ( l o g 2 n ) O(log_2n)O(log2n)
    最坏空间复杂度=O ( n ) O(n)O(n)

若每一次选中的“枢轴”将待排序序列划分为很不均匀的两个部分,则会导致递归深度增加,算法效率变低

若每一次选中的“枢轴”将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高

总结:若初始序列有序或逆序,则快速排序的性能最差(因为每次选择的都是最靠边的元素)

3.稳定性

  • 不稳定

4.适用性

  • 普通的快速排序可以应用于链表,但由于链表结构的特性,其性能优势不如在数组上明显

  • 方法

    • 选择基准:通常选择链表的头节点作为基准元素(pivot)。
    • 划分分区:使用两个指针(例如smallcur)。small指针指向已处理部分中所有小于基准的节点的最后一个。cur指针遍历链表。当cur指向的节点值小于基准值时,将small指针后移,然后交换smallcur节点的值。这样就能将小于基准的节点逐步归拢到链表前半部分。
    • 基准归位:遍历完成后,将基准节点与small指针所指节点交换值。此时,基准节点就位于正确位置,其左边节点的值都小于它,右边节点的值都大于等于它。
    • 递归排序:递归地对基准左边和右边的两个子链表进行快速排序

五.快速排序的优化

1.思路

  • 尽量选择可以把数据中分的枢轴元素。
    eg:①选头、中、尾三个位置的元素,取中间值作为枢轴元素;②随机选一个元素作为枢轴元素

2.随机选取基准代码实现

// 交换函数voidSwap(int*a,int*b){inttemp=*a;*a=*b;*b=temp;}// 快速排序辅助函数(划分左右子表)intPartition(intarr[],intlow,inthigh){// 优化:随机选择基准,避免最坏情况intrandomIndex=low+rand()%(high-low+1);Swap(&arr[low],&arr[randomIndex]);// 将随机基准交换到首位intpivot=arr[low];while(low<high){// 从右往左找到一个小于基准元素的数while(low<high&&arr[high]>=pivot)high--;arr[low]=arr[high];// 覆盖// 从左往右找到一个大于基准元素的数while(low<high&&arr[low]<=pivot)low++;// 修正:low++arr[high]=arr[low];// 覆盖}arr[low]=pivot;// 基准归位returnlow;}// 快速排序主函数voidQuickSort(intarr[],intlow,inthigh){if(low<high){// 划分子表intpivot=Partition(arr,low,high);// 递归左子表QuickSort(arr,low,pivot-1);// 递归右子表QuickSort(arr,pivot+1,high);}}// 快速排序启动函数voidQuickSortStrart(intarr[],intlength){intlow=0,high=length-1;// 将分别用low和high指向数组的首尾QuickSort(arr,low,high);}

3.三数取中法代码实现

// 快速排序辅助函数(划分左右子表)intPartition(intarr[],intlow,inthigh){intpivot=arr[low];// 定下基准元素while(low<high){// 从右往左找到一个小于基准元素的数while(low<high&&arr[high]>=pivot)high--;// 找到后将arr[high]覆盖到arr[low]arr[low]=arr[high];// 从左往右找到一个大于基准元素的数while(low<high&&arr[low]<=pivot)low++;// 找到后将arr[low]覆盖到arr[high]arr[high]=arr[low];}// 将基准元素插入到最终位置arr[low]=pivot;// 返回基准元素的位置returnlow;}// 交换两个元素的值voidswap(int*a,int*b){inttemp=*a;*a=*b;*b=temp;}// 三数取中函数,返回中位数的索引intmedianOfThree(intarr[],intlow,inthigh){// 计算中间位置的索引,避免直接相加可能导致的整数溢出intmid=low+(high-low)/2;// 通过三次比较,对arr[low], arr[mid], arr[high]进行排序// 目标:使得 arr[low] <= arr[mid] <= arr[high]if(arr[mid]<arr[low]){swap(&arr[low],&arr[mid]);}if(arr[high]<arr[low]){swap(&arr[low],&arr[high]);}if(arr[high]<arr[mid]){swap(&arr[mid],&arr[high]);}// 此时,arr[mid]就是三个元素中的中位数// 返回中位数的索引returnmid;}// 三数取中法选择基准快速排序voidMidQuickSort(intarr[],intlow,inthigh){if(low<high){// 1. 使用三数取中法选择基准,并交换到起始位置intmidIndex=medianOfThree(arr,low,high);swap(&arr[low],&arr[midIndex]);// 2. 进行分区操作intpivotIndex=Partition(arr,low,high);// 3. 递归排序左右子数组MidQuickSort(arr,low,pivotIndex-1);MidQuickSort(arr,pivotIndex+1,high);}// 当 low >= high 时,递归终止}

六.知识回顾与重要考点

注:408原题中说,对所有尚未确定最终位置的所有元素进行一遍处理称为“一趟”排序,因此一次“划分”≠一趟排序。一次划分可以确定一个元素的最终位置,而一趟排序也许可以确定多个元素的最终位置。

结语

八更😉

如果想查看更多章节,请点击:一、数据结构专栏导航页

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/27 8:27:47

14、Ubuntu实用软件探索与使用指南

Ubuntu实用软件探索与使用指南 在Ubuntu系统中,有许多实用的软件可以满足我们不同的需求,无论是进行桌面出版、音乐创作,还是学习教育知识,都能找到合适的工具。下面将为大家详细介绍几款实用软件的使用方法和相关资源。 1. Inkscape资源推荐 Inkscape是一款强大的矢量绘…

作者头像 李华
网站建设 2026/2/27 3:14:41

18、Ubuntu服务器安装与管理全解析

Ubuntu服务器安装与管理全解析 1. RAID阵列配置 在Ubuntu服务器安装过程中,RAID(独立磁盘冗余阵列)配置是提升性能和数据安全性的重要步骤。配置RAID阵列时,你可以将其当作真实分区进行操作。具体步骤如下: 1. 在所有参与的物理驱动器上创建相同大小的分区。 2. 选择将…

作者头像 李华
网站建设 2026/2/27 10:37:33

19、Ubuntu 服务器包管理全解析

Ubuntu 服务器包管理全解析 1. APT 源配置 在 Ubuntu 系统中,APT 源的配置信息存于 /etc/apt/sources.list 文件。可以使用文本编辑器打开,若不习惯 vim ,也可用更易上手的 nano : $ vim /etc/apt/sources.list以 # 开头的行是注释行,APT 会自动忽略。文件顶部…

作者头像 李华
网站建设 2026/2/27 22:05:02

用AppSmith让你的应用“主动说话“:Web Push实时通知实战

用AppSmith让你的应用"主动说话"&#xff1a;Web Push实时通知实战 【免费下载链接】appsmith appsmithorg/appsmith: Appsmith 是一个开源的无代码开发平台&#xff0c;允许用户通过拖拽式界面构建企业级Web应用程序&#xff0c;无需编写任何后端代码&#xff0c;简…

作者头像 李华
网站建设 2026/2/28 16:23:02

如何快速掌握kafkactl:Apache Kafka命令行管理的终极指南

如何快速掌握kafkactl&#xff1a;Apache Kafka命令行管理的终极指南 【免费下载链接】kafkactl Command Line Tool for managing Apache Kafka 项目地址: https://gitcode.com/gh_mirrors/ka/kafkactl 在当今数据驱动的世界中&#xff0c;Apache Kafka已成为实时数据处…

作者头像 李华
网站建设 2026/2/28 15:39:11

24、Ubuntu社区交流的多元途径

Ubuntu社区交流的多元途径 在Ubuntu社区中,存在多种有效的交流途径,这些途径各具特色,满足了不同用户和开发者的需求。 邮件列表 邮件列表是Ubuntu社区中最重要的交流方式之一。它为重要公告发布和开发讨论提供了空间。目前,有超过300个公共邮件列表,且数量还在不断增加…

作者头像 李华