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

一、排序的基本概念

1.1 排序的定义

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

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

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

1.2 内部排序 vs 外部排序

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

1.3 排序算法的分类

按基本思想分为五大类:

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

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

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

  • 归并排序

  • 基数排序

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


二、插入排序

2.1 直接插入排序

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

算法步骤(对L2到Ln循环):

  1. 查找Li在L1..i-1中的插入位置k

  2. 将Lk..i-1中的所有元素后移一位

  3. 将Li放入位置k

性能分析:

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

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


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个关键字序列L1..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(A1, Ai); // 输出堆顶到末尾
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年数据结构考研复习指导 王道论坛 组编,其中有一些个人想法,如有任何错误或不妥,欢迎各位大佬指出,如果各位有一些有意思的想法,也可以和我交流一下~感谢!


七、明日计划

排序(下)

相关推荐
yi0118 小时前
LeetCode 219:存在重复元素 II——哈希表记录“最近一次出现的位置”
数据结构·人工智能·笔记·python·算法·leetcode·哈希表
禾小西11 小时前
Redis 数据结构:快速的 Redis 有哪些慢操作?
数据结构·数据库·redis
H.莓飛11 小时前
【数据结构】二叉树_OJ题
linux·开发语言·数据结构·算法
好想像大佬一样能够ak所有12 小时前
Leetcode35.搜索插入位置
数据结构·算法·leetcode
chenbingjie_c13 小时前
C++ STL 适配器:stack & queue,从 vector/list 到 deque 深度解析
数据结构·c++
垆边人似月.16 小时前
华为OD算法题:查找众数及中位数
数据结构·算法·leetcode
CoderYanger16 小时前
一轮复习——F.动态规划模型总结(入门篇)
java·开发语言·数据结构·算法·leetcode·职场和发展·动态规划
一只旭宝17 小时前
数据结构与算法复习手册:从写过,到讲清楚
开发语言·数据结构·c++·算法
All for pursuit.19 小时前
【动态规划-1】121.买卖股票的最佳时机
数据结构·c++·算法·leetcode·动态规划
Logic10119 小时前
C语言/数据结构位运算题解:异或XOR找出优秀项目组中的“独特编号“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质