
作者: 安全风信子 日期: 2026-07-22 主要来源: 王道考研《数据结构复习指导》、严蔚敏《数据结构(C语言版)》 读完你能学到: 掌握有序合并、奇偶拆分、原地逆置等线性表经典应用算法,能独立解决408线性表应用真题
科目:数据结构 | 章节:第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开始,考的是你能不能把这些操作组合起来解决实际问题。如果这关过不了,后面的树、图算法题就更不用想了。
你可能会说:“合并和逆置不就是几个基本操作嘛,有什么难的?”
这个问题,我在后面会详细回答。但现在,请你先带着以下几个问题往下读:
如果你对这五个问题不能秒答,那这篇文章就是为你写的。
≈ 700 字读完这篇文章,你将获得以下具体收获:
收获1:有序合并的完整实现 你将掌握两个递增有序链表合并为一个递增(或递减)有序链表的完整实现。你会理解"归并"思想在链表上的应用,以及为什么合并操作的时间复杂度是O(m+n)。
收获2:头插法与尾插法的灵活运用 你将深刻理解头插法和尾插法在不同场景下的选择策略。合并递增有序表用尾插法,合并递减有序表用头插法——这不是死记硬背,而是对指针操作的本质理解。
收获3:原地逆置的三种方法 你将掌握顺序表原地逆置(双指针交换)和链表原地逆置(头插法/三指针法)的完整实现。你会理解"原地"的含义——空间复杂度为O(1)。
收获4:奇偶拆分的指针操作 你将掌握将一个链表拆分为奇数位和偶数位两个链表的完整实现。你会理解如何用"哑结点"(头结点)简化代码,以及如何同时维护两个链表。
收获5:边界处理的系统方法 你将掌握线性表应用题中常见的边界条件处理:空表、单结点表、全满足条件的表。你会知道命题人喜欢在哪些边界条件上设坑。
收获6:真题解题的系统方法 通过5-8道真题的详细解析,你将掌握线性表应用类真题的解题思路和常见陷阱。你会知道命题人喜欢在哪些地方设坑,以及如何避免。
收获7:AI辅助学习的完整工具链 你将获得一套完整的AI辅助学习Prompt模板,包括命题、讲题、错题复盘、模拟卷四个维度。这些模板可以直接用于日常复习。
收获8:自我检测与查漏补缺 通过文末的Checklist,你将能够系统地检验自己对线性表应用的掌握程度,找到薄弱环节并有针对性地强化。
≈ 10000 字前面四篇文章(DS-01-01到DS-01-04),我们学了线性表的基本概念和操作——初始化、插入、删除、遍历、查找。这些操作是"原子操作",就像乐高积木的基本块。
线性表的应用题,就是把这些"原子操作"组合起来,解决实际问题。比如:
来源: 王道考研《数据结构复习指导》P25
有序合并是指将两个有序线性表合并为一个有序线性表,合并后的表仍然保持有序性。
基本思路(归并思想):

来源: 严蔚敏《数据结构(C语言版)》P41
原地逆置是指在不申请额外存储空间(空间复杂度O(1))的前提下,将线性表中的元素顺序反转。
顺序表的原地逆置:
顺序表的原地逆置非常直观——用两个指针分别指向首尾,交换两个元素,然后向中间靠拢,直到两个指针相遇。
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
奇偶拆分是指将一个链表按元素位序的奇偶性拆分为两个链表——奇数位元素组成一个链表,偶数位元素组成另一个链表。
注意区分两种"奇偶":
408真题中两种都考过,但"按位序拆分"更常见。
基本思路:

在线性表的应用题中,选择头插法还是尾插法是一个关键的决策点。
场景 | 推荐方法 | 原因 |
|---|---|---|
合并为递增有序表 | 尾插法 | 从小到大依次插入,尾插法保持顺序 |
合并为递减有序表 | 头插法 | 从小到大取出,头插法自然形成递减 |
原地逆置 | 头插法 | 头插法天然具有"反转"效果 |
保持原顺序 | 尾插法 | 尾插法不改变元素顺序 |
来源: 王道考研《数据结构复习指导》P25-26
设两个有序链表的长度分别为m和n。
最好情况: 一个链表的所有元素都小于另一个链表的第一个元素。此时只需要比较min(m,n)次,然后将另一个链表直接接上。比较次数为min(m,n)。
最坏情况: 两个链表的元素交替大小。此时需要比较m+n-1次(每次比较后只能移动一个指针,最后一次比较后两个指针同时到达末尾)。
平均情况: 假设两个链表的元素均匀交错,平均比较次数为m+n-1。
因此,有序合并的时间复杂度为 O(m+n)。
空间复杂度分析:
顺序表原地逆置:
需要交换
次,每次交换3次赋值操作。
链表原地逆置(头插法):
需要遍历链表一次,每次将一个结点的next指针反转。遍历n个结点,每次操作O(1)。
来源: 严蔚敏《数据结构(C语言版)》P42
需要遍历原链表一次,每次根据位序将结点接入对应的链表。遍历n个结点,每次操作O(1)。


