---
title: 数据结构与算法笔记
date: 2026-01-13
descriptionHTML: '数据结构与算法详细复习及排序算法深度解析'
tags:
- 笔记
- 数据结构
- 算法
sidebar: true
readingTime: true
hidden: false
recommend: true
---
# 数据结构与算法详细复习笔记
## 第2章:线性表 (Linear List)
### 1. 基本概念
* **定义**:由 $n (n \ge 0)$ 个数据特性相同的元素构成的有限序列。
* **特性**:
* **有序性**:元素之间有逻辑上的顺序(第一个、最后一个、前驱、后继)。
* **有限性**:元素个数有限。
* **抽象数据类型 (ADT)**:
* 数据对象:$D = \{a_i | a_i \in ElemSet, i=1, 2, ..., n\}$
* 数据关系:$R = \{ | a_{i-1}, a_i \in D, i=2, ..., n\}$
* 基本操作:`InitList`, `DestroyList`, `ListInsert`, `ListDelete`, `GetElem`, `LocateElem` 等。
### 2. 顺序表 (Sequential List)
* **定义**:用一组地址连续的存储单元依次存储线性表的数据元素。
* **特点**:
* **随机存取**:通过首地址和下标可在 $O(1)$ 时间内访问任意元素。公式:$Loc(a_i) = Loc(a_1) + (i-1) \times L$。
* **存储密度高**:无需为逻辑关系(指针)额外分配空间。
* **缺点**:插入/删除需要移动大量元素(平均移动 $n/2$),需预分配空间(静态分配易溢出,动态分配需扩容)。
### 3. 链表 (Linked List)
* **单链表 (Singly Linked List)**:
* **结构**:结点 = 数据域 (data) + 指针域 (next)。
* **头结点**:在首元结点前附设的一个结点,便于处理空表和统一插入/删除操作。
* **建立方法**:
* **头插法**:新结点插入头结点之后,读入顺序与链表顺序相反(逆序)。
* **尾插法**:维护一个尾指针 `r`,新结点插在 `r` 之后,读入顺序与链表顺序相同。
* **双向链表 (Doubly Linked List)**:
* **结构**:结点包含 `prior` (前驱) 和 `next` (后继) 指针。
* **插入操作** (`s` 插在 `p` 之后):
1. `s->prior = p;`
2. `s->next = p->next;`
3. `p->next->prior = s;`
4. `p->next = s;` (注意顺序,防止断链)
* **删除操作** (删除 `p` 的后继节点 `q`):
1. `p->next = q->next;`
2. `q->next->prior = p;`
3. `free(q);`
* **循环链表 (Circular Linked List)**:
* **特点**:表中最后一个结点的指针指向头结点,形成环状。
* **判空/遍历结束**:指针是否等于头指针 (or 尾指针)。
* **优势**:从任意结点出发均可遍历全表;若设尾指针 `rear`,则查找头尾的时间均为 $O(1)$(便于合并两个链表)。
* **静态链表**:
* 利用数组实现,元素包含 `data` 和 `cur` (游标,即下个元素的数组下标)。
* 适用于不支持指针的语言(如 BASIC)或数据量固定且需频繁操作指针的场景。
* **块状链表 (Unrolled Linked List)**:
* 每个结点存储多个数据元素(即一个小的顺序表)。平衡了顺序表(空间紧凑)和链表(插入删除快)的优缺点。
### 4. 应用
* **一元多项式相加**:利用链表按指数升序存储。遍历两个链表,比较指数大小:
* 指数相同:系数相加,若非零则保留。
* 指数不同:将指数小的项链入结果链表。
* **大整数处理**:利用链表存储大整数的每一位(或每几位),便于进位处理。
---
## 第3章:栈与队列 (Stack & Queue)
### 1. 栈 (Stack)
* **定义**:只允许在表尾(栈顶 Top)进行插入(Push)和删除(Pop)的线性表。**LIFO** (Last In First Out)。
* **实现**:
* **顺序栈**:`top` 指针指向栈顶元素(或栈顶上一个空位)。需判断 `StackOverflow` (上溢) 和 `StackUnderflow` (下溢,即空栈 pop)。
* **共享栈**:两个栈共享一段空间,栈底在两端,栈顶向中间延伸,`top1 + 1 == top2` 时满。
* **应用**:
* **递归**:系统栈保存每一层调用的局部变量、返回地址。
* **括号匹配**:左括号压栈,右括号弹栈匹配。
* **表达式求值**:
* **中缀转后缀 (RPN)**:遇操作数输出;遇运算符与栈顶比较优先级。
* **后缀计算**:遇操作数压栈;遇运算符弹栈计算结果并压栈。
* **迷宫求解**:DFS 深度优先搜索(回溯法)。
### 2. 队列 (Queue)
* **定义**:只允许在表尾(Rear)插入,表头(Front)删除。**FIFO** (First In First Out)。
* **循环队列 (Circular Queue)**:
* **问题**:顺序队列单纯 `front++` `rear++` 会导致“假溢出”(即前方有空位但 rear 已达上限)。
* **解决**:模运算。`(rear + 1) % MAXSIZE`。
* **判空/判满**:为了区分队空和队满,通常牺牲一个单元。
* **队空**:`front == rear`
* **队满**:`(rear + 1) % MAXSIZE == front`
* **队长**:`(rear - front + MAXSIZE) % MAXSIZE`
* **双端队列 (Deque)**:两端均可入队、出队。
* **变种**:输入受限(一端入两端出)、输出受限(两端入一端出)。
---
## 第4章:字符串 (String)
### 1. 基本概念
* **串**:零个或多个字符组成的有限序列。
* **子串**:主串中任意连续字符组成的子序列。
* **存储**:定长顺序存储(截断)、堆分配存储(动态指针)、块链存储。
### 2. 模式匹配算法
* **朴素模式匹配 (Brute-Force)**:
* 主串指针 `i`,模式串指针 `j`。
* 失配时:`i` 回溯到 `i - j + 1`,`j` 回溯到 `0`。
* 最坏时间复杂度:$O(n \times m)$。
* **KMP 算法 (Knuth-Morris-Pratt)**:
* **核心**:主串指针 `i` **不回溯**,模式串指针 `j` 回退到 `next[j]`。
* **Next 数组**:`next[j]` 表示模式串第 `j` 个字符前的子串中,**最长相等前后缀的长度**。
* 当 `S[i] != P[j]` 时,`j` 移动到 `next[j]` 继续比较。
* **NextVal 数组**(优化):若 `P[j] == P[next[j]]`,则递归优化,避免重复无效比较。
* 时间复杂度:$O(n + m)$。
---
## 第5章:数组和广义表
### 1. 数组
* **存储**:内存是一维的,多维数组需映射。
* **行优先 (Row-Major)**:C/C++。`LOC(a[i][j]) = LOC(0,0) + (i * n + j) * size`。
* **列优先 (Column-Major)**:Fortran。
### 2. 矩阵压缩存储
* **对称矩阵**:$a_{ij} = a_{ji}$。只存上三角或下三角。按行优先存储下三角元素 $(i \ge j)$ 的下标 $k = \frac{i(i+1)}{2} + j$。
* **稀疏矩阵 (Sparse Matrix)**:
* **三元组表**:`(i, j, value)`。空间优化,但丧失随机存取特性。
* **十字链表**:每个非零元节点既在行链表上,又在列链表上。便于频繁插入删除。
### 3. 广义表 (Generalized List)
* **定义**:$LS = (a_1, a_2, ..., a_n)$,其中 $a_i$ 可以是原子或子表。
* **重要操作**:
* **GetHead(L)**:取表头(第一个元素,可能是原子或列表)。
* **GetTail(L)**:取表尾(**除第一个元素外剩余元素构成的表**,结果一定是一个表)。
* *例*:$L=((a,b), c)$,Head(L)=$(a,b)$,Tail(L)=$(c)$。
---
## 第6章:树与二叉树
### 1. 二叉树 (Binary Tree)
* **性质**:
1. 第 $i$ 层至多 $2^{i-1}$ 个结点。
2. 深度为 $k$ 至多 $2^k - 1$ 个结点。
3. **核心性质**:$n_0 = n_2 + 1$(叶子结点数 = 度为2的结点数 + 1)。
* **完全二叉树 (Complete Binary Tree)**:
* 编号规则与满二叉树一一对应。
* 数组存储时,结点 $i$ 的左孩子为 $2i$,右孩子为 $2i+1$,父节点 $\lfloor i/2 \rfloor$。
### 2. 遍历 (Traversal)
* **前序 (Pre)**:根 -> 左 -> 右
* **中序 (In)**:左 -> 根 -> 右
* **后序 (Post)**:左 -> 右 -> 根
* **层序**:利用**队列**实现 BFS。
* **构造**:
* 前序 + 中序 -> 唯一确定二叉树。
* 后序 + 中序 -> 唯一确定二叉树。
* 前序 + 后序 -> **不能**唯一确定(无法区分左右子树)。
### 3. 应用
* **哈夫曼树 (Huffman Tree)**:
* **构造**:每次选取权值最小的两棵树合并,新树权值为两者之和,直到只剩一棵。
* **WPL (带权路径长度)**:$\sum (叶子权值 \times 路径长度)$。
* **哈夫曼编码**:前缀编码(任一字符编码都不是另一字符编码的前缀),用于数据压缩。
* **线索二叉树**:
* 利用空闲指针域(`lchild`若空指向前驱,`rchild`若空指向后继),通过 `ltag/rtag` 标记。
* 加快遍历速度,无需递归或栈。
---
## 第7章:图 (Graph)
### 1. 存储结构
* **邻接矩阵**:`AdjMatrix[i][j] = 1` 或权值。
* 优点:易判断两点是否相连,易算度(行和列和)。
* 缺点:稀疏图浪费空间 $O(n^2)$。
* **邻接表**:顶点表 + 边表(链表)。
* 优点:节省空间 $O(n+e)$。
* 缺点:求入度麻烦(需遍历全表或使用逆邻接表)。
### 2. 遍历
* **DFS (深度优先)**:类似树的先序遍历,使用**递归/栈**。
* **BFS (广度优先)**:类似树的层序遍历,使用**队列**。最短路径特性(无权图)。
---
## 第8章:图的应用 (重难点)
### 1. 最小生成树 (MST)
* **Prim 算法**:
* **思想**:归并点。从一个顶点开始,每次选一条连接“已选集合”和“未选集合”且权值最小的边。
* **复杂度**:$O(n^2)$,适合**稠密图**。
* **Kruskal 算法**:
* **思想**:归并边。将边按权值排序,从小到大选边,若不构成回路(利用并查集判断)则加入。
* **复杂度**:$O(e \log e)$,适合**稀疏图**。
### 2. 最短路径
* **Dijkstra 算法**:
* **单源最短路**。贪心策略。每次从未标记节点中选距离源点最近的节点,松弛其邻接点。
* **限制**:边权不能为负。
* **复杂度**:$O(n^2)$。
* **Floyd 算法**:
* **多源最短路**。动态规划。`A[i][j] = min(A[i][j], A[i][k] + A[k][j])`。
* **复杂度**:$O(n^3)$。
### 3. AOV 网与拓扑排序
* **定义**:顶点表示活动,边表示优先关系。
* **算法**:
1. 选择入度为 0 的顶点输出。
2. 删除该顶点及其出边。
3. 重复直到图空或无入度为 0 的点(说明有环)。
### 4. AOE 网与关键路径
* **定义**:边表示活动,边权为持续时间,顶点表示事件。
* **关键路径**:从源点到汇点的最长路径(决定工程最短工期)。
* **计算**:
* 事件最早发生时间 `ve`(从前向后取max)。
* 事件最晚发生时间 `vl`(从后向前取min)。
* 活动最早/最晚开始时间 `e` / `l`。
* 若 `e == l`,则为关键活动。
---
## 第9章:查找 (Search)
### 1. 动态查找树
* **二叉排序树 (BST)**:左 < 根 < 右。中序遍历有序。插入/删除/查找平均 $O(\log n)$,最坏 $O(n)$(退化为链表)。
* **平衡二叉树 (AVL)**:
* 任一节点左右子树高度差(平衡因子 BF)绝对值 $\le 1$。
* **调整**:
* **LL型**:右旋(Right Rotation)。
* **RR型**:左旋(Left Rotation)。
* **LR型**:先左旋后右旋。
* **RL型**:先右旋后左旋。
### 2. 散列 (Hashing)
* **散列函数**:`H(key)`。除留余数法 `H(key) = key % p` (p 为不大于表长的最大质数)。
* **处理冲突**:
* **开放定址法**:
* 线性探测:`di = 1, 2, 3...` (易产生聚集/堆积现象)。
* 二次探测:`di = 1^2, -1^2, 2^2, -2^2...`。
* **链地址法**:相同 Hash 值的元素存入同一链表。
* **ASL (平均查找长度)**:衡量效率指标,依赖于装填因子 $\alpha = \frac{填入元素个数}{表长}$。
---
## 第10章:排序 (Sorting)
### 1. 算法复杂度与稳定性对比
| 类别 | 排序算法 | 平均时间 | 最坏时间 | 空间 | 稳定性 | 备注 |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| **插入** | **直接插入** | $O(n^2)$ | $O(n^2)$ | $O(1)$ | **稳定** | 适合基本有序/n较小 |
| | **希尔排序** | $O(n^{1.3})$ | $O(n^2)$ | $O(1)$ | 不稳定 | 增量 $d$ 逐渐减半 |
| **交换** | **冒泡排序** | $O(n^2)$ | $O(n^2)$ | $O(1)$ | **稳定** | 标志位优化 |
| | **快速排序** | $O(n \log n)$ | $O(n^2)$ | $O(\log n)$ | 不稳定 | **内排序最快**,枢轴选择关键 |
| **选择** | **简单选择** | $O(n^2)$ | $O(n^2)$ | $O(1)$ | 不稳定 | 移动次数少 |
| | **堆排序** | $O(n \log n)$ | $O(n \log n)$ | $O(1)$ | 不稳定 | 适合 Top K 问题 |
| **归并** | **归并排序** | $O(n \log n)$ | $O(n \log n)$ | $O(n)$ | **稳定** | 空间换时间,外排序基础 |
| **基数** | **基数排序** | $O(d(n+r))$ | $O(d(n+r))$ | $O(r)$ | **稳定** | 无需比较,分配+收集 |
### 2. 核心算法说明
* **快速排序 (Quick Sort)**:
* **Partition**:选取枢轴(pivot),将小于 pivot 的放左边,大于的放右边。
* 最坏情况:有序或逆序(退化为冒泡)。
* **堆排序 (Heap Sort)**:
* **建堆**:从 $\lfloor n/2 \rfloor$ 处开始向下调整 (Heapify)。
* **排序**:交换堆顶与末尾元素,堆大小减1,重新向下调整。
* 升序排序使用**大顶堆**。
* **归并排序**:
* Divide (分):将数组对半切分。
* Conquer (治):递归排序子数组。
* Merge (合):合并两个有序数组。
---
---
## 附录:数据结构小测涉及公式与重点总结
通过分析《小测1 答案》、《小测2 答案》及《小测3_题目答案》,以下是针对考试重点涉及的公式、推导及核心结论的详细总结。
### 1. 栈与队列 (Stack & Queue)
#### **卡特兰数 (Catalan Number)**
* **应用场景**:$n$ 个不同元素进栈,可能的出栈序列总数。
* **公式**:
$$C_n = \frac{1}{n+1} C_{2n}^n = \frac{(2n)!}{(n+1)!n!}$$
* **小测案例** (小测1 第6题):
* $n=3$ 时,出栈序列数为 $C_3 = \frac{C_6^3}{4} = \frac{20}{4} = 5$ 种。
#### **双端队列输出序列**
* **排除法逻辑** (小测1 第8题):
* 对于输入受限或输出受限的双端队列,通常通过模拟和排除法来判断非法序列。
* *核心技巧*:结合栈的后进先出特性与队列的先进先出特性进行模拟。
---
### 2. 数组与矩阵 (Arrays & Matrices)
#### **三对角矩阵压缩存储**
* **定义**:元素仅分布在主对角线及上下两条对角线上。
* **下标映射公式** (小测1 第10题):
* 题目特定映射关系:$A[1..n][1..n]$ 压缩至 $B[1..3n-2]$。
* 给定公式推导结果:$k = 2i + j - 2$。
* *案例验证*:$A[66][65]$ 对应 $k = 2 \times 66 + 65 - 2 = 195$。
* *注意*:通用公式通常为 $k = 2i + j - 3$ 或类似,考试时需严格根据题目给定的数组下标起始点(如从0还是从1开始)及排列方式进行推导。
#### **KMP 算法 Next 数组计算**
* **推导逻辑** (小测1 第13题):
* 若 $S[j] == S[k]$ (其中 $k = next[j-1]$),则 $next[j] = k + 1$。
* 若不等,则令 $k = next[k]$ 继续回溯比较。
* *案例*:$S = 'aaab'$
* $j=1, S='a', next[1]=0$ (固定)
* $j=2, S='a', S[1]==S[0]$? (前缀比较), $next[2]=1$
* $j=3, S='a', S[2]==S[1] \rightarrow next[3]=2$
* $j=4, S='b', S[3] \neq S[2]$, 回溯...
---
### 3. 树与二叉树 (Trees)
#### **二叉树节点性质公式**
1. **度数关系**:
* $$n_0 = n_2 + 1$$ (叶子结点数 = 度为2的结点数 + 1)
* 推导依据:
* 总节点数 $n = n_0 + n_1 + n_2$
* 总分支数 $B = n - 1 = n_1 + 2n_2$
* 联立消去 $n_1$,得 $n_0 = n_2 + 1$。
* *小测陷阱* (小测2 第1题):已知总数 $n=98$,$n_1=48$,求叶子数。
* $n_0 + 48 + n_2 = 98 \Rightarrow n_0 + n_2 = 50$
* 代入 $n_0 = n_2 + 1 \Rightarrow 2n_2 + 1 = 50 \Rightarrow 2n_2 = 49$。
* $n_2$ 必须为整数,故**不存在这样的树**。
2. **完全二叉树 (Complete Binary Tree)**
* **叶子结点数**:$$n_0 = \lceil n/2 \rceil$$ (当 $n$ 为奇数时 $n_0 = (n+1)/2$,偶数时 $n_0 = n/2$)。
* *案例* (小测2 第3题):$n=47$ (奇数),$n_0 = (47+1)/2 = 24$。
3. **哈夫曼树 (Huffman Tree)**
* **特性**:只有度为0和度为2的结点,**不存在度为1的结点** ($n_1 = 0$)。
* **节点总数公式**:
$$n = 2n_0 - 1$$
* *推导*:$n = n_0 + n_2$,且 $n_2 = n_0 - 1 \Rightarrow n = n_0 + (n_0 - 1) = 2n_0 - 1$。
* *小测结论* (小测2 第12题):若有 $n$ 个叶子结点(题目用 $n$ 表示 $n_0$),则总结点数为 $2n - 1$。
4. **森林与树的转换**
* **连通分量/树的数量计算** (小测2 第9题):
* 若森林有 $N$ 个结点,$K$ 条边,则森林中树的棵数 $T$ 为:
$$T = N - K$$
* *推导*:每棵树 (连通分量) 的边数 = 节点数 - 1。设第 $i$ 棵树有 $v_i$ 个节点,则边数 $e_i = v_i - 1$。
* 总边数 $K = \sum (v_i - 1) = \sum v_i - \sum 1 = N - T \Rightarrow T = N - K$。
---
### 4. 图 (Graphs)
#### **握手定理 (Handshaking Lemma)**
* **公式**:所有顶点的度数之和等于边数的2倍。
$$\sum_{v \in V} \deg(v) = 2|E|$$
* **最少顶点数求解** (小测3 第2题):
* 已知:$|E|=16$ (度数和32)。
* 已知点:3个度4,4个度3。已知度数和 $3 \times 4 + 4 \times 3 = 24$。
* 剩余度数:$32 - 24 = 8$。
* 约束:其余顶点度数 $<3$ (即最大为2)。
* 目标:顶点数最少 $\rightarrow$ 剩余顶点度数尽可能大 $\rightarrow$ 设其余点度数均为2。
* 剩余点数:$8 / 2 = 4$ 个。
* 总顶点数:$3 + 4 + 4 = 11$ 个。
#### **最小生成树 (MST)**
* **唯一性判定** (小测3 第8题):
* 当无向连通图的**最小生成树不唯一**时,Prim 和 Kruskal 算法生成的结果可能不同。
* 当最小生成树唯一时,两者结果必然相同。
#### **关键路径 (Critical Path)**
* **计算方法** (小测3 第11题):
* 路径长度 = 路径上所有活动(边)持续时间之和。
* 关键路径 = 源点到汇点的**最长路径**。
* *案例*:路径 $V_0 \rightarrow V_1 \rightarrow V_4 \rightarrow V_6 \rightarrow V_8$。
* 长度计算:$6 + 1 + 9 + 2 = 18$。
## 排序算法深度解析 (Sorting Algorithms Deep Dive)
本文档对常见的排序算法进行展开讲解,包含**核心思想**、**算法步骤**、**过程演示**、**复杂度分析**及**代码逻辑**。
---
### 1. 插入排序类 (Insertion Sorts)
#### 1.1 直接插入排序 (Direct Insertion Sort)
**核心思想**:
将数组分为“已排序区间”和“未排序区间”。每次从未排序区间取出一个元素,在已排序区间中从后向前扫描,找到合适位置插入。类似于**抓扑克牌**的过程。
**算法步骤**:
1. 默认第 1 个元素已排序。
2. 取出第 2 个元素 `temp`,与第 1 个比较。若 `temp < arr[1]`,则 `arr[1]` 后移,`temp` 插入开头。
3. 重复直到所有元素归位。
**过程演示** (排序 `[5, 2, 4, 6, 1, 3]`):
* 初始:`[5]` | `2, 4, 6, 1, 3`
* 取 2:2 比 5 小,5 后移。 -> `[2, 5]` | `4, 6, 1, 3`
* 取 4:4 比 5 小,5 后移;4 比 2 大,停。 -> `[2, 4, 5]` | `6, 1, 3`
* ...以此类推。
**核心代码逻辑 (C/C++)**:
```c
void InsertSort(int A[], int n) {
int i, j, temp;
for (i = 1; i < n; i++) { // 从第2个元素开始
if (A[i] < A[i-1]) { // 若小于前驱,需插入
temp = A[i]; // 暂存当前元素
for (j = i-1; j >= 0 && A[j] > temp; --j) {
A[j+1] = A[j]; // 大于temp的元素后移
}
A[j+1] = temp; // 插入
}
}
}
```
**分析**:
* **最好情况**:$O(n)$(已是有序,只需比较不移动)。
* **最坏情况**:$O(n^2)$(逆序)。
* **稳定性**:**稳定**(因为 `A[j] > temp` 才移动,相等时不移动)。
---
#### 1.2 希尔排序 (Shell Sort)
**核心思想**:
又称“**缩小增量排序**”。是直接插入排序的优化版。通过将数组按增量 `d` 分组,对每组进行直接插入排序,使数组“基本有序”,最后 `d=1` 时再进行一次直接插入。
**为什么比直接插入快?**
直接插入在基本有序时效率极高。希尔排序前期移动步长长,能快速消除逆序对;后期步长短但数组已基本有序,移动次数少。
**过程演示**:
数组 `[8, 9, 1, 7, 2, 3, 5, 4, 6, 0]`,$n=10$。
1. **d=5** (分5组: `8,3`, `9,5`, `1,4`, `7,6`, `2,0`):
* 组内排序后 -> `[3, 5, 1, 6, 0, 8, 9, 4, 7, 2]`
2. **d=2** (分2组: `3,1,0,9,7`, `5,6,8,4,2`):
* 组内排序后 -> `[0, 2, 1, 4, 3, 5, 7, 6, 9, 8]`
3. **d=1** (整体直接插入):
* 微调 -> `[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`
**分析**:
* **复杂度**:依赖于增量序列,平均约 $O(n^{1.3})$。
* **稳定性**:**不稳定**(相同元素可能分在不同组,跳跃式移动导致相对位置改变)。
---
### 2. 交换排序类 (Exchange Sorts)
#### 2.1 冒泡排序 (Bubble Sort)
**核心思想**:
两两比较相邻记录,如果反序则交换,直到没有反序的记录为止。每趟遍历将最大的元素“浮”到最后(或最小的“沉”到最前)。
**优化点**:
设置 `flag` 变量。若某一趟遍历中**没有发生任何交换**,说明已经有序,直接结束算法。
**分析**:
* **最好情况**:$O(n)$(有序,第一趟 `flag` 未变直接退出)。
* **最坏情况**:$O(n^2)$。
* **稳定性**:**稳定**。
---
#### 2.2 快速排序 (Quick Sort) —— **最重要的排序算法**
**核心思想**:
**分治法 (Divide and Conquer)**。
1. **选基准 (Pivot)**:通常选第一个元素。
2. **划分 (Partition)**:将比基准小的移到左边,比基准大的移到右边。基准归位。
3. **递归**:对左右两个子序列重复上述过程。
**Partition 过程详解 (双指针法)**:
设基准 `pivot = A[low]`,`low` 指向首,`high` 指向尾。
1. **High 向左找小**:`while (low < high && A[high] >= pivot) high--;` 找到比 pivot 小的,移到 `low` 处 (`A[low] = A[high]`)。
2. **Low 向右找大**:`while (low < high && A[low] <= pivot) low++;` 找到比 pivot 大的,移到 `high` 处 (`A[high] = A[low]`)。
3. 重复直到 `low == high`,将 `pivot` 放入 `A[low]`。
**代码片段**:
```c
int Partition(int A[], int low, int high) {
int pivot = A[low]; // 选第一个为基准
while (low < high) {
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;
return low; // 返回基准位置
}
```
**分析**:
* **平均复杂度**:$O(n \log n)$。
* **最坏复杂度**:$O(n^2)$。发生在**有序**或**逆序**时,基准每次都选到最大/最小,退化为冒泡。
* **空间复杂度**:$O(\log n)$ (递归栈的高度)。
* **稳定性**:**不稳定**(Partition 过程中的远距离交换)。
---
### 3. 选择排序类 (Selection Sorts)
#### 3.1 简单选择排序 (Simple Selection Sort)
**核心思想**:
第 $i$ 趟从 $A[i...n]$ 中选出最小的元素,与 $A[i]$ 交换。
**特点**:
* **交换次数少**:最多 $n-1$ 次(优于冒泡)。
* **比较次数多**:固定 $\frac{n(n-1)}{2}$ 次。
* **稳定性**:**不稳定**。
* *例子*:`[5, 8, 5, 2, 9]`。第一趟,第一个 `5` 会和 `2` 交换,跑到第二个 `5` 后面。
---
#### 3.2 堆排序 (Heap Sort)
**核心思想**:
利用**堆**(完全二叉树)这种数据结构。
* **大顶堆**:$A[i] \ge A[2i+1]$ 且 $A[i] \ge A[2i+2]$。用于**升序**排序。
* **小顶堆**:用于降序排序。
**算法步骤**:
1. **建堆**:将无序数组构造成一个大顶堆。
* 从最后一个非叶子结点 ($\lfloor n/2 \rfloor - 1$) 开始,从右至左,从下至上进行“下沉”调整 (`HeapAdjust` / `sift_down`)。
2. **排序**:
* 将堆顶元素(最大值)与堆底末尾元素交换。
* 将剩余 $n-1$ 个元素重新调整为大顶堆。
* 重复直至堆大小为 1。
**下沉 (sift_down) 逻辑**:
判断节点 `k`,其左孩子 `2k+1`,右孩子 `2k+2`。
若孩子比父节点大,则将最大的孩子与父节点交换,并继续向下追踪被交换的孩子节点。
**分析**:
* **复杂度**:$O(n \log n)$。建堆 $O(n)$,调整 $O(n \log n)$。
* **优势**:在 Top K 问题(选出前 K 大)中表现极佳。
* **稳定性**:**不稳定**。
---
### 4. 归并排序 (Merge Sort)
**核心思想**:
**分治法**。
1. **分**:将数组从中间切开,递归切分,直到长度为 1。
2. **治 (Merge)**:将两个**有序**的子数组合并成一个有序数组。
**Merge 过程**:
需申请一个辅助数组 `B[]`。
比较左半区 `A[i]` 和右半区 `A[j]`:
* 若 `A[i] <= A[j]`,将 `A[i]` 放入 `B`,`i++`。
* 若 `A[i] > A[j]`,将 `A[j]` 放入 `B`,`j++`。
* 最后将剩余元素复制进 `B`,再把 `B` 拷回 `A`。
**分析**:
* **复杂度**:$O(n \log n)$。
* **空间复杂度**:$O(n)$(需要辅助数组,这是它最大的缺点)。
* **稳定性**:**稳定**(Merge 时 `A[i] <= A[j]` 优先取左边,保证相对位置不变)。
---
### 5. 基数排序 (Radix Sort)
**核心思想**:
**不基于比较**的排序。利用“分配”和“收集”。
通常针对非负整数。将整数按位数切割成不同的数字,按每个位数分别比较。
**步骤 (LSD - Least Significant Digit)**:
1. **按个位排序**:准备 10 个桶 (0-9)。遍历数组,按个位数字放入对应桶。按顺序收集回来。
2. **按十位排序**:基于上一步结果,按十位放入桶,再收集。
3. ...直至最高位。
**分析**:
* **复杂度**:$O(d(n+r))$。
* $d$:最大数字的位数(决定遍历几趟)。
* $n$:元素个数。
* $r$:基数(如十进制 r=10,决定桶的个数)。
* **稳定性**:**稳定**(必须稳定,否则高位排序会打乱低位的顺序)。
---
### 6. 总结对比表
| 算法 | 平均时间 | 最好 | 最坏 | 空间 | 稳定性 | 关键词 |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| **直接插入** | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | **稳定** | 摸牌、越有序越快 |
| **希尔** | $O(n^{1.3})$ | - | $O(n^2)$ | $O(1)$ | 不稳定 | 增量分组 |
| **冒泡** | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | **稳定** | 交换、flag优化 |
| **快速** | $O(n \log n)$ | $O(n \log n)$ | $O(n^2)$ | $O(\log n)$ | 不稳定 | **Partition**、分治 |
| **简单选择** | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | 不稳定 | 选最小 |
| **堆排序** | $O(n \log n)$ | $O(n \log n)$ | $O(n \log n)$ | $O(1)$ | 不稳定 | **建堆**、下沉 |
| **归并** | $O(n \log n)$ | $O(n \log n)$ | $O(n \log n)$ | $O(n)$ | **稳定** | **合并**、空间换时间 |