考研复习 Day 21 | 数据结构与算法--排序(上)

一、排序的基本概念

1.1 排序的定义

排序:将表中的元素重新排列,使其按关键字有序的过程。

稳定性 :若关键字相同的元素在排序前后相对顺序不变,则称该排序算法是稳定的 ;否则为不稳定的

稳定性不能衡量算法优劣,只是描述其性质。当关键字互不重复时,稳定性无关紧要。

1.2 内部排序 vs 外部排序

类型 定义
内部排序 排序期间所有元素都存放在内存中
外部排序 元素无法全部装入内存,需在内、外存之间频繁交换数据

1.3 排序算法的分类

按基本思想分为五大类:

  • 插入排序:直接插入排序、折半插入排序、希尔排序

  • 交换排序:冒泡排序、快速排序

  • 选择排序:简单选择排序、堆排序

  • 归并排序

  • 基数排序

大多数内部排序算法更适用于顺序存储的线性表。


二、插入排序

2.1 直接插入排序

核心思想:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中。

算法步骤(对L[2]到L[n]循环):

  1. 查找L[i]在L[1..i-1]中的插入位置k

  2. 将L[k..i-1]中的所有元素后移一位

  3. 将L[i]放入位置k

性能分析

指标
空间复杂度 O(1)
最好时间复杂度 O(n)(已有序)
最坏时间复杂度 O(n²)(逆序)
平均时间复杂度 O(n²)
稳定性 稳定
适用性 顺序存储 + 链式存储

哨兵:A[0]用作哨兵,避免循环中反复判断数组下标是否越界。


2.2 折半插入排序

改进点:将"边比较边移动"改为"先折半查找确定插入位置,再统一移动元素"。

性能分析

指标
空间复杂度 O(1)
比较次数 O(n log₂n)
移动次数 仍取决于初始序列,最坏O(n²)
时间复杂度 O(n²)
稳定性 稳定
适用性 仅顺序存储

折半插入排序减少了比较次数 ,但移动次数未减少,时间复杂度仍为O(n²)。


2.3 希尔排序(缩小增量排序)

核心思想:先取一个小于n的增量d₁,将表分成d₁个子序列,对各子序列进行直接插入排序;然后取更小的增量d₂,重复;直到d=1。

增量序列要求d₁ > d₂ > ... > d_k = 1

例:初始序列(8 个元素):

49, 38, 65, 97, 76, 13, 27, 49

第一趟:d₁ = 4(n/2 = 4)

分成 4 组,每组对"间隔 4"的元素进行直接插入排序:

子序列(下标从1开始) 排序前 排序后
第1组(1,5) 49, 76 49, 76
第2组(2,6) 38, 13 13, 38
第3组(3,7) 65, 27 27, 65
第4组(4,8) 97, 49 49, 97

第一趟结果

49, 13, 27, 49, 76, 38, 65, 97

第二趟:d₂ = 2

分成 2 组,对"间隔 2"的元素直接插入排序:

子序列 排序前 排序后
第1组(奇数位:1,3,5,7) 49, 27, 76, 65 27, 49, 65, 76
第2组(偶数位:2,4,6,8) 13, 49, 38, 97 13, 38, 49, 97

第二趟结果

27, 13, 49, 38, 65, 49, 76, 97

第三趟:d₃ = 1

整体直接插入排序(过程略)

最终结果

13, 27, 38, 49, 49, 65, 76, 97

性能分析

指标
空间复杂度 O(1)
时间复杂度 约O(n^1.3),最坏O(n²)
稳定性 不稳定(相同关键字可能分到不同子表)
适用性 仅顺序存储

希尔排序的增量序列如何选择是最优的,目前仍是数学上的未解难题。


三、交换排序

3.1 冒泡排序

核心思想:从后往前(或从前往后)依次比较相邻元素,若逆序则交换。每一趟都将当前未排序部分的最小(或最大)元素放到其最终位置。

优化:设置flag标志,若某一趟没有发生交换,说明序列已有序,可提前终止。

性能分析

指标
空间复杂度 O(1)
最好时间复杂度 O(n)(已有序)
最坏时间复杂度 O(n²)(逆序)
平均时间复杂度 O(n²)
稳定性 稳定
适用性 顺序存储 + 链式存储

重要性质 :冒泡排序每趟产生的有序子序列具有全局有序性(已排序部分的所有元素均小于未排序部分的所有元素)。


3.2 快速排序

核心思想 :任取一个元素作为枢轴(pivot),通过一趟划分将表分成两部分,使左边所有元素小于枢轴,右边所有元素大于等于枢轴。然后递归地对两个子表进行相同操作。

一趟划分过程(以首元素为枢轴):

初始:49,38,65,97,76,13,27,49
i→ ←j

取pivot=49
j从右向左找<49 → 找到27,放入i位置:27 ,38,65,97,76,13,27 ,49
i从左向右找>49 → 找到65,放入j位置:27,38,65 ,97,76,13,65 ,49
j继续向左找<49 → 找到13,放入i位置:27,38,13 ,97,76,13 ,65,49
i继续向右找>49 → 找到97,放入j位置:27,38,13,97 ,76,97 ,65,49
i与j相遇 → 将pivot放入:27,38,13,49,76,97,65,49