合并过程(递增有序,尾插法):
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渲染错误: 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'
初始: 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)❌ 错误理解:合并时只写比较循环,忘记处理剩余部分
// 错误的合并代码
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还有剩余,这些结点就丢失了✅ 正确理解:循环结束后,必须将剩余部分接入结果表
// 正确的合并代码
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分。
❌ 错误理解:原地逆置可以创建新结点
// ❌ 这不是"原地"逆置!
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;
}✅ 正确理解:原地逆置必须只修改指针,不申请新结点
// ✅ 这才是原地逆置
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任何新结点。只能修改已有结点的指针。
❌ 错误理解:头插法逆置时,先修改头结点的next
// ❌ 错误的头插法逆置
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;
}等等,上面这段代码其实是正确的!让我重新写一个真正错误的版本:
// ❌ 真正错误的头插法逆置
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指针
// ✅ 正确的头插法逆置
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,原来的后继就丢失了。这和单链表插入操作的道理是一样的。
❌ 错误理解:拆分时只维护一个尾指针
// ❌ 错误的拆分代码
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++;
}上面的代码有一个致命问题:odd和even既是链表的"头",又是"尾",当链表只有一个元素时,头尾混淆会导致错误。
✅ 正确理解:使用哑结点(头结点)简化代码
// ✅ 正确的拆分代码——使用哑结点
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。否则尾结点可能还指向原链表中的后续结点,导致链表不成环或数据混乱。
❌ 错误理解:合并递减有序表也用尾插法
// ❌ 合并为递减有序表,却用尾插法
// 如果两个递增表A: 2→5→8, B: 3→6→7
// 用尾插法合并出来是: 2→3→5→6→7→8(递增!)
// 但题目要求递减!✅ 正确理解:合并递减有序表应该用头插法
// ✅ 合并为递减有序表——头插法
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命题人特别喜欢考的"变式"。如果你只会尾插法合并,考场上遇到"递减"就傻眼了。
≈ 10000 字题目:已知两个递增有序的单链表A和B,设计算法将A和B归并为一个递减有序的单链表C,要求不申请新空间,只利用原来A和B的结点。要求: (1)给出算法的基本设计思想 (2)用C或C++语言描述算法 (3)说明算法的时间复杂度
解题思路:
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;
}答案:算法如上所述。
涉及知识点:有序合并、头插法、归并思想
命题规律:本题是408经典的"归并+变式"题。命题人喜欢在"递增合并"的基础上加一个"递减"的要求,考察学生是否真正理解了头插法和尾插法的区别。
踩坑提醒:⚠️ 很多同学看到"合并"就条件反射地写尾插法,结果合并出来是递增的。一定要看清题目要求!另外,"不申请新空间"意味着不能malloc新结点,只能用原来的结点。
题目:设计一个算法,将带头结点的单链表L逆置。要求: (1)给出算法的基本设计思想 (2)用C或C++语言描述算法 (3)说明算法的时间复杂度和空间复杂度
解题思路:
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前移
}
}也可以用三指针法实现:
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还是原来的首元结点),形成环。
题目:设计一个算法,将带头结点的单链表L中所有奇数值的结点移到偶数值的结点之前。要求:保持奇数结点之间和偶数结点之间的相对顺序不变。空间复杂度为O(1)。
解题思路:
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);
}答案:算法如上所述。
涉及知识点:奇偶拆分、尾插法、哑结点技巧
命题规律:本题是"拆分"类题目的代表。命题人喜欢在"按位序拆分"和"按值拆分"之间切换,但核心思路是一样的——用两个哑结点分别维护两个子链表。
踩坑提醒:⚠️ 最容易犯的错误是忘记将evenTail->next置为NULL。如果原链表的最后一个结点是奇数,那么偶数子链表的尾结点可能还指向原链表中的后续结点,导致链表成环。
题目:设计一个算法,判断带头结点的单链表L中是否存在环。如果存在环,找出环的入口结点。要求空间复杂度为O(1)。
解题思路:
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; // 相遇点就是环的入口
}答案:算法如上所述。
涉及知识点:Floyd判环算法、快慢指针、环的入口
命题规律:Floyd判环算法是408的"明星算法",几乎每年都有人问。命题人可能直接考"判断是否有环",也可能考"找环的入口"或"求环的长度"。
踩坑提醒:⚠️ 快指针的判空条件是fast != NULL && fast->next != NULL,不能只判fast != NULL。因为快指针每次走两步,如果fast->next为NULL,fast->next->next就会访问空指针。
题目:设计一个算法,删除递增有序单链表中值相同的多余结点(即保序去重),使表中没有值相同的结点。要求时间复杂度为O(n)。
解题思路:
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前移
}
}
}答案:算法如上所述。
涉及知识点:保序去重、删除操作、有序链表
命题规律:保序去重是"删除"类题目的经典变式。命题人喜欢在"有序"和"无序"之间切换——有序去重用相邻比较,无序去重用哈希表或排序。
踩坑提醒:⚠️ 删除p->next后,p不应该前移!因为新的p->next可能和p的值相同(比如1→1→1→2),需要继续比较。只有值不同时p才前移。
题目:设计一个算法,将带头结点的单链表L分解为两个链表,使得第一个链表包含所有奇数位元素,第二个链表包含所有偶数位元素。要求保持原有相对顺序。
解题思路:
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); // 释放原头结点
}答案:算法如上所述。
涉及知识点:按位序拆分、尾插法、哑结点
命题规律:按位序拆分和按值拆分是408的"双生子"题目。命题人可能交替出题,也可能在选择题中考你"以下代码是按位序拆分还是按值拆分"。
踩坑提醒:⚠️ 注意区分"奇数位"和"奇数值"。奇数位是指第1、3、5…个位置,与元素的值无关。很多考生看到"奇偶"就条件反射地写p->data % 2,结果搞错了。
题目:设计一个算法,找出带头结点的单链表L中倒数第k个结点。如果不存在,返回NULL。要求时间复杂度为O(n),空间复杂度为O(1)。
解题思路:
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;
}答案:算法如上所述。
涉及知识点:双指针法、倒数第k个、快慢指针
命题规律:倒数第k个结点是"双指针法"的经典应用。命题人可能直接考,也可能变式为"求链表的中间结点"(快指针走两步、慢指针走一步)。
踩坑提醒:⚠️ 注意k的合法性检查。k<=0或k>链表长度时都应该返回NULL。另外,p先走k步后,如果p为NULL,说明倒数第k个就是首元结点(q此时还在首元结点),这种情况要正确处理。
年份 | 题型 | 分值 | 难度 | 核心考点 | 出题角度 |
|---|---|---|---|---|---|
2021 | 算法 | 15分 | L2 | 倒数第k个 | 双指针法 |
2020 | 算法 | 15分 | L2 | 保序去重 | 有序链表删除 |
2019 | 算法 | 15分 | L2 | 奇偶拆分 | 按值拆分 |
2017 | 算法 | 15分 | L3 | 判环 | Floyd算法 |
2015 | 算法 | 15分 | L2 | 按位序拆分 | 哑结点+尾插 |
2014 | 算法 | 15分 | L2 | 递减合并 | 归并+头插 |
2012 | 算法 | 15分 | L2 | 原地逆置 | 头插法 |
趋势分析:
≈ 5000 字【AI命题Prompt - 可复制使用】
请你扮演一位考研408数据结构辅导专家,根据以下要求命制一套练习题:
📌 章节范围:第1章 线性表
📌 知识点范围:有序合并、奇偶拆分、原地逆置、指针操作技巧、边界处理
📋 题目要求:
- 题目数量:共 12 道
- 题型分布:选择题 6 道、算法设计题 6 道
- 难度分布:L1基础 3 道、L2应用 5 道、L3综合 4 道
📋 输出格式要求:
1. 每道题先给出题目
2. 然后给出【参考答案】和【解题思路】
3. 标注每道题考察的知识点
4. 最后给出整体难度评估
📋 特别注意:
- 选择题要贴近408真题风格,选项要有干扰性
- 算法设计题要求用C语言实现,给出时间复杂度和空间复杂度分析
- 题目要覆盖"合并、逆置、拆分"三大类
- 至少2道题涉及边界条件处理
请确保题目贴近真题风格,难度与真题相当。题目:将两个长度分别为m和n的递增有序单链表归并为一个递增有序单链表,在最坏情况下需要进行的比较次数为( )
A. m+n B. m+n-1 C. min(m,n) D. max(m,n)
答案:B
解题思路:最坏情况下,两个链表的元素交替大小,每次比较后只能移动一个指针。当比较了m+n-1次后,只剩下一个元素,不需要再比较。因此最坏比较次数为m+n-1。
涉及知识点:有序合并、复杂度分析
题目:对带头结点的单链表L进行原地逆置,以下说法正确的是( )
A. 逆置后头结点的位置发生变化 B. 逆置后头结点的next指针不变 C. 逆置过程中需要申请O(n)的额外空间 D. 逆置后原首元结点变为尾结点
答案:D
解题思路:原地逆置不改变头结点的位置(A错),头结点的next指针会指向新的首元结点(B错),原地逆置的空间复杂度为O(1)(C错)。原首元结点在逆置后变成最后一个结点,即尾结点(D对)。
涉及知识点:原地逆置、头结点
题目:已知递增有序单链表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):
C的第3个结点值为6。
涉及知识点:有序合并、头插法
题目:设计一个算法,将带头结点的单链表L中所有值为偶数的结点删除。要求时间复杂度为O(n),空间复杂度为O(1)。
参考答案:
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)。
涉及知识点:删除操作、边界处理
题目:设计一个算法,判断带头结点的单链表L是否为回文链表(正读和反读相同)。要求时间复杂度为O(n),空间复杂度为O(1)。
参考答案:
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)。
涉及知识点:快慢指针、原地逆置、回文判断
≈ 5000 字【AI讲题Prompt - 可复制使用】
请你扮演一位考研408数据结构辅导老师,对以下易错题进行详细讲解:
📌 题目:
(在此粘贴需要讲解的题目)
📋 讲解要求:
1. 第一步:分析题目考察的知识点
2. 第二步:指出解题的关键突破口
3. 第三步:逐步推导,每一步都说明"为什么这样做"
4. 第四步:总结此类题的通用解法
5. 第五步:给出2道变式题(难度相近但考法不同)
📋 特别注意:
- 不要直接给答案,采用引导式提问
- 指出常见的错误思路及为什么错
- 用通俗语言解释抽象概念
- 每一步都要画图辅助说明知识点分析:
本题考察三个核心知识点:
解题突破口:
突破口在于"递增→递减"这个转换。如果你熟悉头插法的特点——“先插入的在后面,后插入的在前面”——你就会意识到:从小到大取元素,用头插法插入,自然就形成了从大到小的顺序。
逐步推导:
初始状态:
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(递减有序 ✓)通用解法总结:
遇到"合并两个有序表"的题目,按以下步骤思考:
变式题:
本章学生反馈最多的易错题:
L->next = NULL——根本原因是没有理解头插法的初始化步骤≈ 5000 字【AI错题复盘Prompt - 可复制使用】
请你扮演一位考研408数据结构辅导专家,帮我分析以下错题:
📌 我的错题:
(在此粘贴做错的题目)
📌 我的错误解答:
(在此写下你的错误解题过程)
📌 正确答案:
(粘贴正确答案)
📋 分析要求:
1. 【错因诊断】分析我出错的根本原因:
- 是概念理解错误?计算失误?还是方法选择不当?
- 具体是哪个知识点存在漏洞?
2. 【知识漏洞定位】指出我需要回看的教材章节/知识点
3. 【正确思路】给出正确的解题思路与关键步骤
4. 【强化训练】针对我的薄弱环节,出3道同类型练习题
- 第1道:基础巩固(L1-L2)
- 第2道:中等难度(L3)
- 第3道:综合提升(L3-L4)
5. 【防错提醒】总结一句"下次遇到类似题一定要注意..."的提醒我的错题:
对带头结点的单链表L进行原地逆置,我写的代码如下:
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中"单链表插入操作"的指针顺序。核心原则是:先保存后继,再修改指针。
正确思路:
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;
}
}强化训练:
防错提醒:下次遇到链表指针操作,一定要先问自己三个问题:①后继保存了吗?②修改顺序对吗?③会不会形成环?
错因类型 | 次数 | 占比 | 对应知识点 |
|---|---|---|---|
指针操作顺序错误 | 35% | 最高频 | 插入/删除/逆置 |
边界条件遗漏 | 25% | 次高频 | 空表/单结点/尾部 |
头插法vs尾插法混淆 | 20% | 第三 | 合并/逆置 |
剩余部分未处理 | 15% | 第四 | 合并 |
尾结点未置空 | 5% | 最低 | 拆分 |
≈ 8000 字【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. 最后给出分数段评估建议一、选择题(每题5分,共25分)
二、填空题(每题5分,共25分)
三、算法设计题(共50分)
11.(15分)设计一个算法,将两个递增有序的单链表A和B归并为一个递增有序的单链表C。要求不申请新结点,只利用A和B的原有结点。
12.(15分)设计一个算法,找出带头结点的单链表L的中间结点。如果链表长度为偶数,返回中间两个结点中的前一个。要求时间复杂度O(n),空间复杂度O(1)。
13.(20分)设计一个算法,将带头结点的单链表L按以下规则重新排列:第一个结点、最后一个结点、第二个结点、倒数第二个结点……要求空间复杂度O(1)。
一、选择题
二、填空题
三、算法设计题
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)。
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;
}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;
}
}分数段评估建议:
≈ 3000 字

前置知识:
后续知识:
学习路径建议:
≈ 2500 字L->next = NULL?
A:如果不置空,第一个结点的next会指向自己(因为p->next = L->next,而此时L->next还是原来的首元结点),形成环。
p->next后,p应不应该前移?为什么?
A:不应该。因为新的p->next可能和p的值相同(如1→1→1→2),需要继续比较。
模块 | 状态 | 备注 |
|---|---|---|
知识点讲解 | ⬜ 未开始 / 🔄 进行中 / ✅ 已完成 | |
真题解析 | ⬜ 未开始 / 🔄 进行中 / ✅ 已完成 | |
AI命题练习 | ⬜ 未开始 / 🔄 进行中 / ✅ 已完成 | |
AI讲题学习 | ⬜ 未开始 / 🔄 进行中 / ✅ 已完成 | |
错题复盘 | ⬜ 未开始 / 🔄 进行中 / ✅ 已完成 | |
模拟卷测试 | ⬜ 未开始 / 🔄 进行中 / ✅ 已完成 | 得分:__/100 |
延伸阅读 | ⬜ 未开始 / 🔄 进行中 / ✅ 已完成 |
整体掌握程度评估:
