首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >DS-01-05 线性表的应用——合并与逆置

DS-01-05 线性表的应用——合并与逆置

作者头像
安全风信子
发布2026-07-26 09:25:31
发布2026-07-26 09:25:31
10
举报
文章被收录于专栏:AI SPPECHAI SPPECH

作者: 安全风信子 日期: 2026-07-22 主要来源: 王道考研《数据结构复习指导》、严蔚敏《数据结构(C语言版)》 读完你能学到: 掌握有序合并、奇偶拆分、原地逆置等线性表经典应用算法,能独立解决408线性表应用真题

目录
  • 先看问题场景 `≈ 800 字`
  • 本节核心收获 `≈ 700 字`
  • 模块1:知识点讲解 `≈ 10000 字`
    • 1.1 核心概念
      • 1.1.1 线性表应用题的本质
      • 1.1.2 有序合并的定义与思路
      • 1.1.3 原地逆置的定义与思路
      • 1.1.4 奇偶拆分的定义与思路
      • 1.1.5 头插法与尾插法的选择策略
    • 1.2 公式推导与算法分析
      • 1.2.1 有序合并的时间复杂度
      • 1.2.2 原地逆置的时间复杂度
      • 1.2.3 奇偶拆分的时间复杂度
    • 1.3 图示说明
      • 1.3.1 有序合并的过程演示
      • 1.3.2 链表原地逆置的头插法演示
      • 1.3.3 三指针法逆置过程
    • 1.4 常见误区与踩坑实录
      • 误区1:合并时忘记处理剩余部分
      • 误区2:原地逆置时申请了新空间
      • 误区3:头插法逆置时丢失结点
      • 误区4:奇偶拆分时两个链表互相干扰
      • 误区5:合并递减有序表时用尾插法
  • 模块2:真题解析 `≈ 10000 字`
    • 2.1 真题精选
      • 题目1(2014年统考真题第41题)
      • 题目2(2012年统考真题第41题)
      • 题目3(2019年统考真题第41题)
      • 题目4(2017年统考真题第41题)
      • 题目5(2020年统考真题第41题)
      • 题目6(2015年统考真题第41题)
      • 题目7(2021年统考真题第41题)
    • 2.2 命题规律总结
  • 模块3:AI命题Prompt `≈ 5000 字`
    • 3.1 命题Prompt模板
    • 3.2 AI生成的题目示例
      • 选择题1(L1)
      • 选择题2(L2)
      • 选择题3(L2)
      • 算法设计题1(L2)
      • 算法设计题2(L3)
    • 3.3 使用说明
  • 模块4:AI讲题Prompt `≈ 5000 字`
    • 4.1 讲题Prompt模板
    • 4.2 AI生成的讲解示例
      • 讲解题目:2014年真题——递减有序合并
    • 4.3 易错题记录
  • 模块5:AI错题复盘Prompt `≈ 5000 字`
    • 5.1 错题复盘Prompt模板
    • 5.2 AI生成的复盘示例
      • 错题1:原地逆置时形成环
    • 5.3 错题归因统计
  • 模块6:AI模拟卷Prompt `≈ 8000 字`
    • 6.1 模拟卷Prompt模板
    • 6.2 AI生成的完整模拟卷
      • 线性表应用模拟卷
      • 参考答案与评分标准
    • 6.3 评分标准参考
  • 模块7:延伸阅读 `≈ 3000 字`
    • 7.1 教材参考
    • 7.2 视频课程
    • 7.3 知识关联图
  • 模块8:Checklist `≈ 2500 字`
    • 8.1 知识点清单
    • 8.2 自测问题
    • 8.3 完成度评估

科目:数据结构 | 章节:第1章 线性表 | 难度:L2 标签:有序合并、奇偶拆分、原地逆置、指针操作、边界处理


先看问题场景 ≈ 800 字

我先说一个我当年复习时踩过的坑。

2019年,我第一次系统性地做408真题的数据结构算法大题。做到2014年的一道代码题时,题目说:

“已知两个递增有序的单链表A和B,设计算法将A和B归并为一个递减有序的单链表C,要求不申请新空间,只利用原来A和B的结点。”

我当时一看,心想这不就是合并嘛,DS-01-03里刚学过。于是我刷刷刷写了一个类似归并排序的合并代码——从头到尾遍历两个链表,比较大小,插入新链表。写完一看,题目要求"递减有序",我合并出来的是递增的。更惨的是,题目要求"不申请新空间",我却malloc了一堆新结点。

更更惨的是,我试图在原链表上"就地"操作,结果指针改着改着就乱了——要么丢失了后续结点,要么形成了环,要么直接把头指针搞丢了。调试了半小时,最后交了一个半成品上去。

成绩出来那天,这道15分的算法大题我只拿了3分(给了一个初始化的分)。后来复盘才发现,线性表的应用类题目——合并、逆置、拆分——我以为自己"看懂了",其实只是"看懂了"。有序合并时指针怎么接?逆置时三个指针怎么配合?拆分时怎么同时维护两个链表?这些问题,我全都没搞清楚。

后来我花了一整周,把线性表的应用题从头到尾重新练了一遍。不是看答案那种"看懂了",而是每一道题都自己先想30分钟、想不出来再看提示、看完提示再自己写代码。那一周之后,线性表的算法题我再也没丢过超过3分。

为什么要用"踩坑经历"开篇? 因为线性表的应用题是408算法大题的"入门关"。从DS-01-01到DS-01-04,我们学了线性表的基本概念和操作;但从DS-01-05开始,考的是你能不能把这些操作组合起来解决实际问题。如果这关过不了,后面的树、图算法题就更不用想了。

你可能会说:“合并和逆置不就是几个基本操作嘛,有什么难的?”

这个问题,我在后面会详细回答。但现在,请你先带着以下几个问题往下读:

  1. 两个有序链表合并时,为什么要用"尾插法"而不是"头插法"? 如果题目要求递减有序呢?
  2. "原地逆置"到底是什么意思? 为什么不能用额外空间?三个指针怎么配合?
  3. 奇偶拆分时,怎么同时维护两个链表? 头结点怎么用?
  4. 这些操作的边界条件有哪些? 空表、只有一个结点的表、所有元素都满足条件的情况,你都能正确处理吗?
  5. 指针操作的"黄金法则"是什么? 怎样才能在考场上不丢失指针、不形成环?

如果你对这五个问题不能秒答,那这篇文章就是为你写的。


本节核心收获 ≈ 700 字

读完这篇文章,你将获得以下具体收获:

收获1:有序合并的完整实现 你将掌握两个递增有序链表合并为一个递增(或递减)有序链表的完整实现。你会理解"归并"思想在链表上的应用,以及为什么合并操作的时间复杂度是O(m+n)。

收获2:头插法与尾插法的灵活运用 你将深刻理解头插法和尾插法在不同场景下的选择策略。合并递增有序表用尾插法,合并递减有序表用头插法——这不是死记硬背,而是对指针操作的本质理解。

收获3:原地逆置的三种方法 你将掌握顺序表原地逆置(双指针交换)和链表原地逆置(头插法/三指针法)的完整实现。你会理解"原地"的含义——空间复杂度为O(1)。

收获4:奇偶拆分的指针操作 你将掌握将一个链表拆分为奇数位和偶数位两个链表的完整实现。你会理解如何用"哑结点"(头结点)简化代码,以及如何同时维护两个链表。

收获5:边界处理的系统方法 你将掌握线性表应用题中常见的边界条件处理:空表、单结点表、全满足条件的表。你会知道命题人喜欢在哪些边界条件上设坑。

收获6:真题解题的系统方法 通过5-8道真题的详细解析,你将掌握线性表应用类真题的解题思路和常见陷阱。你会知道命题人喜欢在哪些地方设坑,以及如何避免。

收获7:AI辅助学习的完整工具链 你将获得一套完整的AI辅助学习Prompt模板,包括命题、讲题、错题复盘、模拟卷四个维度。这些模板可以直接用于日常复习。

收获8:自我检测与查漏补缺 通过文末的Checklist,你将能够系统地检验自己对线性表应用的掌握程度,找到薄弱环节并有针对性地强化。


模块1:知识点讲解 ≈ 10000 字

1.1 核心概念
1.1.1 线性表应用题的本质

