当前位置: 首页 > news >正文

S2--单链表

2.1单链表

2.1.1单链表定义:

单链表是一种基础且重要的链式数据结构,它通过指针将一组零散的内存块(节点)串联起来,用于存储逻辑关系为“一对一”的数据。理解单链表是掌握更复杂链表结构(如双向链表、循环链表)和许多高级算法的基础。

特性分类

核心要点

基本结构

每个节点包含数据域(存储数据)和指针域(存储下一个节点的地址)。最后一个节点的指针指向    NULL 。

关键指针

头指针:永远指向链表的第一个节点(可能是头节点或首元节点),是链表的“入口”。

头节点

可选的、不存储实际数据的节点,位于首元节点之前。其引入可以简化插入、删除等操作的代码逻辑。

核心优点

动态扩容:内存按需分配,无需预先申请大块连续空间。高效增删:在已知节点位置时,插入或删除操作仅需修改指针,时间复杂度为 O(1)。

主要缺点

随机访问低效:要访问第 i 个元素,必须从头节点开始顺序遍历,时间复杂度为 O(n)。额外空间开销:每个节点都需要额外的空间来存放指针。

常见操作时间复杂度

访问第 i 个元素:O(n) • 在指定节点后插入:O(1) • 搜索特定值:O(n) • 删除指定节点:O(1)(需先找到前驱节点则为 O(n))

2.1.2单链表设计与实现

节点结构:每个节点由两部分组成:

  • 数据域(vaval):存放数据元素本身的信息。
  • 指针域(next):存放指向下一个节点的内存地址的指针
  • 头指针(Head):这是一个关键变量,它存储着链表第一个节点的地址。无论链表如何变化,头指针是访问和标识整个链表的唯一起点。
  • 头节点(Head->Node):这是一个可选节点,位于链表的真正第一个数据节点(首元节点)之前。头节点的数据域通常不存储信息或存储无关信息,其指针域指向首元节点。
  • 容量大小(cursize):记录链表中的元素个数。
  • 引入头节点的好处:最大的优势在于统一了空表和非空表的操作。例如,在空链表中插入第一个元素,或在链表头部进行插入/删除操作时,因为有头节点作为不变的“前驱”,操作逻辑可以保持一致,无需特殊处理头指针本身,从而简化了代码。
#define ELEM_TYPE inttypedef struct ListNode {ELEM_TYPE val;ListNode* next;}ListNode;typedef struct Linklist {ListNode* head;size_t cursize;}LinkList;

链表初始化

   InitList : 正确创建了头节点(哨兵节点),这确实能简化插入和删除操作。注意检查    plist  为    NULL  。

void InitList(LinkList* plist){assert(plist != NULL);ListNode* p1 = (ListNode*)malloc(sizeof(ListNode));if (p1 == NULL)return;p1->next = NULL;plist->head = p1;plist->cursize = 0;}

获取元素大小:

   GetSize 直接返回   cursize ,效率很高

