【数据结构】单链表详解

【数据结构】单链表详解

单链表详解

1、单链表的概念

链表是一种线性数据结构,有一系列节点组成,每个节点包括两部分:存储当前节点的数据(数据区域)和下一个节点的地址(指针区域)

简单理解,单链表可以比作火车,有车厢挂钩,每一节车厢就是一个节点,节点里存储数据data,车厢之间有挂钩(节点里有指针next)。

在这里插入图片描述

2、单链表的实现

1.创建三个文件

SList.h文件放函数声明

SList.c文件实现.h文件中函数功能

test.c文件测试函数功能

函数功能目录 .h文件

#pragmaonce#include<stdio.h>#include<stdlib.h>#include<assert.h>//定义节点的结构//数据+指向下一个结点的指针typedefint SLTDataType;typedefstructSListNode{ SLTDataType data;structSListNode* next;}SLTNode;//打印voidSLTPrint(SLTNode* phead);//尾插voidSLTPushBack(SLTNode** pphead, SLTDataType x);//头插voidSLTPushFront(SLTNode** pphead, SLTDataType x);//尾删voidSLTPopBack(SLTNode** pphead);//头删voidSLTPopFront(SLTNode** pphead);//查找 SLTNode*SLTFind(SLTNode* phead, SLTDataType x);//在指定位置之前插入数据voidSLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在指定位置之后插入数据voidSLTInsertAfter(SLTNode* pos, SLTDataType x);//在指定位置删除数据voidSLTErase(SLTNode** pphead, SLTNode* pos);//在指定位置之后删除数据voidSLTEraseAfter(SLTNode* pos);//销毁voidSListDestroy(SLTNode** pphead);

2.单链表功能实现

2.1单链表核心结构