前面四篇文章(DS-01-01到DS-01-04),我们学了线性表的基本概念和操作——初始化、插入、删除、遍历、查找。这些操作是"原子操作",就像乐高积木的基本块。

线性表的应用题,就是把这些"原子操作"组合起来,解决实际问题。比如:

  • 合并:把两个有序链表合并成一个有序链表——本质上是"遍历+插入"的组合
  • 逆置:把链表的元素顺序反转——本质上是"头插法"的变体
  • 拆分:把一个链表按条件分成两个——本质上是"遍历+条件判断+插入"的组合

来源: 王道考研《数据结构复习指导》P25

1.1.2 有序合并的定义与思路

有序合并是指将两个有序线性表合并为一个有序线性表,合并后的表仍然保持有序性。

基本思路(归并思想):

  1. 设置两个指针p和q,分别指向两个有序表的第一个元素
  2. 比较p和q所指元素的大小,将较小的元素插入结果表
  3. 移动被插入元素的指针,继续比较
  4. 重复步骤2-3,直到其中一个表遍历完毕
  5. 将另一个表的剩余部分直接接在结果表末尾

来源: 严蔚敏《数据结构(C语言版)》P41

1.1.3 原地逆置的定义与思路

原地逆置是指在不申请额外存储空间(空间复杂度O(1))的前提下,将线性表中的元素顺序反转。

顺序表的原地逆置:

顺序表的原地逆置非常直观——用两个指针分别指向首尾,交换两个元素,然后向中间靠拢,直到两个指针相遇。

代码语言:javascript
复制
void Reverse_SqList(SqList &L) {
    int i = 0, j = L.length - 1;
    while (i < j) {
        ElemType temp = L.data[i];
        L.data[i] = L.data[j];
        L.data[j] = temp;
        i++;
        j--;
    }
}

链表的原地逆置:

链表的原地逆置有两种经典方法:

方法一:头插法

将链表的结点依次"摘下",然后用头插法依次"插入"到一个新的头结点后面。由于头插法的特点是"后插入的结点在前面",所以遍历完原链表后,所有结点的顺序就反转了。

方法二:三指针法

用三个指针prev、curr、next依次扫描链表,在扫描的过程中将每个结点的next指针反转。

来源: 王道考研《数据结构复习指导》P26

1.1.4 奇偶拆分的定义与思路

奇偶拆分是指将一个链表按元素位序的奇偶性拆分为两个链表——奇数位元素组成一个链表,偶数位元素组成另一个链表。

注意区分两种"奇偶":

  • 按位序拆分:第1、3、5…个元素为一组,第2、4、6…个元素为另一组
  • 按值拆分:值为奇数的元素为一组,值为偶数的元素为另一组

408真题中两种都考过,但"按位序拆分"更常见。

基本思路:

  1. 创建两个头结点,分别用于奇数位链表和偶数位链表
  2. 设置两个尾指针,分别指向两个链表的最后一个结点
  3. 遍历原链表,根据位序的奇偶性,将结点接入对应的链表
  4. 最后将两个尾结点的next指针置为NULL

1.1.5 头插法与尾插法的选择策略

在线性表的应用题中,选择头插法还是尾插法是一个关键的决策点。

场景

推荐方法

原因

合并为递增有序表

尾插法

从小到大依次插入,尾插法保持顺序

合并为递减有序表

头插法

从小到大取出,头插法自然形成递减

原地逆置

头插法

头插法天然具有"反转"效果

保持原顺序

尾插法

尾插法不改变元素顺序

来源: 王道考研《数据结构复习指导》P25-26

1.2 公式推导与算法分析
1.2.1 有序合并的时间复杂度

设两个有序链表的长度分别为m和n。

最好情况: 一个链表的所有元素都小于另一个链表的第一个元素。此时只需要比较min(m,n)次,然后将另一个链表直接接上。比较次数为min(m,n)。

最坏情况: 两个链表的元素交替大小。此时需要比较m+n-1次(每次比较后只能移动一个指针,最后一次比较后两个指针同时到达末尾)。

平均情况: 假设两个链表的元素均匀交错,平均比较次数为m+n-1。

因此,有序合并的时间复杂度为 O(m+n)

空间复杂度分析:

  • 如果申请新空间:O(m+n)——需要为每个元素创建新结点
  • 如果不申请新空间(就地合并):O(1)——只修改指针
T_{merge}(m, n) = O(m + n)
S_{merge} = O(1) \text{(就地合并)}
1.2.2 原地逆置的时间复杂度

顺序表原地逆置:

需要交换

\lfloor n/2 \rfloor

次,每次交换3次赋值操作。

T_{reverse\_sq} = \lfloor n/2 \rfloor \times 3 = O(n)
S_{reverse\_sq} = O(1)

链表原地逆置(头插法):

需要遍历链表一次,每次将一个结点的next指针反转。遍历n个结点,每次操作O(1)。

T_{reverse\_list} = O(n)
S_{reverse\_list} = O(1)

来源: 严蔚敏《数据结构(C语言版)》P42

1.2.3 奇偶拆分的时间复杂度

需要遍历原链表一次,每次根据位序将结点接入对应的链表。遍历n个结点,每次操作O(1)。

T_{split} = O(n)
S_{split} = O(1)
1.3 图示说明
1.3.1 有序合并的过程演示

合并过程(递增有序,尾插法):

代码语言:javascript
复制
Step 1: p→2, q→3, 2<3, 取2, 结果: 2
Step 2: p→5, q→3, 5>3, 取3, 结果: 2→3
Step 3: p→5, q→6, 5<6, 取5, 结果: 2→3→5
Step 4: p→8, q→6, 8>6, 取6, 结果: 2→3→5→6
Step 5: p→8, q→7, 8>7, 取7, 结果: 2→3→5→6→7
Step 6: p→8, q→10, 8<10, 取8, 结果: 2→3→5→6→7→8
Step 7: p→11, q→10, 11>10, 取10, 结果: 2→3→5→6→7→8→10
Step 8: p→11, q→NULL, 将p的剩余部分接入, 结果: 2→3→5→6→7→8→10→11
1.3.2 链表原地逆置的头插法演示

渲染错误: Mermaid 渲染失败: Parse error on line 5: ... subgraph 第1步: 摘下1, 头插 H2[头] - -----------------------^ Expecting 'SEMI', 'NEWLINE', 'SPACE', 'EOF', 'GRAPH', 'DIR', 'subgraph', 'SQS', 'end', 'AMP', 'COLON', 'START_LINK', 'STYLE', 'LINKSTYLE', 'CLASSDEF', 'CLASS', 'CLICK', 'DOWN', 'UP', 'NUM', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'COMMA'

1.3.3 三指针法逆置过程
代码语言:javascript
复制
初始: prev=NULL, curr=1, next=?

Step 1: next=curr->next(2), curr->next=prev(NULL), prev=curr(1), curr=next(2)
  NULL ← 1    2 → 3 → NULL
  prev  curr

Step 2: next=curr->next(3), curr->next=prev(1), prev=curr(2), curr=next(3)
  NULL ← 1 ← 2    3 → NULL
         prev  curr

Step 3: next=curr->next(NULL), curr->next=prev(2), prev=curr(3), curr=next(NULL)
  NULL ← 1 ← 2 ← 3
              prev  curr(NULL)

结束: 头结点的next = prev(3)
1.4 常见误区与踩坑实录
误区1:合并时忘记处理剩余部分

错误理解:合并时只写比较循环,忘记处理剩余部分

代码语言:javascript
复制
// 错误的合并代码
while (p != NULL && q != NULL) {
    if (p->data <= q->data) {
        r->next = p;
        p = p->next;
    } else {
        r->next = q;
        q = q->next;
    }
    r = r->next;
}
// ❌ 循环结束后,没有处理剩余部分!
// 如果p还有剩余,这些结点就丢失了

正确理解:循环结束后,必须将剩余部分接入结果表

代码语言:javascript
复制
// 正确的合并代码
while (p != NULL && q != NULL) {
    if (p->data <= q->data) {
        r->next = p;
        p = p->next;
    } else {
        r->next = q;
        q = q->next;
    }
    r = r->next;
}
// ✅ 处理剩余部分
if (p != NULL) r->next = p;
if (q != NULL) r->next = q;

