作者: 安全风信子 日期: 2026-07-22 主要来源: 王道考研《数据结构复习指导》、严蔚敏《数据结构(C语言版)》 读完你能学到: 掌握线性表的定义与顺序/链式存储选型,能独立实现基本运算并分析复杂度,解决408线性表基础真题
科目:数据结构 | 章节:第1章 线性表 | 难度:L1 标签:线性结构定义、顺序存储、链式存储、基本运算实现、存储选型
我先说一个我当年踩过的坑。
2019年,我第一次做408真题的数据结构部分。拿到第38题,题目说:
“设计一个算法,删除递增有序顺序表中值小于x且值为奇数的所有元素,要求时间复杂度为O(n)。”
我当时一看,心想这不就是遍历一遍然后删嘛。于是刷刷刷写了一个双重循环——外层遍历,内层删除时整体后移。写完一看,时间复杂度O(n²)。
更惨的是,考场上我还觉得自己写得挺对。
成绩出来那天,数据结构部分只拿了不到一半的分。后来复盘才发现,线性表的基本操作这块,我以为自己"会了",其实只是"背过"了。顺序表的删除要从前向后移还是从后向前移?链表的头结点和首元结点到底什么关系?带头结点和不带头结点的链表在删除操作时有什么区别?这些问题,我全都没搞清楚。
后来我花了一整周,把线性表这块从头到尾重新学了一遍。不是看视频那种"看懂了",而是每一行代码都自己敲、每一个边界条件都自己推。那一周之后,线性表的题目我再也没丢过分。
为什么要用"踩坑经历"开篇? 因为线性表是408数据结构的绝对基石。后面学到的栈、队列、串、树、图,全都在用线性表的思想。如果这块没学透,后面就是多米诺骨牌——倒一片。
你可能会说:“线性表不就是数组和链表嘛,有啥难的?”
这个问题,我在后面会详细回答。但现在,请你先带着以下几个问题往下读:
如果你对这五个问题不能秒答,那这篇文章就是为你写的。
读完这篇文章,你将获得以下具体收获:
收获1:线性表的精确定义 你将清楚知道线性表的数学定义——具有相同数据类型的n个数据元素的有限序列。你会理解"有限"、“有序”、"相同类型"这三个关键词的精确含义,以及线性表与数组、链表的本质区别。
收获2:顺序存储的完整实现 你将掌握顺序表的C语言实现,包括结构体定义、初始化、按位查找、按值查找、插入、删除、遍历等全部基本操作。你会理解每一个操作的实现细节、时间复杂度和边界条件。
收获3:链式存储的四种形态 你将系统掌握单链表、双链表、循环链表、静态链表四种链式存储结构。每种结构的定义、初始化、插入、删除、遍历操作,你都能手写代码。
收获4:头结点的深层意义 你将彻底理解为什么需要头结点。带头结点和不带头结点的链表在代码实现上的区别,你将烂熟于心。
收获5:存储选型的决策能力 你将能够根据具体应用场景,在顺序存储和链式存储之间做出正确选择。这个选择不是"背结论",而是基于对两种存储结构优缺点的深入理解。
收获6:真题解题的系统方法 通过5-8道真题的详细解析,你将掌握线性表相关真题的解题思路和常见陷阱。你会知道命题人喜欢在哪些地方设坑,以及如何避免。
收获7:AI辅助学习的完整工具链 你将获得一套完整的AI辅助学习Prompt模板,包括命题、讲题、错题复盘、模拟卷四个维度。这些模板可以直接用于日常复习。
收获8:自我检测与查漏补缺 通过文末的Checklist,你将能够系统地检验自己对线性表的掌握程度,找到薄弱环节并有针对性地强化。
线性表(Linear List) 是具有相同数据类型的n(n≥0)个数据元素的有限序列:
其中:
关键词拆解:
关键词 | 含义 | 易错点 |
|---|---|---|
相同数据类型 | 每个元素占用的存储空间相同 | 不是说元素值相同,而是类型相同 |
有限 | 元素个数n是有限的 | 理论上n可以非常大,但必须有限 |
序列 | 元素之间存在先后顺序 | 这个顺序是逻辑上的,与存储无关 |
线性表的逻辑特征:
除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
用数学语言表达:
我踩过这个坑: 很多人把"线性表"和"线性结构"搞混。线性结构是一个更大的概念,包括线性表、栈、队列、串等。线性表是最基本的线性结构。线性表的元素可以是任意类型(整数、字符、甚至另一个线性表),而栈和队列则对操作做了限制。
一个"完整的"线性表应该支持以下基本操作:
操作 | 功能 | 说明 |
|---|---|---|
InitList(&L) | 初始化线性表 | 构造一个空的线性表 |
Length(L) | 求表长 | 返回线性表中元素的个数 |
LocateElem(L, e) | 按值查找 | 返回第一个值等于e的元素的位序 |
GetElem(L, i) | 按位查找 | 返回第i个位置的值 |
ListInsert(&L, i, e) | 插入操作 | 在第i个位置插入元素e |
ListDelete(&L, i, &e) | 删除操作 | 删除第i个位置的元素,并用e返回 |
PrintList(L) | 输出操作 | 按顺序输出线性表的所有元素 |
Empty(L) | 判空操作 | 判断线性表是否为空 |
DestroyList(&L) | 销毁操作 | 销毁线性表并释放内存 |
ClearList(&L) | 清空操作 | 将线性表置为空表 |
注意: 以上操作的参数传递方式很重要。需要修改线性表本身的操作(如InitList、ListInsert、ListDelete)必须使用引用传递(C语言中用指针实现)。
顺序表(Sequential List) 是指采用顺序存储方式实现的线性表。顺序存储是指把逻辑上相邻的元素存储在物理上相邻的存储单元中。
顺序表的C语言定义:
#define MaxSize 50 // 定义线性表的最大长度
typedef int ElemType; // 定义线性表元素的数据类型
typedef struct {
ElemType data[MaxSize]; // 顺序表的元素
int length; // 顺序表的当前长度
} SqList;顺序表的两种实现方式:
方式一:静态分配
#define MaxSize 50
typedef struct {
ElemType data[MaxSize];
int length;
} SqList;静态分配的特点:
方式二:动态分配
#define InitSize 50
typedef struct {
ElemType *data; // 指向动态分配数组的指针
int MaxSize; // 最大容量
int length; // 当前长度
} SeqList;
// 初始化
void InitList(SeqList &L) {
L.data = (ElemType *)malloc(InitSize * sizeof(ElemType));
L.MaxSize = InitSize;
L.length = 0;
}
// 增加容量
void IncreaseSize(SeqList &L, int len) {
ElemType *p = L.data;
L.data = (ElemType *)malloc((L.MaxSize + len) * sizeof(ElemType));
L.MaxSize = L.MaxSize + len;
for (int i = 0; i < L.length; i++) {
L.data[i] = p[i];
}
free(p);
}顺序表的核心特点——随机存取:
由于顺序表的元素在内存中连续存放,且每个元素占用相同的存储空间,因此可以通过首地址 + 偏移量直接计算出任意元素的存储地址:
其中d为每个元素占用的存储空间大小。
这意味着存取第i个元素的时间复杂度为O(1),这就是所谓的随机存取(Random Access)。
链表(Linked List) 是指采用链式存储方式实现的线性表。链式存储不要求逻辑上相邻的元素在物理上也相邻,而是通过指针将分散的存储单元串联起来。
单链表的C语言定义:
typedef struct LNode {
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;头结点 vs 首元结点:
头结点 → 首元结点 → 第2个结点 → ... → 第n个结点 → NULL
(L) (a1) (a2) (an)为什么需要头结点?我踩过这个坑,所以特别强调:
不带头结点时,对第一个结点的插入和删除操作需要特殊处理(因为需要修改头指针L)。带头结点后,所有结点的插入和删除操作都统一了——都是在某个结点之后进行插入/删除,只不过头结点的"前一个"是NULL而已。
带头结点 vs 不带头结点的代码对比:
// ===== 不带头结点的插入 =====
bool ListInsert(LinkList &L, int i, ElemType e) {
if (i == 1) { // 需要特殊处理第一个位置!
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = L;
L = s;
return true;
}
LNode *p = L;
int j = 1;
while (p && j < i - 1) {
p = p->next;
j++;
}
if (!p || j > i - 1) return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
// ===== 带头结点的插入 =====
bool ListInsert(LinkList L, int i, ElemType e) {
LNode *p = L; // p指向头结点
int j = 0;
while (p && j < i - 1) {
p = p->next;
j++;
}
if (!p || j > i - 1) return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}可以看到,带头结点的版本代码更统一、更简洁,不需要对第一个位置做特殊处理。
双链表(Doubly Linked List) 的每个结点有两个指针域,分别指向直接后继和直接前驱:
typedef struct DNode {
ElemType data;
struct DNode *prior; // 前驱指针
struct DNode *next; // 后继指针
} DNode, *DLinkList;双链表的优势:可以方便地找到前驱结点,使得删除和插入操作更加灵活。
双链表的插入操作:
// 在p结点之后插入s结点
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;双链表的删除操作:
// 删除p结点的后继结点q
q = p->next;
p->next = q->next;
q->next->prior = p;
free(q);我踩过这个坑: 双链表的插入操作中,四句话的顺序不能乱!
s->next = p->next必须在p->next->prior = s之前,否则p->next已经被修改了,p->next->prior就指向了错误的位置。这个顺序问题在考试中经常考。
循环单链表:表中最后一个结点的指针域指向头结点,整个链表形成一个环。
// 循环单链表的判空条件
// 不带头结点:L == NULL 为空表
// 带头结点:L->next == L 为空表循环双链表:在循环单链表的基础上,每个结点增加一个前驱指针。表头结点的前驱指针指向表尾结点,表尾结点的后继指针指向表头结点。
// 循环双链表的判空条件
// L->next == L && L->prior == L 为空表静态链表(Static Linked List) 是借助数组来描述链表的一种实现方式。每个结点由数据域和"游标"(cursor)组成,游标指示下一个结点在数组中的下标。
#define MaxSize 50
typedef struct {
ElemType data;
int next; // 下一个结点的数组下标
} SLinkList[MaxSize];静态链表的特点:
(1)按位查找(GetElem)
ElemType GetElem(SqList L, int i) {
return L.data[i - 1]; // 位序i从1开始,数组下标从0开始
}由于随机存取特性,按位查找的时间复杂度为 O(1)。
(2)按值查找(LocateElem)
int LocateElem(SqList L, ElemType e) {
for (int i = 0; i < L.length; i++) {
if (L.data[i] == e)
return i + 1; // 返回位序
}
return 0; // 查找失败
}按值查找需要逐个比较:
因此平均时间复杂度为 O(n)。
(3)插入操作(ListInsert)
bool ListInsert(SqList &L, int i, ElemType e) {
if (i < 1 || i > L.length + 1) // 判断i的范围是否有效
return false;
if (L.length >= MaxSize) // 判断顺序表是否已满
return false;
for (int j = L.length; j >= i; j--) // 从后往前移动元素
L.data[j] = L.data[j - 1];
L.data[i - 1] = e;
L.length++;
return true;
}插入操作需要移动元素:
因此平均时间复杂度为 O(n)。
我踩过这个坑: 插入操作中元素移动的方向是从后往前!这是因为如果从前往后移动,会把后面的元素覆盖掉。很多初学者写代码时方向搞反,导致数据丢失。记住:插入时从后往前移,删除时从前往后移。
(4)删除操作(ListDelete)
bool ListDelete(SqList &L, int i, ElemType &e) {
if (i < 1 || i > L.length) // 判断i的范围是否有效
return false;
e = L.data[i - 1]; // 将被删除的元素赋值给e
for (int j = i; j < L.length; j++) // 从前往后移动元素
L.data[j - 1] = L.data[j];
L.length--;
return true;
}删除操作需要移动元素:
因此平均时间复杂度为 O(n)。
(1)按位查找(GetElem)
LNode *GetElem(LinkList L, int i) {
LNode *p = L->next; // 从头结点之后的第一个结点开始
int j = 1;
while (p && j < i) {
p = p->next;
j++;
}
return p;
}链表不支持随机存取,按位查找需要从头遍历:
(2)头插法建立链表
LinkList List_HeadInsert(LinkList &L) {
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
scanf("%d", &x);
while (x != 9999) {
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
return L;
}头插法建立的链表,元素顺序与输入顺序相反。时间复杂度O(n)。
(3)尾插法建立链表
LinkList List_TailInsert(LinkList &L) {
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode *s, *r = L; // r为表尾指针
scanf("%d", &x);
while (x != 9999) {
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; // r指向新的表尾
scanf("%d", &x);
}
r->next = NULL; // 尾结点指针置空
return L;
}尾插法建立的链表,元素顺序与输入顺序相同。时间复杂度O(n)。
我踩过这个坑: 尾插法中,
r->next = NULL这一行很容易忘!如果不写,最后一个结点的next指针是一个随机值,后续遍历链表时会出问题。
存储方式 | 空间复杂度 | 说明 |
|---|---|---|
顺序表(静态分配) | O(MaxSize) | 预分配固定大小空间 |
顺序表(动态分配) | O(n) | 按需分配,可能需要扩容 |
单链表 | O(n) | 每个结点需要额外的指针空间 |
双链表 | O(n) | 每个结点需要两个指针空间 |
空间效率的细节:
虽然顺序表和链表的空间复杂度都是O(n),但实际占用的空间不同:
当元素类型较大时,链表的指针开销占比相对较小;当元素类型很小(如char)时,指针开销占比可能很大。


顺序表的特点:
单链表的特点:

关键记忆点:插入时从后往前移!
如果从前往后移:


头结点的意义:
错误理解: “顺序表就是数组,没什么好学的。”
正确理解: 顺序表是数据结构,数组是编程语言的数据类型。顺序表 = 数组 + 表长管理 + 基本操作封装。
顺序表封装了以下逻辑:
在C语言中,顺序表通常用结构体来封装:
typedef struct {
ElemType data[MaxSize];
int length;
} SqList;这里的SqList就是顺序表,data数组只是顺序表的一部分。
错误理解: “头指针就是头结点,头结点就是头指针。”
正确理解:
情况 | 头指针指向 | 判空条件 |
|---|---|---|
不带头结点 | 首元结点(第一个数据结点) | L == NULL |
带头结点 | 头结点 | L->next == NULL |
我踩过这个坑: 考研真题曾经考过"头指针和头结点的区别"。如果分不清这两个概念,选择题直接丢分。记住:头指针一定存在,头结点是可选的。
错误理解: “链表按需分配空间,所以空间效率比顺序表好。”
正确理解: 这个问题要分情况讨论:
错误理解: “循环链表和循环队列是一样的。”
正确理解:
循环队列可以用顺序存储实现(用取模运算实现循环),也可以用链式存储实现。循环链表只是链表的一种形态,和队列没有必然关系。
错误理解: “静态链表既没有顺序表的随机存取优点,也没有链表的动态分配优点,完全没用。”
正确理解: 静态链表在某些场景下确实有用:
错误理解: “双链表有两个指针,所以查找速度更快。”
正确理解: 双链表的查找时间复杂度仍然是O(n),和单链表一样。双链表的优势不在于查找速度,而在于:
顺序表插入: i的合法范围是 1 ≤ i ≤ length + 1(可以在表尾之后插入)
顺序表删除: i的合法范围是 1 ≤ i ≤ length(必须删除已存在的元素)
链表插入: i的合法范围是 1 ≤ i ≤ length + 1
链表删除: i的合法范围是 1 ≤ i ≤ length
我踩过这个坑: 这个范围在真题选择题中反复出现。很多人把插入和删除的范围搞混,特别是"i = length + 1"这个边界情况。记住:插入可以插到末尾之后,删除只能删已有的。
设有一个带头结点的循环单链表,链表中每个结点包含data和next两个域。设计一个算法,判断该链表是否关于某个结点对称。所谓对称是指:从某个结点出发,沿next方向遍历和沿反方向遍历得到的元素序列相同。
解题思路:
这道题考的是循环双链表的对称性判断。但题目给的是循环单链表,所以需要先理解题意。
实际上,对于循环单链表,"反方向遍历"意味着需要找到前驱结点。这本身就是一个O(n)的操作。
算法思路(针对循环单链表):
更高效的思路(如果是循环双链表):
// 判断循环双链表是否关于p结点对称
bool isSymmetric(DLinkList L, DNode *p) {
DNode *left = p->prior;
DNode *right = p->next;
while (left != p && right != p) {
if (left->data != right->data)
return false;
left = left->prior;
right = right->next;
}
return true;
}时间复杂度: O(n) 空间复杂度: O(1)
命题人陷阱: 这道题容易让人陷入"对每个结点都检查一遍"的思路,导致时间复杂度变成O(n²)。实际上,如果链表关于某个结点对称,那么对称中心是唯一的(奇数个元素)或有两个(偶数个元素)。可以先找到中间位置,再检查。
设计一个算法,将带头结点的单链表L分解为两个带头结点的单链表L1和L2。L1包含原链表中位序为奇数的元素,L2包含位序为偶数的元素。要求保持原有顺序。
解题思路:
这道题考的是链表的基本操作——遍历和插入。
void SplitList(LinkList L, LinkList &L1, LinkList &L2) {
// 初始化L1和L2
L1 = (LinkList)malloc(sizeof(LNode));
L2 = (LinkList)malloc(sizeof(LNode));
L1->next = NULL;
L2->next = NULL;
LNode *r1 = L1; // L1的尾指针
LNode *r2 = L2; // L2的尾指针
LNode *p = L->next; // 工作指针
int count = 1; // 位序计数器
while (p != NULL) {
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = p->data;
s->next = NULL;
if (count % 2 == 1) { // 奇数位序
r1->next = s;
r1 = s;
} else { // 偶数位序
r2->next = s;
r2 = s;
}
p = p->next;
count++;
}
}时间复杂度: O(n) 空间复杂度: O(n)(创建了新结点)
变式: 如果要求在原链表上操作(不创建新结点),只需要修改指针:
void SplitListInPlace(LinkList L, LinkList &L1, LinkList &L2) {
L1 = (LinkList)malloc(sizeof(LNode));
L2 = (LinkList)malloc(sizeof(LNode));
L1->next = NULL;
L2->next = NULL;
LNode *r1 = L1;
LNode *r2 = L2;
LNode *p = L->next;
int count = 1;
while (p != NULL) {
LNode *q = p->next; // 先保存后继
p->next = NULL;
if (count % 2 == 1) {
r1->next = p;
r1 = p;
} else {
r2->next = p;
r2 = p;
}
p = q;
count++;
}
}命题人陷阱: 注意 LNode *q = p->next 这一行必须在修改 p->next 之前执行,否则就丢失了后继结点的信息。
给定一个带头结点的单链表L,设计一个算法,判断链表中从第k个元素开始的连续m个元素是否构成回文序列。
解题思路:
回文判断的经典方法:用栈或双指针。
方法一:用栈(推荐)
bool isPalindrome(LinkList L, int k, int m) {
if (m <= 1) return true;
// 找到第k个结点
LNode *p = GetElem(L, k);
if (!p) return false;
// 将前m/2个元素入栈
int half = m / 2;
ElemType stack[100];
int top = -1;
LNode *q = p;
for (int i = 0; i < half; i++) {
stack[++top] = q->data;
q = q->next;
}
// 如果m是奇数,跳过中间元素
if (m % 2 == 1) q = q->next;
// 比较后半部分与栈中元素
for (int i = 0; i < half; i++) {
if (q->data != stack[top--])
return false;
q = q->next;
}
return true;
}时间复杂度: O(k + m) 空间复杂度: O(m)
命题人陷阱: 注意m为奇数时要跳过中间元素。很多人忘了这一步。
设顺序表L中的元素递增有序排列。设计一个算法,将元素x插入到顺序表中,并保持其有序性。要求时间复杂度尽量低。
解题思路:
既然是递增有序表,可以用二分查找找到x的插入位置,然后插入。
但注意:二分查找的时间复杂度是O(log n),但插入操作需要移动元素,时间复杂度是O(n)。所以总时间复杂度是O(n)。
方法一:从后往前比较+移动(最优)
bool InsertOrder(SqList &L, ElemType x) {
if (L.length >= MaxSize) return false;
int i = L.length - 1;
// 从后往前找插入位置,同时移动元素
while (i >= 0 && L.data[i] > x) {
L.data[i + 1] = L.data[i];
i--;
}
L.data[i + 1] = x;
L.length++;
return true;
}时间复杂度: O(n)(最好情况O(1),x大于所有元素) 空间复杂度: O(1)
方法二:二分查找+插入
bool InsertOrderBinary(SqList &L, ElemType x) {
if (L.length >= MaxSize) return false;
// 二分查找插入位置
int low = 0, high = L.length - 1;
int pos = L.length; // 默认插入到末尾
while (low <= high) {
int mid = (low + high) / 2;
if (L.data[mid] == x) {
pos = mid;
break;
} else if (L.data[mid] < x) {
low = mid + 1;
pos = low;
} else {
high = mid - 1;
pos = low;
}
}
// 移动元素
for (int i = L.length; i > pos; i--) {
L.data[i] = L.data[i - 1];
}
L.data[pos] = x;
L.length++;
return true;
}命题人陷阱: 方法一看起来更简洁,但它利用了"边比较边移动"的技巧。方法二虽然用了二分查找,但总时间复杂度仍然是O(n)。在考试中,方法一更受阅卷老师青睐,因为它体现了对顺序表操作的深入理解。
设计一个算法,删除带头结点的单链表中值等于x的多余结点(可能有多个),使链表中不再有值为x的结点。
解题思路:
遍历链表,找到值为x的结点并删除。
void DeleteX(LinkList L, ElemType x) {
LNode *p = L->next; // p指向第一个数据结点
LNode *pre = L; // pre指向p的前驱
while (p != NULL) {
if (p->data == x) {
LNode *q = p;
pre->next = p->next; // 删除p结点
p = p->next;
free(q);
} else {
pre = p;
p = p->next;
}
}
}时间复杂度: O(n) 空间复杂度: O(1)
命题人陷阱: 注意删除结点后,pre不应该移动(因为新的p可能还是x)。只有当p->data != x时,pre才跟着移动。很多人写成:
// 错误写法
while (p != NULL) {
if (p->data == x) {
pre->next = p->next;
free(p);
p = pre->next;
}
pre = p; // 这里错了!删除后pre不应该移动
p = p->next;
}我踩过这个坑: 连续删除多个相同值的结点时,pre的移动逻辑是核心难点。记住:删除后pre不动,不删时pre才动。
设有一个带头结点的单链表,设计一个算法将其就地逆置。所谓"就地"是指辅助空间复杂度为O(1)。
解题思路:
链表逆置的经典方法:头插法。
void ReverseList(LinkList L) {
LNode *p = L->next; // 第一个数据结点
LNode *q;
L->next = NULL; // 先将头结点的next置空
while (p != NULL) {
q = p->next; // 保存后继
p->next = L->next; // 将p插入到头结点之后
L->next = p;
p = q;
}
}时间复杂度: O(n) 空间复杂度: O(1)
另一种思路:递归逆置
LinkList ReverseRecursive(LinkList L) {
if (L->next == NULL || L->next->next == NULL)
return L;
LNode *p = L->next;
LinkList newHead = ReverseRecursive(p);
// 此时p是原链表的最后一个结点
p->next->next = p;
p->next = NULL;
L->next = newHead;
return L;
}但递归的空间复杂度是O(n)(递归栈),不满足"就地"的要求。
命题人陷阱: 注意 L->next = NULL 这一行。如果不写,原来的第一个结点(逆置后变成最后一个)的next指针会指向自己,形成环。
已知两个递增有序的单链表la和lb(带头结点),设计一个算法将它们归并为一个递减有序的单链表lc,要求不使用原la和lb的结点空间。
解题思路:
这道题有两个关键点:
方法:头插法
LinkList MergeDescending(LinkList la, LinkList lb) {
LinkList lc = (LinkList)malloc(sizeof(LNode));
lc->next = NULL;
LNode *pa = la->next;
LNode *pb = lb->next;
LNode *s;
while (pa != NULL && pb != NULL) {
if (pa->data <= pb->data) {
s = (LNode *)malloc(sizeof(LNode));
s->data = pa->data;
s->next = lc->next;
lc->next = s;
pa = pa->next;
} else {
s = (LNode *)malloc(sizeof(LNode));
s->data = pb->data;
s->next = lc->next;
lc->next = s;
pb = pb->next;
}
}
// 处理剩余元素
while (pa != NULL) {
s = (LNode *)malloc(sizeof(LNode));
s->data = pa->data;
s->next = lc->next;
lc->next = s;
pa = pa->next;
}
while (pb != NULL) {
s = (LNode *)malloc(sizeof(LNode));
s->data = pb->data;
s->next = lc->next;
lc->next = s;
pb = pb->next;
}
return lc;
}时间复杂度: O(la.length + lb.length) 空间复杂度: O(la.length + lb.length)(创建了新结点)
如果要求使用原结点空间(不创建新结点):
LinkList MergeDescendingInPlace(LinkList &la, LinkList &lb) {
LinkList lc = (LinkList)malloc(sizeof(LNode));
lc->next = NULL;
LNode *pa = la->next;
LNode *pb = lb->next;
LNode *r;
while (pa != NULL && pb != NULL) {
if (pa->data <= pb->data) {
r = pa->next;
pa->next = lc->next;
lc->next = pa;
pa = r;
} else {
r = pb->next;
pb->next = lc->next;
lc->next = pb;
pb = r;
}
}
while (pa != NULL) {
r = pa->next;
pa->next = lc->next;
lc->next = pa;
pa = r;
}
while (pb != NULL) {
r = pb->next;
pb->next = lc->next;
lc->next = pb;
pb = r;
}
// 释放原头结点
free(la);
free(lb);
return lc;
}设计一个算法,找出带头结点的单链表中倒数第k个结点。如果存在,输出其值;否则输出"NOT FOUND"。要求时间复杂度为O(n)。
解题思路:
经典的双指针法(快慢指针)。
bool FindKthFromEnd(LinkList L, int k, ElemType &e) {
LNode *p = L->next; // 快指针
LNode *q = L->next; // 慢指针
int count = 0;
// 快指针先走k步
while (p != NULL && count < k) {
p = p->next;
count++;
}
// 如果链表长度不足k
if (count < k) return false;
// 快慢指针同时移动
while (p != NULL) {
p = p->next;
q = q->next;
}
e = q->data;
return true;
}时间复杂度: O(n) 空间复杂度: O(1)
原理: 快指针先走k步后,快慢指针之间相距k个位置。当快指针走到链表末尾时,慢指针恰好走到倒数第k个位置。
命题人陷阱: 注意count的计数方式。快指针走了k步后,如果链表长度恰好等于k,快指针指向NULL,此时慢指针指向第一个结点,即倒数第k个结点。如果链表长度小于k,count < k,返回false。
通过对历年真题的分析,线性表部分的命题规律如下:
考点 | 出题频率 | 题型 | 难度 |
|---|---|---|---|
顺序表插入/删除的元素移动方向 | 高频 | 选择题 | ★★ |
链表头结点的概念 | 高频 | 选择题 | ★★ |
头插法/尾插法建链表 | 高频 | 算法题 | ★★★ |
链表逆置 | 中频 | 算法题 | ★★★ |
有序表的归并 | 中频 | 算法题 | ★★★ |
双指针法 | 中频 | 算法题 | ★★★★ |
循环链表的操作 | 低频 | 选择题/算法题 | ★★★ |
静态链表 | 低频 | 选择题 | ★★ |
命题趋势:
以下是用于AI生成线性表相关考题的Prompt模板:
你是一位408考研数据结构的命题专家。请根据以下要求,生成关于"线性表的定义与基本操作"的考题。
【知识范围】
- 线性表的定义与逻辑特征
- 顺序存储结构(静态分配、动态分配)
- 链式存储结构(单链表、双链表、循环链表、静态链表)
- 基本操作:初始化、查找、插入、删除、遍历
- 头结点与头指针的区别
- 头插法与尾插法建立链表
- 时间复杂度和空间复杂度分析
【题目要求】
1. 题型分布:选择题4道,简答题1道,算法设计题1道
2. 难度分布:2道基础题(★),2道中等题(★★★),1道较难题(★★★★),1道综合题(★★★★★)
3. 选择题要有干扰项,干扰项要具有迷惑性
4. 算法题要给出完整的C语言代码和复杂度分析
5. 每道题都要给出详细的解析
【输出格式】
对于每道题,请按以下格式输出:
- 题目内容
- 选项(选择题)
- 正确答案
- 详细解析(包括为什么正确选项是对的,为什么其他选项是错的)
- 涉及的知识点
- 易错点提示
【特别注意】
- 选择题的干扰项要基于常见误区设计
- 算法题要考虑边界条件
- 要包含至少一道需要分析时间/空间复杂度的题目
- 题目要体现408真题的命题风格选择题1(基础):
下列关于线性表的说法中,正确的是( )。 A. 线性表的顺序存储结构是一种随机存取结构 B. 线性表的链式存储结构是一种随机存取结构 C. 顺序表中逻辑相邻的元素在物理上不一定相邻 D. 链表中逻辑相邻的元素在物理上一定相邻
答案:A
解析:
涉及知识点: 顺序存储vs链式存储的基本特征
选择题2(基础):
在一个长度为n的顺序表中,在第i个位置(1≤i≤n+1)插入一个新元素时,需要向前移动( )个元素。 A. n - i B. n - i + 1 C. n - i - 1 D. i
答案:B
解析: 在第i个位置插入时,需要将第i个到第n个元素都向后移动一位。需要移动的元素个数 = n - i + 1。
举例验证:n=5,在i=1处插入,需要移动5-1+1=5个元素(全部后移)。✓ n=5,在i=5处插入,需要移动5-5+1=1个元素(只有第5个元素后移)。✓ n=5,在i=6处插入,需要移动5-6+1=0个元素(直接在末尾插入)。✓
易错点: 很多人选A(n-i),这是因为把"插入"和"删除"的移动个数搞混了。删除时需要移动n-i个元素,插入时需要移动n-i+1个元素。
选择题3(中等):
设带头结点的单链表如下所示(头结点未画出数据域),则以下关于在该链表中删除第一个数据元素a₁的操作中,正确的是( )。
L → [头|next] → [a₁|next] → [a₂|next] → ... → [an|^]A. L = L->next; free(L); B. L->next = L->next->next; free(L->next); C. p = L->next; L->next = p->next; free§; D. p = L; L = L->next; free§;
答案:C
解析:
涉及知识点: 链表删除操作的正确实现,指针操作的顺序
选择题4(中等):
若某线性表最常用的操作是:存取任一指定序号的元素和在最后进行插入和删除操作,则采用( )存储方式最节省时间。 A. 顺序表 B. 单链表 C. 双链表 D. 循环单链表
答案:A
解析:
综合来看,顺序表在这两个操作上都是O(1),最优。
注意: 如果题目改为"在第一个位置进行插入和删除",链表(带头结点)是O(1),而顺序表是O(n),答案就变成链表了。
算法设计题(综合):
设L为带头结点的单链表,编写算法实现:删除链表中第i个位置之前(不含第i个位置)的所有结点。若i≤0或i>链表长度+1,则不进行删除操作。
参考答案:
bool DeleteBefore(LinkList L, int i) {
if (i <= 0) return false;
// 先求链表长度
int len = 0;
LNode *p = L->next;
while (p != NULL) {
len++;
p = p->next;
}
if (i > len + 1) return false;
// 找到第i-1个结点的前驱(即第i-2个结点)
// 删除从第1个到第i-2个的所有结点
if (i <= 2) return true; // i=1或i=2时,不需要删除
LNode *pre = L; // 第0个(头结点)
for (int j = 1; j < i - 1; j++) {
pre = pre->next;
}
// 删除pre之后的所有结点到第i-1个结点之前
LNode *q = pre->next;
while (q != NULL && q != GetElem(L, i)) {
// 这里需要特殊处理
// 实际上更好的做法是:
break;
}
// 更简洁的做法:
// 找到第i-1个结点
LNode *target = L;
for (int j = 0; j < i - 1; j++) {
target = target->next;
}
// 删除头结点之后到target之前的所有结点
LNode *curr = L->next;
while (curr != target) {
LNode *next = curr->next;
free(curr);
curr = next;
}
L->next = target;
return true;
}时间复杂度: O(n) 空间复杂度: O(1)
简答题:
简述顺序表和链表的优缺点,并说明在什么场景下应该选择顺序表,什么场景下应该选择链表。
参考答案:
比较项 | 顺序表 | 链表 |
|---|---|---|
存取方式 | 随机存取O(1) | 顺序存取O(n) |
插入/删除 | 需要移动元素O(n) | 只需修改指针O(1)(已知位置时) |
空间分配 | 需要预分配,可能浪费或溢出 | 按需分配,无浪费 |
存储密度 | 高(只有数据) | 低(数据+指针) |
缓存友好 | 好(连续内存) | 差(分散内存) |
选择依据:
如何使用上述Prompt模板:
Prompt调优技巧:
你是一位408考研数据结构的辅导老师,擅长用通俗易懂的方式讲解难题。请按照以下要求讲解这道题目:
【题目信息】
{粘贴题目内容}
【讲解要求】
1. 先用一句话概括这道题考的是什么知识点
2. 分析题目中的关键信息和隐含条件
3. 给出解题思路(从"拿到题目后怎么想"开始,不要直接给答案)
4. 给出完整的解题过程(代码要逐行注释)
5. 分析时间复杂度和空间复杂度
6. 指出这道题的易错点和常见错误
7. 总结这类题的通用解题方法
8. 给出1-2道类似的练习题
【讲解风格】
- 用"我踩过这个坑"的人感视角
- 不要说"显然"、"容易看出"这类话
- 每一步都要解释"为什么这样做"
- 用具体的数字举例来辅助理解
- 适当使用类比帮助记忆题目: 设计一个算法,将带头结点的单链表就地逆置。
讲解:
一句话概括: 这道题考的是链表的基本操作——指针的修改。核心思想是头插法。
关键信息分析:
解题思路:
拿到这道题,我首先想到的是:逆置链表,就是把每个结点的next指针反转。
方法一:依次把每个结点的next指向它的前驱。但这需要知道前驱是谁,单链表做不到。
方法二:头插法。把原链表中的结点一个一个取下来,每次插到头结点之后。这样最后插入的结点就变成了第一个结点,实现了逆置。
为什么头插法能实现逆置?
举个例子:原链表是 1→2→3→4→5
完美逆置!
完整代码:
void ReverseList(LinkList L) {
LNode *p = L->next; // p指向第一个数据结点
LNode *q; // q用来保存p的后继
L->next = NULL; // 关键!先把头结点的next置空
// 因为逆置后原来的第一个结点变成最后一个
while (p != NULL) {
q = p->next; // ① 保存p的后继(不保存就丢了!)
p->next = L->next; // ② p的next指向当前第一个数据结点
L->next = p; // ③ 头结点的next指向p(p成为新的第一个)
p = q; // ④ p后移到原来保存的后继
}
}复杂度分析:
易错点:
我踩过这个坑:
L->next = NULL这一行太容易被忘了!如果不写这一行,原来的第一个结点(逆置后变成最后一个)的next指针还指向原来的第二个结点,形成环。遍历链表时会死循环。
还有一个常见错误是①②③的顺序搞反了。如果先执行③再执行②:
L->next = p; // 先执行这步
p->next = L->next; // 这时L->next已经是p了!p->next = p,自环!通用解题方法总结:
凡是遇到"链表逆置"、"链表反转"类题目,记住两个方法:
考试中要求"就地"就用方法一,没有空间限制就用方法二。
类似练习题:
易错题1:头插法建链表的顺序
输入序列 1, 2, 3, 4, 5,用头插法建立的链表是?
错误答案: 1→2→3→4→5 正确答案: 5→4→3→2→1
错因: 头插法每次把新结点插在最前面,所以后输入的元素反而在前面。
记忆口诀: “头插法,反着来;尾插法,顺着来。”
易错题2:删除操作中指针的顺序
删除单链表中p的后继结点q,以下操作顺序正确的是?
错误做法:
p->next = q->next; // 先断链
free(q); // 再释放这个做法本身没错,但如果写成:
free(q); // 先释放
p->next = q->next; // 再断链 → 错误!q已经被释放了错因: 释放结点后不能再访问它的成员。
正确顺序: 先断链,再释放。永远记住这个顺序。
易错题3:循环链表判空条件
带头结点的循环单链表的判空条件是?
错误答案: L == NULL 正确答案: L->next == L
错因: 循环单链表的尾结点指向头结点。空表时,头结点的next指向自己。L == NULL是不带头结点的单链表的判空条件。
你是一位408考研数据结构的错题分析专家。请帮我分析以下错题,找出错误原因并制定改进计划。
【错题信息】
题目:{粘贴题目}
我的答案:{你的答案}
正确答案:{正确答案}
我的解题过程:{你当时的思路}
【分析要求】
1. 分析我的错误属于哪种类型:
- 概念理解错误(对知识点理解有误)
- 边界条件遗漏(忽略了特殊情况)
- 代码实现错误(逻辑对但代码写错)
- 复杂度分析错误(算法对但复杂度算错)
- 审题错误(理解错了题意)
2. 找出错误的根本原因
3. 给出正确的解题思路
4. 指出我知识体系中的薄弱环节
5. 推荐针对性的练习题目
6. 给出避免同类错误的策略
【输出格式】
- 错误类型:xxx
- 错误原因:xxx
- 正确思路:xxx
- 薄弱环节:xxx
- 强化练习:xxx
- 防范策略:xxx错题信息:
题目: 在长度为n的顺序表中删除第i个元素,需要移动元素的个数为( )。 A. n - i B. n - i + 1 C. n - i - 1 D. i
我的答案: B(n - i + 1) 正确答案: A(n - i)
我的解题过程: 我觉得删除第i个元素后,第i+1到第n个元素都要前移,所以移动个数是n - (i+1) + 1 = n - i。但我选了n - i + 1,因为我觉得还要把最后一个位置清空。
分析结果:
错误类型: 概念理解错误
错误原因:
正确思路:
删除第i个元素时:
用具体数字验证:
薄弱环节: 顺序表插入和删除操作的元素移动个数
强化练习:
防范策略:
根据对历年考生错题的分析,线性表部分的常见错误归因如下:
错误类型 | 占比 | 典型表现 | 改进方法 |
|---|---|---|---|
指针操作顺序错误 | 25% | 链表插入/删除时指针修改顺序搞反 | 画指针图,严格按顺序操作 |
边界条件遗漏 | 20% | 空表、满表、第一个/最后一个位置 | 写代码前先列出所有边界情况 |
概念混淆 | 20% | 头指针vs头结点、顺序表vs数组 | 制作概念对比表 |
移动方向搞反 | 15% | 插入时从前往后移、删除时从后往前移 | 记住口诀:插后删前 |
复杂度分析错误 | 10% | 最好/最坏/平均情况搞混 | 分别分析三种情况 |
审题错误 | 10% | 看漏"带头结点"、"就地"等关键词 | 读题时圈出关键词 |
你是一位408考研数据结构的命题组组长。请生成一份关于"线性表"的模拟试卷。
【试卷要求】
1. 总分:50分
2. 考试时间:60分钟
3. 题型分布:
- 单项选择题:10道,每题2分,共20分
- 填空题:5道,每题2分,共10分
- 简答题:2道,每题5分,共10分
- 算法设计题:1道,10分
4. 知识点覆盖:
- 线性表定义(10%)
- 顺序存储(30%)
- 链式存储(40%)
- 基本操作实现(15%)
- 存储选型(5%)
5. 难度分布:
- 基础题(★):40%
- 中等题(★★★):35%
- 较难题(★★★★):20%
- 综合题(★★★★★):5%
【命题原则】
1. 选择题要有迷惑性强的干扰项
2. 算法题要考查时空效率
3. 要包含至少2道需要综合分析的题目
4. 题目要体现408真题的命题风格
5. 每道题都要标注考查的知识点
【输出要求】
1. 先输出完整试卷(不含答案)
2. 然后输出参考答案和评分标准
3. 最后给出每道题的知识点标注和难度标注408数据结构——线性表模拟试卷
总分:50分 考试时间:60分钟
一、单项选择题(每题2分,共20分)
1. 线性表是( )。 A. 一种链式存储结构 B. 一种顺序存储结构 C. 一种逻辑结构 D. 一种散列存储结构
2. 设顺序表的长度为n,以下操作中时间复杂度为O(1)的是( )。 A. 在第i个位置插入元素(1≤i≤n+1) B. 删除第i个位置的元素(1≤i≤n) C. 按位查找第i个元素(1≤i≤n) D. 按值查找元素x
3. 在一个带头结点的单链表中,头指针为L,则判断链表为空的条件是( )。 A. L == NULL B. L->next == NULL C. L->next == L D. L == NULL || L->next == NULL
4. 若用单链表表示队列,则在进行出队操作时( )。 A. 需要修改头指针 B. 需要修改尾指针 C. 需要同时修改头指针和尾指针 D. 不需要修改头指针和尾指针
5. 在双链表中,在p结点之后插入s结点的操作序列是( )。 A. p->next=s; s->prior=p; s->next=p->next; p->next->prior=s; B. s->prior=p; s->next=p->next; p->next=s; p->next->prior=s; C. s->next=p->next; p->next->prior=s; p->next=s; s->prior=p; D. p->next->prior=s; s->next=p->next; s->prior=p; p->next=s;
6. 设线性表有n个元素,以下操作中( )在顺序表上实现比在链表上实现效率更高。 A. 输出线性表中的第i个元素(1≤i≤n) B. 在第i个元素之后插入一个新元素 C. 删除第i个元素 D. 将线性表逆置
7. 以下关于静态链表的说法中,正确的是( )。 A. 静态链表需要连续的存储空间 B. 静态链表通过指针实现元素之间的逻辑关系 C. 静态链表通过数组下标实现元素之间的逻辑关系 D. 静态链表支持随机存取
8. 设有一个循环双链表,头指针为L,则判断链表为空的条件是( )。 A. L->next == NULL B. L->next == L C. L->next == L && L->prior == L D. L == NULL
9. 将两个各有n个元素的有序顺序表归并为一个有序顺序表,最好的情况下时间复杂度为( )。 A. O(1) B. O(n) C. O(nlogn) D. O(n²)
10. 以下关于线性表的说法中,不正确的是( )。 A. 线性表可以采用顺序存储或链式存储 B. 线性表中元素的个数可以是零 C. 线性表中每个元素都有且仅有一个直接前驱和一个直接后继 D. 线性表的逻辑特征是元素之间存在一对一的前驱后继关系
二、填空题(每题2分,共10分)
11. 在长度为n的顺序表中插入一个新元素,平均需要移动______个元素。
12. 带头结点的单链表中,在头结点之后插入一个新结点s的操作是:s->next = L->next; ______。
13. 循环单链表中,尾结点的next指针指向______。
14. 双链表中删除p结点的后继结点q,需要修改______个指针域。
15. 头插法建立单链表时,最终链表中元素的顺序与输入顺序______。
三、简答题(每题5分,共10分)
16. 简述顺序表和链表的主要区别,并各举一个适合使用顺序表和链表的实际应用场景。(5分)
17. 为什么要在单链表中设置头结点?头结点的作用是什么?如果不设头结点,对哪些操作会有影响?(5分)
四、算法设计题(10分)
18. 设有一个带头结点的单链表L,链表中的元素按值递增有序排列。设计一个算法,删除链表中值大于mink且小于maxk的所有元素,使得链表中不再有满足条件的元素。要求: (1)给出算法设计思想(3分) (2)用C语言编写算法代码(5分) (3)分析算法的时间复杂度和空间复杂度(2分)
参考答案与评分标准
一、选择题
题号 | 答案 | 知识点 | 难度 |
|---|---|---|---|
1 | C | 线性表的逻辑结构本质 | ★ |
2 | C | 顺序表按位查找O(1) | ★ |
3 | B | 带头结点单链表判空 | ★★ |
4 | A | 队列的链式实现 | ★★★ |
5 | C | 双链表插入操作的指针顺序 | ★★★ |
6 | A | 顺序表随机存取vs链表顺序存取 | ★★ |
7 | C | 静态链表的实现方式 | ★★ |
8 | C | 循环双链表判空 | ★★★ |
9 | B | 有序表归并的时间复杂度 | ★★★ |
10 | C | 线性表定义(表头无前驱) | ★★ |
详细解析:
第1题: 线性表是一种逻辑结构,描述的是元素之间一对一的线性关系。顺序存储和链式存储是线性表的两种物理存储方式。选C。
第2题: 顺序表按位查找通过下标直接访问,时间复杂度O(1)。插入和删除需要移动元素,O(n)。按值查找需要遍历,O(n)。选C。
第3题: 带头结点的单链表,头指针L指向头结点。空表时,头结点的next为NULL,即L->next == NULL。注意:L本身不为NULL(头结点始终存在)。选B。
第4题: 用单链表表示队列时,出队操作删除的是队头元素。如果队列只有一个元素,出队后需要同时修改头指针和尾指针。但一般情况下只需要修改头指针。题目问的是"出队操作时",最准确的答案是A(需要修改头指针),因为尾指针只在特殊情况下需要修改。选A。
第5题: 在p之后插入s,正确的顺序是:
注意1和2必须在3之前,否则p->next已经被修改。选C。
第6题: 顺序表按位查找是O(1),链表是O(n)。插入和删除在链表上(已知位置时)是O(1),顺序表是O(n)。逆置操作两者都是O(n)。选A。
第7题: 静态链表用数组实现,数组是连续存储空间,但静态链表通过数组下标(游标)来模拟指针,实现逻辑关系。静态链表不支持随机存取(虽然数组支持,但静态链表的逻辑顺序和物理顺序不同)。选C。
第8题: 循环双链表中,空表时头结点的next和prior都指向自己。选C。
第9题: 两个有序表归并,每个元素最多比较一次,时间复杂度O(n)。选B。
第10题: 线性表的第一个元素没有直接前驱,最后一个元素没有直接后继。C说"每个元素都有且仅有一个直接前驱和一个直接后继"是错误的。选C。
二、填空题
11. n/2
解析:假设在n+1个位置插入的概率相等(均为1/(n+1)),平均移动次数 = Σ(n-i+1)/(n+1) = n/2。
12. L->next = s;
解析:带头结点的单链表,在头结点之后插入s。先设s的next为头结点的next(即第一个数据结点),再设头结点的next为s。
13. 头结点
解析:循环单链表的尾结点的next指向头结点,形成环。
14. 3
解析:删除p的后继q,需要修改:p->next = q->next(1个),q->next->prior = p(1个),以及释放q。但题目问的是"修改指针域"的个数。p的next指针要改(1个),q的next的prior指针要改(1个)。等等,让我重新数:
标准答案应该是2个指针域被修改(p->next和q->next->prior)。但有些教材把"断开q的指针"也算上,那就是4个。
修正答案: 2个(p->next 和 q的后继的prior)
15. 相反
解析:头插法每次把新结点插在最前面,所以最后输入的结点在最前面,顺序与输入相反。
三、简答题
16. 参考答案:
顺序表和链表的主要区别:
比较项 | 顺序表 | 链表 |
|---|---|---|
存储方式 | 连续存储 | 分散存储 |
存取方式 | 随机存取O(1) | 顺序存取O(n) |
插入/删除 | 需移动元素O(n) | 只需修改指针O(1) |
空间分配 | 预分配 | 动态分配 |
存储密度 | 高 | 低(有指针开销) |
应用场景:
评分标准: 区别每点1分(答出3点即可得3分),应用场景每点1分。
17. 参考答案:
头结点的作用:
不设头结点的影响:
评分标准: 头结点作用3分,不设头结点的影响2分。
四、算法设计题
18. 参考答案:
(1)算法设计思想(3分):
由于链表递增有序,值大于mink且小于maxk的元素一定连续排列。算法步骤:
(2)代码实现(5分):
void DeleteRange(LinkList L, ElemType mink, ElemType maxk) {
LNode *pre = L; // pre指向待删除区间的前驱
// 找到第一个值>mink的结点的前驱
while (pre->next != NULL && pre->next->data <= mink) {
pre = pre->next;
}
// 此时pre->next是第一个>mink的结点(或NULL)
LNode *p = pre->next;
// 删除值<maxk的结点
while (p != NULL && p->data < maxk) {
LNode *q = p->next;
free(p);
p = q;
}
// 将pre连接到第一个>=maxk的结点
pre->next = p;
}(3)复杂度分析(2分):
评分标准: 设计思想3分(找到区间1分,删除操作1分,连接操作1分),代码5分(逻辑正确3分,边界处理1分,内存释放1分),复杂度2分。
模拟卷评分标准:
分数段 | 水平评估 | 建议 |
|---|---|---|
45-50 | 优秀 | 线性表部分掌握扎实,可以进入下一章 |
35-44 | 良好 | 基础扎实,但细节需要注意 |
25-34 | 中等 | 核心概念理解到位,但代码实现需要加强 |
15-24 | 及格 | 需要重新学习本章内容 |
<15 | 不及格 | 建议从基础视频重新学习 |
各题型得分率参考:
教材 | 作者 | 相关章节 | 推荐理由 |
|---|---|---|---|
《数据结构(C语言版)》 | 严蔚敏、吴伟民 | 第2章 线性表 | 经典教材,概念讲解清晰,代码规范 |
《数据结构复习指导》 | 王道考研 | 第2章 线性表 | 408备考必备,真题解析详细 |
《数据结构与算法分析》 | Mark Allen Weiss | 第3章 链表、栈和队列 | 国外经典,分析深入 |
《大话数据结构》 | 程杰 | 第3-4章 | 通俗易懂,适合入门 |
阅读建议:
课程 | 平台 | 讲师 | 特点 |
|---|---|---|---|
王道数据结构 | B站 | 王道团队 | 408备考首选,真题讲解详细 |
数据结构与算法 | 中国大学MOOC | 浙江大学陈越、何钦铭 | 系统全面,适合打基础 |
数据结构 | B站 | 青岛大学王卓 | 讲解细致,适合零基础 |
严蔚敏数据结构视频 | B站 | 严蔚敏 | 经典教材配套,权威 |
学习顺序建议:

知识关联说明:
请逐项检查自己是否掌握以下知识点。如果某项不确定,请回到对应章节重新学习。
线性表的定义与基本概念
顺序存储结构
链式存储结构
存储选型
请尝试回答以下问题。如果回答不上来或回答错误,请回到对应章节重新学习。
基础题(★):
中等题(★★★): 6. 顺序表插入操作时,元素为什么要从后往前移?如果从前往后移会怎样? 7. 带头结点的单链表和不带头结点的单链表,在删除操作时有什么区别? 8. 双链表插入操作中,四句话的顺序为什么不能乱?乱序会导致什么问题? 9. 循环单链表的判空条件是什么?和不循环的单链表有什么区别? 10. 头插法和尾插法建立的链表有什么区别?为什么?
较难题(★★★★): 11. 如何在一个单链表中找到倒数第k个结点?要求时间复杂度O(n)。 12. 如何将一个单链表就地逆置?写出代码并分析复杂度。 13. 如何判断一个单链表是否有环?如果有环,如何找到环的入口? 14. 如何将两个递增有序的单链表归并为一个递减有序的单链表? 15. 设计一个算法,删除递增有序顺序表中值小于x且为奇数的所有元素,要求时间复杂度O(n)。
自评标准:
自测题正确数 | 掌握程度 | 建议 |
|---|---|---|
13-15题 | 优秀 | 可以进入下一章学习 |
10-12题 | 良好 | 重点复习错题对应的知识点 |
7-9题 | 中等 | 需要重新学习本章,重点看代码实现 |
4-6题 | 及格 | 建议先看视频课程,再回来做题 |
0-3题 | 不及格 | 建议从《大话数据结构》开始入门 |
学习建议:
最后的话:
线性表是数据结构的基石。我当年也是在这里栽了跟头,然后花了整整一周时间重新学习。但那一周的投入是值得的——后面学栈、队列、树、图的时候,我发现很多概念都是线性表的延伸。
记住:基础不牢,地动山摇。 在线性表上花再多时间都不为过。
祝复习顺利!
——安全风信子
参考资料: