> 技术文档 > C/C++数据结构 — 单链表_c++单链表

C/C++数据结构 — 单链表_c++单链表

在这里插入图片描述
在这里插入图片描述

🎁个人主页:工藤新一¹

🔍系列专栏:C++面向对象(类和对象篇)

​ 🌟心中的天空之城,终会照亮我前方的路

🎉欢迎大家点赞👍评论📝收藏⭐文章

文章目录

  • 链表
    • 一、对于顺序表的思考
    • 二、单链表
      • 2.1概念与结构
      • 2.2节点
      • 2.3链表结构的创建
        • 2.3.1值传递与地址传递的讨论
      • 2.4尾插
        • 2.4.1值传递
        • 2.4.2**指针也是一个变量**
        • 2.4.3指针与二级指针的关系
      • 2.5头插 + 尾删 + 头删
      • 2.6*查找
      • 2.7在指定位置插入元素
        • 2.7.1在指定位置前插入元素
        • 2.7.2在指定位置后插入元素
      • 2.8删除操作
        • 2.8.1删除指定节点
        • 2.8.2删除指定节点后的节点
      • 2.9销毁链表
    • 3链表实现

单链表

一、对于顺序表的思考

  • 尾插时间复杂度:O(1);中间/头部的插入或删除,时间复杂度:O(N)
  • 增容需要申请新空间,拷贝旧数据,释放旧空间,这势必会遭成不小的消耗
  • 增容一般是呈2倍的增长,势必会有一定空间的浪费

思考,如何解决一下问题:

  • 头插时间复杂度是否可以变为O(1)
  • 是否可以省去增容操作就可以存储数据?并且不会浪费空间?

链表:不存在空间浪费的情况

顺序表 更适尾部的插入/删除:O(1)

链表 更适合头部的插入/删除:O(1)


二、单链表

  • 基于以上思考,我们便有了 链表 的概念
  • 单链表头部操作的时间复杂度:O(1)
  • 顺序表尾部操作的时间复杂度:O(1)

2.1概念与结构

  • 单链表是一种常见的线性数据结构,它由一系列节点组成,每个节点包含两部分:数据域和指针域

  • 概念:链表是一种 物理存储结构上非连续、非顺序的存储结构,数据元素的 “逻辑顺序”是通过链表中的 指针链接次序 实现的

  • 逻辑结构:线性的

  • 物理结构:非线性的(严谨一点:不一定…,因为系统有可能会随机申请到一块连续的内存)(链表由一个一个的节点组成,每个节点所向操作系统申请的空间都是独立的)>

C/C++数据结构 — 单链表_c++单链表


2.2节点

C/C++数据结构 — 单链表_c++单链表

  • 链表是由一系列节点(车厢)组成的,其中节点包括:数据域(存储当前节点的数据)、指针域(存储下一节点的地址),同时每个节点也都是一个独立的空间
  • 删除指定位置后的节点,时间复杂度:O(1)

2.3链表结构的创建

  • SList.h文件
#pragma once#include#include#include#includetypedef int SLDataType;//将存储数据(data)的类型都变为SLDataType,方便替换//定义链表的结构 -- 结点的结构(结点:数据域 + 指针域)typedef struct SListNode{SLDataType data;//存储的数据,数据域 struct SListNode* next;//指向下一个结点,指针域}SLTNode;--->结构体指针/*typedef int SLTDateType;typedef struct SListNode{ SLTDateType data; struct SListNode* next; SListNode(int val) : data(val), next(nullptr) {}}SLTNode;*///链表的输出void SLTPrint(SLTNode* phead);//头节点
  • test.cpp 链表结点的定义
#include\"SList.h\"void test01(){SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));node1->data = 1;node2->data = 2;node3->data = 3;node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;SLTNode* plist = node1;//定义车头,plist指向 node1 SLTPrint(plist);//调用 SLTPrint 输出链表}int main(){test01();return 0;}
  • SList.cpp文件,链表的输出实现
#include\"SList.h\"void SLTPrint(SLTNode* phead)//phead 存储的值就是地址(值传递){ SLTNode* pcur = phead;//使 phead 始终指向头结点,方便再次遍历时,可以始终从左至右进行遍历循环 while(pcur)//pcur != NULL; { printf(\"%d -> \", pcur->data); pcur = pcur->next; } printf(\"NULL\\n\");}

2.3.1值传递与地址传递的讨论

C/C++数据结构 — 单链表_c++单链表


2.4尾插

  • 申请节点 – 调用 BuyNode 方法 .cpp
C //通过 BuyNode 方法创建新节点SLTNode* SLTBuyNode(SLDataType x){//根据 x 创建节点SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); if (newnode == NULL){perror(\"malloc fail!\");exit(1);}newnode->data = x;//为新节点初始化数据域newnode->next = NULL;//初始化指针域return newnode;//返回是申请好的 newnode} 
  • 尾插.cpp
C void SLTPushBack(SLTNode* phead, SLDataType x){ SLTNode* newnode = SLTBuyNode(x); if(phead == NULL)//链表为空 { phead = newnode;//那么newnode就是头节点 } //链表不为空 else{ SLTNode* ptail = phead; while(ptail->next) { ptail = ptail->next; } //找到了尾节点 ptail->next = newnode; }}
  • 创建链表.test
Cvoid test02(){//创建空链表SLTNode* plist = NULL;SLTPrint(plist); SLTPushBack(plist, 1);SLTPrint(plist);}
  • 输出结果

C/C++数据结构 — 单链表_c++单链表


调试结果:
C/C++数据结构 — 单链表_c++单链表

在调试窗口中,我们发现,newnode正确的调用 BuyNode函数(正确被初始化),但代码调用结束后,plist并未发生任何变化,无法读取 data、next 数据,因此,这就发生了非常常见的问题:“形参是实参的一份临时拷贝,值传递 - 形参无法影响实参”


方法一:一级指针引用
C/C++数据结构 — 单链表_c++单链表


方法二:二级指针
C/C++数据结构 — 单链表_c++单链表


方法三:返回指针


2.4.1值传递

C/C++数据结构 — 单链表_c++单链表


  • 得出结论

C/C++数据结构 — 单链表_c++单链表

因此,我们便明白了问题所在 —— 值传递,形参只是实参的一份临时拷贝,形参无法改变实参


2.4.2指针也是一个变量

C/C++数据结构 — 单链表_c++单链表


2.4.3指针与二级指针的关系
  • 二级指针接收一级指针的地址
Ctest.cpp SLTPushBack(&plist, 1);//使用二级指针来接收一级指针的地址SList.h void SLTPushBack(SLTNode** pphead, SLDataType x);SList.cpp void SLTPushBack(SLTNode** pphead, SLDataType x){}
C SList.cpp void SLTPushBack(SLTNode** pphead, SLDataType x){SLTNode* newnode = SLTBuyNode(x);//直接调用 BuyNode 方法 //对二级指针 --->(解引用)---> 一级指针if (*pphead == NULL){*pphead = newnode;}else {SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}ptail->next = newnode;}}

C/C++数据结构 — 单链表_c++单链表

C/C++数据结构 — 单链表_c++单链表


  • 调试
C void test02(){//创建空链表SLTNode* plist = NULL; //为使形参改变实参,因此需要地址传递SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);}

C/C++数据结构 — 单链表_c++单链表


2.5头插 + 尾删 + 头删

  • .cpp文件
C//头插 - 使形参可改变实参(因此,需要地址传递)void SLTFrontBack(SLTNode** pphead, SLDataType x){assert(pphead);SLTNode* newnode = SLTBuyNode(x); newnode->next = *pphead;*pphead = newnode;}//尾删 - 使形参可改变实参(因此,需要地址传递)void SLTPopBack(SLTNode** pphead){//pphead传参、*pphead链表首节点不能为空assert(pphead && *pphead);//-> 的优先级 > * 操作if ((*pphead)->next == NULL)//当节点只有 1 个时{ //直接释放头结点的地址free(*pphead);*pphead = NULL;}else {SLTNode* prev = NULL;//前驱节点SLTNode* ptail = *pphead;while (ptail->next){//使前驱节点指向当前节点prev = ptail;//使当前节点指向下一个节点ptail = ptail->next;}prev->next = NULL;free(ptail);ptail = NULL;}}//头删void SLTPopFront(SLTNode** pphead){assert(pphead && *pphead);SLTNode* next = (*pphead)->next;free(*pphead);//释放(*pphead)所指向的数据*pphead = next;//使(*pphead)指向 next 节点}void SLTFrontPop(SLTNode** pphead){ // 空链表 if(!*pphead) return; SLTNode* pcur = *pphead; *pphead = (*pphead)->next; delete pcur;}

2.6*查找

  • test.cpp
C void test02(){ int position = -1;//地址传递:&position,使形参改变实参SLTNode* find = SLTFind(plist, 10, &position);//id = 0 开始if (find) {printf(\"找到了!位置:%d\\n\", position);printf(\"节点数据:%d\\n\", find->data);}else {printf(\"未找到!\\n\");}}

  • SList.cpp
C //查找SLTNode* SLTFind(SLTNode* phead, SLDataType x, int* position){SLTNode* pcur = phead;int pos = 0; // 用于记录当前节点的位置while (pcur){if (pcur->data == x){ *position = pos; return pcur;}pcur = pcur->next;pos++;}return NULL;}

2.7在指定位置插入元素

2.7.1在指定位置前插入元素

时间复杂度:O(N)

  • test.cpp
C SLTInsert(&plist, find, 100);SLTPrint(plist);

  • SList.cpp
  • 空指针异常
C //指定位置前插入数据void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x){assert(pphead && pos);SLTNode* prev = *pphead;SLTNode* newnode = SLTBuyNode(x);//如果 pos 刚好指向头结点,就会出现 \"空指针异常\"//因此处理 当 pos 指向头结点 - 直接头插if (pos == *pphead){SLTPushFront(pphead, x);}else {while (prev->next != pos){prev = prev->next;}//prev--> newnode--> pos //前驱节点的后继指针指向newnode节点prev->next = newnode;newnode->next = pos;}} 

C/C++数据结构 — 单链表_c++单链表


2.7.2在指定位置后插入元素

时间复杂度:O(1)

  • 在指定位置后插入元素,是没有空指针异常的
C //在指定位置后插入数据void SLTInsertAfter(SLTNode* pos, SLDataType x){assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode; }

2.8删除操作

2.8.1删除指定节点

时间复杂度:O(N)

  • 处理空指针异常
C //删除 pos 位置上的节点void SLTErase(SLTNode** pphead, SLTNode* pos){assert(pphead && pos);SLTNode* prev = *pphead;//处理 \"空指针\" 异常 - pos 是首节点if (pos == *pphead){SLTPopFront(pphead);}else {while (prev->next != pos){prev = prev->next;}//prev pos pos->nextprev->next = pos->next;free(pos);pos = NULL;}}

2.8.2删除指定节点后的节点

时间复杂度:O(1)

Cvoid SLTEraseAfter(SLTNode* pos) {//保证 pos->next 不为空,避免 free(NULL) 操作 - 断言报错assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;}

2.9销毁链表

  • 前面我们有提到过,顺序表、链表的底层是数组,所以有过 free(ps -> arr) 操作(因为数组的存储结构是连续的,因此free(ps -> arr) 会将数组中所以的数据都释放掉)
  • 但链表中,其每个节点都是独立申请的,释放其中某一节点,不会影响其他节点正常运行
C //销毁链表 pphead - 指向链表节点的起始地址void SListDestory(SLTNode** pphead){assert(pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* dest = pcur->next;//存储下一个要释放的目标free(pcur);//释放的是 pcur 所指向的数据pcur = dest;}*pphead = NULL;//避免 *pphead 成为野指针,手动置空}

3链表实现

  • SList.h
#pragma once#include#include#include#includetypedef int SLDataType;//将存储的数据类型都变为SLDataType,方便替换//定义链表的结构 -- 结点的结构(结点:数据域 + 指针域)typedef struct SListNode{SLDataType data;//存储的数据,数据域struct SListNode* next;//指向下一个结点,指针域}SLTNode;//链表的输出 void SLTPrint(SLTNode* phead);//头节点//尾插void SLTPushBack(SLTNode** pphead, SLDataType x);//头插void SLTPushFront(SLTNode** pphead, SLDataType x);//尾删void SLTPopBack(SLTNode** pphead);void SLTPopFront(SLTNode** pphead);SLTNode* SLTFind(SLTNode* phead, SLDataType x, int* position);//指定位置前插入数据void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);//指定位置后插入数据void SLTInsertAfter(SLTNode* pos, SLDataType x);//删除 pos 位置上的节点void SLTErase(SLTNode** pphead, SLTNode* pos);//删除 pos 位置后的节点void SLTEraseAfter(SLTNode* pos);//销毁链表 pphead - 指向链表节点的起始地址void SListDestory(SLTNode** pphead);

  • SList.test
#include\"SList.h\"void test01(){SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));node1->data = 1;node2->data = 2;node3->data = 3;node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;SLTNode* plist = node1;//定义车头SLTPrint(plist);}void test02(){//创建空链表SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);SLTPushFront(&plist, 10);SLTPushFront(&plist, 20);SLTPushFront(&plist, 30);SLTPushFront(&plist, 40);SLTPrint(plist);//为使形参改变实参,因此需要地址传递SLTPopBack(&plist);SLTPopBack(&plist);SLTPopBack(&plist);SLTPrint(plist);int position = -1;//地址传递:&position,使形参改变实参SLTNode* find = SLTFind(plist, 40, &position);//id = 0 开始if (find) {printf(\"找到了!位置:%d\\n\", position);printf(\"节点数据:%d\\n\", find->data);}else {printf(\"未找到!\\n\");}//此时,find = 0;SLTInsert(&plist, find, 100);SLTPrint(plist);//此时,find = 1;(查找40)SLTErase(&plist, find);SLTPrint(plist);SListDestory(&plist);}int main(){//test01();test02();return 0;}

  • SList.cpp
#include\"SList.h\"void SLTPrint(SLTNode* phead){SLTNode* pcur = phead;while (pcur){printf(\"%d -> \", pcur->data);pcur = pcur->next;}printf(\"NULL\\n\");}//申请节点 - 调用 BuyNode方法SLTNode* SLTBuyNode(SLDataType x){//根据 x 创建节点SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror(\"malloc fail!\");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;}//尾插void SLTPushBack(SLTNode** pphead, SLDataType x){assert(pphead);SLTNode* newnode = SLTBuyNode(x);//直接调用 BuyNode 方法//链表为空if (*pphead == NULL){*pphead = newnode;}/*注意区分:如果链表为空(phead == NULL),访问 phead->next 会导致未定义行为(如段错误)。因此,不能直接使用:if(phead->next == NULL)因为 phead 所存储的本就是一段地址,phead->next是指所存储的下一节点的地址因此,会有 phead 本身就为空的情况*/ else {//找尾节点SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}ptail->next = newnode;}}//头插 - 使形参可改变实参(因此,需要地址传递)void SLTPushFront(SLTNode** pphead, SLDataType x){assert(pphead);SLTNode* newnode = SLTBuyNode(x); newnode->next = *pphead;*pphead = newnode;}//尾删 - 使形参可改变实参(因此,需要地址传递)void SLTPopBack(SLTNode** pphead){//pphead传参、*pphead链表首节点不能为空assert(pphead && *pphead);//-> 的优先级 > * 操作if ((*pphead)->next == NULL)//当节点只有 1{free(*pphead);*pphead = NULL;}else {SLTNode* prev = NULL;//前驱节点SLTNode* ptail = *pphead;while (ptail->next){//使前驱节点指向当前节点prev = ptail;//使当前节点指向下一个节点ptail = ptail->next;}prev->next = NULL;free(ptail);ptail = NULL;}}//头删void SLTPopFront(SLTNode** pphead){assert(pphead && *pphead);SLTNode* next = (*pphead)->next;free(*pphead);//释放(*pphead)所指向的数据*pphead = next;//使(*pphead)指向 next 节点}//查找SLTNode* SLTFind(SLTNode* phead, SLDataType x, int* position){SLTNode* pcur = phead;int pos = 0; // 用于记录当前节点的位置while (pcur){if (pcur->data == x){ *position = pos; return pcur;}pcur = pcur->next;pos++;}return NULL;}//指定位置前插入数据void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x){assert(pphead && pos);SLTNode* prev = *pphead;SLTNode* newnode = SLTBuyNode(x);//如果 pos 刚好指向头结点,就会出现 \"空指针异常\"//因此处理 当 pos 指向头结点 - 直接头插if (pos == *pphead){SLTPushFront(pphead, x);}else {while (prev->next != pos){prev = prev->next;}//prev--> newnode--> pos//前驱节点的后继指针指向newnode节点prev->next = newnode;newnode->next = pos;}} //在指定位置后插入数据void SLTInsertAfter(SLTNode* pos, SLDataType x){assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode; }//删除 pos 位置上的节点void SLTErase(SLTNode** pphead, SLTNode* pos){assert(pphead && pos);SLTNode* prev = *pphead;//处理 \"空指针\" 异常 - pos 是首节点if (pos == *pphead){SLTPopFront(pphead);}else {while (prev->next != pos){prev = prev->next;}//prev pos pos->nextprev->next = pos->next;free(pos);pos = NULL;}}void SLTEraseAfter(SLTNode* pos) {//保证 pos->next 不为空,避免 free(NULL) 操作 - 断言报错assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;}//销毁链表 pphead - 指向链表节点的起始地址void SListDestory(SLTNode** pphead){assert(pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* dest = pcur->next;//存储下一个要释放的目标free(pcur);//释放的是 pcur 所指向的数据pcur = dest;}*pphead = NULL;//避免 *pphead 成为野指针,手动置空}
  • 由此,我们便可发现,单链表对头部操作的时间复杂度:O(1)
  • 顺序表对尾部操作的时间复杂度:O(1)

C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


防范悬垂指针:
C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


删除链表中的指定元素

C/C++数据结构 — 单链表_c++单链表
C/C++数据结构 — 单链表_c++单链表
C/C++数据结构 — 单链表_c++单链表


翻转链表

C/C++数据结构 — 单链表_c++单链表
C/C++数据结构 — 单链表_c++单链表


C/C++数据结构 — 单链表_c++单链表


🌟 各位看官好,我是工藤新一¹呀~

🌈 愿各位心中所想,终有所致!
在这里插入图片描述