C语言链表完全指南:从单节点到链表管理
引言在数据结构的学习中我们首先学习了顺序表数组。顺序表虽然访问速度快但插入和删除操作需要移动大量元素效率较低。此外顺序表的大小固定扩容需要重新分配内存并拷贝数据。链表解决了这些问题。链表中的元素在内存中不连续存储通过指针连接形成逻辑上的线性结构。今天我们将从零开始实现一个完整的带头结点的单向链表涵盖初始化、插入、删除、遍历等核心操作。第一部分链表的基本概念一、什么是链表链表是一种物理存储单元上非连续、非顺序的线性结构。数据元素之间的逻辑关系通过指针链接实现。链表的分类分类标准类型说明方向单向链表每个节点只有一个指向后继的指针双向链表每个节点有前驱和后继两个指针是否循环非循环链表尾节点指针指向NULL循环链表尾节点指针指向头节点是否有头结点带头结点头结点不存储数据简化操作不带头结点第一个节点就存储数据本文实现的是带头结点的单向非循环链表。二、带头结点的优势特点不带头结点带头结点空表判断head NULLhead-next NULL头插操作需要修改头指针操作统一不修改头指针删除头节点需要修改头指针操作统一代码复杂度较高较低第二部分数据结构设计一、节点结构体typedef int ElemType; // 链表存储的数据类型 // 节点结构体 typedef struct ListNode { ElemType data; // 数据域存储实际数据 struct ListNode* next; // 指针域指向下一个节点 } ListNode;节点结构体大小┌─────────────────────────────────────────────┐ │ ListNode 节点 │ ├─────────────────────────────────────────────┤ │ data (4字节) │ next (8字节64位系统) │ └─────────────────────────────────────────────┘ 总大小12字节可能内存对齐后为16字节二、链表管理结构体// 链表管理结构体 struct LinkList { ListNode* head; // 指向头结点不存储数据 size_t cursize; // 当前链表中的元素个数 };设计意图head始终指向头结点头结点的next指向第一个实际存储数据的节点cursize记录链表长度避免每次遍历计算第三部分链表的基本操作一、初始化原理创建头结点让head指向它头结点的next指向NULL。void InitLinkList(struct LinkList* ps) { assert(ps ! NULL); // 分配头结点内存 ListNode* p (ListNode*)malloc(sizeof(ListNode)); if (p NULL) return; p-data 0; // 头结点数据域无用可任意赋值 p-next NULL; // 初始时没有实际节点 ps-head p; ps-cursize 0; }初始化后的内存布局二、创建新节点辅助函数ListNode* BuyNode(ElemType val) { ListNode* p (ListNode*)malloc(sizeof(ListNode)); if (p NULL) return p; p-data val; p-next NULL; return p; }三、遍历与打印void PrintLinkList(struct LinkList* ps) { assert(ps ! NULL); // 从第一个实际节点开始遍历跳过带头结点 for (ListNode* p ps-head-next; p ! NULL; p p-next) { printf(%d , p-data); } printf(\n); }第四部分插入操作一、尾插在尾部插入原理遍历到尾节点将新节点链接到尾部。// 时间复杂度O(n) bool InsertBack(struct LinkList* ps, ElemType val) { assert(ps ! NULL); ListNode* p BuyNode(val); if (p NULL) return false; // 找到尾节点 ListNode* q ps-head; while (q-next ! NULL) { q q-next; } // 链接新节点 p-next q-next; // 等价于 p-next NULL q-next p; ps-cursize; return true; }示意图插入前 : head → ┌───┐ ┌───┐ ┌───┐ │10 │→ │20 │→ │30 │→ NULL └───┘ └───┘ └───┘ 插入val40后 ┌───┐ ┌───┐ ┌───┐ ┌───┐ │10 │→ │20 │→ │30 │→ │40 │→ NULL └───┘ └───┘ └───┘ └───┘二、头插在头部插入原理新节点的next指向原来的第一个节点头结点指向新节点。// 时间复杂度O(1) bool InsertFront(struct LinkList* ps, ElemType val) { assert(ps ! NULL); ListNode* p BuyNode(val); if (p NULL) return false; p-next ps-head-next; // 新节点指向原第一个节点 ps-head-next p; // 头结点指向新节点 ps-cursize; return true; }示意图三、按位置插入原理找到第pos-1个节点在其后插入新节点。// 时间复杂度O(n) bool InsertPos(struct LinkList* ps, ElemType val, int pos) { assert(ps ! NULL); // 位置有效性检查1 ≤ pos ≤ cursize1 if (pos 1 || pos ps-cursize 1) return false; ListNode* p BuyNode(val); if (p NULL) return false; // 找到第 pos-1 个节点 ListNode* q ps-head; for (int i 0; i pos - 1; i) { q q-next; } // 插入新节点 p-next q-next; q-next p; ps-cursize; return true; }第五部分删除操作一、尾删删除尾部节点原理找到倒数第二个节点将其next设为NULL释放尾节点。// 时间复杂度O(n) bool PopBack(struct LinkList* ps) { assert(ps ! NULL); if (ps-cursize 0) return false; // 空链表 // 找到倒数第二个节点 ListNode* q ps-head; while (q-next-next ! NULL) { q q-next; } // 释放尾节点 ListNode* p q-next; q-next p-next; // 即 q-next NULL free(p); p NULL; ps-cursize--; return true; }二、头删删除头部节点原理头结点绕过第一个节点指向第二个节点释放第一个节点。// 时间复杂度O(1) bool PopFront(struct LinkList* ps) { assert(ps ! NULL); if (ps-cursize 0) return false; ListNode* p ps-head-next; // 要删除的节点 ps-head-next p-next; // 头结点指向第二个节点 free(p); p NULL; ps-cursize--; return true; }三、按位置删除// 时间复杂度O(n) bool PopPos(struct LinkList* ps, int pos) { assert(ps ! NULL); if (ps-cursize 0) return false; if (pos 1 || pos ps-cursize) return false; // 找到第 pos-1 个节点 ListNode* q ps-head; for (int i 0; i pos - 1; i) { q q-next; } // 删除第 pos 个节点 ListNode* p q-next; q-next p-next; free(p); p NULL; ps-cursize--; return true; }第六部分完整测试代码#include List.h #include stdio.h int main() { struct LinkList list; // 1. 初始化 InitLinkList(list); printf(初始化后元素个数%zu\n, list.cursize); // 2. 头插 InsertFront(list, 10); InsertFront(list, 20); InsertFront(list, 30); printf(头插后); PrintLinkList(list); // 30 20 10 // 3. 尾插 InsertBack(list, 40); InsertBack(list, 50); printf(尾插后); PrintLinkList(list); // 30 20 10 40 50 // 4. 按位置插入 InsertPos(list, 100, 3); // 在第3个位置插入100 printf(位置插入后); PrintLinkList(list); // 30 20 100 10 40 50 // 5. 头删 PopFront(list); printf(头删后); PrintLinkList(list); // 20 100 10 40 50 // 6. 尾删 PopBack(list); printf(尾删后); PrintLinkList(list); // 20 100 10 40 // 7. 按位置删除 PopPos(list, 2); printf(删除位置2后); PrintLinkList(list); // 20 10 40 return 0; }第七部分顺序表 vs 链表操作顺序表链表说明按下标访问O(1)O(n)链表需要遍历头部插入O(n)O(1)链表只需改指针头部删除O(n)O(1)链表只需改指针尾部插入O(1)扩容时O(n)O(n)链表需遍历到尾中间插入O(n)O(n)都需要定位内存占用较少较多链表有指针开销缓存友好好差顺序表内存连续选择建议频繁按下标访问 → 顺序表频繁头插/头删 → 链表频繁尾插 → 顺序表或维护尾指针的链表大小频繁变化 → 链表第八部分常见错误与注意事项一、指针操作错误// ❌ 错误p未初始化就修改next ListNode* p; p-next NULL; // ✅ 正确先分配内存 ListNode* p (ListNode*)malloc(sizeof(ListNode)); p-next NULL;二、内存泄漏// ❌ 错误删除节点后未释放内存 q-next p-next; // p 的内存泄漏了 // ✅ 正确释放内存 q-next p-next; free(p); p NULL;三、空指针解引用// ❌ 错误未检查链表是否为空 bool PopFront(struct LinkList* ps) { ListNode* p ps-head-next; ps-head-next p-next; // 如果p是NULL这里崩溃 free(p); return true; } // ✅ 正确检查空链表 bool PopFront(struct LinkList* ps) { if (ps-cursize 0) return false; // ... 正常删除逻辑 }总结一、链表操作复杂度总结操作时间复杂度代码关键点初始化O(1)创建头结点尾插O(n)遍历到尾节点头插O(1)新节点指向原第一个节点中间插入O(n)定位到前驱节点尾删O(n)找到倒数第二个节点头删O(1)头结点指向第二个节点中间删除O(n)定位到前驱节点遍历O(n)从头结点下一个开始二、带头结点的优势场景不带头结点带头结点空表判断head NULLhead-next NULL头插操作需要修改头指针操作统一删除头节点需要修改头指针操作统一三、核心函数速查函数功能关键操作InitLinkList初始化创建头结点BuyNode创建节点malloc 赋值InsertBack尾插遍历到尾 链接InsertFront头插头结点后插入InsertPos位置插入找前驱 链接PopBack尾删找倒数第二个 释放PopFront头删头结点绕过第一个PopPos位置删除找前驱 释放链表是数据结构中非常重要的基础结构。本文实现了带头结点的单向链表涵盖以下知识点节点结构包含数据域和指针域头结点简化插入删除操作插入操作头插、尾插、按位置插入删除操作头删、尾删、按位置删除内存管理malloc分配、free释放复杂度分析理解各操作的时间复杂度学习建议画图理解指针操作不要只靠想象注意边界条件空链表、单节点链表插入和删除时确保指针指向正确释放内存后将指针置NULL防止野指针
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2590109.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!