typedefint SLTDataType;typedefstructSListNode{ SLTDataType data;//存储数据structSListNode* next;//存储下一个节点的地址}SLTNode;

2.2单链表的打印

voidSLTPrint(SLTNode* phead){//只用来打印链表 不修改链表 不用二级指针 SLTNode* pcur = phead;//要用pcur充当头节点 遍历后phead指向末尾while(pcur){printf("%d->", pcur->data); pcur = pcur->next;}printf("NULL\n");}

· 只看不改,所以这里不用二级指针

· 用pcur遍历,不动原来的头指针

· 只要节点不为空,就打印数据,然后走到下一个

2.3创建新的节点

SLTNode*SLTBuyNode(SLTDataType x){ SLTNode* newnode =(SLTNode*)malloc(sizeof(SLTNode));//内存不够 报错退出if(newnode ==NULL){perror("malloc error");exit(1);} newnode->data = x; newnode->next =NULL;return newnode;}

· 先向内存要一块能存一个节点的空间

· 把数据data赋值为x

· 先让next指针置为空

· 返回这个节点

2.4查找节点

SLTNode*SLTFind(SLTNode* phead, SLTDataType x){ SLTNode* pcur = phead;while(pcur){if(pcur->data == x){return pcur;} pcur = pcur->next;}returnNULL;}

· 用pcur代替phead遍历链表,找到返回节点地址,找不到返回NULL

3.单链表的插入和删除

3.1尾插

voidSLTPushBack(SLTNode** pphead, SLTDataType x){assert(pphead);//*pphead不能为空 pphead为空说明没有头节点//*pphead就是指向第一个节点的指针//空链表和非空链表 SLTNode* newnode =SLTBuyNode(x);if(*pphead ==NULL){*pphead = newnode;}else{//找尾 SLTNode* ptail =*pphead;while(ptail->next!=NULL){//假如链表为空就是非法访问了 所以先判断是否为空 ptail = ptail->next;}//ptail指向的是尾节点 ptail->next = newnode;}}

· 为什么用二级指针? 因为要修改头指针本身

· 先创建一个节点,用ptail遍历链表,直到ptail走到尾节点的位置停止遍历,把最后一个节点的指针指向新节点的地址

· 如果链表是空链表,直接将新创建的节点作为头节点

3.2头插

voidSLTPushFront(SLTNode** pphead, SLTDataType x){assert(pphead);//申请新的节点 SLTNode* newnode =SLTBuyNode(x);//newnode 和*pphead连接在一起 newnode->next =*pphead;*pphead = newnode;}

· 头插要改变头指针存放的地址,所以要用二级指针

· 新节点的next指针指向原来的头节点,新节点变成头节点

3.3在指定位置之前插入数据

**voidSLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x){assert(pphead &&*pphead);assert(pos); SLTNode* newnode =SLTBuyNode(x);//当pos等于*pphead时是头插if(pos ==*pphead){SLTPushFront(pphead, x);}else{ SLTNode* prev =*pphead;while(prev->next != pos){ prev = prev->next;}//prev newnode pos newnode->next = pos; prev->next = newnode;}}**

· 用prev遍历链表,直到遍历到pos的前一个节点的位置停止遍历,此时需要把prev,newnode,pos连接到一起

3.4在指定位置之后插入数据

**voidSLTInsertAfter(SLTNode* pos, SLTDataType x){//第一种代码//newnode->next=pos->next//pos->next=newnode//第二种代码//pos->next=newnode//newnode->next=pos->nextassert(pos); SLTNode* newnode =SLTBuyNode(x);//顺序不能乱//pos newnode pos->next newnode->next = pos->next; pos->next = newnode;}**

这里需要注意,有两种顺序

· 先让newnode的指针域指向pos的后一个节点的地址,再让pos的指针域指向newnode的地址(正确)

· 先让pos的指针域指向newnode的地址,再让newnode的指针域指向pos的后一个节点的地址(错误)

错误的代码操作

pos:节点A

pos→next:节点B

第一步:pos→next=newnode 节点A的箭头指向了节点N 此时pos→next已经不是节点B,而是N

第二步:newnode→next=pos→next 这是pos→next为N,newnode→next也为N 节点N自己指向了自己,形成死循环

3.5头删

voidSLTPopFront(SLTNode** pphead){//链表不能为空assert(pphead &&*pphead); SLTNode* next =(*pphead)->next;free(*pphead);*pphead = next;}

· 头删要改变头指针中存放的地址,所以用二级指针

· 改变头节点前需要先保留原来头节点的地址,否则更换完头节点后找不到原来的头节点了

3.6尾删

voidSLTPopBack(SLTNode** pphead){assert(pphead);//链表不能为空assert(*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); ptail =NULL; prev->next =NULL;//prev是ptail前一个节点 需要将里面的next部分释放为空}}

· 尾删先要遍历一遍链表找到最后一个节点将其释放,要先找到倒数第二个节点,把倒数第二个节点的next指针置为空,再删除尾节点

3.7在指定位置删除数据

voidSLTErase(SLTNode** pphead, SLTNode* pos){assert(pphead &&*pphead);assert(pos);//pos是头节点if(pos ==*pphead){//头删SLTPopFront(pphead);}else{ SLTNode* prev =*pphead;while(prev->next != pos){ prev = prev->next;}//prev pos pos->next prev->next = pos->next;free(pos); pos =NULL;}}

· 当pos是头节点时,调用头删函数

· 用prev找到pos的前一个节点

· 让perv跳过pos,指向pos的下一个节点

· 释放pos

3.8删除pos位置后面的数据

voidSLTEraseAfter(SLTNode* pos){//不能删除pos节点assert(pos && pos->next); SLTNode* del = pos->next;//pos del del->next pos->next = del->next;free(del); del =NULL;}

· 要删除的节点是del,所以把pos的next指针指向del的下一个节点,最后释放del节点

4.单链表的销毁

voidSListDestroy(SLTNode** pphead){assert(pphead &&*pphead); SLTNode* pcur =*pphead;while(pcur){ SLTNode* next = pcur->next;free(pcur); pcur = next;}*pphead =NULL;}

· 所有节点全部释放

· 头节点置为空

3、测试文件

#include"SList.h"voidtest01(){//链表是由一个一个的节点组成//创造几个节点 SLTNode* nodel1 =(SLTNode*)malloc(sizeof(SLTNode)); nodel1->data =1; SLTNode* nodel2 =(SLTNode*)malloc(sizeof(SLTNode)); nodel2->data =2; SLTNode* nodel3 =(SLTNode*)malloc(sizeof(SLTNode)); nodel3->data =3; SLTNode* nodel4 =(SLTNode*)malloc(sizeof(SLTNode)); nodel4->data =4;//结果 1->2->3->4->NULL//将四个节点连接起来 nodel1->next = nodel2; nodel2->next = nodel3; nodel3->next = nodel4; nodel4->next =NULL;//调用链表的打印 SLTNode* plist1 = nodel1;SLTPrint(plist1);}voidtest02(){ SLTNode* plist =NULL;SLTPushBack(&plist,1);SLTPushBack(&plist,2);SLTPushBack(&plist,3);SLTPushBack(&plist,4);//结果 1->2->3->4->NULL//测试头插SLTPushFront(&plist,6);SLTPrint(plist);SLTPushFront(&plist,7);SLTPrint(plist);SLTPushFront(&plist,8);SLTPrint(plist);//结果 8->7->6->1->2->3->4->NULL//测试尾删SLTPopBack(&plist);SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);//结果 8->7->6->1->2->NULL//测试头删SLTPopFront(&plist);SLTPrint(plist);SLTPopFront(&plist);SLTPrint(plist);//结果 6->1->2->NULL//测试查找 SLTNode* find =SLTFind(plist,1);if(find ==NULL){printf("没有找到\n");}else{printf("找到了\n");}//测试指定位置之前插入SLTInsert(&plist, find,11);SLTPrint(plist);//结果 6->11->1->2->NULL//测试指定位置之后插入SLTInsertAfter(find,12);SLTPrint(plist);//结果 6->11->1->12->2->NULL//测试在指定位置前删除数据//SLTErase(&plist,find);//SLTPrint(plist);//测试在指定位置后删除数据SLTEraseAfter(find);SLTPrint(plist);//结果 6->11->1->12->NULL}intmain(){//test01();test02();return0;}

如果文章中有错误或不足,欢迎大家指正交流。

Read more

Effective Modern C++ 条款37:使std::thread在所有路径最后都不可结合

Effective Modern C++ 条款37:使std::thread在所有路径最后都不可结合

Effective Modern C++ 条款37:使std::thread在所有路径最后都不可结合 * 引言:线程生命周期的关键问题 * 线程的两种状态:可结合与不可结合 * 可结合(Joinable)状态的特征 * 不可结合(Unjoinable)状态的四种情况 * 为什么可结合性如此重要? * 两种被拒绝的替代方案 * RAII拯救方案:ThreadRAII类 * ThreadRAII实现详解 * 关键设计决策 * 实际应用案例 * 高级讨论:何时选择join或detach * 性能考量与最佳实践 * 结论:让线程管理无忧 BiliBili上对应的视频为:https://www.bilibili.com/video/BV1iZZgBiE9j 引言:线程生命周期的关键问题 在多线程程序设计中,std::thread的管理是一个看似简单实则暗藏玄机的话题。想象一下,你精心设计的并发程序在大多数情况下运行良好,却在某些边缘情况下突然崩溃——这正是许多开发者在使用原生线程时遇到的噩梦场景。本文将深入探讨std::thread对象

By Ne0inhk
RabbitMQ如何成为分布式系统的“神经中枢“?——从安装部署到C++调用实战的完整流程,带你体验它的奥妙所在!​

RabbitMQ如何成为分布式系统的“神经中枢“?——从安装部署到C++调用实战的完整流程,带你体验它的奥妙所在!​

文章目录 * 本篇摘要 * ①·RabbitMq(轻量级消息队列中间件) 介绍 * RabbitMQ 是什么? * 核心功能与特点 * 1. **核心功能** * 2. **核心优势** * RabbitMQ 的核心概念 * 1. **生产者(Producer)** * 2. **消费者(Consumer)** * 3. **队列(Queue)** * 4. **交换机(Exchange)** * 5. **绑定(Binding)** * 工作流程(以 Direct 交换机为例) * 常见应用场景 * RabbitMQ 与相关技术对比 * 图像理解 * 总结一句话 * ②·RabbitMq 安装教程 * RabbitMq安装 * **1. 安装 RabbitMQ** * **2. 启动 & 检查状态** * **3. 创建管理员用户(

By Ne0inhk
【STL】手撕 vector:从 0 到 1 模拟实现 STL 容器

【STL】手撕 vector:从 0 到 1 模拟实现 STL 容器

前言 STL 容器是 C++ 开发中绕不开的 “神兵利器”,而vector作为最常用的动态数组容器,更是新手入门 STL 的核心内容。但多数时候,我们只是 “会用”vector,却对它的底层逻辑一知半解 —— 比如它如何动态扩容?push_back的内存管理是怎样的?构造函数的匹配规则为何如此复杂? 与其停留在 “黑盒调用” 的层面,不如亲手模拟实现一个 vector:从底层的指针管理(_start/_finish/_endofstorage),到核心接口(push_back/resize/operator[]),再到构造、拷贝等特殊函数的实现,一步步揭开 STL 容器的面纱。 本文不会纠结过于晦涩的标准细节,而是以 “实用、易懂” 为核心,带你用 C++ 手动实现一个具备基础功能的vector—— 既能加深对容器原理的理解,也能锻炼 C++ 的底层编程能力。

By Ne0inhk
手把手实现 STL Set/Map:从零编写一棵红黑树到完整容器封装

手把手实现 STL Set/Map:从零编写一棵红黑树到完整容器封装

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 架构与实现:总览设计框架,深入源码细节 * 二. 核心设计思路:红黑树的泛型复用 * 2.1 红黑树的模板参数设计 * 2.2 仿函数 KeyOfT:统一 key 提取逻辑 * 2.3 核心约束:key 不可修改 * 三. 基础组件实现:红黑树与仿函数 * 3.1 红黑树节点结构 * 3.2 仿函数实现(map/set 层) * 3.2.1

By Ne0inhk