链表与数组的区别
- 数组:连续内存空间,支持随机访问,但插入删除效率低,扩容麻烦。
- 链表:离散内存空间,每个节点包含数据域 + 指针域,通过指针串联,不支持随机访问,但插入删除高效。
单链表结构
单链表每个节点只有一个指针,指向下一个节点,最后一个节点指针指向 NULL 表示结束。
头指针 -> 节点1 -> 节点2 -> 节点3 -> NULL
一,单链表的结构定义
首先我们需要定义单链表的节点结构,每个节点包含数据域和指针域:
- 数据域:存储节点的具体数据(本文以
int为例); - 指针域:存储下一个节点的地址,实现节点的串联。
对应的头文件SList.h核心定义如下:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 单链表数据类型(可按需修改,如char、float等)
typedef int SLDataType;
// 单链表节点结构
typedef struct SListNode
{
SLDataType x; // 数据域
struct SListNode* next;// 指针域:指向后继节点
}SLNode;
二,单链表核心操作接口设计
我们先在头文件中声明所有对应函数
// 打印链表
void SLPrint(SLNode* phead);
// 头尾插删
void SLPushBack(SLNode** pphead, SLDataType x); // 尾插
void SLPushFront(SLNode** pphead, SLDataType x); // 头插
void SLPopBack(SLNode** pphead); // 尾删
void SLPopFront(SLNode** pphead); // 头删
// 查找节点
SLNode* SLFind(SLNode* phead, SLDataType x);
// 指定位置插删
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x); // pos前插入
void SLErase(SLNode** pphead, SLNode* pos); // 删除pos节点
void SLInsertAfter(SLNode* pos, SLDataType x); // pos后插入
void SLEraseAfter(SLNode* pos); // 删除pos后节点
// 销毁链表
void SListDesTroy(SLNode** pphead);
三,核心操作实现详解
1. 打印链表(SLPrint)
遍历链表并打印每个节点的数据,断言链表头节点非空:
void SLPrint(SLNode* phead)
{
assert(phead); // 链表非空断言
SLNode* pcur = phead;
while (pcur) // 遍历至链表尾(pcur为NULL时结束)
{
printf("%d ", pcur->x);
pcur = pcur->next;
}
printf("\n"); // 换行优化输出
}
2. 尾部插入(SLPushBack)
尾插需要考虑两种情况:链表为空、链表非空。通过二级指针修改链表头节点(空链表时需更新头指针):
void SLPushBack(SLNode** pphead, SLDataType x)
{
assert(pphead); // 二级指针非空(避免传入NULL)
// 1. 创建新节点
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
newnode->next = NULL;
newnode->x = x;
// 2. 空链表:直接让头指针指向新节点
if (*pphead == NULL)
{
*pphead = newnode;
}
// 3. 非空链表:遍历至尾节点,尾节点next指向新节点
else
{
SLNode* pcur = *pphead;
while (pcur->next) // 找到最后一个节点(pcur->next为NULL)
{
pcur = pcur->next;
}
pcur->next = newnode;
}
}
3. 头部插入(SLPushFront)
头插无需遍历链表,只需让新节点的next指向原头节点,再更新头指针即可:
void SLPushFront(SLNode** pphead, SLDataType x)
{
assert(pphead);
// 1. 创建新节点
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
newnode->next = NULL;
newnode->x = x;
// 2. 空链表:直接更新头指针
if (*pphead == NULL)
{
*pphead = newnode;
}
// 3. 非空链表:新节点next指向原头节点,更新头指针
else
{
newnode->next = *pphead;
*pphead = newnode;
}
}
4. 尾部删除(SLPopBack)
尾删需考虑两种情况:链表只有一个节点、链表有多个节点。注意释放节点内存并避免野指针:
void SLPopBack(SLNode** pphead)
{
assert(pphead);
assert(*pphead); // 链表非空(避免删除空链表)
// 1. 只有一个节点:释放节点,头指针置空
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
// 2. 多个节点:遍历至倒数第二个节点,释放尾节点
else
{
SLNode* pcur = *pphead;
SLNode* prev = *pphead;
while (pcur->next) // 找到尾节点
{
prev = pcur;
pcur = pcur->next;
}
free(pcur); // 释放尾节点
prev->next = NULL; // 倒数第二个节点next置空
}
}
5. 头部删除(SLPopFront)
头删逻辑简单:保存原头节点的后继节点,释放原头节点,更新头指针:
void SLPopFront(SLNode** pphead)
{
assert(pphead);
assert(*pphead); // 链表非空
SLNode* next = (*pphead)->next; // 保存后继节点
free(*pphead); // 释放原头节点
*pphead = next; // 更新头指针
}
6. 查找节点(SLFind)
遍历链表,找到数据等于目标值的节点并返回其地址,未找到则返回NULL:
SLNode* SLFind(SLNode* phead, SLDataType x)
{
assert(phead);
SLNode* pcur = phead;
while (pcur)
{
if (pcur->x == x)
return pcur; // 找到目标节点,返回地址
pcur = pcur->next;
}
return NULL; // 未找到
}
7. 指定位置前插入(SLInsert)
需区分两种情况:插入位置是头节点、插入位置是中间 / 尾节点。核心是找到pos的前驱节点:
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
assert(pphead);
assert(*pphead);
// 1. 创建新节点
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
newnode->next = NULL;
newnode->x = x;
// 2. 插入位置是头节点:等价于头插
if (*pphead == pos)
{
newnode->next = *pphead;
*pphead = newnode;
}
// 3. 插入位置是中间/尾节点:找到pos前驱节点
else
{
SLNode* pcur = *pphead;
while (pcur->next != pos) // 找到pos的前驱节点
{
pcur = pcur->next;
}
pcur->next = newnode; // 前驱节点指向新节点
newnode->next = pos; // 新节点指向pos
}
}
8. 删除指定位置节点(SLErase)
同理区分头节点和非头节点,找到pos的前驱节点后,修改指针并释放pos节点:
void SLErase(SLNode** pphead, SLNode* pos)
{
assert(pphead);
assert(*pphead);
// 1. 删除头节点
if (*pphead == pos)
{
free(*pphead);
*pphead = NULL;
}
// 2. 删除中间/尾节点
else
{
SLNode* pcur = *pphead;
while (pcur->next != pos) // 找到pos前驱节点
{
pcur = pcur->next;
}
pcur->next = pos->next; // 前驱节点指向pos后继节点
free(pos); // 释放pos节点
}
}
9. 指定位置后插入(SLInsertAfter)
无需遍历链表,直接修改pos和新节点的指针即可:
void SLInsertAfter(SLNode* pos, SLDataType x)
{
assert(pos); // pos节点非空
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
newnode->next = NULL;
newnode->x = x;
newnode->next = pos->next; // 新节点指向pos后继
pos->next = newnode; // pos指向新节点
}
10. 删除指定位置后节点(SLEraseAfter)
保存pos的后继节点,修改指针后释放该节点:
void SLEraseAfter(SLNode* pos)
{
assert(pos);
SLNode* next = pos->next; // 保存pos后继节点
pos->next = next->next; // pos指向后继的后继
free(next); // 释放后继节点
}
11. 销毁链表(SListDesTroy)
遍历链表,逐个释放节点内存,最终将头指针置空(避免野指针):
void SListDesTroy(SLNode** pphead)
{
SLNode* pcur = *pphead;
while (pcur->next) // 遍历至最后一个节点
{
SLNode* next = pcur->next;
free(pcur); // 释放当前节点
pcur = next; // 移动至下一个节点
}
*pphead = NULL; // 头指针置空
四,单链表 vs 顺序表(核心对比)
| 特性 | 单链表 | 顺序表(数组) |
|---|---|---|
| 随机访问 | 不支持(O (n)) | 支持(O (1)) |
| 插入 / 删除(头部) | O(1) | O (n)(移动元素) |
| 插入 / 删除(尾部) | O (n)(无尾指针) | O (1)(不扩容) |
| 内存分配 | 动态、非连续 | 静态 / 动态、连续 |
| 空间开销 | 额外指针域 | 无额外开销(可能浪费) |
| 缓存友好性 | 差(节点分散) | 好(连续内存) |


被折叠的 条评论
为什么被折叠?



