📋 线性表详解:顺序与链式存储
学习链表最好的学习方法就是画图,不懂就画图!
一、线性表概述
线性表 是最基本、最简单、也是最常用的一种数据结构。线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。
线性表的特点
| 特点 | 说明 |
|---|---|
| 有穷性 | 元素个数有限 |
| 有序性 | 元素之间有先后顺序 |
| 相同性 | 所有元素类型相同 |
| 唯一性 | 每个元素有唯一的前驱和后继(首尾除外) |
二、顺序存储结构
2.1 结构设计
线性表的顺序存储使用的是数组,将节点的地址都存在这个数组中以此来形成线性表。
c
// 顺序线性表结构定义
typedef struct {
unsigned int** node; // 存储节点地址的数组
int capacity; // 容量
int length; // 当前长度
} SeqList;
开辟空间时要记得为这个数组开辟空间,其大小为 (unsigned int*) * capacity。
2.2 操作实现
c
#include <stdio.h>
#include <stdlib.h>
#define CAPACITY 10
typedef void* SeqListNode;
typedef struct {
SeqListNode* node; // 节点指针数组
int capacity; // 容量
int length; // 当前长度
} SeqList;
// 创建顺序线性表
SeqList* SeqList_Create(int capacity) {
SeqList* list = (SeqList*)malloc(sizeof(SeqList));
list->node = (SeqListNode*)malloc(sizeof(SeqListNode) * capacity);
list->capacity = capacity;
list->length = 0;
return list;
}
// 销毁顺序线性表
void SeqList_Destroy(SeqList* list) {
if (list != NULL) {
if (list->node != NULL) {
free(list->node);
}
free(list);
}
}
// 清空顺序线性表
void SeqList_Clear(SeqList* list) {
if (list != NULL) {
list->length = 0;
}
}
// 获取长度
int SeqList_Length(SeqList* list) {
return (list != NULL) ? list->length : 0;
}
// 插入元素
int SeqList_Insert(SeqList* list, SeqListNode node, int pos) {
if (list == NULL || pos < 0 || pos > list->length) {
return -1;
}
// 从后往前移动元素
for (int i = list->length; i > pos; i--) {
list->node[i] = list->node[i - 1];
}
list->node[pos] = node;
list->length++;
return 0;
}
// 获取元素
SeqListNode SeqList_Get(SeqList* list, int pos) {
if (list == NULL || pos < 0 || pos >= list->length) {
return NULL;
}
return list->node[pos];
}
// 删除元素
SeqListNode SeqList_Delete(SeqList* list, int pos) {
if (list == NULL || pos < 0 || pos >= list->length) {
return NULL;
}
SeqListNode ret = list->node[pos];
// 从前往后移动元素
for (int i = pos; i < list->length - 1; i++) {
list->node[i] = list->node[i + 1];
}
list->length--;
return ret;
}
2.3 时间复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 获取元素 | O(1) | 直接通过下标访问 |
| 插入元素 | O(n) | 需要移动后续元素 |
| 删除元素 | O(n) | 需要移动后续元素 |
三、链式存储结构
3.1 链表技术推演
线性表的链式存储有一个头域 ,每个节点都包含这个头域就可以将各个节点串起来,形成线性表。头域保存的是下一个节点的地址。
链表的核心思想:
- 不需要连续的内存空间
- 每个节点包含数据和指向下一个节点的指针
- 通过指针将节点串联起来
3.2 结构设计
c
// 链表节点头域
typedef struct _tag_LinkListNode {
struct _tag_LinkListNode* next;
} LinkListNode;
// 链表结构
typedef void LinkList;
// 链表头节点(内部实现)
typedef struct _tag_LinkList {
LinkListNode header; // 头节点
int length; // 链表长度
} TLinkList;
💡 难点说明 :
typedef void LinkList;表示LinkList*相当于一个指向链表头节点的指针。这种设计使用户不需要知道内部实现细节,直接使用LinkList来创建并使用链表即可。
3.3 插入操作
c
// 在链式线性表list的pos位置插入节点node
int LinkList_Insert(LinkList* list, LinkListNode* node, int pos) {
TLinkList* sList = (TLinkList*)list;
if (sList == NULL || node == NULL || pos < 0) {
return -1;
}
// 从头节点开始遍历
LinkListNode* current = (LinkListNode*)sList;
// 移动pos次后,current指向pos-1位置
for (int i = 0; (i < pos) && (current->next != NULL); i++) {
current = current->next;
}
// 插入节点
node->next = current->next;
current->next = node;
sList->length++;
return 0;
}
图解插入过程:
插入前:A → B → C → D
在位置2插入节点X:
步骤1:移动到位置1(pos-1)
current → B
步骤2:X的next指向C
X → C
步骤3:B的next指向X
B → X
插入后:A → B → X → C → D
3.4 删除操作
c
// 删除链式线性表list的pos位置的节点
LinkListNode* LinkList_Delete(LinkList* list, int pos) {
TLinkList* sList = (TLinkList*)list;
if (sList == NULL || pos < 0) {
return NULL;
}
// 从头节点开始遍历
LinkListNode* current = (LinkListNode*)sList;
// 移动到要删除节点的前一个位置
for (int i = 0; (i < pos) && (current->next != NULL); i++) {
current = current->next;
}
// 删除节点
LinkListNode* ret = current->next;
current->next = ret->next;
sList->length--;
return ret;
}
3.5 完整实现
c
#include <stdio.h>
#include <stdlib.h>
typedef void LinkList;
typedef struct _tag_LinkListNode {
struct _tag_LinkListNode* next;
} LinkListNode;
typedef struct _tag_LinkList {
LinkListNode header;
int length;
} TLinkList;
// 创建链式线性表
LinkList* LinkList_Create() {
TLinkList* list = (TLinkList*)malloc(sizeof(TLinkList));
if (list != NULL) {
list->header.next = NULL;
list->length = 0;
}
return list;
}
// 销毁链式线性表
void LinkList_Destroy(LinkList* list) {
free(list);
}
// 清空链式线性表
void LinkList_Clear(LinkList* list) {
TLinkList* sList = (TLinkList*)list;
if (sList != NULL) {
sList->header.next = NULL;
sList->length = 0;
}
}
// 获取链式线性表的长度
int LinkList_Length(LinkList* list) {
TLinkList* sList = (TLinkList*)list;
return (sList != NULL) ? sList->length : -1;
}
// 在链式线性表list的pos位置插入节点node
int LinkList_Insert(LinkList* list, LinkListNode* node, int pos) {
TLinkList* sList = (TLinkList*)list;
if (sList == NULL || node == NULL || pos < 0) {
return -1;
}
LinkListNode* current = (LinkListNode*)sList;
for (int i = 0; (i < pos) && (current->next != NULL); i++) {
current = current->next;
}
node->next = current->next;
current->next = node;
sList->length++;
return 0;
}
// 获取链式线性表list的pos位置的节点
LinkListNode* LinkList_Get(LinkList* list, int pos) {
TLinkList* sList = (TLinkList*)list;
if (sList == NULL || pos < 0) {
return NULL;
}
LinkListNode* current = (LinkListNode*)sList;
for (int i = 0; (i < pos) && (current->next != NULL); i++) {
current = current->next;
}
return current->next;
}
// 删除链式线性表list的pos位置的节点
LinkListNode* LinkList_Delete(LinkList* list, int pos) {
TLinkList* sList = (TLinkList*)list;
if (sList == NULL || pos < 0) {
return NULL;
}
LinkListNode* current = (LinkListNode*)sList;
for (int i = 0; (i < pos) && (current->next != NULL); i++) {
current = current->next;
}
LinkListNode* ret = current->next;
if (ret != NULL) {
current->next = ret->next;
sList->length--;
}
return ret;
}
3.6 使用示例
c
// 定义业务节点结构
typedef struct {
LinkListNode header; // 必须放在第一个位置
int data;
} MyNode;
int main() {
// 创建链表
LinkList* list = LinkList_Create();
// 创建并插入节点
MyNode n1 = {{NULL}, 10};
MyNode n2 = {{NULL}, 20};
MyNode n3 = {{NULL}, 30};
LinkList_Insert(list, (LinkListNode*)&n1, 0);
LinkList_Insert(list, (LinkListNode*)&n2, 1);
LinkList_Insert(list, (LinkListNode*)&n3, 2);
// 遍历链表
for (int i = 0; i < LinkList_Length(list); i++) {
MyNode* node = (MyNode*)LinkList_Get(list, i);
printf("data[%d] = %d\n", i, node->data);
}
// 销毁链表
LinkList_Destroy(list);
return 0;
}
📌 关键点 :带头结点,位置从0开始的单链表,返回链表中第pos个位置处元素的值时,需要返回
current->next。
四、循环链表
4.1 循环链表的插入
循环链表的最后一个节点的next指针指向头节点,形成一个环。
c
// 循环链表插入算法
int CircleList_Insert(LinkList* list, LinkListNode* node, int pos) {
// ... 与单链表类似
// 区别:插入时需要考虑循环特性
}
4.2 循环链表的删除
c
// 循环链表删除算法
LinkListNode* CircleList_Delete(LinkList* list, int pos) {
// 删除时需要更新尾节点的next指针
// 保持循环特性
}
五、双向链表
双向链表的每个节点有两个指针域,分别指向前驱和后继。
c
typedef struct _tag_DLinkListNode {
struct _tag_DLinkListNode* prev; // 前驱指针
struct _tag_DLinkListNode* next; // 后继指针
} DLinkListNode;
双向链表的优点:
- 可以双向遍历
- 删除操作更简单(不需要找前驱节点)
- 查找效率更高
双向链表的缺点:
- 每个节点需要额外的空间存储prev指针
- 插入和删除操作需要修改更多的指针
六、顺序存储 vs 链式存储对比
| 比较项 | 顺序存储 | 链式存储 |
|---|---|---|
| 存储方式 | 连续内存空间 | 分散内存空间 |
| 空间利用率 | 需要预分配,可能浪费 | 按需分配,无浪费 |
| 随机访问 | ✅ O(1) | ❌ O(n) |
| 插入删除 | ❌ O(n),需要移动元素 | ✅ O(1),只需修改指针 |
| 空间开销 | 只存数据 | 额外存储指针 |
| 内存连续性 | 连续,缓存友好 | 分散,缓存不友好 |
七、实际应用场景
顺序存储适用场景
- 需要频繁随机访问:如数组访问、矩阵运算
- 数据量固定或变化不大:如静态配置表
- 内存资源充足:不需要频繁插入删除
链式存储适用场景
- 频繁插入删除:如消息队列、任务调度
- 数据量不确定:动态增长
- 内存碎片化:无法分配连续大块内存
八、总结
| 数据结构 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 顺序表 | 随机访问快、存储密度高 | 插入删除慢、需要预分配 | 查询多、修改少 |
| 单链表 | 插入删除快、动态分配 | 不能随机访问、额外指针空间 | 修改多、查询少 |
| 循环链表 | 可从任意节点遍历全表 | 实现稍复杂 | 轮询调度 |
| 双向链表 | 双向遍历、删除简单 | 空间开销大 | LRU缓存、浏览器历史 |