C语言数据结构之单链表完整实现详解
前言
单链表是数据结构中最基础且重要的线性数据结构之一。相比于数组,链表具有动态分配内存、插入删除效率高等优点。本文将详细介绍单链表的概念、结构以及完整的C语言实现。
1. 单链表的基本概念
1.1 什么是单链表
单链表是一种链式存储的线性数据结构,每个节点包含两部分:
- 数据域(data):存储实际数据
- 指针域(next):指向下一个节点的指针
1 -> 2 -> 3 -> 4 -> NULL
1.2 单链表的特点
- 动态性:可以在运行时动态分配和释放内存
- 非连续性:节点在内存中不一定连续存储
- 单向性:只能从头节点开始单向遍历
- 插入删除高效:O(1)时间复杂度(已知位置)
2. 单链表的结构定义
2.1 头文件设计(SList.h)
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 定义数据类型,便于修改
typedef int SLTDataType;
// 定义链表节点结构
typedef struct SListNode {
SLTDataType data; // 存储的数据
struct SListNode* next; // 指向下一个节点
} SLTNode;
// 函数声明
void SLTPrint(SLTNode* phead); // 打印链表
SLTNode* SLTbuyNode(SLTDataType x); // 创建新节点
void SLTPushBack(SLTNode** pphead, SLTDataType x); // 尾插
void SLTPushFront(SLTNode** pphead, SLTDataType x); // 头插
void SLTPopBack(SLTNode** pphead); // 尾删
void SLTPopFront(SLTNode** pphead); // 头删
SLTNode* SLTFind(SLTNode* phead, SLTDataType x); // 查找
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x); // 指定位置前插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x); // 指定位置后插入
void SLTErase(SLTNode** pphead, SLTNode* pos); // 删除指定节点
void SLTEraseAfter(SLTNode* pos); // 删除指定节点后的节点
void SListDestroy(SLTNode** pphead); // 销毁链表
3. 基础操作实现
3.1 创建新节点
SLTNode* SLTbuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
关键点:
- 动态分配内存
- 检查内存分配是否成功
- 初始化数据域和指针域
3.2 打印链表
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur)
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
输出效果:1 -> 2 -> 3 -> 4 -> NULL
4. 插入操作详解
4.1 尾插(尾部插入)
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTbuyNode(x);
// 链表为空的情况
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
// 找到尾节点
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
// 连接新节点
ptail->next = newnode;
}
}
图解过程:
原链表: 1 -> 2 -> 3 -> NULL
插入4后: 1 -> 2 -> 3 -> 4 -> NULL
4.2 头插(头部插入)
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTbuyNode(x);
// 新节点指向原头节点
newnode->next = *pphead;
// 更新头指针
*pphead = newnode;
}
图解过程:
原链表: 1 -> 2 -> 3 -> NULL
插入0后: 0 -> 1 -> 2 -> 3 -> NULL
4.3 指定位置插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
// 如果在头节点前插入,调用头插
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* newnode = SLTbuyNode(x);
// 找到pos的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
// 插入新节点:prev -> newnode -> pos
prev->next = newnode;
newnode->next = pos;
}
}
5. 删除操作详解
5.1 尾删(删除尾节点)
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
// 只有一个节点的情况
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
// 找到尾节点和其前一个节点
SLTNode* prev = NULL;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
// 删除尾节点
prev->next = NULL;
free(ptail);
}
}
5.2 头删(删除头节点)
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
图解过程:
原链表: 1 -> 2 -> 3 -> NULL
头删后: 2 -> 3 -> NULL
5.3 删除指定节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos);
// 删除头节点
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
// 找到pos的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
// 删除pos节点
prev->next = pos->next;
free(pos);
}
}
6. 查找和销毁操作
6.1 查找操作
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL; // 未找到
}
6.2 销毁链表
void SListDestroy(SLTNode** pphead)
{
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
重要性:防止内存泄漏,程序结束前必须调用。
7. 测试代码示例
#include "SList.h"
void test()
{
SLTNode* plist = NULL;
// 测试尾插
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPrint(plist); // 输出: 1 -> 2 -> 3 -> NULL
// 测试头插
SLTPushFront(&plist, 0);
SLTPrint(plist); // 输出: 0 -> 1 -> 2 -> 3 -> NULL
// 测试查找和插入
SLTNode* find = SLTFind(plist, 2);
if (find)
{
SLTInsert(&plist, find, 100);
SLTPrint(plist); // 输出: 0 -> 1 -> 100 -> 2 -> 3 -> NULL
}
// 销毁链表
SListDestroy(&plist);
}
int main()
{
test();
return 0;
}
8. 重要知识点总结
8.1 二级指针的使用
为什么使用二级指针?
- 需要修改头指针的值时必须使用二级指针
- 一级指针只能修改指针指向的内容,不能修改指针本身
// 错误示例(一级指针)
void wrong_insert(SLTNode* phead, int x) {
// 无法修改main函数中的phead
}
// 正确示例(二级指针)
void correct_insert(SLTNode** pphead, int x) {
// 可以修改main函数中的phead
}
8.2 内存管理要点
- 动态分配:使用malloc分配内存
- 检查分配:判断malloc返回值是否为NULL
- 及时释放:使用free释放内存
- 避免泄漏:程序结束前销毁整个链表
8.3 边界条件处理
- 空链表:头指针为NULL的情况
- 单节点:只有一个节点的特殊情况
- 头节点操作:需要特殊处理的情况
9. 时间复杂度分析
操作 | 时间复杂度 | 说明 |
---|---|---|
头插/头删 | O(1) | 直接操作头节点 |
尾插/尾删 | O(n) | 需要遍历到尾节点 |
查找 | O(n) | 最坏情况遍历整个链表 |
指定位置插入/删除 | O(n) | 需要找到前驱节点 |
10. 应用场景
- 动态数据管理:不确定数据量大小的场景
- 频繁插入删除:需要经常在中间位置操作的场景
- 内存利用:避免数组的内存浪费
- 算法实现:栈、队列等数据结构的底层实现
总结
单链表是数据结构学习的重要基础,掌握其实现原理对后续学习其他数据结构非常重要。本文详细介绍了单链表的完整实现,包括所有基本操作的代码和原理。
学习建议:
- 理解指针的使用,特别是二级指针
- 注意内存管理,避免内存泄漏
- 画图理解插入删除的过程
- 多写代码,熟练掌握各种操作
希望这篇文章能帮助大家更好地理解和掌握单链表的实现!