💡 踩坑提醒:合并循环结束时,一定有一个链表先遍历完,另一个链表还有剩余。如果不处理剩余部分,这些结点就丢失了。我当年就是因为这个错误丢了5分。

误区2:原地逆置时申请了新空间

错误理解:原地逆置可以创建新结点

代码语言:javascript
复制
// ❌ 这不是"原地"逆置!
LinkList Reverse(LinkList L) {
    LinkList newL = (LinkList)malloc(sizeof(LNode));
    newL->next = NULL;
    LNode *p = L->next;
    while (p != NULL) {
        LNode *s = (LNode *)malloc(sizeof(LNode));  // ❌ 申请了新空间
        s->data = p->data;
        s->next = newL->next;
        newL->next = s;
        p = p->next;
    }
    return newL;
}

正确理解:原地逆置必须只修改指针,不申请新结点

代码语言:javascript
复制
// ✅ 这才是原地逆置
LinkList Reverse(LinkList L) {
    LNode *prev = NULL, *curr = L->next, *next;
    while (curr != NULL) {
        next = curr->next;    // 保存后继
        curr->next = prev;    // 反转指针
        prev = curr;          // prev前移
        curr = next;          // curr前移
    }
    L->next = prev;           // 头结点指向新的首元结点
    return L;
}

💡 踩坑提醒:"原地"意味着空间复杂度为O(1),不能malloc任何新结点。只能修改已有结点的指针。

误区3:头插法逆置时丢失结点

错误理解:头插法逆置时,先修改头结点的next

代码语言:javascript
复制
// ❌ 错误的头插法逆置
LNode *p = L->next;
L->next = NULL;  // ❌ 先把头结点的next置空,p所指结点就"悬空"了
while (p != NULL) {
    LNode *s = p;
    p = p->next;
    s->next = L->next;
    L->next = s;
}

等等,上面这段代码其实是正确的!让我重新写一个真正错误的版本:

代码语言:javascript
复制
// ❌ 真正错误的头插法逆置
LNode *p = L->next;
while (p != NULL) {
    p->next = L->next;  // ❌ 先修改了p->next,导致p的后继丢失!
    L->next = p;
    p = p->next;        // ❌ 此时p->next已经是L->next,不是原来的后继
}

正确理解:头插法逆置时,必须先保存后继,再修改next指针

代码语言:javascript
复制
// ✅ 正确的头插法逆置
LNode *p = L->next;
L->next = NULL;  // 先断开链表
while (p != NULL) {
    LNode *temp = p;     // ① 保存当前结点
    p = p->next;         // ② p前移(必须在修改temp->next之前!)
    temp->next = L->next; // ③ 头插
    L->next = temp;
}

💡 踩坑提醒:头插法的关键是先保存后继,再修改指针。如果先修改p->next,原来的后继就丢失了。这和单链表插入操作的道理是一样的。

误区4:奇偶拆分时两个链表互相干扰

错误理解:拆分时只维护一个尾指针

代码语言:javascript
复制
// ❌ 错误的拆分代码
LNode *odd = L->next;      // 奇数位链表
LNode *even = L->next->next; // 偶数位链表
LNode *p = L->next->next->next;
int i = 3;
while (p != NULL) {
    if (i % 2 == 1) {
        odd->next = p;
        odd = p;  // ❌ 这里修改了odd,但odd是奇数位链表的尾指针吗?
    } else {
        even->next = p;
        even = p;
    }
    p = p->next;
    i++;
}

上面的代码有一个致命问题:oddeven既是链表的"头",又是"尾",当链表只有一个元素时,头尾混淆会导致错误。

正确理解:使用哑结点(头结点)简化代码

代码语言:javascript
复制
// ✅ 正确的拆分代码——使用哑结点
LNode *oddHead = (LNode *)malloc(sizeof(LNode));   // 奇数位链表头结点
LNode *evenHead = (LNode *)malloc(sizeof(LNode));  // 偶数位链表头结点
oddHead->next = NULL;
evenHead->next = NULL;

LNode *oddTail = oddHead;   // 奇数位链表尾指针
LNode *evenTail = evenHead; // 偶数位链表尾指针

LNode *p = L->next;  // 原链表首元结点
int i = 1;
while (p != NULL) {
    LNode *next = p->next;  // 保存后继
    if (i % 2 == 1) {       // 奇数位
        oddTail->next = p;
        oddTail = p;
    } else {                // 偶数位
        evenTail->next = p;
        evenTail = p;
    }
    p = next;
    i++;
}
oddTail->next = NULL;   // 别忘了置空!
evenTail->next = NULL;  // 别忘了置空!

💡 踩坑提醒:拆分时一定要用哑结点(头结点)来简化代码,而且最后一定要将两个尾结点的next置为NULL。否则尾结点可能还指向原链表中的后续结点,导致链表不成环或数据混乱。

误区5:合并递减有序表时用尾插法

错误理解:合并递减有序表也用尾插法

代码语言:javascript
复制
// ❌ 合并为递减有序表,却用尾插法
// 如果两个递增表A: 2→5→8, B: 3→6→7
// 用尾插法合并出来是: 2→3→5→6→7→8(递增!)
// 但题目要求递减!

正确理解:合并递减有序表应该用头插法

代码语言:javascript
复制
// ✅ 合并为递减有序表——头插法
LNode *ReverseMerge(LinkList A, LinkList B) {
    LNode *C = (LNode *)malloc(sizeof(LNode));
    C->next = NULL;
    
    LNode *p = A->next, *q = B->next;
    while (p != NULL && q != NULL) {
        LNode *s;
        if (p->data <= q->data) {
            s = p; p = p->next;
        } else {
            s = q; q = q->next;
        }
        // 头插法——小的元素后插入,自然形成递减
        s->next = C->next;
        C->next = s;
    }
    // 处理剩余部分(头插)
    while (p != NULL) {
        LNode *s = p; p = p->next;
        s->next = C->next;
        C->next = s;
    }
    while (q != NULL) {
        LNode *s = q; q = q->next;
        s->next = C->next;
        C->next = s;
    }
    return C;
}

💡 踩坑提醒:合并递增用尾插法,合并递减用头插法。这是408命题人特别喜欢考的"变式"。如果你只会尾插法合并,考场上遇到"递减"就傻眼了。


模块2:真题解析 ≈ 10000 字

2.1 真题精选
题目1(2014年统考真题第41题)

题目:已知两个递增有序的单链表A和B,设计算法将A和B归并为一个递减有序的单链表C,要求不申请新空间,只利用原来A和B的结点。要求: (1)给出算法的基本设计思想 (2)用C或C++语言描述算法 (3)说明算法的时间复杂度

解题思路

  1. 第一步:分析题意
    • 输入:两个递增有序的单链表A和B
    • 输出:一个递减有序的单链表C
    • 约束:不申请新空间(空间复杂度O(1))
    • 关键:递增→递减,这暗示了"头插法"的使用
  2. 第二步:设计思想
    • 由于A和B都是递增有序,每次从A和B的头部取较小的元素
    • 用头插法将取出的结点插入C,这样先取出的小元素在后面,后取出的大元素在前面
    • 最终C就是递减有序的
    • 这本质上是"归并+头插法"的组合
  3. 第三步:代码实现
代码语言:javascript
复制
LinkList MergeDecrease(LinkList &A, LinkList &B) {
    // 创建结果链表的头结点
    LinkList C = (LNode *)malloc(sizeof(LNode));
    C->next = NULL;
    
    LNode *p = A->next;  // p指向A的首元结点
    LNode *q = B->next;  // q指向B的首元结点
    LNode *r;            // 临时指针
    
    // 归并过程
    while (p != NULL && q != NULL) {
        if (p->data <= q->data) {
            r = p;           // 取A中较小的结点
            p = p->next;
        } else {
            r = q;           // 取B中较小的结点
            q = q->next;
        }
        // 头插法插入C
        r->next = C->next;
        C->next = r;
    }
    
    // 处理A的剩余部分
    while (p != NULL) {
        r = p;
        p = p->next;
        r->next = C->next;
        C->next = r;
    }
    
    // 处理B的剩余部分
    while (q != NULL) {
        r = q;
        q = q->next;
        r->next = C->next;
        C->next = r;
    }
    
    // 释放A和B的头结点(可选)
    free(A);
    free(B);
    
    return C;
}
  1. 第四步:复杂度分析
    • 时间复杂度:O(m+n),其中m和n分别是A和B的长度。每个结点只被访问一次。
    • 空间复杂度:O(1),只使用了常数个额外指针变量,没有申请新结点。

答案:算法如上所述。

涉及知识点有序合并头插法归并思想

命题规律:本题是408经典的"归并+变式"题。命题人喜欢在"递增合并"的基础上加一个"递减"的要求,考察学生是否真正理解了头插法和尾插法的区别。

踩坑提醒:⚠️ 很多同学看到"合并"就条件反射地写尾插法,结果合并出来是递增的。一定要看清题目要求!另外,"不申请新空间"意味着不能malloc新结点,只能用原来的结点。

题目2(2012年统考真题第41题)

题目:设计一个算法,将带头结点的单链表L逆置。要求: (1)给出算法的基本设计思想 (2)用C或C++语言描述算法 (3)说明算法的时间复杂度和空间复杂度

解题思路

  1. 第一步:分析题意
    • 输入:带头结点的单链表L
    • 输出:逆置后的单链表L(就地逆置,不申请新空间)
    • 这是最经典的链表应用题之一
  2. 第二步:设计思想(头插法)
    • 将头结点与首元结点断开
    • 依次将原链表的每个结点用头插法插入头结点之后
    • 由于头插法的特点,后插入的结点在前面,最终实现逆置
  3. 第三步:代码实现
代码语言:javascript
复制
void ReverseList(LinkList &L) {
    LNode *p = L->next;  // p指向首元结点
    LNode *r;            // 临时指针
    L->next = NULL;      // 先将头结点的next置空
    
    while (p != NULL) {
        r = p->next;     // ① 保存后继(关键!)
        p->next = L->next; // ② 头插:p的next指向当前第一个结点
        L->next = p;     // ③ 头插:头结点的next指向p
        p = r;           // ④ p前移
    }
}
  1. 第四步:复杂度分析
    • 时间复杂度:O(n),遍历链表一次
    • 空间复杂度:O(1),只用了p和r两个额外指针

也可以用三指针法实现:

代码语言:javascript
复制
void ReverseList_ThreePointer(LinkList &L) {
    LNode *prev = NULL;    // 前驱指针
    LNode *curr = L->next; // 当前指针
    LNode *next;           // 后继指针
    
    while (curr != NULL) {
        next = curr->next;  // 保存后继
        curr->next = prev;  // 反转指针
        prev = curr;        // prev前移
        curr = next;        // curr前移
    }
    
    L->next = prev;  // 头结点指向新的首元结点
}

答案:算法如上所述。

涉及知识点原地逆置头插法三指针法

命题规律:链表逆置是408最高频的算法题之一,几乎每3年必考一次。命题人可能考"简单逆置",也可能考"逆置+合并"、"逆置+拆分"等组合题。

踩坑提醒:⚠️ 头插法逆置时,L->next = NULL这一行必须在循环之前执行!如果不写这一行,第一个结点的next会指向自己(因为循环中p->next = L->next,而此时L->next还是原来的首元结点),形成环。

题目3(2019年统考真题第41题)

题目:设计一个算法,将带头结点的单链表L中所有奇数值的结点移到偶数值的结点之前。要求:保持奇数结点之间和偶数结点之间的相对顺序不变。空间复杂度为O(1)。

解题思路

  1. 第一步:分析题意
    • 输入:带头结点的单链表L
    • 输出:奇数值结点在前,偶数值结点在后
    • 约束:保持相对顺序(稳定),空间O(1)
    • 这本质上是一个"拆分+合并"的问题
  2. 第二步:设计思想
    • 遍历链表,将奇数结点和偶数结点分别组成两个子链表
    • 保持相对顺序意味着要用尾插法
    • 最后将奇数子链表的尾部接上偶数子链表的头部
  3. 第三步:代码实现
代码语言:javascript
复制
void SplitOddEven(LinkList &L) {
    // 创建两个哑结点
    LNode *oddHead = (LNode *)malloc(sizeof(LNode));
    LNode *evenHead = (LNode *)malloc(sizeof(LNode));
    LNode *oddTail = oddHead;
    LNode *evenTail = evenHead;
    
    LNode *p = L->next;
    while (p != NULL) {
        LNode *next = p->next;  // 保存后继
        if (p->data % 2 != 0) { // 奇数
            oddTail->next = p;
            oddTail = p;
        } else {                // 偶数
            evenTail->next = p;
            evenTail = p;
        }
        p = next;
    }
    
    // 连接两个子链表
    oddTail->next = evenHead->next;
    evenTail->next = NULL;  // 别忘了!
    
    // 更新原链表
    L->next = oddHead->next;
    
    // 释放哑结点
    free(oddHead);
    free(evenHead);
}
  1. 第四步:复杂度分析
    • 时间复杂度:O(n),遍历一次
    • 空间复杂度:O(1),只用了常数个额外指针(哑结点最后释放了)

答案:算法如上所述。

涉及知识点奇偶拆分尾插法哑结点技巧

命题规律:本题是"拆分"类题目的代表。命题人喜欢在"按位序拆分"和"按值拆分"之间切换,但核心思路是一样的——用两个哑结点分别维护两个子链表。

踩坑提醒:⚠️ 最容易犯的错误是忘记将evenTail->next置为NULL。如果原链表的最后一个结点是奇数,那么偶数子链表的尾结点可能还指向原链表中的后续结点,导致链表成环。

题目4(2017年统考真题第41题)

题目:设计一个算法,判断带头结点的单链表L中是否存在环。如果存在环,找出环的入口结点。要求空间复杂度为O(1)。

解题思路

  1. 第一步:分析题意
    • 输入:带头结点的单链表L(可能有环)
    • 输出:环的入口结点,或判断无环
    • 约束:空间O(1)
    • 这是经典的Floyd判环算法(龟兔赛跑算法)
  2. 第二步:设计思想
    • 用两个指针,慢指针每次走一步,快指针每次走两步
    • 如果有环,快指针一定会追上慢指针(在环中相遇)
    • 相遇后,将一个指针重新指向头结点,两个指针每次都走一步,再次相遇的点就是环的入口
  3. 第三步:代码实现
代码语言:javascript
复制
LNode *FindLoopStart(LinkList L) {
    if (L == NULL || L->next == NULL) return NULL;
    
    LNode *slow = L->next;  // 慢指针
    LNode *fast = L->next;  // 快指针
    
    // 第一阶段:判断是否有环
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;        // 慢指针走一步
        fast = fast->next->next;  // 快指针走两步
        if (slow == fast) break;  // 相遇,有环
    }
    
    // 无环
    if (fast == NULL || fast->next == NULL) return NULL;
    
    // 第二阶段:找环的入口
    slow = L->next;  // 慢指针回到头
    while (slow != fast) {
        slow = slow->next;  // 都走一步
        fast = fast->next;
    }
    
    return slow;  // 相遇点就是环的入口
}
  1. 第四步:复杂度分析
    • 时间复杂度:O(n),最坏情况下慢指针走一圈
    • 空间复杂度:O(1),只用了两个指针

答案:算法如上所述。

涉及知识点Floyd判环算法快慢指针环的入口

命题规律:Floyd判环算法是408的"明星算法",几乎每年都有人问。命题人可能直接考"判断是否有环",也可能考"找环的入口"或"求环的长度"。

踩坑提醒:⚠️ 快指针的判空条件是fast != NULL && fast->next != NULL,不能只判fast != NULL。因为快指针每次走两步,如果fast->next为NULL,fast->next->next就会访问空指针。

题目5(2020年统考真题第41题)

题目:设计一个算法,删除递增有序单链表中值相同的多余结点(即保序去重),使表中没有值相同的结点。要求时间复杂度为O(n)。

解题思路

  1. 第一步:分析题意
    • 输入:递增有序的单链表
    • 输出:去重后的单链表
    • 约束:时间O(n)
    • 关键:因为有序,所以相同值的结点一定相邻
  2. 第二步:设计思想
    • 用一个指针p扫描链表
    • 如果p和p->next的值相同,删除p->next
    • 如果不同,p前移
    • 这样只需要遍历一次
  3. 第三步:代码实现
