--- 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)$ | **稳定** | **合并**、空间换时间 |