一、线性表的基本概念
✅ 定义
线性表是由 n(n ≥ 0)个相同类型 的数据元素组成的有限序列 。
记作:L = (a₁, a₂, ..., aₙ)
- n = 0:空表
- aᵢ 的前驱:aᵢ₋₁(i > 1)
- aᵢ 的后继:aᵢ₊₁(i < n)
✅ 基本特性
- 元素具有相同数据类型
- 元素之间存在一对一的线性关系
- 支持位置访问(第 i 个元素)
二、线性表的两种存储结构
线性表可通过两种方式在计算机中实现:
线性表
├── 顺序存储 → 顺序表(SqList)
└── 链式存储 → 链表(LinkList)
├── 单链表(最常用)
├── 双链表
├── 循环链表
└── 静态链表(数组模拟指针)
三、基本操作列表(通用接口)
无论采用哪种存储方式,线性表通常支持以下9种基本操作:
| 操作 | 功能 |
|---|---|
InitList(&L) |
初始化空表 |
DestroyList(&L) |
销毁表,释放内存 |
ListEmpty(L) |
判断是否为空 |
ListLength(L) |
返回表长 |
GetElem(L, i, &e) |
获取第 i 个元素值 |
LocateElem(L, e) |
查找元素 e 的位置 |
ListInsert(&L, i, e) |
在第 i 位插入 e |
ListDelete(&L, i, &e) |
删除第 i 位元素,并返回其值 |
PrintList(L) |
打印所有元素 |
⚠️ 注意:i 从 1 开始计数(逻辑序号),不是数组下标!
四、顺序表(SqList)实现
1. 结构定义
(1)静态分配
cpp
#define MaxSize 50 // 定义线性表的最大长度
// 静态顺序表结构
typedef struct {
ElemType data[MaxSize]; // 顺序表的元素
int length; // 顺序表的当前长度
} SqList; // 顺序表的类型定义
(2)动态分配
cpp
#define InitSize 100 // 表长度的初始定义
// 动态顺序表结构
typedef struct {
ElemType *data; // 顺序表的元素
int MaxSize; // 顺序表的最大长度
int length; // 顺序表的当前长度
} SeqList; // 顺序表的类型定义
L.data = new ElemType[InitSize]; // 初始动态分配
2. 操作实现(C++语言)
(1)初始化
静态分配初始化
cpp
#define MaxSize 50 // 定义线性表的的最大长度
// 静态顺序表结构
typedef struct {
ElemType data[MaxSize]; // 顺序表的元素
int length; // 顺序表的当前长度
} SqList; // 顺序表的类型定义
// 顺序表静态初始化
void initSqList(SqList &L) {
L.length = 0;
}
int main()
{
SqList L; //声明一个顺序表
initSqList(L); //顺序表初始长度为0
}
动态分配初始化
cpp
#define InitSize 100 // 表长度的初始定义
// 动态顺序表结构
typedef struct {
ElemType *data; // 顺序表的元素
int MaxSize; // 顺序表的最大长度
int length; // 顺序表的当前长度
} SeqList; // 顺序表的类型定义
// 顺序表动态初始化
void initSeqList(SeqList &L) {
L.data = new ElemType[InitSize]; // 分配存储空间
L.MaxSize = InitSize; // 初始化存储容量
L.length = 0; // 初始化顺序表的当前长度
}
int main()
{
SeqList L; //声明一个顺序表
initSeqList(L); //纯徐表动态初始化
}
(2)插入操作
cpp
#define MaxSize 50 // 定义线性表的的最大长度
// 静态顺序表结构
typedef struct {
ElemType data[MaxSize]; // 顺序表的元素
int length; // 顺序表的当前长度
} SqList; // 顺序表的类型定义
// 在位置i插入指定元素 L:顺序表 i:顺序表位序(非数组下标!!!) e: 插入元素
bool listInsert(SqList &L, int i, ElemType e) {
// 1. 判断i的范围是否有效
if(i < 1 || i > L.length + 1) {
return false;
}
// 2. 判断当前存储空间是否已满
if(L.length >= MaxSize) {
return false;
}
// 3. 将第i个之后的元素全部往后移动一位(包括第i个元素)
for(int j = L.length; j >= i; j--) {
L.data[j] = L.data[j - 1];
}
// 4. 在位置i插入指定元素
L.data[i - 1] = e;
// 5. 线性表长度加1
L.length++;
return true;
}
(3)删除操作
cpp
#define MaxSize 50 // 定义线性表的的最大长度
// 静态顺序表结构
typedef struct {
ElemType data[MaxSize]; // 顺序表的元素
int length; // 顺序表的当前长度
} SqList; // 顺序表的类型定义
// 删除并返回指定元素 L: 顺序表 i: 顺序表位序(非数组下标!!!) e: 删除并返回的指定元素
bool ListDelete(SqList &L, int i, ElemType &e) {
// 1. 判断i的范围是否有效
if(i < 1 || i > L.length) {
return false;
}
// 2. 返回删除的指定元素
e = L.data[i -1];
// 3. 将第i个之后的元素全部往前移动一位
for(int j = i; j < L.length; j++) {
L.data[j - 1] = L.data[j];
}
// 4. 线性表长度减1
L.length--;
return true;
}
(4)按值查找
cpp
#define MaxSize 50 // 定义线性表的的最大长度
// 静态顺序表结构
typedef struct {
ElemType data[MaxSize]; // 顺序表的元素
int length; // 顺序表的当前长度
} SqList; // 顺序表的类型定义
// 按值查找 L: 顺序表 e: 指定元素
int LocateElem(SqList L, ElemType e) {
// 遍历数组
for(int i = 0; i < L.length; i++) {
// 下标为i的值等于e,返回位序++i(非下标!!!)
if(L.data[i] == e) {
return i + 1;
}
}
return 0; // 未找到
}
3. 顺序表特点总结
| 项目 | 说明 |
|---|---|
| 随机访问 | ✅ O(1) |
| 插入/删除 | ❌ O(n),需移动元素 |
| 内存连续 | ✅ 是 |
| 空间预分配 | ✅ 需提前设定 MAXSIZE |
| 适用场景 | 频繁查找、较少修改 |
五、单链表(LinkList)实现(带头节点)
1. 结构定义
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
2. 操作实现(C++语言)
(1)初始化
带头节点
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 带头节点的单链表的初始化
bool InitList(LinkList &L) {
L = new LNode(); // 创建头节点
L->next = NULL; // 头节点之后暂时还没有元素节点
return true;
}
不带头节点
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 不带头节点的单链表的初始化
bool InitList(LinkList &L) {
L = NULL; //不带头结点的单链表的初始化
return true;
}
(2)求表长操作
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 求表长 L: 单链表指针(指向表头)
int LLength(LinkList L) {
int len = 0;
LNode *p = L;
while(p -> next != NULL) {
p = p -> next;
len++;
}
return len;
}
(3)按序号查找表结点
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 按序号查找结点 L: 单链表指针 i: 序号
LNode* GetElem(LinkList L, int i) {
LNode *p = L; //指针p指向当前扫描到的结点
int j = 0; //记录当前结点的位序,头结点是第0个结点
// 注:这里用p != NULL而不是 p -> next != NULL是因为查找时第i个序号可能查找失败(即链表内没有第i个结点),如果使用p -> next != NULL,遍历完链表,没有找到元素,p最终指向的是最后一个非空结点
while(p != NULL && j < i) { //循环找到第i个结点
p = p -> next;
j++;
}
return p; //返回第i个结点的指针或NULL
}
(4)按值查找表结点
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 按值查找表结点 L: 单链表 e: 指定数据
LNode* LocateElem(LinkList L, ElemType e) {
LNode *p = L -> next;
// 注:这里用p != NULL而不是 p -> next != NULL是因为查找指定结点时可能查找失败(即链表内没有等值的结点),如果使用p -> next != NULL,遍历完链表,没有找到元素,p最终指向的是最后一个非空结点
while(p != NULL && p -> data != e) { // 从头节点的下一个节点开始查找数据域为e的结点
p = p -> next;
}
return p; // 返回该结点指针
}
(5)插入结点操作(第 i 位)
后插法
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 单链表插入
bool listInsert(LinkList &L, int i, ElemType e) {
//指针p指向当前扫描到的结点
LNode *p = L;
// 查找插入位置的前一个结点(第i -1 个结点)
int j = 0;
while(p != NULL && j < i - 1) {
p = p -> next;
j++;
}
// 结点不存在或为空返回false
if(p == NULL) {
return false;
}
// 新建待插入结点s
LNode *s = new LNode();
s -> data = e; // 设置数据域
// s -> next指针指向p的下一个结点
s -> next = p -> next;
// p -> next指针指向s
p -> next = s;
return true;
}
前插法
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 单链表前插法 L: 单链表 i: 插入位置 e: 插入数据
bool listBeforeInsert(LinkList &L, int i, ElemType e) {
//指针p指向当前扫描到的结点
LNode *p = L;
// 查找插入位置的结点(第i个结点)
int j = 0;
while(p != NULL && j < i) {
p = p -> next;
}
// 结点不存在或为空返回false
if(p == NULL) {
return false;
}
// 新建待插入结点s
LNode *s = new LNode();
// s -> next 指向p结点当前的下一个结点
s -> next = p -> next;
// p -> next 指向s
p -> next = s;
// 结点s和结点p之间的数据值互换
s -> data = p -> data;
p -> data = e;
return true;
}
(6)删除结点(第 i 位)
方法一:
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 删除结点 L: 单链表 i: 删除序号 e: 返回的删除数据值
bool listDelete(LinkList &L, int i, ElemType &e) {
//指针p指向当前扫描到的结点
LNode *p = L;
// 查找第 i - 1个结点
int j = 0;
while(p != NULL && j < i - 1) {
p = p -> next;
j++;
}
// 当前结点或下一结点不存在或为空返回false
if(p == NULL || p->next == NULL) {
return false;
}
// 新建指针q,指向p结点当前的下一个结点
LNode *q = p -> next;
// 返回删除数据值
e = q -> data;
// p -> next 指向q结点的下一个结点(即当前p结点后面第二个结点)
p -> next = q -> next;
// 释放q结点
free(q);
return true;
}
方法二:
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 删除结点 L: 单链表 i: 删除序号 e: 返回的删除数据值
bool listDelete(LinkList &L, int i, ElemType &e) {
//指针p指向当前扫描到的结点
LNode *p = L;
// 查找第 i个结点
int j = 0;
while(p != NULL && j < i) {
p = p -> next;
j++;
}
// 当前结点或下一结点不存在或为空返回false
if(p == NULL || p->next == NULL) {
return false;
}
// 新建指针q,指向p结点当前的下一个结点
LNode *q = p -> next;
// 返回删除数据值
e = p -> data;
// 将q的数据值赋值到p中
p -> data = q -> data;
// p -> next 指向q结点的下一个结点(即当前p结点后面第二个结点)
p -> next = q -> next;
// 释放q结点
free(q);
return true;
}
(7)采用头插法建立单链表
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 建立链表前插法 L:单链表
void List_HeadInsert(LinkList &L) {
// 创建单链表头节点
L = new LNode();
L -> next = NULL;
// 输入插入结点的数据值
int x;
scanf("%d", &x);
// 循环前插结点
while(x != 9999) {
// 创建插入结点
LNode *s = new LNode();
// 设置数据值
s -> data = x;
// 插入操作
s -> next = L -> next;
L -> next = s;
// 输入下一个待插入结点的数据值
scanf("%d", &x);
}
}
(8)采用尾插法建立单链表
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 建立链表尾插法 L:单链表
void List_TailInsert(LinkList &L) {
// 创建单链表头结点
L = new LNode();
// p指针指向头结点
LNode *p = L;
// 输入插入结点的数据值
int x;
scanf("%d", &x);
// 循环尾插结点
while(x != 9999) {
// 创建待插入结点s
LNode *s = new LNode();
// 设置数据值
s -> data = x;
// p -> next指向插入结点
p -> next = s;
// p指针指向下一个结点(即刚刚插入的结点)
p = s;
scanf("%d", &x);
}
p -> next = NULL;
}
踩坑:
-
遍历链表结点时,循环条件的选择依据操作目的有所不同:
-
while(p->next != NULL)适用于需要访问链表中每个有效结点的情况,此时仅需遍历无需查找条件
-
while(p != NULL && j < i)适用于按序号或值查找特定结点的情况,带有定位条件,需区分查找成功与失败状态
六、双链表(Doubly Linked List)
1. 结构定义
cpp
// 双链表结构
typedef struct DNode { // 定义双链表结点类型
ElemType data; // 数据域
struct DNode *prior, *next; // 前驱和后继指针
} DNode, *DLinkList;
2. 插入操作(在 p 节点后插入 s)
cpp
// 双链表结构
typedef struct DNode { // 定义双链表结点类型
ElemType data; // 数据域
struct DNode *prior, *next; // 前驱和后继指针
} DNode, *DLinkList;
// 插入操作(在 p 节点后插入 s)
void DListInsert(DNode *p, DNode *s) {
// 待插入的结点s -> next 指向p结点当前的下一个结点
s -> next = p -> next;
// 待插入的结点s -> prior指向p结点
s -> prior = p;
// p -> next指向插入结点s
p -> next = s;
if(s -> next != NULL){ // ④ 加边界判断,规避崩溃
s -> next -> prior = s;
}
}
3. 删除操作(删除 p 节点)
cpp
// 双链表结构
typedef struct DNode { // 定义双链表结点类型
ElemType data; // 数据域
struct DNode *prior, *next; // 前驱和后继指针
} DNode, *DLinkList;
// 删除操作(删除 p 节点)
void DListDelete(DNode *p) {
// 获取p结点的前一个结点q
DNode *q = p -> prior;
// q -> next 指向p结点当前的下一个结点
q -> next = p -> next;
// p结点的下一个结点的prior指针指向q
if(p -> next != NULL) {
p -> next -> prior = q;
}
// 释放q结点
free(p);
}
4. 特点与考点
| 项目 | 说明 |
|---|---|
| 空间开销 | 每个节点多一个指针 |
| 访问方向 | ✅ 可前向/后向遍历 |
| 插入/删除 | 无需查找前驱,效率更高 |
| 典型应用 | 浏览器"前进/后退"、LRU缓存 |
✅ 高频考点:
"双链表中删除一个非头尾节点需修改几个指针?" → 2个
七、循环链表(Circular Linked List)
1. 逻辑特点
- 尾节点的
next指向头节点(或头指针) - 可为单循环或双循环
2. 判空条件(带头节点)
循环单链表
cpp
L->next == L // 表示空表
循环双链表
cpp
L -> next = L;
L -> prior = L;
3. 遍历示例
循环单链表
cpp
// 单链表结构
typedef struct LNode{ //定义单链表结点类型
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 单链表遍历示例
void PrintCircularList(LinkList L) {
LNode *p = L;
while(p -> next != L) {
p = p -> next;
printf("%d", p -> data);
}
}
循环双链表与单链表一致
4. 特点与考点
| 优势 | 应用场景 |
|---|---|
| 从任意节点出发可遍历全表 | 约瑟夫问题(Josephus) |
| 无需额外指针记录尾部 | 操作系统进程调度轮转 |
✅ 高频考点:
"带头节点的循环单链表为空的条件?" →
L->next == L
八、静态链表(Static Linked List)
1. 设计思想
- 用数组模拟链表
- 用下标(cursor)代替指针
2. 结构定义
cpp
#define MaxSize 50 //静态链表的最大长度
typedef struct { //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
} SLinkList[MaxSize];
3. 关键机制
- 0 号单元:通常作为头节点
- 空闲链表 :未用分量通过
cursor链接 - Malloc/Free:从空闲链表取/还节点
4. 特点与考点
| 项目 | 说明 |
|---|---|
| 无指针 | 适合不支持指针的语言 |
| 逻辑关系 | 由游标(下标)实现 |
| 插入/删除 | 不需移动元素 |
| 考研地位 | 了解即可,可能出选择题 |
✅ 典型考题:
"静态链表中,游标的作用是什么?" → 存储下一个元素的数组下标
九、顺序表 vs 链表 对比(必背表格)
| 比较项 | 顺序表 | 单链表 | 双链表 | 循环链表 | 静态链表 |
|---|---|---|---|---|---|
| 存储方式 | 连续内存 | 非连续+指针 | 非连续+双指针 | 首尾相连 | 数组+游标 |
| 随机访问 | ✅ O(1) | ❌ O(n) | ❌ O(n) | ❌ O(n) | ❌ O(n) |
| 插入/删除 | ❌ O(n) | ✅ 定位后 O(1) | ✅ 更灵活 | ✅ 同单/双 | ✅ 同链表 |
| 空间开销 | 最小 | 中 | 大 | 同单/双 | 小(无指针) |
| 适用场景 | 查找多 | 修改多 | 双向遍历 | 轮询问题 | 无指针环境 |
十、高频考点与真题提示
✅ 必考内容:
- 顺序表插入/删除时移动元素个数(如:插入第 i 位,移动 n-i+1 个)
- 链表操作的指针变化图(画图题)
- 时间复杂度分析(顺序表 vs 链表)
- "带头节点" vs "不带头节点"的区别(考研默认带头)
✅ 典型真题:
【2020年408】在一个长度为 n 的顺序表中删除第 i 个元素,平均移动次数为?
答案:(n-1)/2
【某校自命题】静态链表的逻辑关系由什么实现?
答案:游标(下标)
【2022年模拟题】双链表删除一个非头尾节点需修改几个指针?
答案:2 个