int GetSize(const LinkList* plist) {assert(plist != NULL);return plist->cursize; // O(1)时间复杂度}

链表判空:

   Is_Empty 判断链表是否为空

bool Is_Empty(const LinkList* plist) {assert(plist != NULL);return GetSize(plist) == 0;// 或者也可以判断头节点的next是否为空:return plist->head->next == NULL;}

按位置查找结点:

   FindPos 函数返回指向第   pos 个节点的指针。这里的位置约定是:头节点(哨兵节点)的位置是0,第一个数据节点(首元节点)的位置是1。函数通过循环   pos 次移动指针来定位。

   PrevFindPos 通过调用   FindPos(pos - 1) 来获得第   pos 个节点的前驱节点。

ListNode* FindPos(const LinkList* plist, int pos) {assert(plist != NULL);if (pos < 0 || pos > GetSize(plist)) { // 注意:pos可以等于GetSize(plist),此时返回的是最后一个节点的下一个位置(NULL)printf("pos位置不符\n");return NULL;}ListNode* p = plist->head; // p指向头节点,位置0if (pos == 0) return p;while (pos--) {p = p->next;}return p;}ListNode* PrevFindPos(const LinkList* plist, int pos) {assert(plist != NULL);return FindPos(plist, pos - 1); // 直接复用FindPos函数}

创建新节点 (   buynode )

ListNode* buynode(ELEM_TYPE val) {ListNode* p = (ListNode*)malloc(sizeof(ListNode));if (p == NULL) return NULL; // 内存分配失败检查p->val = val;p->next = NULL;return p;}

插入如新节点:

在指定节点后插入 (   InsertNext )。

bool InsertNext(LinkList* plist, ListNode* ptr, ELEM_TYPE val) {assert(plist != NULL && ptr != NULL); // 修改了条件,使用&&ListNode* p = buynode(val);if (p == NULL) return false;p->next = ptr->next;ptr->next = p;plist->cursize++;return true;}

代码解读:这是最核心的插入操作,时间复杂度为O(1)。关键在于先连接新节点与后继节点,再连接前驱节点与新节点。

按位置插入 (   InsertPos )。

bool InsertPos(LinkList* plist, int pos, ELEM_TYPE val) {assert(plist != NULL);ListNode* ptr = PrevFindPos(plist, pos); // 找到第pos个节点的前驱节点if (ptr == NULL) return false;return InsertNext(plist, ptr, val); // 复用InsertNext}

代码解读

  • 先找到第   pos 个位置的前驱节点,然后调用   InsertNext 插入。
  • 查找前驱节点的时间复杂度是O(n),因此按位置插入的整体复杂度是O(n)

头插法 (   Push_Front ) 与尾插法 (   Push_Back )

bool Push_Front(LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);// 直接在头节点后插入,即位置1的前驱就是头节点return InsertNext(plist, plist->head, val); // 复用InsertNext}bool Push_Back(LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);ListNode* p = plist->head;while (p->next != NULL) p = p->next; // 遍历找到最后一个节点return InsertNext(plist, p, val); // 在最后一个节点后插入}

代码解读

  •    Push_Front 时间复杂度为O(1)
  •    Push_Back 需要遍历找到尾节点,时间复杂度为O(n)。如果频繁进行尾插,可以考虑维护一个尾指针   tail 成员。

遍历打印 (   PrintInfo ):

void PrintInfo(const LinkList* plist) {assert(plist != NULL);for (ListNode* p = plist->head->next; p != NULL; p = p->next) {printf("%d ", p->val);}}

代码解读:从首元节点开始打印,直到   NULL 。

删除节点:

删除指定节点的后继节点 (   DelNext )。

bool DelNext(LinkList* plist, ListNode* ptr) {assert(plist != NULL && ptr != NULL && ptr->next != NULL);if (Is_Empty(plist)) return false;ListNode* p = ptr->next;ptr->next = ptr->next->next;free(p);p = NULL; // 注意:这里应该是 p = NULL; 而不是 p == NULL;plist->cursize--;return true;}

代码解读:这是核心的删除操作,时间复杂度为O(1)。关键在于先保存要删除的节点,修改指针连接,然后释放内存。

按位置删除 (   DelPos )。

bool DelPos(LinkList* plist, int pos) {assert(plist != NULL);if (Is_Empty(plist)) return false;ListNode* ptr = plist->head;while (--pos) { // 循环pos-1次,找到第pos个节点的前驱ptr = ptr->next;}ListNode* p = ptr->next;ptr->next = p->next;free(p);p = NULL; // 同样,这里应该是 p = NULL;plist->cursize--;return true;}

代码解读:先找到要删除节点的前驱,然后执行删除。查找过程O(n),删除本身O(1),整体O(n)

头删 (   Pop_Front ) 与尾删 (   Pop_Back )。

bool Pop_Front(LinkList* plist) {assert(plist != NULL);if (Is_Empty(plist)) return false;// 删除头节点后的第一个节点ListNode* ptr = plist->head->next;plist->head->next = ptr->next;free(ptr);ptr = NULL;plist->cursize--;return true;}bool Pop_Back(LinkList* plist) {assert(plist != NULL);if (Is_Empty(plist)) return false;ListNode* ptr = plist->head;while (ptr->next->next != NULL) ptr = ptr->next; // 找到倒数第二个节点ListNode* p = ptr->next;ptr->next = NULL; // 或者 ptr->next = p->next; 但此时p->next为NULLfree(p);p = NULL;plist->cursize--;return true;}

代码解读

  •    Pop_Front 时间复杂度为O(1)
  •    Pop_Back 需要找到倒数第二个节点,时间复杂度为O(n)

按值查找:

查找节点 (   FindValue )。

ListNode* FindValue(const LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);ListNode* p = plist->head->next; // 从第一个数据节点开始找while (p != NULL) {if (p->val == val) return p;p = p->next;}return NULL;}

代码解读:顺序遍历,时间复杂度O(n)。查找节点的前驱 (   PreFindValue )。

ListNode* PreFindValue(const LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);ListNode* p = plist->head;while (p->next != NULL) {if (p->next->val == val) return p;p = p->next;}return NULL;}

代码解读:通过判断   p->next->val 来定位,返回的是目标节点的前驱节点。这在删除操作中很有用。

清空与销毁:

清空链表 (   ClearList )。

void ClearList(LinkList* plist) {assert(plist != NULL);ListNode* p = plist->head->next;while (p != NULL) {ListNode* n = p;p = p->next;free(n);n = NULL;}plist->head->next = NULL; // 重要:清空后头节点的next应指向NULLplist->cursize = 0;}

代码解读:释放所有数据节点,但保留头节点,链表可再次使用。

销毁链表 (   DestroyList )。

void DestroyList(LinkList* plist) {assert(plist != NULL);ListNode* p = plist->head;while (p != NULL) {ListNode* n = p;p = p->next;free(n);n = NULL;}// 重要:应将plist->head置为NULL,避免成为野指针。// 但plist本身是外部变量,通常由调用者管理。// 可以在函数内添加:plist->head = NULL; plist->cursize = 0;}

代码解读:释放所有节点,包括头节点。链表结构不再可用。

2.1.3总结与关键点回顾

操作

时间复杂度

关键要点

初始化

O(1)

创建头节点,初始化大小

获取大小

O(1)

直接返回   cursize 

按位置查找

O(n)

顺序遍历

指定节点后插入

O(1)

修改指针顺序:新节点->后继,前驱->新节点

按位置插入

O(n)

查找O(n)+插入O(1)

头插/头删

O(1)

操作头节点后

尾插/尾删

O(n)

需遍历找尾

按值查找

O(n)

顺序遍历比较

清空/销毁

O(n)

逐个节点释放

http://www.dtcms.com/a/499306.html

相关文章:

  • jdk.random 包详解
  • 如何做网站接口关于电子商务网站建设的现状
  • 网站栏目设计内容谷歌在线浏览器入口
  • 聊聊 Unity(小白专享、C# 小程序 之 自动更新)
  • 截取网站流量dede购物网站
  • 某Boss直聘数据获取
  • Spring Boot 3零基础教程,WEB 开发 默认欢迎页 笔记28
  • Redis极简入门 整合springboot
  • 漫蛙漫画官网入口 - 免费漫画在线看|防走失页入口
  • MySQL中的约束详解
  • 服务流程企业网站东莞市建设安监监督网站
  • leetcode 206. 反转链表 python
  • 【C语言】自定义类型(附源码与图片分析)
  • 用户头像文件存储功能是如何实现的?
  • 网站设计大概在什么价位渠道销售
  • C++竞赛递推算法-斐波那契数列常见题型与例题详解
  • 单元测试-例子
  • 网站顶部素材山西制作网站
  • PHP 高效 JSON 库 JsonMachine
  • 网站建设内部因素百度站长平台有哪些功能
  • Linux内核IPoIB驱动深度解析:在InfiniBand上跑IP网络的高性能之道
  • 275TOPS算力边缘计算盒子的价值洞察与市场定位---视程空间
  • 对话 MoonBit 张宏波:为 AI 重构编程语言
  • QGIS制图专题4:缓冲区分析与服务半径专题图制作
  • IP 资源会枯竭吗?IPv6 能解决代理市场的矛盾吗?
  • 物联网运维中的边缘计算任务调度优化策略
  • TensorFlow2 Python深度学习 - 循环神经网络(LSTM)示例
  • C++第二十三课:猜数字游戏等练习
  • 河南省建设厅网站中州杯企业网站推广怎么做
  • 【数论】最大公因数 (gcd) 与最小公倍数 (lcm)