同学们,数据结构是 408 的分值大头(和数据结构程序设计合计 45 分),也是唯一能靠「手熟」拿满的科目。这份讲义的原则:每个必考算法给一段能默写的标准代码 + 一个考场判断要点。最后两个月,把这里的代码在纸上各写三遍,比刷十套模拟卷都有用。
一、排序全家桶:先背表,再背代码
1.1 总表(选择题年年考)
| 算法 | 平均 | 最坏 | 最好 | 空间 | 稳定 | 一句话特征 |
|---|---|---|---|---|---|---|
| 直接插入 | O(n²) | O(n²) | O(n) | O(1) | ✔ | 基本有序时最快 |
| 折半插入 | O(n²) | O(n²) | — | O(1) | ✔ | 比较次数固定 O(nlogn) |
| 希尔 | — | — | — | O(1) | ✘ | 缩小增量,最后一趟必为 1 |
| 冒泡 | O(n²) | O(n²) | O(n) | O(1) | ✔ | 每趟定一个最大值 |
| 快排 | O(nlogn) | O(n²) | O(nlogn) | O(logn) | ✘ | 最坏=已有序 |
| 简单选择 | O(n²) | O(n²) | O(n²) | O(1) | ✘ | 比较次数与初始序列无关 |
| 堆排 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | ✘ | 比较次数也无关初始序列 |
| 归并 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | ✔ | nlogn 里唯一稳定 |
| 基数 | O(d(n+r)) | 同左 | 同左 | O(r) | ✔ | 不比较,按位分配 |
【高频坑】「比较次数与初始序列无关」的:简单选择、堆排(初始建堆后)、归并近似、折半插入;「移动次数与初始序列有关但比较无关」的是折半插入。「一趟结束后能保证一个元素最终位置」的:冒泡、简单选择、快排;归并和插入排序不能。
1.2 快速排序(挖坑法,标准默写版)
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]; // 比枢轴小的填坑,high 成新坑 while (low < high && a[low] <= pivot) low++; a[high] = a[low]; // 比枢轴大的填坑,low 成新坑 } a[low] = pivot; // 枢轴归位 return low; // 返回枢轴最终位置}
void QuickSort(int a[], int low, int high) { if (low < high) { int pos = Partition(a, low, high); QuickSort(a, low, pos - 1); QuickSort(a, pos + 1, high); }}【考场要点】快排第一趟结束后:枢轴左边全部 ≤ 它、右边全部 ≥ 它,但左右内部无序——「一趟后能否出现 XX 局面」的判断题就从这里出。空间复杂度 O(logn) 来自递归栈,最坏 O(n)。
1.3 堆排序(大根堆,1 号位存根)
void AdjustDown(int 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; // 比孩子大,就位 a[k] = a[i]; k = i; // 孩子上移,继续向下 } a[k] = a[0];}
void BuildMaxHeap(int a[], int len) { for (int i = len/2; i > 0; i--) // 从最后一个非叶结点开始 AdjustDown(a, i, len);}
void HeapSort(int a[], int len) { BuildMaxHeap(a, len); for (int i = len; i > 1; i--) { // n-1 趟 Swap(a[i], a[1]); // 堆顶(最大)换到末尾 AdjustDown(a, 1, i - 1); // 剩余元素重新调整 }}【考场要点】建堆时间 O(n),每次调整 O(logn);插入用「向上调整」、删除用「向下调整」;n 个元素建堆从 ⌊n/2⌋ 号结点开始。「输出前两个最大元素后数组的状态」是真题画风。
1.4 归并排序
void Merge(int a[], int b[], int low, int mid, int high) { int i = low, j = mid + 1, k = low; while (i <= mid && j <= high) b[k++] = (a[i] <= a[j]) ? a[i++] : a[j++]; // <= 保证稳定 while (i <= mid) b[k++] = a[i++]; while (j <= high) b[k++] = a[j++]; for (k = low; k <= high; k++) a[k] = b[k];}
void MergeSort(int a[], int b[], int low, int high) { if (low < high) { int mid = (low + high) / 2; MergeSort(a, b, low, mid); MergeSort(a, b, mid + 1, high); Merge(a, b, low, mid, high); }}【考场要点】2 路归并第 h 趟后有序子表长度为 2^h;n 个元素需要 ⌈log₂n⌉ 趟。「第 k 趟归并后的序列」先算子表长度再手动归并。
二、查找:折半 + 散列
2.1 折半查找(标准代码)
int BinarySearch(int a[], int n, int key) { int low = 0, high = n - 1, mid; while (low <= high) { mid = (low + high) / 2; if (a[mid] == key) return mid; else if (a[mid] > key) high = mid - 1; else low = mid + 1; } return -1;}- 判定树:mid = ⌊(low+high)/2⌋ 向下取整,左子树结点多时树形固定
- 成功 ASL ≈ log₂(n+1) − 1;失败对应外结点,失败比较次数 = 外结点所在层−1
- 判定树是平衡二叉排序树,中序遍历有序——判断题常考
2.2 散列表:ASL 计算(大题模板)
- 除留余数法:H(key) = key % p(p 取不大于表长的最大素数)
- 线性探测:冲突向后挪,会产生「堆积(聚集)」;平方探测、链地址法
- 成功 ASL = 每个元素探测次数之和 ÷ 元素个数;失败 ASL 的分母是 p(模 p 有 p 种余数),从每个余数位置数到空位为止
三、链表:算法设计题的主战场
3.1 头插法逆置(背到肌肉记忆)
LinkList Reverse(LinkList L) { LNode *p = L->next, *q; L->next = NULL; while (p) { q = p->next; // 保住后继 p->next = L->next; // 头插 L->next = p; p = q; } return L;}3.2 快慢指针找中点 / 判环
LNode *FindMiddle(LinkList L) { LNode *fast = L->next, *slow = L->next; while (fast && fast->next) { fast = fast->next->next; slow = slow->next; } return slow; // 奇数中点,偶数为前一个中点}判环:fast 每次两步、slow 每次一步,相遇则有环(fast 先到 NULL 则无环)。
【设计题套路】链表大题三板斧:头插逆置、双指针(快慢/前后)、递归合并。两个有序链表归并和「找倒数第 k 个」(前后指针间距 k 再同步走)务必动手写熟。
四、二叉树:遍历是一切的基础
4.1 非递归中序遍历(栈)
void InOrder2(BiTree T) { Stack S; InitStack(S); BiTree p = T; while (p || !IsEmpty(S)) { if (p) { // 一路向左,沿途入栈 Push(S, p); p = p->lchild; } else { // 左边到头,出栈访问,转右 Pop(S, p); Visit(p); p = p->rchild; } }}4.2 层次遍历(队列)
void LevelOrder(BiTree T) { Queue Q; InitQueue(Q); BiTree p; EnQueue(Q, T); while (!IsEmpty(Q)) { DeQueue(Q, p); Visit(p); if (p->lchild) EnQueue(Q, p->lchild); if (p->rchild) EnQueue(Q, p->rchild); }}4.3 遍历互推(选择题最高频没有之一)
- 中序 +(先序 | 后序 | 层序 任一)→ 唯一确定二叉树;先序 + 后序不能唯一确定
- 先序 = 中序的镜像思路:先序第一个是根,后序最后一个是根,回中序切两半递归
- 【真题手感】给先序 abdc 和中序 bdac 手画一棵树再数叶子——30 秒内必须完成
- 线索二叉树:ltag=1 时 lchild 指向前驱;n 个结点的二叉链表有 n+1 个空链域
4.4 树与森林
- 树 → 二叉树:「左孩子右兄弟」,转换后右子树必空(树的根无兄弟)
- 森林 → 二叉树:各树先各自转换,再依次挂到前一棵的右子树
- 树的先根遍历 = 对应二叉树的先序;后根遍历 = 二叉树的中序(高频判断题)
五、KMP:只考 next 数组
手算规则(统考从 1 编号,next[1]=0,next[2]=1):next[j] = 串前 j−1 个字符的最长相等前后缀长度 + 1。
以 abaabc 为例:next = 0 1 1 2 2 3。
优化 nextval:若 T[j] == T[next[j]],则 nextval[j] = nextval[next[j]],否则照抄 next[j]。
【考场要点】next 数组求错一位全题报废,算完用「失配时模式串滑动到哪」回验一遍;KMP 时间复杂度 O(m+n),与主串无关的部分只算模式串自身。
六、图:六大算法各有考点
| 算法 | 思想 | 复杂度 | 关键考点 |
|---|---|---|---|
| BFS/DFS | 队列/栈(递归) | 邻接矩阵 O(n²);邻接表 O(n+e) | 空间复杂度 O(n);DFS 判连通性 |
| Dijkstra | 贪心,按路径长度递增 | O(n²) | 不能处理负权边;贪心不回头 |
| Floyd | DP,依次以每个点为中转 | O(n³) | 三重循环顺序:中转点在最外层 |
| Prim | 从点扩张,维护 lowcost | O(n²) | 适合稠密图 |
| Kruskal | 边升序 + 并查集判环 | O(eloge) | 适合稀疏图,森林合并 |
| 拓扑排序 | 入度 0 入栈输出 | O(n+e) | 输出数 < 顶点数 ⇔ 有环 |
拓扑排序代码:
bool TopologicalSort(Graph G) { InitStack(S); for (int i = 0; i < G.vexnum; i++) if (indegree[i] == 0) Push(S, i); int count = 0; while (!IsEmpty(S)) { Pop(S, i); print[count++] = i; for (ArcNode *p = G.vertices[i].first; p; p = p->next) if (--indegree[p->adjvex] == 0) Push(S, p->adjvex); } return count >= G.vexnum; // 小于说明有回路}关键路径(AOE 网)四步法
- 事件最早发生:VE(j) = max{ VE(i) + w(i,j) },源点起从左往右
- 事件最迟发生:VL(i) = min{ VL(j) − w(i,j) },汇点起从右往左(初值 = VE(汇点))
- 活动最早开始:e = VE(弧尾事件);活动最迟开始:l = VL(弧头事件) − w
- 关键活动:l − e = 0;关键路径 = 关键活动连成的源到汇最长路径
【易错】缩短关键活动时间不一定缩短工期(可能有多条关键路径);缩短到一定程度,关键路径会换路——判断题陷阱专业户。
七、哈夫曼树与二叉排序树
哈夫曼
- n 个叶结点 → 共 2n−1 个结点,无度为 1 的结点
- WPL = Σ wᵢ × 深度ᵢ = 构造过程中每次合并之和的累加(两种算法结果相同)
- 哈夫曼树不唯一,但 WPL 唯一且最优;前缀码特性:任何编码不是其他编码的前缀
二叉排序树与 AVL
- BST 中序遍历递增;删除:叶直接删;只有一孩让孩子顶上;有两孩用中序前驱(左子树最右)或后继替代再删
- AVL 平衡因子 = 左高 − 右高,插入后找最小失衡子树的根判断四种旋转:
| 失配类型 | 判断 | 旋转 |
|---|---|---|
| LL | 新结点在失衡点左孩子的左子树 | 右单旋 |
| RR | 右孩子的右子树 | 左单旋 |
| LR | 左孩子的右子树 | 先左旋孩子再右旋自己 |
| RL | 右孩子的左子树 | 先右旋孩子再左旋自己 |
【口诀】「同侧单旋、异侧双旋,先旋孩子再旋自己」。
B 树与 B+ 树(m 阶)
- B 树非根内部结点关键字数 ⌈m/2⌉−1 ~ m−1,子树 ⌈m/2⌉ ~ m;根最少 1 个关键字 2 棵子树
- B+ 树:n 棵子树的结点有 n 个关键字;叶结点包含全部关键字并链表相连;非叶仅作索引——与 B 树的两大区别必背
冲刺建议:数据结构的大题只有三种命运——排序代码补全、树/图算法手工模拟、链表设计题。每天默写一个代码块(快排→堆排→归并→二分→非递归遍历→拓扑),同时每天手玩一道「给序列画 AVL / 哈夫曼 / Dijkstra」的模拟题。手上有代码,心里不慌,这是数据结构这科唯一的捷径。