Files
handsomezhuzhu.github.io/docs/sop/notes/data-structure-algorithm-notes.md
2026-01-13 01:00:54 +08:00

598 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 数据结构与算法笔记
date: 2026-01-13
descriptionHTML: '<span style="color:var(--description-font-color);">数据结构与算法详细复习及排序算法深度解析</span>'
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> | 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个度44个度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`
* 22 5 5 后移 -> `[2, 5]` | `4, 6, 1, 3`
* 取 44 比 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)$ | **稳定** | **合并**、空间换时间 |