排序的基本概念
排序、关键字与稳定性
排序、关键字与稳定性
- 排序:将一组数据元素按关键字的大小重新排列成递增或递减序列的过程
- 关键字:数据元素中用于比较排序的属性,可以是单个数据项,也可以是若干数据项的组合
- 稳定性:若待排序表中两个关键字相同的元素 、(排序前 在 之前),排序后 仍在 之前,则称该排序算法稳定;否则称不稳定
内部排序与外部排序
内部排序与外部排序
- 内部排序:排序期间元素全部存放在内存中
- 外部排序:元素过多、无法一次全部放入内存,排序过程中需要不断在内存与外存之间移动数据
- 评价外部排序的核心指标是外存 I/O 次数,而不是关键字比较次数
- 原地排序:辅助空间为 。递归调用栈等额外空间也要计入辅助空间
408 中默认待排序表采用顺序存储(数组),可以随机访问;若改为链表,元素移动的代价和部分结论会发生变化,题目会另行说明。
评价指标与算法分类
评价指标与算法分类
按思想分类(本章主线):
- 插入排序:直接插入、折半插入、希尔排序
- 交换排序:冒泡排序、快速排序
- 选择排序:简单选择排序、堆排序
- 归并排序:二路归并排序
- 基数排序:按位分配与收集,不属于比较排序
插入排序
插入排序的核心思想:每次把一个待排序元素插入到前面已经有序的子序列中。直接插入排序:思想与手算过程
直接插入排序:思想与手算过程
基本思想:
- 初始时,把第一个元素看作长度为 1 的有序子序列
- 从第 2 个元素起,取出当前元素 ,在有序区 中从后向前查找插入位置
- 比 大的元素依次后移一位
- 把 放入空出的位置
49 38 65 97 76 13 27 为例,每趟插入一个元素:共 趟;第 趟结束后,前 个元素有序。
直接插入排序的复杂度与适用场景
直接插入排序的复杂度与适用场景
- 最好情况:原序列已经有序,比较 次、移动 0 次,时间
- 最坏情况:原序列逆序,比较和移动次数均约为 ,时间
- 平均情况:
- 空间复杂度:
- 稳定性:稳定
适用: 较小,或序列基本有序时,直接插入排序往往是最快的内部排序之一。
折半插入排序:只减少比较,不减少移动
折半插入排序:只减少比较,不减少移动
基本思想:先用折半查找在有序区 中确定插入位置,再统一后移元素、插入。与直接插入排序的区别:
稳定性的细节:为保证稳定,折半查找应定位到“最后一个不大于待插入元素的元素之后”,使相等元素排在待插入元素之前。
希尔排序:缩小增量,分组插入
希尔排序:缩小增量,分组插入
基本思想:取一个增量 ,把相隔 的元素分成一组,各组分别做直接插入排序;然后缩小 ,重复;当 时对整个序列做一次直接插入排序。增量序列的要求:
- 最后一个增量必须为 1,否则不能保证最终有序
- 增量之间通常取互质,避免同一元素在同一组内反复出现,减少重复劳动
49 38 65 97 76 13 27 49 为例,取增量 :为什么最后一趟必须是 :只有增量为 1 时,整个序列才作为一组参与插入排序;前面的分组排序只是让序列“更接近有序”,并不能保证全局有序。
希尔排序的复杂度与稳定性
希尔排序的复杂度与稳定性
为什么不稳定:相同关键字的元素可能被分到不同的子序列,跨组移动后相对次序会被打乱。
交换排序
交换排序的核心思想:通过交换两个元素的位置,使关键字逐渐满足排序要求。冒泡排序:每趟「沉」一个最值
冒泡排序:每趟「沉」一个最值
基本思想:从前往后依次比较相邻元素,若逆序则交换;一趟结束后,最大元素“冒”到末尾,有序区从后向前扩大。关键点:
- 每趟至少确定一个元素的最终位置,最多 趟
- 若某一趟没有发生交换,说明序列已经有序,可以提前结束
- 相等元素不交换,因此稳定
49 38 65 97 76 13 27 49 为例(从前往后、大数下沉):第 6 趟没有发生交换,说明序列已经有序,可以提前结束;最坏情况下才需要 趟。
冒泡排序的复杂度
冒泡排序的复杂度
- 最好情况:正序且设置提前结束标志,比较 次,时间
- 最坏情况:逆序,比较 次,移动 次,时间
- 平均情况:
- 空间复杂度:
- 稳定性:稳定
与简单选择排序对比:冒泡的交换次数可能很多;简单选择的交换次数最多 次,但比较次数固定。两者最坏时间同为 。
快速排序:分治与一趟划分
快速排序:分治与一趟划分
基本思想(分治):
- 选择一个枢轴
pivot(常取第一个元素) - 一趟划分:把序列分成左右两部分,左边元素都不大于枢轴,右边元素都不小于枢轴
- 枢轴落到最终位置
- 对左右子序列递归执行上述过程
- 用
low、high分别指向待划分区间的两端,pivot = A[low] - 先从
high向左找第一个小于pivot的元素,填入A[low],low右移 - 再从
low向右找第一个大于pivot的元素,填入A[high],high左移 - 重复直到
low == high,把pivot放入该位置
49 38 65 97 76 13 27 49 为例,取首元素 49 为枢轴:表中
* 表示当前的待填位置;第 5 步把枢轴放入,得到最终划分结果。快速排序的复杂度与考点
快速排序的复杂度与考点
- 空间复杂度:平均 ,最坏 ,来自递归调用栈
- 稳定性:不稳定
- 每趟划分后,枢轴元素到达最终位置,但左右子序列内部仍可能无序
- 递归深度取决于划分是否均衡;最坏退化为 与 两部分,递归深度
- 待排序序列基本有序(正序或逆序)且取首元素为枢轴时,最坏
- 快速排序的平均性能在比较排序中最好,但最坏情况不如堆排序、归并排序稳定
- 枢轴优化:随机选取、三数取中,可降低出现最坏情况的概率
选择排序
选择排序的核心思想:每一趟从待排序序列中选出最小(或最大)元素,放到最终位置。简单选择排序:每趟选最小,交换到前面
简单选择排序:每趟选最小,交换到前面
基本思想:第 趟从 中选出最小元素,与 交换; 趟后全部有序。
- 比较次数:固定为 ,与初始序列无关
- 移动次数:最多 次交换,即 次移动
- 时间复杂度:最好、平均、最坏均为
- 空间复杂度:
- 稳定性:不稳定
堆的定义与顺序存储
堆的定义与顺序存储
- 大根堆:每个结点的值都不小于其左右孩子的值,堆顶为最大值
- 小根堆:每个结点的值都不大于其左右孩子的值,堆顶为最小值
- 堆是一棵完全二叉树,通常采用顺序存储
若采用从 0 开始的下标,则左孩子为 、右孩子为 、双亲为 。做题时先看题目约定。
建堆:从最后一个非叶结点向前调整
建堆:从最后一个非叶结点向前调整
建堆过程:从最后一个非叶结点 开始,依次向前对每个结点执行向下调整。以
为什么建堆是 :越靠近叶子的结点越多,但需要向下调整的高度越低;总代价约为 量级,最终为 ,不是 。
53 17 78 9 45 65 87 建大根堆:第 3 步中,53 先与 87 交换,再继续向下与 78 交换,才得到最终结果。建堆完成后:
堆排序:反复取堆顶
堆排序:反复取堆顶
排序过程:
- 建立初始大根堆
- 堆顶(最大值)与堆的最后一个元素交换
- 堆的有效长度减 1,对堆顶执行向下调整
- 重复第 2、3 步,直到堆中只剩一个元素
堆的插入与删除:
- 插入:新元素放到堆尾,再向上调整,
- 删除堆顶:堆尾元素放到堆顶,再向下调整,
- 在堆中查找某个元素:
堆排序的复杂度与考点
堆排序的复杂度与考点
- 最好、平均、最坏:均为
- 空间复杂度:
- 稳定性:不稳定
归并排序
二路归并的思想与合并过程
二路归并的思想与合并过程
基本思想(分治):
- 把长度为 的序列不断二分,直到每个子序列长度为 1
- 把相邻的两个有序子序列合并成一个有序序列
- 反复合并,直到只剩一个序列
- 两个指针分别指向两个子序列的起始位置
- 比较指针所指元素,把较小者放入辅助数组,并移动对应指针
- 某个子序列取完后,把另一个子序列的剩余元素直接复制过去
归并趟数与手动模拟
归并趟数与手动模拟
每趟归并的时间为 ,共需 趟,因此总时间为 。以
49 38 65 97 76 13 27 为例:归并排序的复杂度与特点
归并排序的复杂度与特点
- 时间复杂度始终为 ,不受初始序列影响
- 需要与原序列等长的辅助数组,空间开销较大
- 适合要求稳定且能接受额外空间的场景,也是外部排序的基础
基数排序
低位优先(LSD)的分配与收集
低位优先(LSD)的分配与收集
基本思想:不比较关键字大小,而是按关键字的每一位分配、收集,从最低位到最高位依次进行。关键概念:
- 位数 :关键字有多少位
- 基数 :每一位可能的取值个数(十进制数 )
- 分配:按当前位的值,把元素放入对应的队列
- 收集:按队列编号从小到大把元素串起来
三趟基数排序的手动模拟
三趟基数排序的手动模拟
以
278 109 063 930 589 184 505 269 008 083 为例,基数为 10:不足 位的关键字高位补 0(如
008),保证每一趟都按相同的位数处理。基数排序的复杂度与适用条件
基数排序的复杂度与适用条件
- 属于非比较排序,不受 下界限制
- 时间复杂度与初始序列无关,只取决于 、、
- 适合关键字位数少、位数统一、基数小的数据
- 不适合位数很多、位数差异大或浮点数数据
有的教材只统计 个队列的辅助空间,把空间复杂度记作 。
计数排序(拓展)
统计、前缀和与回填
统计、前缀和与回填
基本思想:
- 统计每个关键字值出现的次数
- 对次数做前缀和,得到每个值在有序序列中的最后位置
- 从后向前遍历原序列,把元素放到对应位置,并让该位置前移
- 时间复杂度:, 为关键字值域大小
- 空间复杂度:(结果数组 + 计数数组)
- 稳定性:按上述“从后向前回填”实现时稳定
- 适用条件:关键字为整数且值域较小
计数排序不在 408 大纲要求内,作为与基数排序对照的拓展了解即可。
内部排序算法的比较
复杂度与稳定性总表
复杂度与稳定性总表
时间复杂度
空间复杂度与稳定性
稳定性与复杂度的记忆
稳定性与复杂度的记忆
稳定:直接插入、折半插入、冒泡、归并、基数不稳定:希尔、快速、简单选择、堆按时间复杂度分层:
- :直接插入、折半插入、冒泡、简单选择、希尔(常见最坏)
- :快速(平均)、堆、归并
- 线性级:基数 、计数
比较排序的时间下界
比较排序的时间下界
基于关键字比较的排序算法,最坏情况至少需要 次比较。
- 快速排序、堆排序、归并排序都属于比较排序
- 基数排序、计数排序通过“分配、统计”确定位置,不属于比较排序,因此不受该下界限制
直观理解: 个元素有 种排列,每次比较最多把可能性减半,至少需要 次比较。
特殊结论速查
特殊结论速查
“一趟能确定最终位置”不等于“一趟后序列有序”。快排一趟只确定枢轴,归并一趟可能让整体更有序,但不能确定任何元素的最终位置。
内部排序算法的选择
按题目要求选算法
按题目要求选算法
几组容易混淆的对比
几组容易混淆的对比
快速排序与归并排序
- 快排:平均最快、辅助空间小、不稳定、最坏
- 归并:稳定、最坏也有 、需要 辅助空间
快速排序与堆排序
- 快排:平均 ,最坏
- 堆排序:最坏仍为 ,但实际常数较大、不稳定
冒泡排序与简单选择排序
- 冒泡:稳定、可提前结束、最好
- 简单选择:不稳定、比较次数固定为 、交换次数少
直接插入与希尔排序
- 直接插入:稳定、适合基本有序的数据
- 希尔:不稳定、通过分组让序列快速接近有序
外部排序
为什么外部排序关注 I/O
为什么外部排序关注 I/O
- 待排序文件太大,无法一次全部放入内存,需要借助外存
- 排序过程分为两个阶段:
- 生成初始归并段:分批读入内存,排好序后写回外存
- 多路归并:把多个有序归并段合并,直到得到一个完整的有序文件
- 总时间约为“内部排序时间 + 外存读写时间 + 内部归并时间”;数据量大时,读写时间占主导
- 主要性能瓶颈是磁盘 I/O 次数,而不是关键字比较次数
每趟归并都要把全部数据读入、写出各一次,因此减少归并趟数是外部排序优化的核心。
多路平衡归并与归并趟数
多路平衡归并与归并趟数
每次同时归并 个有序段,称为 路归并。若有 个初始归并段,则归并趟数为:
- 增大 可以减少归并趟数,从而减少 I/O
- 越大,需要的内存缓冲区越多,且从 个元素中选最小值的比较开销越大
- 败者树正是为了降低“从 路中选最小”的开销
若文件共 个记录,内存工作区可容纳 个记录,传统方法生成的初始归并段数为 ;置换-选择排序可使平均段长约为 ,从而减少 。
败者树:快速选出 k 路中的最小值
败者树:快速选出 k 路中的最小值
作用:在多路归并中快速确定当前 个待选元素中的最小者。结构:
- 叶结点:各归并段当前参与比较的元素
- 内部结点:记录一次比较中的败者
- 另设一个变量记录冠军(当前最小元素)
ls[0] 中;根结点记录的是最后一次比较的败者 b3。调整过程:- 输出冠军所在归并段的下一个元素,替换该叶结点
- 从该叶结点向上,与路径上记录的败者比较
- 胜者继续向上,败者留在当前结点
- 根结点上方得到新的冠军
- 初始化建树:
- 每次输出后调整:
置换-选择排序:生成长度超过内存的归并段
置换-选择排序:生成长度超过内存的归并段
目的:传统方法中,初始归并段长度不超过内存工作区容量 ,段数多、归并趟数多。置换-选择排序可以生成长度大于 的归并段。过程:
- 把内存工作区装满记录
- 选出其中最小者输出,记下刚输出的值
last - 从外存读入一条新记录补入工作区
- 若新记录不小于
last,它可以留在当前归并段;否则冻结,留给下一个归并段 - 当前归并段无法继续输出时,本段结束;解冻冻结记录,开始生成下一段
结论:随机输入下,置换-选择排序生成的初始归并段平均长度约为 。
意义:段数 减少,归并趟数 减少,I/O 次数随之减少。
最佳归并树:让总 I/O 最小
最佳归并树:让总 I/O 最小
各初始归并段长度不同,不同的归并顺序会产生不同的总读写代价。把归并段长度作为叶结点权值,构造 叉哈夫曼树,即为最佳归并树。构造方法:
- 每次选取权值最小的 个结点
- 合并成一个新结点,权值为这 个结点权值之和
- 把新结点放回,重复直到只剩一个结点
2 3 5 7 11。,无需补虚段。- 合并 2、3、5,得到 10
- 合并 7、10、11,得到 28
高频易错点
结论判断易错清单
结论判断易错清单
过程分析易错点
过程分析易错点
- 直接插入第 趟是把 插入前 个元素,注意不要数错趟数
- 冒泡每趟确定的是当前无序区的最大值,放在无序区末尾
- 快排一趟只保证枢轴到位,枢轴两侧内部仍可能无序
- 堆排序中交换堆顶与堆尾后,堆的有效长度要减 1,调整时不能越过边界
- 归并排序的手算要按“两两归并”进行,不要跨段随意合并
- 基数排序通常按低位优先(LSD),且每趟稳定
考前速记
一页纸结论
一页纸结论
稳定性:稳定——直接插入、折半插入、冒泡、归并、基数;不稳定——希尔、快速、简单选择、堆。
- :直接插入、折半插入、冒泡、简单选择、希尔(常见最坏)
- :快速(平均)、堆、归并
- 线性级:基数 、计数
- 最好 :直接插入、冒泡
- 最坏仍 :堆、归并
- 辅助空间 :直接插入、折半插入、希尔、冒泡、简单选择、堆
- 辅助空间 :快速排序(递归栈)
- 辅助空间 :归并排序;基数排序为
关键特殊结论
关键特殊结论
- 折半插入:减少比较,不减少移动
- 冒泡:一趟确定一个最值的最终位置
- 快速:一趟划分确定枢轴最终位置
- 堆:建堆 ,整体
- 归并:稳定,但需要辅助空间
- 基数:每一趟必须稳定
- 外部排序:核心是减少 I/O 次数
- 败者树:提高多路归并选最小值的效率
- 最佳归并树:哈夫曼树思想, 最小
建议重点掌握的题型
建议重点掌握的题型
- 排序过程分析:给定初始序列,写出每一趟排序后的结果
- 快速排序:枢轴划分过程、递归过程、最好与最坏情况
- 堆排序:建堆、删除堆顶、向下调整
- 排序算法比较:时间复杂度、空间复杂度、稳定性
- 归并排序:归并过程、归并趟数、辅助空间
- 外部排序:多路归并、败者树、置换-选择、最佳归并树