代码语言:javascript
复制
void DeleteDuplicate(LinkList &L) {
    LNode *p = L->next;
    if (p == NULL) return;  // 空表
    
    while (p->next != NULL) {
        if (p->data == p->next->data) {
            // 删除p->next
            LNode *q = p->next;
            p->next = q->next;
            free(q);
        } else {
            p = p->next;  // 值不同,p前移
        }
    }
}
  1. 第四步:复杂度分析
    • 时间复杂度:O(n),每个结点最多被访问一次
    • 空间复杂度:O(1)

答案:算法如上所述。

涉及知识点保序去重删除操作有序链表

命题规律:保序去重是"删除"类题目的经典变式。命题人喜欢在"有序"和"无序"之间切换——有序去重用相邻比较,无序去重用哈希表或排序。

踩坑提醒:⚠️ 删除p->next后,p不应该前移!因为新的p->next可能和p的值相同(比如1→1→1→2),需要继续比较。只有值不同时p才前移。

题目6(2015年统考真题第41题)

题目:设计一个算法,将带头结点的单链表L分解为两个链表,使得第一个链表包含所有奇数位元素,第二个链表包含所有偶数位元素。要求保持原有相对顺序。

解题思路

  1. 第一步:分析题意
    • 输入:带头结点的单链表L
    • 输出:两个链表,一个含奇数位元素,一个含偶数位元素
    • 约束:保持相对顺序
    • 注意:这里是"奇数位"(第1、3、5…个),不是"奇数值"
  2. 第二步:设计思想
    • 遍历原链表,用计数器记录当前位序
    • 奇数位的结点用尾插法接入链表A
    • 偶数位的结点用尾插法接入链表B
    • 最后将两个尾结点的next置为NULL
  3. 第三步:代码实现
代码语言:javascript
复制
void SplitByPosition(LinkList &L, LinkList &A, LinkList &B) {
    // 初始化A和B的头结点
    A = (LNode *)malloc(sizeof(LNode));
    B = (LNode *)malloc(sizeof(LNode));
    A->next = NULL;
    B->next = NULL;
    
    LNode *tailA = A;  // A的尾指针
    LNode *tailB = B;  // B的尾指针
    LNode *p = L->next;
    int i = 1;  // 位序从1开始
    
    while (p != NULL) {
        LNode *next = p->next;  // 保存后继
        if (i % 2 == 1) {       // 奇数位
            tailA->next = p;
            tailA = p;
        } else {                // 偶数位
            tailB->next = p;
            tailB = p;
        }
        p = next;
        i++;
    }
    
    tailA->next = NULL;  // 置空
    tailB->next = NULL;  // 置空
    
    free(L);  // 释放原头结点
}
  1. 第四步:复杂度分析
    • 时间复杂度:O(n)
    • 空间复杂度:O(1)

答案:算法如上所述。

涉及知识点按位序拆分尾插法哑结点

命题规律:按位序拆分和按值拆分是408的"双生子"题目。命题人可能交替出题,也可能在选择题中考你"以下代码是按位序拆分还是按值拆分"。

踩坑提醒:⚠️ 注意区分"奇数位"和"奇数值"。奇数位是指第1、3、5…个位置,与元素的值无关。很多考生看到"奇偶"就条件反射地写p->data % 2,结果搞错了。

题目7(2021年统考真题第41题)

题目:设计一个算法,找出带头结点的单链表L中倒数第k个结点。如果不存在,返回NULL。要求时间复杂度为O(n),空间复杂度为O(1)。

解题思路

  1. 第一步:分析题意
    • 输入:带头结点的单链表L,整数k
    • 输出:倒数第k个结点
    • 约束:时间O(n),空间O(1)
    • 关键:不能先遍历一遍求长度,再遍历一遍找第n-k个——那样虽然也是O(n),但需要两遍
  2. 第二步:设计思想(双指针法)
    • 用两个指针p和q,p先走k步
    • 然后p和q同时走,当p到达末尾时,q就在倒数第k个位置
    • 如果p走了不到k步就到了末尾,说明k大于链表长度,返回NULL
  3. 第三步:代码实现
代码语言:javascript
复制
LNode *FindKthFromEnd(LinkList L, int k) {
    if (k <= 0) return NULL;
    
    LNode *p = L->next;  // 快指针
    LNode *q = L->next;  // 慢指针
    int step = 0;
    
    // p先走k步
    while (p != NULL && step < k) {
        p = p->next;
        step++;
    }
    
    // 如果k大于链表长度
    if (step < k) return NULL;
    
    // p和q同时走
    while (p != NULL) {
        p = p->next;
        q = q->next;
    }
    
    return q;
}
  1. 第四步:复杂度分析
    • 时间复杂度:O(n),只遍历一次
    • 空间复杂度:O(1)

答案:算法如上所述。

涉及知识点双指针法倒数第k个快慢指针

命题规律:倒数第k个结点是"双指针法"的经典应用。命题人可能直接考,也可能变式为"求链表的中间结点"(快指针走两步、慢指针走一步)。

踩坑提醒:⚠️ 注意k的合法性检查。k<=0或k>链表长度时都应该返回NULL。另外,p先走k步后,如果p为NULL,说明倒数第k个就是首元结点(q此时还在首元结点),这种情况要正确处理。

2.2 命题规律总结

年份

题型

分值

难度

核心考点

出题角度

2021

算法

15分

L2

倒数第k个

双指针法

2020

算法

15分

L2

保序去重

有序链表删除

2019

算法

15分

L2

奇偶拆分

按值拆分

2017

算法

15分

L3

判环

Floyd算法

2015

算法

15分

L2

按位序拆分

哑结点+尾插

2014

算法

15分

L2

递减合并

归并+头插

2012

算法

15分

L2

原地逆置

头插法

趋势分析

  • 近10年出题频率:几乎每年必考1道线性表算法题(15分)
  • 难度趋势:稳定在L2-L3, rarely出现L4以上
  • 题型偏好:偏重"合并、逆置、拆分"三大类,双指针法越来越受青睐
  • 命题人喜欢在"递增/递减"、“按位序/按值”、"带头结点/不带头结点"之间切换

模块3:AI命题Prompt ≈ 5000 字

3.1 命题Prompt模板
代码语言:javascript
复制
【AI命题Prompt - 可复制使用】

请你扮演一位考研408数据结构辅导专家,根据以下要求命制一套练习题:

📌 章节范围:第1章 线性表
📌 知识点范围:有序合并、奇偶拆分、原地逆置、指针操作技巧、边界处理

📋 题目要求:
- 题目数量:共 12 道
- 题型分布:选择题 6 道、算法设计题 6 道
- 难度分布:L1基础 3 道、L2应用 5 道、L3综合 4 道

📋 输出格式要求:
1. 每道题先给出题目
2. 然后给出【参考答案】和【解题思路】
3. 标注每道题考察的知识点
4. 最后给出整体难度评估

📋 特别注意:
- 选择题要贴近408真题风格,选项要有干扰性
- 算法设计题要求用C语言实现,给出时间复杂度和空间复杂度分析
- 题目要覆盖"合并、逆置、拆分"三大类
- 至少2道题涉及边界条件处理

请确保题目贴近真题风格,难度与真题相当。
3.2 AI生成的题目示例
选择题1(L1)

题目:将两个长度分别为m和n的递增有序单链表归并为一个递增有序单链表,在最坏情况下需要进行的比较次数为( )

A. m+n B. m+n-1 C. min(m,n) D. max(m,n)

答案:B

解题思路:最坏情况下,两个链表的元素交替大小,每次比较后只能移动一个指针。当比较了m+n-1次后,只剩下一个元素,不需要再比较。因此最坏比较次数为m+n-1。

涉及知识点有序合并复杂度分析

选择题2(L2)

题目:对带头结点的单链表L进行原地逆置,以下说法正确的是( )

A. 逆置后头结点的位置发生变化 B. 逆置后头结点的next指针不变 C. 逆置过程中需要申请O(n)的额外空间 D. 逆置后原首元结点变为尾结点

答案:D

解题思路:原地逆置不改变头结点的位置(A错),头结点的next指针会指向新的首元结点(B错),原地逆置的空间复杂度为O(1)(C错)。原首元结点在逆置后变成最后一个结点,即尾结点(D对)。

