
🎬 博主名称 :键盘敲碎了雾霭
🔥 个人专栏 : 《C语言》《数据结构》
⛺️指尖敲代码,雾霭皆可破

文章目录
一、线性表概述
线性表 (Linear List)是最基本、最简单的一种数据结构。一个线性表是n个数据元素的有限序列,它们之间的关系构成一个线性序列。常见的线性表有顺序表、链表等。
线性表具有以下特点:
- 存在唯一的第一个元素和最后一个元素;
- 除第一个元素外,每个元素有且仅有一个直接前驱;
- 除最后一个元素外,每个元素有且仅有一个直接后继。
根据存储方式的不同,线性表可以分为顺序存储结构 (顺序表)和链式存储结构(链表)。本文将详细介绍这两种结构,并使用C语言实现它们的基本操作。
二、顺序表
顺序表是用一段连续的存储单元依次存储线性表的数据元素。在C语言中通常使用数组实现。根据数组的分配方式,顺序表可分为静态顺序表 (定长数组)和动态顺序表(动态开辟的数组)。动态顺序表更灵活,本文将重点介绍动态顺序表的实现。
2.1 结构定义
c
typedef int SLDataType; // 可根据需要修改元素类型
typedef struct SeqList {
SLDataType* arr; // 指向动态开辟的数组
int size; // 当前有效元素个数
int capacity; // 当前容量
} SL;
2.2 基本操作
初始化
c
void SeqListInit(SL* ps) {
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
容量检查与扩容
c
void CheckCapacity(SL* ps) {
if (ps->size == ps->capacity) {
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
if (tmp == NULL) {
perror("realloc");
return;
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
尾插
c
void SeqListPushBack(SL* ps, SLDataType x) {
assert(ps);
CheckCapacity(ps);
ps->arr[ps->size++] = x;
}
头插
c
void SeqListPushFront(SL* ps, SLDataType x) {
assert(ps);
CheckCapacity(ps);
// 将所有元素后移一位
for (int i = ps->size; i > 0; --i) {
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
尾删
c
void SeqListPopBack(SL* ps) {
assert(ps && ps->size > 0);
ps->size--;
}
头删
c
void SeqListPopFront(SL* ps) {
assert(ps && ps->size > 0);
for (int i = 0; i < ps->size - 1; ++i) {
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
指定位置插入
c
void SeqListInsert(SL* ps, int pos, SLDataType x) {
assert(ps);
assert(pos >= 0 && pos <= ps->size);
CheckCapacity(ps);
for (int i = ps->size; i > pos; --i) {
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
指定位置删除
c
void SeqListErase(SL* ps, int pos) {
assert(ps);
assert(pos >= 0 && pos < ps->size);
for (int i = pos; i < ps->size - 1; ++i) {
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
查找
c
int SeqListFind(SL* ps, SLDataType x) {
assert(ps);
for (int i = 0; i < ps->size; ++i) {
if (ps->arr[i] == x)
return i;
}
return -1;
}
修改
c
void SeqListModify(SL* ps, int pos, SLDataType x) {
assert(ps && pos >= 0 && pos < ps->size);
ps->arr[pos] = x;
}
打印
c
void SeqListPrint(SL* ps) {
for (int i = 0; i < ps->size; ++i) {
printf("%d ", ps->arr[i]);
}
printf("\n");
}
销毁
c
void SeqListDestroy(SL* ps) {
if (ps->arr) {
free(ps->arr);
ps->arr = NULL;
}
ps->size = ps->capacity = 0;
}
三、链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的。每个节点包含数据域和指针域。
链表有多种形式:单链表 、双向链表 、循环链表 等。本文将分别实现不带头结点的单链表 和带头结点的双向循环链表。
3.1 单链表
结构定义
c
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
} SLNode;
基本操作
创建新节点
c
SLNode* BuySListNode(SLTDataType x) {
SLNode* newNode = (SLNode*)malloc(sizeof(SLNode));
if (newNode == NULL) {
perror("malloc");
exit(1);
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
尾插
c
void SListPushBack(SLNode** pphead, SLTDataType x) {
assert(pphead);
SLNode* newNode = BuySListNode(x);
if (*pphead == NULL) {
*pphead = newNode;
} else {
SLNode* tail = *pphead;
while (tail->next) {
tail = tail->next;
}
tail->next = newNode;
}
}
头插
c
void SListPushFront(SLNode** pphead, SLTDataType x) {
assert(pphead);
SLNode* newNode = BuySListNode(x);
newNode->next = *pphead;
*pphead = newNode;
}
尾删
c
void SListPopBack(SLNode** pphead) {
assert(pphead && *pphead);
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;
} else {
SLNode* prev = NULL;
SLNode* tail = *pphead;
while (tail->next) {
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
头删
c
void SListPopFront(SLNode** pphead) {
assert(pphead && *pphead);
SLNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
查找
c
SLNode* SListFind(SLNode** pphead, SLTDataType x) {
assert(pphead && *pphead);
SLNode* cur = *pphead;
while (cur) {
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
在指定位置之前插入
c
void SListInsertBefore(SLNode** pphead, SLNode* pos, SLTDataType x) {
assert(pphead && pos);
if (*pphead == pos) {
SListPushFront(pphead, x);
} else {
SLNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
SLNode* newNode = BuySListNode(x);
newNode->next = pos;
prev->next = newNode;
}
}
在指定位置之后插入
c
void SListInsertAfter(SLNode* pos, SLTDataType x) {
assert(pos);
SLNode* newNode = BuySListNode(x);
newNode->next = pos->next;
pos->next = newNode;
}
删除指定位置节点
c
void SListErase(SLNode** pphead, SLNode* pos) {
assert(pphead && pos);
if (*pphead == pos) {
SListPopFront(pphead);
} else {
SLNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
销毁链表
c
void SListDestroy(SLNode** pphead) {
assert(pphead);
SLNode* cur = *pphead;
while (cur) {
SLNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
打印
c
void SListPrint(SLNode* phead) {
SLNode* cur = phead;
while (cur) {
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
3.2 带头结点的双向循环链表
带头结点的双向循环链表每个节点有pre和next两个指针,并且头结点的pre指向最后一个节点,最后一个节点的next指向头结点,形成一个环。带头结点使得操作更加统一,不需要单独处理空表情况。
结构定义
c
typedef int LTDataType;
typedef struct ListNode {
struct ListNode* pre;
struct ListNode* next;
LTDataType data;
} LTNode;
基本操作
创建新节点
c
LTNode* BuyListNode(LTDataType x) {
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (newNode == NULL) {
perror("malloc");
exit(1);
}
newNode->data = x;
newNode->pre = newNode->next = newNode; // 初始指向自己
return newNode;
}
初始化(创建头结点)
c
LTNode* ListInit() {
return BuyListNode(-1); // 头结点不存储有效数据
}
尾插
c
void ListPushBack(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* newNode = BuyListNode(x);
LTNode* tail = phead->pre; // 尾节点
newNode->pre = tail;
newNode->next = phead;
tail->next = newNode;
phead->pre = newNode;
}
头插
c
void ListPushFront(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* newNode = BuyListNode(x);
LTNode* first = phead->next;
newNode->pre = phead;
newNode->next = first;
phead->next = newNode;
first->pre = newNode;
}
尾删
c
void ListPopBack(LTNode* phead) {
assert(phead && phead->next != phead); // 不能为空表
LTNode* tail = phead->pre;
tail->pre->next = phead;
phead->pre = tail->pre;
free(tail);
}
头删
c
void ListPopFront(LTNode* phead) {
assert(phead && phead->next != phead);
LTNode* first = phead->next;
first->next->pre = phead;
phead->next = first->next;
free(first);
}
查找
c
LTNode* ListFind(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* cur = phead->next;
while (cur != phead) {
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
在指定位置之前插入
c
void ListInsertBefore(LTNode* pos, LTDataType x) {
assert(pos);
LTNode* newNode = BuyListNode(x);
newNode->next = pos;
newNode->pre = pos->pre;
pos->pre->next = newNode;
pos->pre = newNode;
}
删除指定位置节点
c
void ListErase(LTNode* pos) {
assert(pos);
pos->pre->next = pos->next;
pos->next->pre = pos->pre;
free(pos);
}
销毁链表
c
void ListDestroy(LTNode* phead) {
assert(phead);
LTNode* cur = phead->next;
while (cur != phead) {
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
打印
c
void ListPrint(LTNode* phead) {
assert(phead);
LTNode* cur = phead->next;
while (cur != phead) {
printf("%d->", cur->data);
cur = cur->next;
}
printf("\n");
}
四、顺序表与链表的比较
| 特性 | 顺序表 | 链表 |
|---|---|---|
| 存储空间 | 连续,预分配或动态扩展 | 非连续,按需分配 |
| 访问元素 | 随机访问,时间复杂度 O(1) | 顺序访问,时间复杂度 O(n) |
| 插入/删除 | 平均 O(n)(需移动大量元素) | 已知位置时 O(1)(仅修改指针) |
| 空间利用率 | 无额外指针开销,但可能有空间浪费 | 有指针开销,但无闲置空间 |
| 缓存友好性 | 高(连续内存,利用CPU缓存) | 低(节点分散,缓存命中率低) |
| 适用场景 | 频繁访问、较少插入删除 | 频繁插入删除、元素个数不确定 |
五、总结
线性表是数据结构的基础,顺序表和链表各有优劣。顺序表适合静态数据、频繁随机访问的场景;链表适合动态变化、频繁插入删除的场景。在实际开发中,应根据具体需求选择合适的结构。
本文通过C语言完整实现了顺序表、单链表和双向循环链表的核心操作,希望读者能通过代码加深对线性表的理解,并能够灵活运用。
(注:本文代码均在VS环境下测试通过,使用C语言标准库函数。)