线性表(Linear List)完整学习笔记
一、基本概念
1.1 定义
线性表 是具有相同数据类型的 n(n≥0)个数据元素的有限序列
1.2 数学表示
L = (a₁, a₂, ..., aᵢ, ..., aₙ)
-
n:表长(n=0时为空表)
-
aᵢ:数据元素,i为位序(从1开始)
1.3 基本特征
-
有限性:元素个数有限
-
有序性:逻辑上顺序排列
-
唯一性:每个元素有且仅有一个直接前驱和直接后继(首尾元素除外)
-
同质性:所有元素数据类型相同
二、顺序表(Sequence List)
2.1 存储结构
// 静态分配
#define MAXSIZE 100
typedef struct {
ElemType data[MAXSIZE]; // 存储空间
int length; // 当前长度
} SqList;
// 动态分配
typedef struct {
ElemType *data; // 动态数组指针
int length; // 当前长度
int capacity; // 总容量
} SeqList;
2.2 核心特点
-
物理结构:内存连续存储
-
地址计算:LOC(aᵢ) = LOC(a₁) + (i-1) × sizeof(ElemType)
-
随机访问:O(1)时间复杂度
2.3 基本操作
2.3.1 初始化
Status InitList(SqList *L) {
L->length = 0; // 空表长度为0
return OK;
}
2.3.2 插入操作
// 在第i个位置插入元素e
Status ListInsert(SqList *L, int i, ElemType e) {
// 1. 参数检查
if (i < 1 || i > L->length + 1)
return ERROR; // i的范围不合法
if (L->length >= MAXSIZE)
return ERROR; // 存储空间已满
// 2. 元素后移(从最后一个开始)
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j - 1];
}
// 3. 插入新元素
L->data[i - 1] = e;
L->length++;
return OK;
}
时间复杂度:O(n),平均移动 n/2 个元素
2.3.3 删除操作
// 删除第i个位置的元素,用e返回其值
Status ListDelete(SqList *L, int i, ElemType *e) {
// 1. 参数检查
if (i < 1 || i > L->length)
return ERROR;
// 2. 保存被删除元素
*e = L->data[i - 1];
// 3. 元素前移
for (int j = i; j < L->length; j++) {
L->data[j - 1] = L->data[j];
}
L->length--;
return OK;
}
时间复杂度:O(n),平均移动 (n-1)/2 个元素
2.3.4 按值查找
int LocateElem(SqList L, ElemType e) {
for (int i = 0; i < L.length; i++) {
if (L.data[i] == e)
return i + 1; // 返回位序(从1开始)
}
return 0; // 查找失败
}
时间复杂度:O(n),平均比较 (n+1)/2 次
2.4 优缺点总结
| 优点 | 缺点 |
|---|---|
| 1. 随机访问,支持下标操作 | 1. 插入删除需要移动大量元素 |
| 2. 存储密度高(无指针开销) | 2. 容量固定(静态分配) |
| 3. 缓存友好(空间局部性) | 3. 扩容成本高(动态分配) |
三、单链表(Singly Linked List)
3.1 存储结构
// 节点定义
typedef struct LNode {
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
3.2 链表形态
3.2.1 不带头节点
head → [a₁|→] → [a₂|→] → ... → [aₙ|NULL]
3.2.2 带头节点(推荐)
head → [头节点|→] → [a₁|→] → [a₂|→] → ... → [aₙ|NULL]
↑
头指针
优点:统一操作,简化代码(空表和非空表操作一致)
3.3 基本操作
3.3.1 头插法建立链表
// 逆向建立链表(输入顺序与链表顺序相反)
LinkList CreateList_HeadInsert(int n) {
LinkList L = (LinkList)malloc(sizeof(LNode)); // 创建头节点
L->next = NULL; // 初始为空链表
for (int i = 0; i < n; i++) {
LNode *s = (LNode*)malloc(sizeof(LNode));
scanf("%d", &s->data);
s->next = L->next; // 新节点指向原第一个节点
L->next = s; // 头节点指向新节点
}
return L;
}
3.3.2 尾插法建立链表
// 正向建立链表(保持输入顺序)
LinkList CreateList_TailInsert(int n) {
LinkList L = (LinkList)malloc(sizeof(LNode));
LNode *r = L; // r始终指向尾节点
for (int i = 0; i < n; i++) {
LNode *s = (LNode*)malloc(sizeof(LNode));
scanf("%d", &s->data);
r->next = s; // 尾节点指向新节点
r = s; // r指向新的尾节点
}
r->next = NULL; // 尾节点指针置空
return L;
}
3.3.3 按序号查找
LNode *GetElem(LinkList L, int i) {
if (i < 0) return NULL; // i不合法
LNode *p = L; // p指向头节点
int j = 0; // 当前p指向的节点序号
while (p != NULL && j < i) {
p = p->next;
j++;
}
return p; // 返回第i个节点指针
}
时间复杂度:O(n)
3.3.4 按值查找
LNode *LocateElem(LinkList L, ElemType e) {
LNode *p = L->next; // 从第一个节点开始
while (p != NULL && p->data != e) {
p = p->next;
}
return p; // 找到返回指针,否则返回NULL
}
时间复杂度:O(n)
3.3.5 插入节点
// 在第i个位置插入元素e
Status ListInsert(LinkList L, int i, ElemType e) {
// 1. 找到第i-1个节点
LNode *p = GetElem(L, i - 1);
if (p == NULL) return ERROR; // i值不合法
// 2. 创建新节点
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e;
// 3. 插入操作
s->next = p->next;
p->next = s;
return OK;
}
时间复杂度:O(n)(主要花费在查找上,插入本身O(1))
3.3.6 删除节点
// 删除第i个位置的节点
Status ListDelete(LinkList L, int i, ElemType *e) {
// 1. 找到第i-1个节点
LNode *p = GetElem(L, i - 1);
if (p == NULL || p->next == NULL)
return ERROR; // i值不合法
// 2. 删除操作
LNode *q = p->next; // q指向要删除的节点
*e = q->data; // 保存被删除元素的值
p->next = q->next; // 修改指针
free(q); // 释放节点空间
return OK;
}
3.4 优缺点总结
| 优点 | 缺点 |
|---|---|
| 1. 动态分配,无需预先确定容量 | 1. 不支持随机访问 |
| 2. 插入删除只需修改指针 | 2. 存储密度低(有指针开销) |
| 3. 物理上可以不连续存储 | 3. 查找需要遍历,效率较低 |
四、特殊链表
4.1 循环链表(Circular Linked List)
// 初始化循环链表(带头节点)
Status InitCircularList(LinkList *L) {
*L = (LinkList)malloc(sizeof(LNode));
if (*L == NULL) return ERROR;
(*L)->next = *L; // 头节点指向自身
return OK;
}
// 判断循环链表是否为空
int IsEmpty(LinkList L) {
return (L->next == L);
}
特点:
-
尾节点指向头节点(带头节点)或首节点(不带头节点)
-
从任意节点出发都可访问所有节点
-
约瑟夫环是其典型应用
4.2 双向链表(Doubly Linked List)
typedef struct DuLNode {
ElemType data; // 数据域
struct DuLNode *prior; // 前驱指针
struct DuLNode *next; // 后继指针
} DuLNode, *DuLinkList;
4.2.1 插入操作
// 在节点p之后插入节点s
Status InsertAfterNode(DuLNode *p, DuLNode *s) {
if (!p || !s) return ERROR;
s->prior = p;
s->next = p->next;
if (p->next != NULL) {
p->next->prior = s;
}
p->next = s;
return OK;
}
4.2.2 删除操作
// 删除节点p
Status DeleteNode(DuLNode *p) {
if (!p) return ERROR;
if (p->prior != NULL) {
p->prior->next = p->next;
}
if (p->next != NULL) {
p->next->prior = p->prior;
}
free(p);
return OK;
}
五、性能对比总结
| 操作/特性 | 顺序表 | 单链表 | 双向链表 | 循环链表 |
|---|---|---|---|---|
| 存储方式 | 连续存储 | 离散存储 | 离散存储 | 离散存储 |
| 随机访问 | O(1) | O(n) | O(n) | O(n) |
| 头部插入 | O(n) | O(1) | O(1) | O(1) |
| 尾部插入 | O(1) | O(n) | O(1)* | O(1)* |
| 中间插入 | O(n) | O(n) | O(n) | O(n) |
| 查找元素 | O(n) | O(n) | O(n) | O(n) |
| 空间开销 | 小(无指针) | 中(1指针) | 大(2指针) | 中(1指针) |
| 缓存友好 | 好 | 差 | 差 | 差 |
| 扩容难度 | 困难 | 容易 | 容易 | 容易 |
注:如果维护尾指针,尾部操作可达O(1)
六、应用场景选择指南
6.1 选择顺序表的情况:
-
数据量稳定或可预估
-
需要频繁随机访问元素
-
插入删除操作较少
-
对内存空间要求严格
-
追求最高访问速度
6.2 选择链表的情况:
-
数据量变化大,无法预估
-
频繁在任意位置插入删除
-
内存碎片化严重
-
需要频繁动态调整结构
-
不要求随机访问
6.3 具体应用示例:
| 数据结构 | 典型应用 |
|---|---|
| 顺序表 | 数组、矩阵、查找表、静态数据库 |
| 单链表 | 栈、队列、多项式表示、稀疏矩阵 |
| 循环链表 | 约瑟夫环、轮询调度、循环缓冲区 |
| 双向链表 | 浏览器历史记录、文本编辑器、LRU缓存 |
七、重要考点与常见问题
7.1 易错点
-
边界处理:头节点、尾节点、空表
-
指针更新顺序:链表操作中指针修改的顺序很重要
-
内存泄漏:malloc后必须free
-
野指针:释放后要及时置NULL
7.2 经典算法
-
链表逆置
-
链表合并
-
链表判环
-
求链表中间节点
-
删除重复节点
7.3 代码模板
// 链表遍历模板
void TraverseList(LinkList L) {
LNode *p = L->next; // 从第一个节点开始
while (p != NULL) {
// 处理p->data
p = p->next;
}
}