前言
链表是数据结构中最基础也是最重要的一种线性结构,与数组不同,链表不需要连续的内存空间,通过指针将零散的内存块连接起来。本文将详细分析一个完整的C语言单向链表实现,涵盖创建、插入、删除、查找等核心操作,并通过完整的测试案例验证其正确性。
目录
[1. 链表打印](#1. 链表打印)
[2. 节点创建](#2. 节点创建)
[3. 尾插操作](#3. 尾插操作)
[4. 头插操作](#4. 头插操作)
[5. 尾删操作](#5. 尾删操作)
[6. 查找操作](#6. 查找操作)
[7. 指定位置插入](#7. 指定位置插入)
[8. 删除操作](#8. 删除操作)
正文
链表结构定义
在头文件SList.h中,我们定义了链表的基本结构:
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
这是一个典型的单向链表节点结构,包含数据域data和指向下一个节点的指针next。
核心功能实现
1. 链表打印
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
这个函数遍历整个链表并打印每个节点的数据,用"->"连接,最后以"NULL"结束,直观展示链表结构。
2. 节点创建
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. 尾插操作
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;
}
}
关键点分析:
-
使用二级指针
pphead,因为可能修改头指针(当链表为空时) -
区分空链表和非空链表两种情况
-
时间复杂度为O(n),需要遍历找到尾节点
4. 头插操作
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
头插操作相对简单,时间复杂度为O(1),直接将新节点作为新的头节点。
5. 尾删操作
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
prev->next = NULL;
}
}
关键点分析:
-
需要维护前驱指针
prev,因为单链表无法直接访问前一个节点 -
处理单节点和多节点两种情况的边界条件
-
释放内存后要将指针置为NULL,避免野指针
6. 查找操作
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
assert(phead);
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
return pcur;
pcur = pcur->next;
}
return NULL;
}
线性查找,时间复杂度O(n),返回找到的节点指针或NULL。
7. 指定位置插入
在指定位置之前插入:
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);
if (*pphead == pos)
{
SLTPushFront(pphead,x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos;
prev->next = newnode;
}
}
在指定位置之后插入:
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
对比分析:
-
前插需要找到前驱节点,时间复杂度O(n)
-
后插直接操作,时间复杂度O(1)
-
前插在头节点位置时退化为头插操作
8. 删除操作
删除指定节点:
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead &&*pphead);
assert(pos);
if (*pphead == pos)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
删除指定位置后的节点:
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
}
测试代码分析
测试函数SListTest02全面验证了链表的各种操作:
void SListTest02()
{
SLTNode* plist = NULL;
// 构建基础链表:1->2->3->4->5->NULL
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist); // 预期:1->2->3->4->5->NULL
// 头插构建:6->7->8->9->10->1->2->3->4->5->NULL
SLTPushFront(&plist, 10);
SLTPushFront(&plist, 9);
SLTPushFront(&plist, 8);
SLTPushFront(&plist, 7);
SLTPushFront(&plist, 6);
SLTPrint(plist);
// 删除操作验证
SLTPopBack(&plist); // 删除尾节点5
SLTPrint(plist); // 6->7->8->9->10->1->2->3->4->NULL
SLTPopFront(&plist); // 删除头节点6
SLTPrint(plist); // 7->8->9->10->1->2->3->4->NULL
// 查找功能测试
SLTNode* find1 = SLTFind(plist, 8);
if (find1 != NULL)
{
printf("找到了\n");
}
// 复杂插入操作
SLTNode* find2 = SLTFind(plist, 7);
SLTInsert(&plist, find2, 100); // 在7之前插入100
SLTPrint(plist); // 100->7->8->9->10->1->2->3->4->NULL
// 后续各种插入、删除操作...
// 最终销毁链表
SListDestroy(&plist);
SLTPrint(plist); // 应该打印NULL
}
测试亮点:
-
覆盖了所有边界情况(头节点、尾节点操作)
-
验证了操作的顺序正确性
-
测试了查找失败的情况
-
确保内存正确释放
总结
通过这个完整的单向链表实现,我们可以得出以下几点重要结论:
-
二级指针的必要性:在需要修改头指针的函数中必须使用二级指针,这是很多初学者容易出错的地方。
-
边界条件处理:链表操作要特别注意空链表、单节点链表、头尾节点等边界情况的处理。
-
时间复杂度分析:
-
头插、头删:O(1)
-
尾插、尾删:O(n)
-
查找:O(n)
-
指定位置前插:O(n)
-
指定位置后插:O(1)
-
-
内存管理:每次分配内存都要检查是否成功,释放内存后要及时置空指针。
-
代码复用:通过复用已有的头插、头删函数,提高了代码的复用性和可维护性。
这个链表实现展示了良好的编程实践,包括错误处理、断言检查、代码复用等,是一个很好的学习范例。理解这个实现对于掌握更复杂的数据结构和指针操作具有重要意义。