一、什么链表
在上一篇文章中就已经介绍了线性表的顺序表示,分别是从逻辑结构、物理结构和基本操作三个方向去讲解。本文将到的链表大体上也是从这三个方面去讲解,但由于链表最大的一个特点就是灵活性比较强,因此除了介绍传统的单链表外,还会讲解双链表、循环链表和静态链表。
那么什么是链表?对于不同类型的链表有不同的定义,其中在传统的教材中对单链表的定义是:线性表的链式存储也称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除了存放元素自身信息外,还需要存放一个指向其后继的指针。
flowchart TD
A[头指针 L] --> B
subgraph 结点1
B[data: 50] --> C[next]
end
C --> D
subgraph 结点2
D[data: 100] --> E[next]
end
E --> F
subgraph 结点3
F[data: 200] --> G[NULL]
end
如上图所示,这就是一个简单的单链表结构。这种简单的单向链接机制实际上是构建更复杂链表结构的基础:双链表(Doubly Linked List)就是在单链表的基础上,在每个节点中增加一个指向前驱节点的指针,实现双向遍历。循环链表(Circular Linked List)则是将单链表的尾节点指针指向头节点,形成一个环状结构。
二、链表的逻辑结构
在逻辑结构上,链表与顺序表是相同的。 顺序表的逻辑结构体现了最简单、最直观的数据组织方式——线性关系。
2.1. 逻辑结构的核心特征
1、元素间的"一对一"关系 每个元素有且仅有一个直接前驱(首元素除外) 每个元素有且仅有一个直接后继(尾元素除外)
2、确定的先后次序 每个元素有且仅有一个直接前驱(首元素除外) 每个元素有且仅有一个直接后继(尾元素除外)
3、有限的元素集合
4、元素都是数据元素,每个元素都是单个元素
三、链表的物理结构
链表的物理存储采用离散分配方式,通过指针将数据元素在非连续的内存空间中链接起来。每个结点独立分配,包含数据域和指针域,通过指针维护元素间的逻辑关系。这种存储结构具有高度的灵活性,支持动态内存管理,但存储密度较低且仅支持顺序访问。
优点:
动态内存分配:结点可随时申请和释放,无需预先估计线性表规模
灵活扩展性:插入删除操作只需修改指针,无需移动大量元素
内存利用率高:可充分利用零散内存空间,避免外部碎片问题
缺点:
存储密度低:每个结点需额外空间存储指针信息
访问效率局限:仅支持顺序访问,无法实现随机存取
指针开销:指针操作增加编程复杂度和运行时间开销
存储结构对比:
顺序表:物理连续 → 逻辑连续(通过物理位置体现)
链表:物理离散 → 逻辑连续(通过指针链接体现)
这种根本的物理存储差异直接决定了顺序表适合静态查询密集型应用,而链表更适合动态更新密集型场景。
四、链表的基本操作
链表的基本操作主要包括创建、销毁、增、删、改、查。根据链表结构的不同,可以分为单链表、双链表和循环链表。
4.1 单链表的基本操作
4.1.1 单链表结构定义
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 定义单链表结点结构
typedef struct LNode {
int data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
// 初始化单链表(带头结点)
bool InitList(LinkList *L) {
*L = (LNode*)malloc(sizeof(LNode)); // 分配头结点
if (*L == NULL) {
printf("内存分配失败!\n");
return false;
}
(*L)->next = NULL; // 头结点指针域置空
printf("单链表初始化成功!\n");
return true;
}
// 判断链表是否为空
bool ListEmpty(LinkList L) {
return L->next == NULL;
}
// 获取链表长度
int ListLength(LinkList L) {
int len = 0;
LNode *p = L->next; // 从第一个结点开始
while (p != NULL) {
len++;
p = p->next;
}
return len;
}
4.1.2 插入操作
// 按位置插入元素
bool ListInsert(LinkList L, int i, int e) {
if (i < 1) {
printf("插入位置不合法!\n");
return false;
}
LNode *p = L; // p指向头结点
int j = 0; // 当前p指向的是第0个结点(头结点)
// 寻找第i-1个结点
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL) {
printf("插入位置超出范围!\n");
return false;
}
// 创建新结点
LNode *s = (LNode*)malloc(sizeof(LNode));
if (s == NULL) {
printf("内存分配失败!\n");
return false;
}
s->data = e;
s->next = p->next; // 新结点指向原第i个结点
p->next = s; // 第i-1个结点指向新结点
printf("元素 %d 插入成功!\n", e);
return true;
}
// 头插法建立链表
void CreateList_Head(LinkList *L, int arr[], int n) {
*L = (LNode*)malloc(sizeof(LNode));
(*L)->next = NULL;
for (int i = 0; i < n; i++) {
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = arr[i];
s->next = (*L)->next;
(*L)->next = s;
}
printf("头插法建立链表成功!\n");
}
// 尾插法建立链表
void CreateList_Tail(LinkList *L, int arr[], int n) {
*L = (LNode*)malloc(sizeof(LNode));
LNode *r = *L; // 尾指针
for (int i = 0; i < n; i++) {
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = arr[i];
s->next = NULL;
r->next = s;
r = s; // 更新尾指针
}
printf("尾插法建立链表成功!\n");
}
4.1.3 删除操作
// 按位置删除元素
bool ListDelete(LinkList L, int i, int *e) {
if (i < 1) {
printf("删除位置不合法!\n");
return false;
}
LNode *p = L;
int j = 0;
// 寻找第i-1个结点
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL || p->next == NULL) {
printf("删除位置超出范围!\n");
return false;
}
LNode *q = p->next; // q指向待删除结点
*e = q->data;
p->next = q->next; // 绕过待删除结点
free(q); // 释放结点内存
printf("元素 %d 删除成功!\n", *e);
return true;
}
// 按值删除元素
bool DeleteElem(LinkList L, int e) {
LNode *p = L;
while (p->next != NULL && p->next->data != e) {
p = p->next;
}
if (p->next == NULL) {
printf("未找到元素 %d\n", e);
return false;
}
LNode *q = p->next;
p->next = q->next;
free(q);
printf("元素 %d 删除成功!\n", e);
return true;
}
4.1.4 查找操作
// 按位置查找元素
bool GetElem(LinkList L, int i, int *e) {
if (i < 1) {
printf("查找位置不合法!\n");
return false;
}
LNode *p = L->next; // 从第一个结点开始
int j = 1;
while (p != NULL && j < i) {
p = p->next;
j++;
}
if (p == NULL) {
printf("查找位置超出范围!\n");
return false;
}
*e = p->data;
printf("第 %d 个元素是:%d\n", i, *e);
return true;
}
// 按值查找元素位置
LNode* LocateElem(LinkList L, int e) {
LNode *p = L->next;
while (p != NULL && p->data != e) {
p = p->next;
}
if (p != NULL) {
printf("元素 %d 找到,地址:%p\n", e, p);
} else {
printf("未找到元素 %d\n", e);
}
return p;
}
4.1.5 遍历和其他操作
// 遍历输出链表
void PrintList(LinkList L) {
if (ListEmpty(L)) {
printf("链表为空!\n");
return;
}
LNode *p = L->next;
printf("链表元素:");
while (p != NULL) {
printf("%d → ", p->data);
p = p->next;
}
printf("NULL\n");
}
// 清空链表(保留头结点)
void ClearList(LinkList L) {
LNode *p = L->next;
LNode *q;
while (p != NULL) {
q = p->next;
free(p);
p = q;
}
L->next = NULL;
printf("链表已清空!\n");
}
// 销毁链表(包括头结点)
void DestroyList(LinkList *L) {
ClearList(*L);
free(*L);
*L = NULL;
printf("链表已销毁!\n");
}
// 反转链表
void ReverseList(LinkList L) {
if (L->next == NULL || L->next->next == NULL) {
return; // 空表或只有一个结点无需反转
}
LNode *pre = NULL;
LNode *cur = L->next;
LNode *next;
while (cur != NULL) {
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
L->next = pre;
printf("链表反转成功!\n");
}
4.1.6 测试
int main() {
LinkList L;
int deletedElem;
// 初始化链表
InitList(&L);
// 测试插入操作
printf("\n=== 测试插入操作 ===\n");
ListInsert(L, 1, 10);
ListInsert(L, 2, 20);
ListInsert(L, 3, 30);
ListInsert(L, 2, 15);
PrintList(L);
// 测试查找操作
printf("\n=== 测试查找操作 ===\n");
GetElem(L, 3, &deletedElem);
LocateElem(L, 20);
// 测试删除操作
printf("\n=== 测试删除操作 ===\n");
ListDelete(L, 2, &deletedElem);
PrintList(L);
// 测试按值删除
DeleteElem(L, 30);
PrintList(L);
// 测试反转操作
printf("\n=== 测试反转操作 ===\n");
ListInsert(L, 1, 5);
ListInsert(L, 2, 15);
ListInsert(L, 3, 25);
PrintList(L);
ReverseList(L);
PrintList(L);
// 测试清空和销毁
printf("\n=== 测试清空操作 ===\n");
ClearList(L);
PrintList(L);
// 重新插入测试
ListInsert(L, 1, 100);
ListInsert(L, 2, 200);
PrintList(L);
// 最终销毁
DestroyList(&L);
return 0;
}
4.2 双链表的基本操作
// 定义双链表结点结构
typedef struct DNode {
int data;
struct DNode *prior; // 前驱指针
struct DNode *next; // 后继指针
} DNode, *DLinkList;
// 初始化双链表
bool InitDList(DLinkList *L) {
*L = (DNode*)malloc(sizeof(DNode));
if (*L == NULL) return false;
(*L)->prior = NULL;
(*L)->next = NULL;
printf("双链表初始化成功!\n");
return true;
}
// 双链表插入操作
bool DListInsert(DLinkList L, int i, int e) {
if (i < 1) return false;
DNode *p = L;
int j = 0;
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL) return false;
DNode *s = (DNode*)malloc(sizeof(DNode));
s->data = e;
s->next = p->next;
if (p->next != NULL) {
p->next->prior = s;
}
s->prior = p;
p->next = s;
printf("双链表插入成功!\n");
return true;
}
// 双链表删除操作
bool DListDelete(DLinkList L, int i, int *e) {
if (i < 1) return false;
DNode *p = L->next;
int j = 1;
while (p != NULL && j < i) {
p = p->next;
j++;
}
if (p == NULL) return false;
*e = p->data;
p->prior->next = p->next;
if (p->next != NULL) {
p->next->prior = p->prior;
}
free(p);
printf("双链表删除成功!\n");
return true;
}
4.3 循环单链表
// 初始化循环单链表
bool InitCircularList(LinkList *L) {
*L = (LNode*)malloc(sizeof(LNode));
if (*L == NULL) return false;
(*L)->next = *L; // 指向自己形成循环
printf("循环单链表初始化成功!\n");
return true;
}
// 判断循环链表是否为空
bool CircularListEmpty(LinkList L) {
return L->next == L;
}
上述,详细介绍单链表的基本操作,对于双链表和循环链表的介绍简化了很多。因为本质上来说,三者的基本操作的思想是一致的。比如插入结点、删除结点,要先修改哪一个指针后修改哪一个指针。所以,只要掌握单链表的操作,其他类型的链表操作也是万变不离其宗。
五、链表操作特点总结
4.1 优点
动态内存管理:无需预先分配固定空间
高效插入删除:时间复杂度O(1)(已知位置时)
灵活扩展:可随时添加新结点
内存利用率高:避免内存浪费
4.2 缺点
存储密度低:需要额外指针空间
访问效率低:必须顺序访问,无法随机存取
实现复杂度高:指针操作容易出错
缓存不友好:结点离散存储,缓存命中率低
4.3 适用场景
频繁插入删除:如编辑器、实时系统
数据规模不确定:如动态数据结构
内存碎片严重:可利用零散内存空间
实现复杂数据结构:如树、图等
链表通过指针链接实现逻辑上的线性关系,在动态性和灵活性方面优于顺序表,但在访问效率方面有所牺牲。选择合适的链表类型(单链、双链、循环链)可以根据具体应用需求来决定。
以上在链表这部分我只选择单链、双链、循环链进行讲解,而没有选择上述的静态链表,是因为静态链表在物理存储上仍然使用顺序表,所以在这里就选择这三个比较典型的讲解。对于链表这部分的知识来说,只要掌握基本的增删改查在考研当中基本上不太大,由于链表没有随机查找的特性,所以在考试中大多数这些基本操作为主。