第一趟结果:27,38,13,【49】,76,97,65,49
左边<49 右边≥49

(后续排序结果略,即左右两边不断递归排序)

性能分析

指标
空间复杂度 最好O(log₂n),最坏O(n)(递归栈)
最好时间复杂度 O(n log₂n)(每次划分均匀)
最坏时间复杂度 O(n²)(基本有序或逆序)
平均时间复杂度 O(n log₂n)
稳定性 不稳定
适用性 仅顺序存储

重要 :快速排序是所有内部排序算法中平均性能最优的排序方法。

改进划分平衡性的方法

  • 三数取中法:从首、尾、中间三个位置取中位数作为枢轴

  • 随机选取枢轴

性质 :每趟划分能确保枢轴元素被放到其最终位置


四、选择排序

4.1 简单选择排序

核心思想:每一趟从待排序元素中选出关键字最小的元素,放到已排序序列的末尾。

性能分析

指标
空间复杂度 O(1)
比较次数 始终为 n(n-1)/2
移动次数 最多3(n-1)次,最好0次
时间复杂度 O(n²)(与初始序列无关)
稳定性 不稳定
适用性 顺序存储 + 链式存储

简单选择排序的比较次数与初始序列无关,总比较次数恒为n(n-1)/2。


4.2 堆排序

4.2.1 堆的定义

:n个关键字序列L[1..n]满足:

  • 大根堆L[i] ≥ L[2i]L[i] ≥ L[2i+1](最大元素在根)

  • 小根堆L[i] ≤ L[2i]L[i] ≤ L[2i+1](最小元素在根)

堆可以视为一棵完全二叉树

4.2.2 堆排序的基本思路

  1. 将待排序序列构建成初始堆(大根堆)
  2. 交换堆顶元素与堆底元素,最大值归位
  3. 将剩余n-1个元素重新调整为堆
  4. 重复2-3,直到堆中只剩一个元素

4.2.3 建堆过程(自底向上)

最后一个分支结点 (编号⌊n/2⌋)开始,依次向前处理到根结点,对每个结点执行向下调整:若该结点小于其左右孩子中的较大者,则交换,并继续向下调整,直到满足堆性质。

初始序列:53,17,78,09,45,65,87,32

最后一个分支结点:i=4 → 09<32,交换
i=3 → 78<87,交换
i=2 → 17<45,交换
i=1 → 53<87,交换 → 破坏L(3)子树 → 53<78,交换
建堆完成

(1)初始序列(看作完全二叉树)

序列:53, 17, 78, 09, 45, 65, 87, 32

对应的完全二叉树(编号从1开始):

1(53)
/ \
2(17) 3(78)
/ \ / \
4(09) 5(45) 6(65) 7(87)
/
8(32)

最后一个分支结点编号 = ⌊8/2⌋ = 4,即结点4(09)。


(2)第一步:调整结点4(09)

结点4(09)的左右孩子:左=8(32),右=无。

4(09)
/
8(32)

09 < 32 → 交换。

调整后:

4(32)
/
8(09)

整棵树变为:

1(53)
/ \
2(17) 3(78)
/ \ / \
4(32) 5(45) 6(65) 7(87)
/
8(09)


(3)第二步:调整结点3(78)

结点3(78)的左右孩子:左=6(65),右=7(87)。

较大孩子是7(87),78 < 87 → 交换。

3(87)
/ \
6(65) 7(78)

整棵树变为:

1(53)
/ \
2(17) 3(87)
/ \ / \
4(32) 5(45) 6(65) 7(78)
/
8(09)


(4)第三步:调整结点2(17)

结点2(17)的左右孩子:左=4(32),右=5(45)。

较大孩子是5(45),17 < 45 → 交换。

2(45)
/ \
4(32) 5(17)

整棵树变为:

1(53)
/ \
2(45) 3(87)
/ \ / \
4(32) 5(17) 6(65) 7(78)
/
8(09)


(5)第四步:调整结点1(53)------这里会发生"破坏"

结点1(53)的左右孩子:左=2(45),右=3(87)。

较大孩子是3(87),53 < 87 → 交换。

1(87)
/ \
2(45) 3(53)

此时整棵树变为:

1(87)
/ \
2(45) 3(53) ← 53被换下来了
/ \ / \
4(32) 5(17) 6(65) 7(78)
/
8(09)


(6)"破坏L(3)子树"的意思

原来结点3(78)已经调整好了(78的子树满足堆性质)。

但当把53换到结点3的位置后:

3(53)
/ \
6(65) 7(78)

53 < 65 ?是,53 < 65。
53 < 78?是,53 < 78。

但是最大堆要求 :父结点 ≥ 子结点。
这里 53 比左右孩子都小 → 堆性质被破坏了。


(7)继续向下调整:对结点3(53)再次执行"下沉"

3(53)
/ \
6(65) 7(78)

较大孩子是7(78),53 < 78 → 交换。

3(78)
/ \
6(65) 7(53)

整棵树最终变为:

1(87)
/ \
2(45) 3(78)
/ \ / \
4(32) 5(17) 6(65) 7(53)
/
8(09)


(8)最终大根堆

87
/ \
45 78
/ \ / \
32 17 65 53
/
09

建堆时间复杂度:O(n)

4.2.4 堆的调整(向下筛选)

复制代码
void HeapAdjust(ElemType A[], int k, int len) {
    A[0] = A[k];
    for (int i = 2 * k; i <= len; i *= 2) {
        if (i < len && A[i] < A[i + 1]) i++;  // 取较大孩子
        if (A[0] >= A[i]) break;
        else {
            A[k] = A[i];
            k = i;
        }
    }
    A[k] = A[0];
}

堆调整的时间复杂度:O(log₂n)

4.2.5 堆排序的完整过程

  1. BuildMaxHeap(A, len) // 建堆,O(n)
  2. for (i = len; i > 1; i--)

{
Swap(A[1], A[i]); // 输出堆顶到末尾
HeapAdjust(A, 1, i-1); // 调整剩余元素
}

4.2.6 堆的插入操作

将新元素放在堆的末端,然后自下而上与父结点比较,若违反堆性质则交换,直至满足堆定义。

4.2.7 堆的应用------Top-K问题

在1亿个数中找出最大的100个数:

  1. 读入前100个数,构建小顶堆

  2. 依次读入剩余数字,若小于堆顶则舍弃,否则替换堆顶并向下调整

  3. 读取完毕后,堆中的100个数即为所求

时间复杂度:O(n log₂k),空间复杂度:O(1)

4.2.8 堆排序性能分析

指标
空间复杂度 O(1)
建堆时间复杂度 O(n)
排序时间复杂度 O(n log₂n)
总时间复杂度 O(n log₂n)(与初始序列无关)
稳定性 不稳定
适用性 仅顺序存储

五、思考

1. 直接插入排序 ≈ 整理扑克牌

手里已经有一堆排好序的牌,新摸一张牌,从右往左找到合适的位置插入。这就是直接插入排序。
2. 希尔排序 ≈ 军训分组训练

先按身高间隔5个人分成小组,组内对齐;再按间隔3个人分组,再对齐;最后全体对齐。虽然每次不是最终顺序,但越来越接近有序。
3. 冒泡排序 ≈ 气泡上浮

轻的气泡(小元素)一路向上"冒",重的石头(大元素)一路向下"沉"。每一趟都会有一个元素到达最终位置。
4. 快速排序 ≈ 班级选人

选一个"基准"同学,比他矮的站左边,比他高的站右边。然后左右两组各自重复这个过程。每次都能确定一个人的最终位置。
5. 堆排序 ≈ 锦标赛淘汰赛

先通过小组赛(建堆)找出冠军(堆顶),然后把冠军拿走,剩下的选手重新比赛找出新冠军。每次都能选出当前最大(或最小)。


六、各算法复杂度表

算法 时间复杂度(最好) 时间复杂度(最坏) 时间复杂度(平均) 空间复杂度 稳定性
直接插入排序 O(n) O(n²) O(n²) O(1) 稳定
折半插入排序 O(n log₂n) O(n²) O(n²) O(1) 稳定
希尔排序 O(n^1.3) O(n²) O(n^1.3) O(1) 不稳定
冒泡排序 O(n) O(n²) O(n²) O(1) 稳定
快速排序 O(n log₂n) O(n²) O(n log₂n) O(log₂n)~O(n) 不稳定
简单选择排序 O(n²) O(n²) O(n²) O(1) 不稳定
堆排序 O(n log₂n) O(n log₂n) O(n log₂n) O(1) 不稳定

注:以上内容参考 2027年数据结构考研复习指导 王道论坛 组编,其中有一些个人想法,如有任何错误或不妥,欢迎各位大佬指出,如果各位有一些有意思的想法,也可以和我交流一下~感谢!


七、明日计划

排序(下)

相关推荐
hnjzsyjyj2 小时前
全排列问题DFS实现执行示意图
数据结构·dfs
故事和你913 小时前
洛谷-算法2-2-常见优化技巧3
开发语言·数据结构·c++·算法·深度优先·动态规划·图论
菜鸟555553 小时前
2025江西省CCPC省赛暨全国邀请赛(南昌)
数据结构·c++·算法·acm·思维·ccpc·xcpc
꧁细听勿语情꧂4 小时前
用队列实现栈、用栈实现队列,树、二叉树、满二叉树、完全二叉树,堆、向下向上调整算法、出堆入堆、堆排序
c语言·开发语言·数据结构·算法
周末也要写八哥4 小时前
什么是快速选择及案例分析
数据结构
Felven4 小时前
B. Make Almost Equal With Mod
数据结构·算法
数智化精益手记局4 小时前
拆解红牌作战的步骤:掌握红牌作战的步骤,解决现场管理难题
大数据·数据结构·人工智能·制造·精益工程
喜欢吃燃面4 小时前
Linux 信号保存机制深度解析:从内核数据结构到进程状态管理
linux·运维·数据结构·学习
hi_ro_a4 小时前
C++ 手撕 STL 底层:红黑树封装 mymap/myset
数据结构·c++·算法