408《数据结构》——第二章:线性表
文章目录
- 第二章:线性表
- 核心内容总结
- 1. 线性表的定义与基本概念
- 2. 线性表的顺序表示与实现(顺序表)
- 3. 线性表的链式表示与实现(链表)
- 4. 顺序表 vs. 链表的比较
- 5. 线性表的应用
- 考研备考重点与难点
- 备考建议
考研408《数据结构》第二章“线性表”的详细总结,紧密结合考研要求,突出重点和难点。
第二章:线性表
线性表是整个数据结构的基础,也是考研中的绝对重点。本章内容逻辑清晰,但细节繁多,需要深入理解两种存储结构(顺序和链式)的实现原理、操作算法及其复杂度分析。
核心内容总结
1. 线性表的定义与基本概念
- 定义: 线性表(Linear List)是具有相同数据类型的
n (n ≥ 0)
个数据元素的有限序列。记作:L = (a₁, a₂, ..., aᵢ, ..., aₙ)
。 - 关键特性:
- 有限性: 元素个数有限。
- 相同类型: 所有元素属于同一数据对象。
- 序列性: 元素之间存在严格的顺序关系。
- 存在唯一的“第一个”元素(表头元素,无直接前驱)。
- 存在唯一的“最后一个”元素(表尾元素,无直接后继)。
- 除表头和表尾元素外,每个元素
aᵢ
(1 < i < n) 都有且仅有一个直接前驱aᵢ₋₁
和一个直接后继aᵢ₊₁
。
- 逻辑结构: 一对一的线性关系。是线性结构的典型代表。
- 基本操作(ADT定义的核心):
- 初始化 InitList(&L): 构造一个空的线性表L。
- 销毁 DestroyList(&L): 释放线性表L占用的内存空间。
- 判空 ListEmpty(L): 若L为空表,则返回true,否则返回false。
- 求长度 ListLength(L): 返回L中数据元素的个数。
- 按位查找 GetElem(L, i, &e): 用e返回L中第
i
(1 ≤ i ≤ n) 个元素的值。 - 按值查找 LocateElem(L, e): 返回L中第一个其值与
e
相等的元素的位序。若不存在,则返回0(或特定值)。 - 插入 ListInsert(&L, i, e): 在L的第
i
(1 ≤ i ≤ n+1) 个位置之前插入新的元素e
。L的长度增1。 - 删除 ListDelete(&L, i, &e): 删除L的第
i
(1 ≤ i ≤ n) 个位置的元素,并用e
返回其值。L的长度减1。 - 遍历 ListTraverse(L, visit()): 依次对L的每个元素调用函数
visit()
进行操作(如打印)。 - (其他操作:清空、求前驱、求后继等,视具体ADT定义而定)
- ADT定义:
ADT List { ... }
(包含上述基本操作)
2. 线性表的顺序表示与实现(顺序表)
- 存储原理: 用一组地址连续的存储单元依次存储线性表中的数据元素。逻辑上相邻的元素,其物理位置也相邻。
- 实现方式:
- 静态分配: 使用定长数组。
#define MaxSize 50; ElemType data[MaxSize]; int length;
- 优点: 简单。
- 缺点: 空间大小固定,一旦
length > MaxSize
则发生上溢,无法动态扩展;容易造成空间浪费。
- 动态分配: 使用指针和动态内存分配。
ElemType *data; int MaxSize, length;
初始化时data = (ElemType*)malloc(sizeof(ElemType)*InitSize);
- 优点: 可以动态扩展空间(使用
realloc
)。 - 缺点: 扩展时需要移动大量元素,开销大;仍可能存在空间分配失败问题。
- 优点: 可以动态扩展空间(使用
- 静态分配: 使用定长数组。
- 特点:
- 随机访问: 通过首地址和元素序号(下标)可以在
O(1)
时间内访问任意元素。LOC(aᵢ) = LOC(a₁) + (i-1) * sizeof(ElemType)
。 - 存储密度高: 只存储数据元素本身。
- 随机访问: 通过首地址和元素序号(下标)可以在
- 基本操作实现与时间复杂度分析:
操作 算法描述 最好情况 最坏情况 平均情况 说明 插入 1. 检查i合法性 (1≤i≤n+1)
2. 检查空间满?
3. 将第i至第n个元素后移一位
4. 在位置i放入e
5. length++O(1) (插表尾) O(n) (插表头) O(n) 移动次数 = n - i + 1 删除 1. 检查i合法性 (1≤i≤n)
2. 取aᵢ到e
3. 将第i+1至第n个元素前移一位
4. length–O(1) (删表尾) O(n) (删表头) O(n) 移动次数 = n - i 按位查找 直接通过下标访问 data[i-1] O(1) O(1) O(1) 随机访问特性 按值查找 顺序扫描数组,比较元素值 O(1) (在表头) O(n) (在表尾/不存在) O(n) 求长度 返回length O(1) O(1) O(1) 判空 判断length == 0 O(1) O(1) O(1) - 优缺点总结:
- 优点: 随机访问快(
O(1)
);存储密度高。 - 缺点: 插入/删除需要移动大量元素(
O(n)
);需要预分配/连续空间,静态分配不灵活,动态分配扩展开销大。
- 优点: 随机访问快(
3. 线性表的链式表示与实现(链表)
链式存储通过指针表示元素间的逻辑关系。不需要连续存储空间。核心概念是结点(Node)。
-
单链表 (Singly Linked List):
- 结点结构:
数据域 (data)
+指针域 (next)
typedef struct LNode {ElemType data; // 数据域struct LNode *next; // 指针域,指向下一个结点 } LNode, *LinkList;
- 头指针: 指向链表中第一个结点(首元结点)的指针。是链表的标识。
LinkList L;
- 头结点:
- 概念:在单链表的第一个结点之前附加的一个结点。
- 数据域:通常不存储信息(或存储表长等元信息)。
- 指针域:指向首元结点。
- 引入头结点的优点:
- 统一操作: 对首元结点的插入/删除操作与其他位置结点的操作逻辑完全一致(无需特殊处理空表或在表头插入/删除)。
- 简化判空: 空表时,头结点的指针域为
NULL
(L->next == NULL
)。 - 头指针不变: 头指针始终指向头结点,无论链表如何变化(插入/删除首元结点)。
- 引入头结点的缺点: 多占用一个结点的空间(存储密度略微降低)。
- 基本操作实现与时间复杂度分析:
操作 算法描述 时间复杂度 说明 建立 头插法: 新结点始终插入头结点之后。生成链表顺序与输入顺序相反。
尾插法: 需维护尾指针,新结点插入尾部。生成链表顺序与输入顺序相同。O(n) 头插法常用于逆序。尾插法需要记住尾指针位置。 插入 1. 找到第 i-1
个结点(前驱结点p)
2. 创建新结点s,s->data = e
3. s->next = p->next
4. p->next = sO(1) (已知p)
O(n) (需查找p)核心:修改指针顺序(先连后断)。在p后插入只需O(1)。按位插入需查找O(n)。 删除 1. 找到第 i-1
个结点(前驱结点p)
2. q = p->next (指向要删除的结点)
3. p->next = q->next
4. e = q->data; free(q)O(1) (已知p)
O(n) (需查找p)核心:修改指针并释放空间。删除p后的结点只需O(1)。按位删除需查找O(n)。 按位查找 从首元结点开始,顺指针next域逐个向后搜索,直到第i个结点。 O(n) 按值查找 从首元结点开始,顺指针next域逐个向后比较data域,直到找到值等于e的结点。 O(n) 求长度 设置计数器,从头结点(或首元结点)开始遍历整个链表计数。 O(n) 顺序表O(1),链表O(n)是重要区别。 判空 (带头结点) 判断 L->next == NULL
O(1) - 优缺点总结:
- 优点: 插入/删除操作方便快速(在已知位置插入/删除只需修改指针O(1));不需要预分配连续空间,空间分配灵活。
- 缺点: 不能随机访问,查找元素需要顺序扫描(O(n));存储密度较低(需要额外空间存储指针);访问元素需要从头指针开始遍历。
- 结点结构:
-
双链表 (Doubly Linked List):
- 结点结构:
prior指针域
+数据域 (data)
+next指针域
typedef struct DNode {ElemType data;struct DNode *prior, *next; // 指向前驱和后继 } DNode, *DLinklist;
- 优点: 可以双向遍历链表。插入和删除操作更灵活(尤其当需要定位前驱结点时,单链表需要O(n)时间查找前驱)。
- 插入操作 (在p结点后插入s):
s->next = p->next; if (p->next != NULL) p->next->prior = s; // 如果p不是最后一个结点 s->prior = p; p->next = s;
- 删除操作 (删除p结点):
p->prior->next = p->next; if (p->next != NULL) p->next->prior = p->prior; free(p);
- 时间复杂度: 在已知结点p位置进行插入或删除操作的时间复杂度为 O(1)(无需查找前驱)。
- 结点结构:
-
循环链表 (Circular Linked List):
- 单循环链表: 表中最后一个结点的指针域指向头结点(带头结点时)或首元结点(不带头结点时)。整个链表形成一个环。
- 判空:
L->next == L
(带头结点) - 优点:从表中任意结点出发均可访问到表中所有结点。常用于需要循环处理的场景(如约瑟夫问题)。
- 判空:
- 双循环链表: 在双链表的基础上,头结点的prior指向尾结点,尾结点的next指向头结点。
- 判空:
L->next == L && L->prior == L
(带头结点)
- 判空:
- 单循环链表: 表中最后一个结点的指针域指向头结点(带头结点时)或首元结点(不带头结点时)。整个链表形成一个环。
-
静态链表 (Static Linked List):
- 原理: 借助数组来描述链式结构。数组元素包含
data
和游标 cur
(相当于指针,存储下一个元素在数组中的下标)。 - 实现:
#define MaxSize 50 typedef struct {ElemType data;int cur; // 游标,0号单元cur指向第一个备用结点(空闲结点),最后一个空闲结点cur=0 } SLinkList[MaxSize];
- 特点:
- 需要预先分配一个较大的连续数组空间。
- 插入/删除操作不需要移动元素,只需要修改游标(类似链表修改指针)。
- 失去了顺序表随机存取的特性。
- 解决了在不支持指针的高级语言(如早期Basic、Fortran)中实现链表的问题。现代应用较少,但考研中可能涉及原理理解。
- 操作: 分配空闲结点(从备用链表中取)、回收空闲结点(放回备用链表)、插入、删除等操作需维护游标。
- 原理: 借助数组来描述链式结构。数组元素包含
4. 顺序表 vs. 链表的比较
比较项目 | 顺序表 | 链表 | 适用场景 |
---|---|---|---|
存储空间 | 连续存储单元 | 离散存储单元(通过指针链接) | |
存储密度 | 高(=1,只存数据) | 较低(<1,需额外存储指针) | 空间紧张优先顺序表 |
随机访问 | 支持,O(1) | 不支持,O(n) | 需要频繁按序号访问用顺序表 |
插入/删除操作 | 平均需移动约一半元素,O(n) | 修改指针,O(1) (已知位置) / O(n) (需查找) | 需要频繁插入/删除用链表 |
空间分配 | 静态分配:固定大小,易溢出/浪费 动态分配:可扩展但效率低 | 动态分配,按需申请,灵活高效 | 表长变化大,难以预估用链表 |
缓存友好性 | 好(空间局部性) | 差 | |
实现难度 | 简单 | 较复杂(指针操作需谨慎) | |
典型应用 | 数组、堆栈、队列(顺序实现) | 堆栈、队列(链式实现)、树、图的邻接表 |
5. 线性表的应用
- 基于线性表的操作实现更复杂的结构或算法(如多项式相加、集合运算)。
- 作为其他数据结构(栈、队列、字符串)的基础实现方式(顺序存储或链式存储)。
考研备考重点与难点
- 线性表ADT的理解: 熟练掌握线性表的定义、特性(序列性、前驱后继关系)和基本操作的含义。
- 顺序表的实现与操作:
- 深刻理解顺序存储的原理(逻辑相邻=物理相邻)。
- 重点掌握插入、删除操作的算法步骤和代码实现,及其时间复杂度分析(移动元素的计算)。
- 理解静态分配和动态分配的区别与限制。
- 单链表的实现与操作 (重中之重!):
- 透彻理解带头结点和不带头结点单链表的区别(特别是对表头操作的影响)。 考研中绝大多数链表操作基于带头结点链表。
- 熟练掌握单链表的建立(头插法、尾插法)、插入、删除操作的算法步骤、代码实现和指针修改顺序。 “先连后断”是核心口诀。
- 熟练掌握按位查找、按值查找、求长度、判空等操作的实现和时间复杂度。
- 理解头插法产生逆序的原因。
- 双链表与循环链表:
- 掌握双链表结点的结构和特点(prior指针)。
- 熟练掌握在已知结点p位置进行插入和删除操作的算法步骤和指针修改(注意修改prior和next两个方向)。
- 理解循环单链表/双链表的判空条件。
- 了解循环链表在解决特定问题(如约瑟夫环)时的优势。
- 静态链表: 理解其工作原理(用数组+游标模拟指针)、优缺点和适用场景。能看懂静态链表的基本操作。
- 顺序表 vs. 链表的综合比较: 能根据应用场景(访问模式、插入删除频率、空间要求)选择合适的存储结构。表格对比中的各项内容是选择题和简答题高频考点。
- 算法设计与分析:
- 链表操作边界条件: 空表、表头/表尾插入删除、查找不到元素等情况的处理必须严谨。
- 双指针技巧: 链表问题中常用技巧(如快慢指针找中点、判断环、倒数第k个结点;前后指针删除结点)。
- 递归: 理解链表遍历、反转等操作的递归实现(虽然空间复杂度O(n),但有助于理解递归思想)。
- 复杂度分析: 能准确分析给定链表算法(特别是涉及遍历、查找的操作)的时间复杂度和空间复杂度。
备考建议
- 动手写代码: 链表操作极易出错(指针丢失、内存泄漏、边界条件)。务必亲自编写、调试本章所有核心操作的代码(建立、插入、删除、查找、反转、合并等),理解每一步指针的变化。画图辅助理解指针操作过程。
- 深入理解指针: 链表的核心在于指针操作。确保对C语言的指针概念(指向、解引用、指针运算)有扎实掌握。理解
LinkList L
(头指针) 和LNode *p
(结点指针) 的区别与联系。 - 重视时间复杂度: 时刻牢记顺序表和链表在不同操作上的时间复杂度差异,这是选择题和分析题的核心考点。
- 对比记忆: 将顺序表和链表的关键特性、操作复杂度做成对比表格,反复记忆。
- 研究真题: 历年考研真题中关于线性表(尤其是链表)的题目非常多,题型涵盖选择题(概念、复杂度、简单操作结果)、应用题(设计算法解决特定问题)、算法题(实现指定操作)。认真分析真题,掌握命题规律和答题技巧。
- 理解“带头结点”的优越性: 考研题目和标准实现几乎都使用带头结点的链表,务必习惯并深刻理解其带来的操作统一性。
- 注意边界和异常: 在编写和阅读算法时,特别注意处理非法位置(i<1或i>n+1)、空表、空间分配失败等异常情况。健壮性是算法设计的重要要求。
总结: 第二章“线性表”是数据结构大厦的第一块坚实基石,其重要性不言而喻。核心在于透彻理解顺序存储和链式存储(特别是带头结点的单链表)的实现原理、操作算法(插入、删除、建立)及其时间复杂度分析,并熟练掌握两者的优缺点对比和应用场景选择。 大量的代码实践和真题演练是攻克本章的关键。