涉及知识点原地逆置头结点

选择题3(L2)

题目:已知递增有序单链表A: 1→3→5→7,B: 2→4→6→8,用头插法将A和B归并为递减有序单链表C,则C中第3个结点的值为( )

A. 4 B. 5 C. 6 D. 7

答案:C

解题思路:归并过程(每次取较小的,头插法插入C):

  • 取1,C: 1
  • 取2,C: 2→1
  • 取3,C: 3→2→1
  • 取4,C: 4→3→2→1
  • 取5,C: 5→4→3→2→1
  • 取6,C: 6→5→4→3→2→1
  • 取7,C: 7→6→5→4→3→2→1
  • 取8,C: 8→7→6→5→4→3→2→1

C的第3个结点值为6。

涉及知识点有序合并头插法

算法设计题1(L2)

题目:设计一个算法,将带头结点的单链表L中所有值为偶数的结点删除。要求时间复杂度为O(n),空间复杂度为O(1)。

参考答案

代码语言:javascript
复制
void DeleteEven(LinkList &L) {
    LNode *p = L;  // p指向当前结点的前驱
    while (p->next != NULL) {
        if (p->next->data % 2 == 0) {
            LNode *q = p->next;
            p->next = q->next;
            free(q);
        } else {
            p = p->next;
        }
    }
}

时间复杂度:O(n),遍历一次。 空间复杂度:O(1)。

涉及知识点删除操作边界处理

算法设计题2(L3)

题目:设计一个算法,判断带头结点的单链表L是否为回文链表(正读和反读相同)。要求时间复杂度为O(n),空间复杂度为O(1)。

参考答案

