算法设计题解题框架

🔥 高优先级
408 大题第 41、42 题通常为算法设计题(数据结构部分),满分 13~15 分,是拉开差距的关键。 掌握套路、熟悉模板,考场上即使无法一步到位写出最优解,也能争取大部分分数。

答题格式规范

算法设计题通常分三个子问:

  1. 给出算法的基本设计思想(约 4 分)
  2. 用 C/C++ 描述算法,关键处给注释(约 7~9 分)
  3. 说明时间复杂度和空间复杂度(约 2 分)

💡 答题技巧:即使写不出完整代码,只要 设计思想正确伪代码逻辑合理,通常可以拿到一半以上分数。

复杂度写法模板:

时间复杂度:O(n)    // 说明为何是此量级(扫描了几遍 / 循环嵌套层数)
空间复杂度:O(1)    // 仅使用常数个辅助变量

历年考题数据结构分布

年份41 题42 题
2009图(最短路径判断)链表(倒数第 k 个结点,双指针)
2010散列表(构造)顺序表(循环左移,三次逆置)
2011图(邻接矩阵/关键路径)顺序表(两升序序列中位数,二分)
2012哈夫曼树/归并排序链表(共享后缀,找公共结点)
2013顺序表(主元素,Boyer-Moore 投票)查找(最优二叉查找树/散列表)
2014二叉树(WPL 计算,层序遍历)图(OSPF 最短路径,网络题)
2015链表(绝对值去重,哈希辅助)图(邻接矩阵,DFS/BFS)
2016网络(TCP 拥塞控制)树(正则 k 叉树结点数推导)
2017二叉树(表达式树→中缀,中序遍历)图(Prim 最小生成树)
2018顺序表(未出现的最小正整数,桶标记)图(最小生成树,城市光缆)
2019链表(重排 L’,快慢指针+逆置)队列(栈实现队列)
2020顺序表(三元组最小距离,三指针)前缀编码(哈夫曼/字典树)
2021图(EL 路径判断,度的统计)排序(分析某排序算法)
2022二叉排序树(顺序存储,层序恢复)顺序表(最小的 10 个数,堆)
2023图(邻接矩阵,DFS/BFS 应用)外部排序(置换-选择,归并段)
2024图(拓扑排序,邻接矩阵)散列表(构造与查找)
2025顺序表(子数组最大乘积,后缀最值)图(AOE 网,关键路径)
2026二叉搜索树(最近差值结点,中序)AOE 网(关键活动)

规律总结:

  • 41 题:近年偏向 顺序表/数组,偶发链表、二叉树。
  • 42 题:近年偏向 图(生成树、拓扑、路径)排序/查找,也有树题。
  • 数组、链表、树、图 四类结构轮流出现,不会连续两年考同一类。

解题思路选择框架

拿到题目,先判断数据结构类型,再套对应套路:

输入的数据结构是什么?数组 / 顺序表链表二叉树有序数组 / 两端关系双指针 / 二分查找值域明确 / 出现性判断桶标记 / 原地哈希每个位置依赖左右信息前缀 / 后缀预处理排序兜底 O(n log n)找中间 / 倒数 k快慢双指针需要逆序访问原地逆置两个链表共享后缀长度对齐 / 换头BST / 递增序列中序遍历找差值、合法性、第 k 小按层处理 / 顺序存储层序遍历 BFS队列驱动路径 / 统计 / 高度递归 DFS连通性 / 可达性 / 路径BFS / DFS前驱约束 / 工程顺序拓扑排序最短路 / 最小生成树Dijkstra / PrimKruskal 也常用AOE 关键路径考场使用顺序:先识别数据结构 → 再看题目关键词 → 选择模板 → 最后补复杂度关键词示例:有序 / 第 k / 共享后缀 / BST / 入度 / 最短路 / 工期 / 只允许 O(1) 空间
输入是数组/顺序表?
  ├─ 有序数组 → 优先考虑双指针 / 二分查找
  ├─ 无序数组 → 排序兜底 / 桶标记 / 后缀预处理
  └─ 需要 O(n) 且不能用额外空间 → 原地标记 / 投票算法

输入是链表?
  ├─ 找中间 / 倒数 k → 快慢双指针
  ├─ 需要逆序访问 → 原地逆置后再处理
  └─ 共享结构 → 从尾部对齐,找公共结点

输入是二叉树?
  ├─ 遍历相关 → 中序(BST 有序性)/ 层序(BFS)
  └─ 路径/统计 → 递归 DFS,注意返回值设计

输入是图?
  ├─ 连通性 / 路径 → BFS / DFS
  ├─ 最短路径 → Dijkstra(不能用贪心的局部最优证明)
  ├─ 最小生成树 → Prim / Kruskal
  └─ 依赖关系 → 拓扑排序 / AOE 关键路径

数组题套路

兜底方案:排序法

对于数组类题目,如果想不到 O(n) 方法,先排序再处理 几乎总能把复杂度降到 O(n log n),并且逻辑简单:

  • 对有序数组,双指针夹逼效果极好
  • 查找、去重、区间问题在有序状态下可以二分
// 模板:排序 + 双指针
qsort(a, n, sizeof(int), cmp);
int left = 0, right = n - 1;
while (left < right) {
    // 根据题意移动指针
    if (condition) left++;
    else right--;
}

⚠️ 排序会破坏原始顺序,如果题目要求 不改变顺序 则不能用此法。

桶标记(原地哈希)

适用:数组元素取值范围已知(如 0 ≤ a[i] < n),需要 O(n) 时间 O(1) 空间判断某值是否出现。

思路:将 a[i] 映射到下标,用符号(正负号)或额外标记位记录"出现"状态。

Boyer-Moore 投票算法 — 主元素01234567334231333/13/23/13/0→2/13/23/13/23/3num/cntcnt+1(相同)cnt-1(不同)cnt→0 时更换候选最终候选 = 3① 第一遍扫描(对消法)→ 候选 num = 3② 第二遍验证:出现 5 次 > 8/2 = 4 → 3 是主元素 ✓桶标记(原地哈希) — 未出现的最小正整数0123-5323① 将范围外的元素置 00-5 → 0保留保留保留② 用下标符号标记出现过的正整数a[3-1]=a[2] 标负;a[2-1]=a[1] 标负0-32 出现 ✓-23 出现 ✓+3仍正 → 4 未出现③ 找第一个仍为正数的位置答:未出现的最小正整数 = 4

经典题:2018年41题——数组中未出现的最小正整数

int findMissMin(int a[], int n) {
    // 第一遍:把 [1,n] 范围外的元素置 0(不关心)
    for (int i = 0; i < n; i++)
        if (a[i] <= 0 || a[i] > n) a[i] = 0;
    // 第二遍:用下标 a[i]-1 处的符号标记 a[i] 出现过
    for (int i = 0; i < n; i++) {
        int v = abs(a[i]);
        if (v >= 1 && v <= n && a[v-1] > 0)
            a[v-1] = -a[v-1];
        // v==0 说明原来就是"不关心"值,跳过
    }
    // 第三遍:第一个正数位置就是答案
    for (int i = 0; i < n; i++)
        if (a[i] > 0) return i + 1;
    return n + 1;
}

后缀/前缀预处理

适用:需要对每个元素计算"它右边/左边的最值/乘积",一次预处理可把 O(n²) 降到 O(n)。

经典题:2025年41题——子数组乘积最大值

void calMulMax(int A[], int res[], int n) {
    int maxVal = INT_MIN, minVal = INT_MAX;
    // 从右向左维护后缀最大值和最小值
    for (int i = n - 1; i >= 0; i--) {
        if (A[i] > maxVal) maxVal = A[i];
        if (A[i] < minVal) minVal = A[i];
        // 正数乘以最大值,负数乘以最小值
        res[i] = (A[i] >= 0) ? A[i] * maxVal : A[i] * minVal;
    }
}

投票算法

适用:在 O(n) 时间、O(1) 空间内找出 出现次数超过半数 的"主元素"。

经典题:2013年41题——主元素

int majority(int a[], int n) {
    int num = a[0], cnt = 1;
    // 第一遍:筛选候选元素(对消法)
    for (int i = 1; i < n; i++) {
        if (a[i] == num) cnt++;
        else if (--cnt == 0) { num = a[i]; cnt = 1; }
    }
    // 第二遍:验证候选元素是否真的超过半数
    int m = 0;
    for (int i = 0; i < n; i++) if (a[i] == num) m++;
    return (m > n / 2) ? num : -1;
}

多指针夹逼

适用:多个有序数组/集合,需要找最优组合(最小距离、最小差值等)。

三指针夹逼 — 三元组最小距离S1:-109S2:-25-101011S3:29173041步骤i(S1)j(S2)k(S3)min值D移动1-1-252-2552j++2-1-102-1024i++30-102-1022j++40102016i++910992 ✓最小规则:每步移动三者中的 最小值 对应的指针(移大值只会让D增大)关键直觉三值中最小者 = 距离贡献最大方移动它才有机会缩短距离后缀预处理 — 维护右侧最大值/最小值A:14-96← 从右到左扫描,维护 maxVal / minVali=3: max=6, min=6 → res[3] = 6×6=36res:6248136O(n)/O(1)

经典题:2020年41题——三元组最小距离

// 三指针分别指向三个升序数组头部
// 每次移动三者中最小值对应的指针
// 因为移动最大值只会让距离更大
int i = 0, j = 0, k = 0;
while (i < n1 && j < n2 && k < n3) {
    int D = calc_D(S1[i], S2[j], S3[k]);
    ans = min(ans, D);
    int minVal = min(S1[i], min(S2[j], S3[k]));
    if (S1[i] == minVal) i++;
    else if (S2[j] == minVal) j++;
    else k++;
}

链表题套路

快慢双指针 — 找倒数第 k 个结点head123456qp① p 先走 k=3 步此时 q 在结点 1q→4p→∅② p、q 同步移动直到 p == NULLq 即为倒数第 k 个答:结点 4(倒数第3)原地逆置 — 三指针迭代before:NULLprev1cur2nxt3NULLcur.next = prevafter full pass:NULL321NULL← new head (prev)

快慢双指针

用途:找中间结点、倒数第 k 个结点。

// 找中间结点(慢指针到中间时快指针到末尾)
NODE *slow = head->next, *fast = head->next;
while (fast != NULL && fast->next != NULL) {
    slow = slow->next;
    fast = fast->next->next;
}
// slow 此时指向中间结点

// 找倒数第 k 个(快指针先走 k 步)
NODE *p = head->next, *q = head->next;
for (int i = 0; i < k; i++) {
    if (p == NULL) return 0;  // k 超出链表长度
    p = p->next;
}
while (p != NULL) { p = p->next; q = q->next; }
// q 即为倒数第 k 个结点

原地逆置

用途:链表后半段逆置后可从两端同步处理,避免 O(n) 的随机访问。

// 将 start 及之后的链表原地逆置,返回新头
NODE *reverse(NODE *start) {
    NODE *prev = NULL, *cur = start;
    while (cur != NULL) {
        NODE *next = cur->next;
        cur->next = prev;
        prev = cur;
        cur = next;
    }
    return prev;  // 新头
}

经典组合:快慢指针找中点 + 逆置后半段 + 双指针归并(2019年41题)

找公共结点(Y 形链表)

两链表分别从头走到尾再换头,相遇点即公共结点(路径等长)。

NODE *p = headA, *q = headB;
while (p != q) {
    p = (p != NULL) ? p->next : headB;
    q = (q != NULL) ? q->next : headA;
}
return p;  // NULL(无公共结点)或公共结点

二叉树题套路

BST 中序遍历 — 天然递增序列10515371218中序遍历(左→根→右):35710121518中序结果有序 → 可借助它判断 BST 合法性、找差值最小结点(2026年)层序遍历(BFS)— 队列驱动ABCDEFQueue 状态:入:A → 出 A,入 B C队列 [B,C] → 出 B,入 D E;出 C,入 F队列 [D,E,F] → 依次出队(叶结点)访问顺序:ABCDEF

递归三要素

写递归前先明确:

  1. 函数语义:这个函数返回/做什么?
  2. 递归终止条件root == NULL 时返回什么?
  3. 递归关系:如何用子问题的结果合并得到当前问题的答案?

中序遍历利用 BST 有序性

BST 的中序遍历是递增序列,可用于:

  • 找第 k 小元素
  • 找与目标值差最小的结点(2026年41题)
  • 判断是否为合法 BST
// 中序遍历框架
void inorder(BTNode *root) {
    if (!root) return;
    inorder(root->left);
    visit(root);       // 处理当前结点
    inorder(root->right);
}

层序遍历(BFS)

适用:按层处理、顺序存储二叉树的恢复(2022年41题)。

// 用队列实现层序遍历
Queue Q; enqueue(Q, root);
while (!isEmpty(Q)) {
    BTNode *node = dequeue(Q);
    visit(node);
    if (node->left)  enqueue(Q, node->left);
    if (node->right) enqueue(Q, node->right);
}

图题套路

拓扑排序 — 入度队列法012345入度=0入度=1入度=1入度=2入度=2入度=1输出顺序(一种合法拓扑序):021345若最终 count < n(顶点数),说明图中有环,无法完成拓扑排序欧拉路径 — 奇数度顶点判断ABCDE度=3度=4度=3度=3度=3奇数度顶点:C、D(共 2 个)→ 存在欧拉路径 ✓(从C到D或D到C)奇数度=0 → 欧拉回路(任意起点)奇数度=2 → 欧拉路径(起/终为奇度点)奇数度=其他 → 不存在

DFS / BFS 框架

// DFS(邻接矩阵)
int visited[MAXV] = {0};
void DFS(MGraph G, int v) {
    visited[v] = 1;
    visit(v);
    for (int w = 0; w < G.numVertices; w++)
        if (G.Edge[v][w] && !visited[w])
            DFS(G, w);
}

// BFS(邻接矩阵)
void BFS(MGraph G, int v) {
    int visited[MAXV] = {0};
    Queue Q; enqueue(Q, v); visited[v] = 1;
    while (!isEmpty(Q)) {
        int u = dequeue(Q); visit(u);
        for (int w = 0; w < G.numVertices; w++)
            if (G.Edge[u][w] && !visited[w]) {
                visited[w] = 1; enqueue(Q, w);
            }
    }
}

拓扑排序

适用:判断有向图是否有环、给出工程先后顺序(2024年41题)。

// 基于入度数组的拓扑排序
int indegree[MAXV] = {0};
// 初始化入度...
Queue Q;
for (int v = 0; v < n; v++)
    if (indegree[v] == 0) enqueue(Q, v);
int count = 0;
while (!isEmpty(Q)) {
    int u = dequeue(Q); count++;
    for each neighbor w of u:
        if (--indegree[w] == 0) enqueue(Q, w);
}
return (count == n);  // true 说明无环

度的统计

适用:欧拉路径/回路判断(2021年41题)。

// 邻接矩阵中,第 v 行非零元素个数即为 v 的度
int degree(MGraph G, int v) {
    int d = 0;
    for (int w = 0; w < G.numVertices; w++)
        d += (G.Edge[v][w] != 0);
    return d;
}

常见暴力法及其优化方向

场景暴力法优化方向
在 n 个数中找第 k 小排序 O(n log n)建大根堆维护 top-k,O(n log k)
两升序数组找中位数合并后取中,O(n) 空间二分夹逼,O(log n) 时间 O(1) 空间
链表倒数第 k 个两次遍历 O(n)快慢双指针,一次遍历 O(n)
主元素(超半数)统计每个元素出现次数 O(n) 空间Boyer-Moore 投票 O(1) 空间
数组中位置关联问题双重循环 O(n²)前/后缀预处理 O(n)
三个有序数组最优组合三重循环 O(n³)三指针夹逼 O(n)
链表是否有公共结点存哈希集合 O(n) 空间双指针换头 O(1) 空间

💡 评分规则惯例:O(n) 空间换 O(n) 时间通常比 O(n²) 多得 1~2 分;O(1) 空间的最优解满分。


答题语言规范

设计思想描述模板(以数组题为例):

算法分两步: ① 先对数组进行一次预处理(说明做了什么),时间复杂度 O(…); ② 再用双指针/桶标记/…(说明核心操作),时间复杂度 O(…)。 整体时间复杂度 O(…),空间复杂度 O(…)。

代码注释要点:

// 关键变量含义要注释,e.g.
int cnt = 1;   // 候选主元素的"计数器",cnt>0 时 num 为候选

// 循环不变量要注释,e.g.
// 循环结束后,a[0..i-1] 中所有正整数已被标记

// 非显然的下标计算要注释,e.g.
a[v-1] = -a[v-1];  // 用 v-1 位置的符号记录"v 出现过"