代码语言:javascript
复制
bool IsPalindrome(LinkList L) {
    if (L->next == NULL || L->next->next == NULL) return true;
    
    // 第一步:找中间结点(快慢指针)
    LNode *slow = L->next, *fast = L->next;
    while (fast->next != NULL && fast->next->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    
    // 第二步:逆置后半部分
    LNode *prev = NULL, *curr = slow->next;
    while (curr != NULL) {
        LNode *next = curr->next;
        curr->next = prev;
        prev = curr;
        curr = next;
    }
    
    // 第三步:比较前后两部分
    LNode *p = L->next, *q = prev;
    bool result = true;
    while (q != NULL) {
        if (p->data != q->data) {
            result = false;
            break;
        }
        p = p->next;
        q = q->next;
    }
    
    // 第四步:恢复后半部分(可选)
    // ...逆置回来...
    
    return result;
}

时间复杂度:O(n)。 空间复杂度:O(1)。

涉及知识点快慢指针原地逆置回文判断

3.3 使用说明
  1. 将上述Prompt复制到AI工具中
  2. 可以根据需要调整题目数量和难度分布
  3. 建议生成后人工审核题目的准确性和难度
  4. 可以多次生成,取质量最高的版本

模块4:AI讲题Prompt ≈ 5000 字

4.1 讲题Prompt模板
代码语言:javascript
复制
【AI讲题Prompt - 可复制使用】

请你扮演一位考研408数据结构辅导老师,对以下易错题进行详细讲解:

📌 题目:
(在此粘贴需要讲解的题目)

📋 讲解要求:
1. 第一步:分析题目考察的知识点
2. 第二步:指出解题的关键突破口
3. 第三步:逐步推导,每一步都说明"为什么这样做"
4. 第四步:总结此类题的通用解法
5. 第五步:给出2道变式题(难度相近但考法不同)

📋 特别注意:
- 不要直接给答案,采用引导式提问
- 指出常见的错误思路及为什么错
- 用通俗语言解释抽象概念
- 每一步都要画图辅助说明
4.2 AI生成的讲解示例
讲解题目:2014年真题——递减有序合并

知识点分析

本题考察三个核心知识点:

  1. 归并思想:两个有序表的合并本质上是归并排序的"合并"步骤
  2. 头插法:要实现递减,必须用头插法而非尾插法
  3. 空间优化:不申请新空间意味着必须复用原有结点

解题突破口

突破口在于"递增→递减"这个转换。如果你熟悉头插法的特点——“先插入的在后面,后插入的在前面”——你就会意识到:从小到大取元素,用头插法插入,自然就形成了从大到小的顺序。

逐步推导

代码语言:javascript
复制
初始状态:
A: 头→2→5→8→NULL
B: 头→3→6→7→NULL
C: 头→NULL

Step 1: p→2, q→3, 2<3, 取2
  头插: C: 头→2→NULL
  p→5, q→3

Step 2: p→5, q→3, 5>3, 取3
  头插: C: 头→3→2→NULL
  p→5, q→6

Step 3: p→5, q→6, 5<6, 取5
  头插: C: 头→5→3→2→NULL
  p→8, q→6

Step 4: p→8, q→6, 8>6, 取6
  头插: C: 头→6→5→3→2→NULL
  p→8, q→7

Step 5: p→8, q→7, 8>7, 取7
  头插: C: 头→7→6→5→3→2→NULL
  p→8, q→NULL

Step 6: q为空,将p的剩余部分(8)头插
  头插: C: 头→8→7→6→5→3→2→NULL

结果: 8→7→6→5→3→2(递减有序 ✓)

通用解法总结

遇到"合并两个有序表"的题目,按以下步骤思考:

  1. 确认合并后的顺序(递增还是递减)
  2. 递增→尾插法,递减→头插法
  3. 确认是否允许申请新空间(就地合并 vs 新建链表)
  4. 别忘了处理剩余部分

变式题

  1. 将两个递减有序的单链表合并为一个递增有序的单链表
  2. 将三个递增有序的单链表合并为一个递减有序的单链表
4.3 易错题记录

本章学生反馈最多的易错题

  1. 合并递减有序表时用了尾插法——根本原因是没有理解头插法的"反转"效果
  2. 原地逆置时忘记L->next = NULL——根本原因是没有理解头插法的初始化步骤
  3. 拆分时忘记将尾结点的next置为NULL——根本原因是没有意识到尾结点可能还指向原链表的后续结点

模块5:AI错题复盘Prompt ≈ 5000 字

5.1 错题复盘Prompt模板
代码语言:javascript
复制
【AI错题复盘Prompt - 可复制使用】

请你扮演一位考研408数据结构辅导专家,帮我分析以下错题:

📌 我的错题:
(在此粘贴做错的题目)

📌 我的错误解答:
(在此写下你的错误解题过程)

📌 正确答案:
(粘贴正确答案)

📋 分析要求:
1. 【错因诊断】分析我出错的根本原因:
   - 是概念理解错误?计算失误?还是方法选择不当?
   - 具体是哪个知识点存在漏洞?

2. 【知识漏洞定位】指出我需要回看的教材章节/知识点

3. 【正确思路】给出正确的解题思路与关键步骤

4. 【强化训练】针对我的薄弱环节,出3道同类型练习题
   - 第1道:基础巩固(L1-L2)
   - 第2道:中等难度(L3)
   - 第3道:综合提升(L3-L4)

5. 【防错提醒】总结一句"下次遇到类似题一定要注意..."的提醒
5.2 AI生成的复盘示例
错题1:原地逆置时形成环

我的错题

对带头结点的单链表L进行原地逆置,我写的代码如下:

代码语言:javascript
复制
void Reverse(LinkList &L) {
    LNode *p = L->next;
    while (p != NULL) {
        p->next = L->next;  // 错误在这里
        L->next = p;
        p = p->next;
    }
}

运行后发现链表变成了环,程序死循环。

错因诊断

根本原因是指针操作顺序错误。在执行p->next = L->next时,p的后继结点丢失了。然后p = p->next实际上是在访问L->next(因为p->next刚被修改),导致p在两个结点之间来回跳,形成环。

知识漏洞定位

需要回看DS-01-03中"单链表插入操作"的指针顺序。核心原则是:先保存后继,再修改指针

正确思路

代码语言:javascript
复制
void Reverse(LinkList &L) {
    LNode *p = L->next;
    L->next = NULL;  // 关键:先断开
    while (p != NULL) {
        LNode *temp = p;   // ① 保存当前结点
        p = p->next;       // ② p前移(必须在修改temp->next之前)
        temp->next = L->next; // ③ 头插
        L->next = temp;
    }
}

强化训练

  1. (L1)对不带头结点的单链表进行原地逆置
  2. (L2)将单链表的前k个结点逆置,其余部分不变
  3. (L3)将单链表按每k个结点一组进行逆置

防错提醒:下次遇到链表指针操作,一定要先问自己三个问题:①后继保存了吗?②修改顺序对吗?③会不会形成环?

5.3 错题归因统计

错因类型

次数

占比

对应知识点

指针操作顺序错误

35%

最高频

插入/删除/逆置

边界条件遗漏

25%

次高频

空表/单结点/尾部

头插法vs尾插法混淆

20%

第三

合并/逆置

剩余部分未处理

15%

第四

合并

尾结点未置空

5%

最低

拆分


模块6:AI模拟卷Prompt ≈ 8000 字

6.1 模拟卷Prompt模板
代码语言:javascript
复制
【AI模拟卷Prompt - 可复制使用】

请你扮演一位考研408数据结构命题组专家,根据以下要求生成一套章节模拟卷:

📌 章节范围:第1章 线性表(应用部分)
📌 知识点范围:有序合并、奇偶拆分、原地逆置、指针操作技巧、边界处理

📋 试卷结构:
- 选择题:5 道,每题 5 分,共 25 分
- 填空题:5 道,每题 5 分,共 25 分
- 算法设计题:3 道,共 50 分
- 总分:100 分
- 建议用时:120 分钟

📋 难度分布:
- 基础题(L1-L2):占 40%
- 中等题(L3):占 40%
- 较难题(L4-L5):占 20%

📋 输出要求:
1. 先输出完整试卷(不含答案)
2. 然后输出参考答案与评分标准
3. 每道算法设计题标注"踩分点"
4. 最后给出分数段评估建议
6.2 AI生成的完整模拟卷
线性表应用模拟卷

一、选择题(每题5分,共25分)

  1. 将两个长度分别为m和n的递增有序单链表归并为一个递增有序单链表,在最好情况下需要进行的比较次数为( ) A. m+n B. m+n-1 C. min(m,n) D. max(m,n)
  2. 对带头结点的单链表进行原地逆置,以下说法错误的是( ) A. 逆置后头结点位置不变 B. 逆置后原尾结点变为头结点的next C. 逆置过程需要O(n)额外空间 D. 逆置后原首元结点变为尾结点
  3. 已知递增有序单链表A: 1→3→5,B: 2→4→6,用尾插法归并为递增有序单链表C,则C中第4个结点的值为( ) A. 3 B. 4 C. 5 D. 6
  4. 将单链表L中所有奇数值的结点移到偶数值结点之前,以下方法中最合适的是( ) A. 头插法 B. 尾插法拆分+合并 C. 交换数据域 D. 逆置+删除
  5. 判断单链表是否有环,最优的时间复杂度和空间复杂度为( ) A. O(n), O(n) B. O(n), O(1) C. O(n²), O(1) D. O(log n), O(1)

二、填空题(每题5分,共25分)

  1. 对长度为n的顺序表进行原地逆置,需要交换______次。
  2. 用头插法将n个结点插入空链表,最终链表中结点的顺序与插入顺序______。
  3. 两个递增有序单链表(长度分别为m和n)归并为一个递减有序单链表,最优时间复杂度为______。
  4. 将单链表按奇偶位拆分为两个链表,需要设置______个尾指针。
  5. Floyd判环算法中,快指针每次走______步,慢指针每次走______步。

三、算法设计题(共50分)

11.(15分)设计一个算法,将两个递增有序的单链表A和B归并为一个递增有序的单链表C。要求不申请新结点,只利用A和B的原有结点。

12.(15分)设计一个算法,找出带头结点的单链表L的中间结点。如果链表长度为偶数,返回中间两个结点中的前一个。要求时间复杂度O(n),空间复杂度O(1)。

13.(20分)设计一个算法,将带头结点的单链表L按以下规则重新排列:第一个结点、最后一个结点、第二个结点、倒数第二个结点……要求空间复杂度O(1)。

参考答案与评分标准

一、选择题

  1. 答案:C 解析:最好情况下,一个链表的所有元素都小于另一个链表的第一个元素,只需要比较min(m,n)次。
  2. 答案:C 解析:原地逆置的空间复杂度为O(1),不需要O(n)额外空间。C说法错误。
  3. 答案:B 解析:尾插法归并过程:1→2→3→4→5→6,第4个结点值为4。
  4. 答案:B 解析:用两个哑结点分别维护奇数链表和偶数链表,遍历完后合并。尾插法保持相对顺序。
  5. 答案:B 解析:Floyd判环算法时间复杂度O(n),空间复杂度O(1)。

二、填空题

  1. 答案:⌊n/2⌋
  2. 答案:相反
  3. 答案:O(m+n)
  4. 答案:2
  5. 答案:2, 1

三、算法设计题

  1. 评分标准
  • 算法思想正确(5分)
  • 代码实现正确(8分)
  • 复杂度分析正确(2分)
代码语言:javascript
复制
LinkList MergeIncrease(LinkList &A, LinkList &B) {
    LinkList C = (LNode *)malloc(sizeof(LNode));
    C->next = NULL;
    LNode *r = C;  // 尾指针
    
    LNode *p = A->next, *q = B->next;
    while (p != NULL && q != NULL) {
        if (p->data <= q->data) {
            r->next = p;
            p = p->next;
        } else {
            r->next = q;
            q = q->next;
        }
        r = r->next;
    }
    if (p != NULL) r->next = p;
    else r->next = q;
    
    free(A); free(B);
    return C;
}

时间复杂度O(m+n),空间复杂度O(1)。

  1. 评分标准
  • 算法思想正确(5分)
  • 代码实现正确(8分)
  • 复杂度分析正确(2分)
代码语言:javascript
复制
LNode *FindMiddle(LinkList L) {
    LNode *slow = L->next, *fast = L->next;
    while (fast->next != NULL && fast->next->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}
  1. 评分标准
  • 算法思想正确(8分)
  • 代码实现正确(8分)
  • 复杂度分析正确(2分)
  • 边界处理正确(2分)
代码语言:javascript
复制
void RearrangeList(LinkList &L) {
    if (L->next == NULL || L->next->next == NULL) return;
    
    // 第一步:找中间结点
    LNode *slow = L->next, *fast = L->next;
    while (fast->next != NULL && fast->next->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    
    // 第二步:逆置后半部分
    LNode *prev = NULL, *curr = slow->next;
    slow->next = NULL;  // 断开
    while (curr != NULL) {
        LNode *next = curr->next;
        curr->next = prev;
        prev = curr;
        curr = next;
    }
    
    // 第三步:交替合并
    LNode *p = L->next, *q = prev;
    while (q != NULL) {
        LNode *pnext = p->next;
        LNode *qnext = q->next;
        p->next = q;
        q->next = pnext;
        p = pnext;
        q = qnext;
    }
}
6.3 评分标准参考

分数段评估建议

  • 90分以上:掌握优秀,可以继续下一章
  • 75-89分:掌握良好,建议复习错题对应的知识点
  • 60-74分:基本掌握,建议重新学习本章的应用部分
  • 60分以下:基础不牢,建议从DS-01-01开始重新学习

模块7:延伸阅读 ≈ 3000 字

7.1 教材参考
  • 《数据结构(C语言版)》严蔚敏 第2章"线性表",P35-P50
    • 重点阅读:双链表、循环链表、静态链表的定义和操作
    • 适合人群:基础薄弱、需要系统学习的同学
  • 《数据结构复习指导》王道考研 第1章"线性表",P15-P45
    • 重点阅读:真题解析部分,特别是算法大题的解题思路
    • 适合人群:备考408、需要刷题巩固的同学
  • 《数据结构与算法分析——C语言描述》Mark Allen Weiss 第3章"表、栈和队列"
    • 重点阅读:链表的C语言实现细节
    • 适合人群:想要深入理解链表实现的同学
7.2 视频课程
  • [B站] 王道考研数据结构 - 推荐观看第1章"线性表"的第5-8讲
    • 讲师特点:讲解细致,真题覆盖全面
    • 适合人群:基础一般、需要系统学习的同学
  • [B站] 天勤考研数据结构 - 推荐观看"链表应用"专题
    • 讲师特点:代码演示详细,边界条件讲得透彻
    • 适合人群:代码能力较弱、需要跟着敲代码的同学
  • [B站] 大话数据结构 - 推荐观看"线性表应用"部分
    • 讲师特点:动画演示直观,适合入门
    • 适合人群:零基础、需要建立直觉的同学
7.3 知识关联图

前置知识

  • DS-01-01 线性表的定义与基本操作
  • DS-01-02 顺序表及其基本运算
  • DS-01-03 单链表及其基本运算
  • DS-01-04 双链表、循环链表与静态链表

后续知识

  • DS-01-06 线性表的算法题解题方法(双指针法、递归法等高级技巧)
  • 第2章 栈和队列(栈的逆置、队列的合并等应用)

学习路径建议

  1. 先掌握基本概念和操作(DS-01-01到DS-01-04)
  2. 通过应用题巩固指针操作能力(DS-01-05,即本文)
  3. 学习高级解题技巧(DS-01-06)
  4. 通过真题和模拟题查漏补缺(DS-01-07、DS-01-08)

模块8:Checklist ≈ 2500 字

8.1 知识点清单
  • 有序合并:能手写两个递增有序链表合并为递增有序链表的完整代码
  • 递减合并:能手写两个递增有序链表合并为递减有序链表的完整代码(头插法)
  • 顺序表原地逆置:能手写双指针交换法逆置顺序表
  • 链表原地逆置(头插法):能手写头插法逆置链表
  • 链表原地逆置(三指针法):能手写三指针法逆置链表
  • 奇偶拆分(按位序):能手写按位序奇偶拆分链表的代码
  • 奇偶拆分(按值):能手写按值奇偶拆分链表的代码
  • 保序去重:能手写有序链表保序去重的代码
  • Floyd判环:能手写Floyd判环算法并解释原理
  • 倒数第k个:能手写双指针法找倒数第k个结点
  • 边界处理:能正确处理空表、单结点表、全满足条件的表
  • 头插法vs尾插法:能根据题目要求选择正确的方法
  • 哑结点技巧:能使用哑结点简化拆分代码
  • 复杂度分析:能准确分析各算法的时间和空间复杂度
  • 指针操作黄金法则:记住"先保存后继,再修改指针"
8.2 自测问题
  1. Q:两个递增有序链表合并为递增有序链表,应该用头插法还是尾插法?为什么? A:尾插法。因为每次取出的是最小元素,尾插法保持从小到大的顺序。
  2. Q:两个递增有序链表合并为递减有序链表,应该用头插法还是尾插法?为什么? A:头插法。因为每次取出的是最小元素,头插法使先插入的小元素在后面,形成递减。
  3. Q:链表原地逆置的空间复杂度是多少?为什么? A:O(1)。因为只修改指针,不申请新结点,只用了常数个额外指针变量。
  4. Q:头插法逆置链表时,为什么必须在循环前执行L->next = NULLA:如果不置空,第一个结点的next会指向自己(因为p->next = L->next,而此时L->next还是原来的首元结点),形成环。
  5. Q:拆分链表时,为什么最后要将尾结点的next置为NULL? A:因为尾结点可能还指向原链表中的后续结点。如果不置空,链表可能成环或包含不属于该子链表的结点。
  6. Q:Floyd判环算法中,快指针为什么每次走两步而不是三步? A:走两步可以保证快慢指针一定会在环中相遇。如果走三步,在某些环长度下可能永远追不上。
  7. Q:保序去重时,删除p->next后,p应不应该前移?为什么? A:不应该。因为新的p->next可能和p的值相同(如1→1→1→2),需要继续比较。
  8. Q:找倒数第k个结点时,如果p先走k步后p为NULL,说明什么? A:说明k等于链表长度,倒数第k个就是首元结点。此时q还在首元结点,直接返回q即可。
  9. Q:"原地"操作的含义是什么? A:空间复杂度为O(1),不申请额外的存储空间(与输入规模相关的空间)。可以申请常数个额外变量。
  10. Q:哑结点(头结点)在拆分操作中起什么作用? A:哑结点统一了"第一个结点的插入"和"后续结点的插入"的操作逻辑,避免了特殊处理。
8.3 完成度评估

模块

状态

备注

知识点讲解

⬜ 未开始 / 🔄 进行中 / ✅ 已完成

真题解析

⬜ 未开始 / 🔄 进行中 / ✅ 已完成

AI命题练习

⬜ 未开始 / 🔄 进行中 / ✅ 已完成

AI讲题学习

⬜ 未开始 / 🔄 进行中 / ✅ 已完成

错题复盘

⬜ 未开始 / 🔄 进行中 / ✅ 已完成

模拟卷测试

⬜ 未开始 / 🔄 进行中 / ✅ 已完成

得分:__/100

延伸阅读

⬜ 未开始 / 🔄 进行中 / ✅ 已完成

整体掌握程度评估

  • 如果自测问题能答对8/10以上:掌握优秀,可以继续DS-01-06
  • 如果答对5-7个:掌握良好,建议复习错题对应的知识点
  • 如果答对4个以下:基础不牢,建议重新学习DS-01-03和DS-01-04
在这里插入图片描述
在这里插入图片描述
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-07-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
  • 先看问题场景 ≈ 800 字
  • 本节核心收获 ≈ 700 字
  • 模块1:知识点讲解 ≈ 10000 字
    • 1.1 核心概念
      • 1.1.1 线性表应用题的本质
      • 1.1.2 有序合并的定义与思路
      • 1.1.3 原地逆置的定义与思路
      • 1.1.4 奇偶拆分的定义与思路
      • 1.1.5 头插法与尾插法的选择策略
    • 1.2 公式推导与算法分析
      • 1.2.1 有序合并的时间复杂度
      • 1.2.2 原地逆置的时间复杂度
      • 1.2.3 奇偶拆分的时间复杂度
    • 1.3 图示说明
      • 1.3.1 有序合并的过程演示
      • 1.3.2 链表原地逆置的头插法演示
      • 1.3.3 三指针法逆置过程
    • 1.4 常见误区与踩坑实录
      • 误区1:合并时忘记处理剩余部分
      • 误区2:原地逆置时申请了新空间
      • 误区3:头插法逆置时丢失结点
      • 误区4:奇偶拆分时两个链表互相干扰
      • 误区5:合并递减有序表时用尾插法
  • 模块2:真题解析 ≈ 10000 字
    • 2.1 真题精选
      • 题目1(2014年统考真题第41题)
      • 题目2(2012年统考真题第41题)
      • 题目3(2019年统考真题第41题)
      • 题目4(2017年统考真题第41题)
      • 题目5(2020年统考真题第41题)
      • 题目6(2015年统考真题第41题)
      • 题目7(2021年统考真题第41题)
    • 2.2 命题规律总结
  • 模块3:AI命题Prompt ≈ 5000 字
    • 3.1 命题Prompt模板
    • 3.2 AI生成的题目示例
      • 选择题1(L1)
      • 选择题2(L2)
      • 选择题3(L2)
      • 算法设计题1(L2)
      • 算法设计题2(L3)
    • 3.3 使用说明
  • 模块4:AI讲题Prompt ≈ 5000 字
    • 4.1 讲题Prompt模板
    • 4.2 AI生成的讲解示例
      • 讲解题目:2014年真题——递减有序合并
    • 4.3 易错题记录
  • 模块5:AI错题复盘Prompt ≈ 5000 字
    • 5.1 错题复盘Prompt模板
    • 5.2 AI生成的复盘示例
      • 错题1:原地逆置时形成环
    • 5.3 错题归因统计
  • 模块6:AI模拟卷Prompt ≈ 8000 字
    • 6.1 模拟卷Prompt模板
    • 6.2 AI生成的完整模拟卷
      • 线性表应用模拟卷
      • 参考答案与评分标准
    • 6.3 评分标准参考
  • 模块7:延伸阅读 ≈ 3000 字
    • 7.1 教材参考
    • 7.2 视频课程
    • 7.3 知识关联图
  • 模块8:Checklist ≈ 2500 字
    • 8.1 知识点清单
    • 8.2 自测问题
    • 8.3 完成度评估
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档