数据结构体进阶链表【带头双向循环链表,单向链表的优化,从根部解决了顺序表的缺点】一文带你深入理解链表

news2025/7/20 20:56:03

 前言:

   对于链表,上一篇的单链表解决了顺序表的一部分缺陷,但并没有彻底的解决顺序表的问题,比如在进行单链表尾插尾删的时候还是需要进行遍历找尾,并没有达到全部的O(1),并且在头插的时候还要分情况来考虑,比如传入为空指针和不是空指针时候还要分情况考虑,对于指针的改变还要传二级指针,这对于一部分人来说并不熟悉,所以!!!今天看完这篇文章,掌握带双向循环数据表,让我们不再害怕链表的增删插改😎😎

     💞 💞    欢迎来到小马学习代码博客!!!!

                

 思维导图:

目录

一、链表实现前的准备 

💜1.1结构

图:

💜1.2初步的理解:

二、带头双向链表功能实现前的准备

🤎 2.1链表实现所需要的头文件:

🤎2.2链表实现的初始化:

🤎2.2链表实现的打印:

🤎2.3定义一个节点为了实现插入:

🤎2.4判断节点是否为空节点:

🤎2.5链表实现后的销毁:

3、链表功能的实现

❤️3.1链表的头插:

❤️3.2链表的尾插:

❤️3.3链表的头删:

❤️3.4链表的尾删:

❤️3.5链表的pos位插入:

❤️3.6链表的pos位删除:

❤️3.7链表的查找:

4、带头双向循环链表的源码

💚4.1 List.h

💚4.2List.c

💚4.3Test.c

总结:


一、链表实现前的准备 

💜1.1结构图:

~带头双向循环链表我们不要被他的结构图所吓到,只要我么深入理解他的原理,你会发现它的代码实现起来比单链表还要简单。

💜1.2初步的理解:

1.2.1 对于带头双向循环链表,要知道它的结构是比较复杂的,但是他的代码实现是比较简单的,所以,对于代码实现前,我想先讲述一下带头双向循环链表:对于带头双向循环链表我们先一步步理解,👉带头的意思就是在插入之前我们先初始化一个结构体,他并不是用来存储数据,而是保证链表有一个头节点,就可以减少讨论(当传入指针为空时,当传入指针不为空时),👉双向是指我们的结构体中存量结构体指针,一个是指向下一个,另一个是指向他的前一个,这样就保证在寻找的时候找不到前一个的位置。 👉循环是指在尾节点我们并不让他指向空指针,而是让他指向头节点,而头节点的前一个也不让他指向空指针,而是让他指向尾节点,这样就会达成一个循环的作用,他的好处就是在于我们们寻找尾节点的时候就不用遍历了,而是通过找头节点的前一个就能找到尾节点,从而达到时间复杂度为O(1)来完成尾插和尾删!

二、带头双向链表功能实现前的准备

🤎 2.1链表实现所需要的头文件:

#include <stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

💖  这里是害怕有的小伙伴有的头文件并不知道^ _ ^ 。

🤎2.2链表实现的初始化:

LTNode* ListInit() //返回结构体指针
{
    LTNode*guard=(LTNode*)malloc(sizeof(LTNode)); //这里就像定义一个哨兵,也就是头节点,就不用再担心头节点是不是NULL了
    if(guard==NULL) //确保能够正常扩容
    {
        perror("ListNodeInit");
    }
    guard->next=guard; //头节点的下一个先指向自己
    guard->prev=guard; //头节点的上一个也先指向自己
    return guard;
}

🤎2.2链表实现的打印:

void ListNodePrint(LTNode* phead)
{
    assert(phead);   //保证传入的不是空指针
    LTNode* cur =phead->next; //定一个结构体指针进行遍历打印
    printf("guard<=>");  //这里是为了打印效果好看一点
    while(cur!=phead)
    {
        printf("%d<=>",cur->data);
        cur=cur->next;
    }
    printf("\n");
}

🤎2.3定义一个节点为了实现插入:

LTNode*  BuyListNode(LTDataType x)
{
    LTNode* newnode=(LTNode*)malloc(sizeof(ListNode)); //节点申请空间
    if(newnode==NULL)
    {
        perror("BuyListNode");
    }
    newnode->data=x;  //节点所带的值
    newnode->prev=NULL;
    newnode->next=NULL;
    return newnode;
}

🤎2.4判断节点是否为空节点:

bool ListEmpty(LTNode*phead)
{
    assert(phead);
    return phead->next==phead; //如果头和头的下一个指向同一个则返回真,证明只有一个哨兵
}

🤎2.5链表实现后的销毁:

void ListDestroy(LTNode*phead)
{
    LTNode*cur=phead->next; //定义一个结构体指针进行遍历销毁
    while(cur!=phead)
    {
        LTNode*next=cur->next;
        free(cur);
        cur=next;
    }
    free(phead); //最后释放头节点
}

3、链表功能的实现

❤️3.1链表的头插:

void  ListPushFront(LTNode*phead,LTDataType x)
{
    assert(phead);
    LTNode* newnode=BuyListNode(x); //先定义一个节点进行头插
    newnode->next=phead->next;  //这里要考虑顺序问题,不然很容易出错,先让定义的next指向哨兵的下一个,然后哨兵下一个的prev指向我们新的节点
        //然后让我们我们哨兵的next指向新节点,新节点的prev指向哨兵, 这里如果不理解的可以画一下图
       //或者我么先定义一个节点把哨兵的下一个进行保存这里就不用考虑先后顺序了,大家可以试一试
    phead->next->prev=newnode;
    phead->next=newnode;
    newnode->prev=phead;
}

❤️3.2链表的尾插:

void ListPushBack(LTNode*phead,LTDataType x)
{
    assert(phead);
    LTNode* newnode=BuyListNode(x);  //定义一个新节点
    LTNode* tail=phead->prev;  //找到尾节点
    tail->next=newnode;  //然后就是交换结构体里面的next prev指向的位置来实现插入。头插我已经讲述过程了,主要是大家尝试一下画画图
    newnode->prev=tail;
    newnode->next=phead;
    phead->prev=newnode;
}

❤️3.3链表的头删:

void ListPopFront(LTNode*phead)
{
    assert(phead);
    assert(!ListEmpty(phead)); //这里防止只剩下一个哨兵,如果只剩下一个哨兵还进行删除就强制类型报错
    LTNode*first=phead->next;  //定义一个first指向哨兵的next
    LTNode* second=first->next; //在定义一个second指向first的next
    phead->next=second;
    second->prev=phead;
    free(first);  //把first删除后进行free
    first=NULL;  //first指针指向空,防止出现野指针
}

❤️3.4链表的尾删:

void ListPopBack(LTNode*phead)
{
    assert(phead);
    assert(!ListEmpty(phead));//这里防止只剩下一个哨兵,如果只剩下一个哨兵还进行删除就强制类型报错
    LTNode*tail=phead->prev;
    tail->prev->next=phead;
    phead->prev=tail->prev;
    free(tail);
    tail=NULL;
}

❤️3.5链表的pos位插入:

void ListInsert(LTNode*pos,LTDataType x)
{
    assert(pos); //保证传入的pos位不是空指针
    LTNode*prev=pos->prev; //找到pos位的前一位
    LTNode*newnode=BuyListNode(x);  //定义一个新节点进行插入
    pos->prev=newnode;
    newnode->next=pos;
    prev->next=newnode;
    newnode->prev=prev;
}

❤️3.6链表的pos位删除:

void ListErase(LTNode*pos)
{
    LTNode*prev=pos->prev;//找到pos位的前一个
    LTNode*next=pos->next; //找到pos的后一个
    prev->next=next;
    next->prev=prev;
    free(pos);  //释放pos
}

❤️3.7链表的查找:

LTNode* ListNodeFind(LTNode*phead,LTDataType x)  //查找后返回指针,如果找到返回找的到的结构体指针,没有找到返回空指针
{
    assert(phead);
    LTNode* cur=phead->next;  //定义一个结构体指针进行遍历寻找
    while(cur!=phead)
    {
        if(cur->data==x)
        {
            return cur;  //找到后返回的指针
        }
        cur=cur->next;  
    }
    return NULL;
}

4、带头双向循环链表的源码

💚4.1 List.h

#ifndef List_hpp
#define List_hpp

#include <stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
    struct ListNode*next;
    struct ListNode*prev;
    LTDataType data;
}LTNode;
LTNode* ListInit();
LTNode*  BuyListNode(LTDataType x);
void  ListPushBack(LTNode*phead,LTDataType x);
void ListNodePrint(LTNode* phead);
void  ListPushFront(LTNode*phead,LTDataType x);
void ListPopBack(LTNode*phead);
bool ListEmpty(LTNode*phead);
void ListPopFront(LTNode*phead);
size_t ListSize(LTNode*phead);
LTNode* ListNodeFind(LTNode*phead,LTDataType x);
void ListInsert(LTNode*pos,LTDataType x);
void ListErase(LTNode*pos);
void ListDestroy(LTNode*phead);

#endif /* List_hpp */

💚4.2List.c

#include "List.hpp"
LTNode* ListInit()
{
    LTNode*guard=(LTNode*)malloc(sizeof(LTNode)); //这里就像定义一个哨兵,也就是头节点,就不用再担心头节点是不是NULL了
    if(guard==NULL) //确保能够正常扩容
    {
        perror("ListNodeInit");
    }
    guard->next=guard; //头节点的下一个先指向自己
    guard->prev=guard; //头节点的上一个也先指向自己
    return guard;
}
LTNode*  BuyListNode(LTDataType x)
{
    LTNode* newnode=(LTNode*)malloc(sizeof(ListNode)); //节点申请空间
    if(newnode==NULL)
    {
        perror("BuyListNode");
    }
    newnode->data=x;  //节点所带的值
    newnode->prev=NULL;
    newnode->next=NULL;
    return newnode;
}
void ListNodePrint(LTNode* phead)
{
    assert(phead);   //保证传入的不是空指针
    LTNode* cur =phead->next; //定一个结构体指针进行遍历打印
    printf("guard<=>");  //这里是为了打印效果好看一点
    while(cur!=phead)
    {
        printf("%d<=>",cur->data);
        cur=cur->next;
    }
    printf("\n");
}
void ListPushBack(LTNode*phead,LTDataType x)
{
    assert(phead);
    LTNode* newnode=BuyListNode(x);  //定义一个新节点
    LTNode* tail=phead->prev;  //找到尾节点
    tail->next=newnode;  //然后就是交换结构体里面的next prev指向的位置来实现插入。头插我已经讲述过程了,主要是大家尝试一下画画图
    newnode->prev=tail;
    newnode->next=phead;
    phead->prev=newnode;
}
void  ListPushFront(LTNode*phead,LTDataType x)
{
    assert(phead);
    LTNode* newnode=BuyListNode(x); //先定义一个节点进行头插
    newnode->next=phead->next;  //这里要考虑顺序问题,不然很容易出错,先让定义的next指向哨兵的下一个,然后哨兵下一个的prev指向我们新的节点
        //然后让我们我们哨兵的next指向新节点,新节点的prev指向哨兵, 这里如果不理解的可以画一下图
       //或者我么先定义一个节点把哨兵的下一个进行保存这里就不用考虑先后顺序了,大家可以试一试
    phead->next->prev=newnode;
    phead->next=newnode;
    newnode->prev=phead;
}
bool ListEmpty(LTNode*phead)
{
    assert(phead);
    return phead->next==phead; //如果头和头的下一个指向同一个则返回真,证明只有一个哨兵
}
void ListPopBack(LTNode*phead)
{
    assert(phead);
    assert(!ListEmpty(phead));//这里防止只剩下一个哨兵,如果只剩下一个哨兵还进行删除就强制类型报错
    LTNode*tail=phead->prev;
    tail->prev->next=phead;
    phead->prev=tail->prev;
    free(tail);
    tail=NULL;
}
void ListPopFront(LTNode*phead)
{
    assert(phead);
    assert(!ListEmpty(phead)); //这里防止只剩下一个哨兵,如果只剩下一个哨兵还进行删除就强制类型报错
    LTNode*first=phead->next;  //定义一个first指向哨兵的next
    LTNode* second=first->next; //在定义一个second指向first的next
    phead->next=second;
    second->prev=phead;
    free(first);  //把first删除后进行free
    first=NULL;  //first指针指向空,防止出现野指针
}
size_t ListSize(LTNode*phead)
{
    assert(phead);
    LTNode*cur=phead->next;
    size_t n=0;
    while(cur!=phead)
    {
        ++n;
        cur=cur->next;
    }
    return n;
}
LTNode* ListNodeFind(LTNode*phead,LTDataType x)  //查找后返回指针,如果找到返回找的到的结构体指针,没有找到返回空指针
{
    assert(phead);
    LTNode* cur=phead->next;  //定义一个结构体指针进行遍历寻找
    while(cur!=phead)
    {
        if(cur->data==x)
        {
            return cur;  //找到后返回的指针
        }
        cur=cur->next;  
    }
    return NULL;
}
void ListInsert(LTNode*pos,LTDataType x)
{
    assert(pos); //保证传入的pos位不是空指针
    LTNode*prev=pos->prev; //找到pos位的前一位
    LTNode*newnode=BuyListNode(x);  //定义一个新节点进行插入
    pos->prev=newnode;
    newnode->next=pos;
    prev->next=newnode;
    newnode->prev=prev;
}
void ListErase(LTNode*pos)
{
    LTNode*prev=pos->prev;//找到pos位的前一个
    LTNode*next=pos->next; //找到pos的后一个
    prev->next=next;
    next->prev=prev;
    free(pos);  //释放pos
}
void ListDestroy(LTNode*phead)
{
    LTNode*cur=phead->next; //定义一个结构体指针进行遍历销毁
    while(cur!=phead)
    {
        LTNode*next=cur->next;
        free(cur);
        cur=next;
    }
    free(phead); //最后释放头节点
}

💚4.3Test.c

#include "List.hpp"
void test1()
{
    LTNode* plist=ListInit();
    ListPushBack(plist,1);
    ListPushBack(plist,2);
    ListPushBack(plist,3);
    ListPushBack(plist,4);
    ListPushBack(plist,5);
    ListPopBack(plist);
    ListPopBack(plist);
    ListNodePrint(plist);
    ListDestroy(plist);
    plist=NULL;
}
void test2()
{
    LTNode* plist=ListInit();
    ListPushFront(plist,1);
    ListPushFront(plist,2);
    ListPushFront(plist,3);
    ListPushFront(plist,4);
    ListPushFront(plist,5);
    ListPopFront(plist);
    ListPopFront(plist);
    size_t ret=ListSize(plist);
    printf("%zu\n",ret);
    ListNodePrint(plist);
    
}
int main()
{
    test1();
    test2();
    return 0;
}

总结:

     对于带头双向循环链表的实现我还是那句话,不要害怕!!!他只是结构体比较复杂,但是代码实现它确比较容易的,它不像顺序表那样要考虑是否要扩容,也不需要像单链表那样要考虑传入是否为空指针,所以他的实现还是比较容易的,而本文对于他的所有实现,我想小马应该写的很详细啦,如果有不懂的可以私信问我或者直接在评论区中问我啦!!

       再者,其实思考一下,双向循环链表pos插入删除时,当我们pos等于phead时,你会发现他就像尾插和尾删,发现就不需要我们写的那么复杂啦,只需要调用一下pos函数,使pos指向phead就行啦,这里我给大家讲一个思路,对于他的实现,大家下去可以尝试一下

  最后的最后,小马码文不易,如果觉得文章有帮助的话,就多多支持小马啦!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/17203.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【ASM】字节码操作 工具类与常用类 AdviceAdapter 介绍 打印方法进入 和 方法退出 的参数

文章目录 1.概述2. AdviceAdapter类2.1 class info2.2 fields2.3 constructors2. 4 methods2.案例2.1 打印方法参数3.总结1.概述 在上一篇文章:【ASM】字节码操作 工具类与常用类 Printer、ASMifier、Textifier 介绍 我们简单的介绍了 Printer、ASMifier、Textifier 三个类的…

idea2021版本创建一个javaweb项目(含额外知识--添加tomcat相关jar包)

前言&#xff1a; 建立一个javaweb项目需要用到JDK、idea、Tomcat 1. JDK是11版本的 2.IDEA是2021版本的 3.Tomcat是8.5版本的 这些下载在我的其他文章里面也有讲到下载和应用&#xff0c;大致都是殊途同归的 一、打开ided&#xff0c;按照以下步骤点击创建一个工程项目…

X11 Xlib截屏代码所遇问题及初步分析

综合了两篇博客中的例程并做一定修改&#xff0c;得到了基于X11 Xlib的截屏代码。 两篇博客链接分别如下&#xff1a; X11 截图与鼠标事件-SkyMei777-ChinaUnix博客 xlib实现截图报错-编程语言-CSDN问答 C代码如下&#xff1a; #include <stdio.h> #include <std…

[附源码]SSM计算机毕业设计自治小区物业设备维护管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

TK选品技巧 | 听说TikTok畅销品都是这样来的

9月16日消息&#xff0c;TikTok Shop发布东南亚99大促战报&#xff0c;此次战报数据包含了马来西亚、新加坡、菲律宾、越南和泰国五国销售数据&#xff0c;战报显示&#xff0c;此次东南亚99大促总GMV增幅达156%&#xff0c;总live GPM增幅达130%&#xff0c;订单量增幅达128%&…

基于springboot的社区疫情管理系统源码

1、项目介绍 基于springboot的社区疫情管理系统拥有三种角色&#xff1a;超级管理员、普通管理员、用户 超级管理员&#xff1a;管理员和用户管理、人员检测信息管理、外来人员报备管理、防疫须知管理、疫情用品类型管理、疫情用品管理、订单管理、评论管理、审核外来人员、新…

【ML-SVM案例学习】案例一:对鸢尾花数据进行SVM分类(附源码)

文章目录前言一、完整源码分步实现1.引入库2.读入数据3.编码数据4.数据分割5.数据SVM分类器构建6.计算模型的准确率/精度7.计算决策函数的结构值以及预测值8.画图总结前言 【ML-SVM案例学习】会有十种SVM案例&#xff0c;供大家用来学习。本章实现SVM鸢尾花数据的分类任务。 一…

PDF能编辑修改吗?教你必备的几种编辑方法

可以把PDF文件编辑修改吗&#xff1f;相信这是很多萌新在刚接触PDF文件时所产生的疑惑&#xff0c;因为在想要改动PDF文件的时候&#xff0c;发现无法直接编辑内容&#xff0c;就会有PDF文件可以编辑吗的疑问了&#xff0c;其实有这种想法非常正常&#xff0c;大家都是经历过的…

【设计模式】单例模式

单例模式属于创建型模式&#xff0c;是最简单的一种设计模式。当一个类在程序中只需要创建唯一全局对象时&#xff08;如网站计数类、日志管理类、线程池类……&#xff09;&#xff0c;就可以使用单例模式。单例模式规定一个类只能创建一个实例&#xff0c;之后不能再创建新的…

Verilog语法

Verilog语法 Verilog简介 Verilog是一种硬件描述语言&#xff0c;以文本形式来描述数字系统硬件的结构和行为的语言&#xff0c;用它可以表示逻辑电路图、逻辑表达式&#xff0c;还可以表示数字逻辑系统所完成的逻辑功能。 Verilog 和 C 的区别&#xff1a; Verilog是硬件描…

Linux篇【5】:Linux 进程概念(二)

目录 3.5、查看进程 3.6、通过系统调用接口获取时实进程的标识符 3.7、通过系统调用接口创建子进程 - fork 初识 3.5、查看进程 [HJMhjmlcc ~]$ clear [HJMhjmlcc ~]$ pwd /home/HJM [HJMhjmlcc ~]$ ls [HJMhjmlcc ~]$ touch mytest.c [HJMhjmlcc ~]$ ls mytest.c [HJMhjml…

G1D16-fraud-SVM

早上复习了一下昨天学的内容&#xff0c;发现这零碎时间用来复习&#xff0c;真的很不错。 但是遇到了一个问题&#xff1a;知识推理和知识挖掘有什么区别&#xff1f; 知识规则挖掘是对知识结构的挖掘&#xff0c;可以针对现有的知识体系&#xff0c; 利用部分规则&#xff0…

[附源码]java毕业设计冷链物流管理系统论文

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Redis数据类型之hash

文章目录hashⅠ. 增删查改Ⅱ. 加法操作Ⅲ. 业务场景1 - 购物车Ⅳ. 业务场景2 - 秒杀Ⅴ. 注意事项提示&#xff1a;以下是本篇文章正文内容&#xff0c;Redis系列学习将会持续更新 hash ● 新的存储需求&#xff1a;对一系列存储的数据进行编组&#xff0c;方便管理&#xff0c;…

基于PHP的Laravel框架实现学生管理系统(1+X Web前端开发中级 例题)——初稿

文章目录 &#x1f4c4;题目要求 &#x1f9e9;说明 &#x1f9e9;效果图 &#x1f4bb;题目代码 &#x1f3af;实现效果 &#x1f4f0;完整答案 &#x1f4c4;题目要求 阅读下列说明、效果图和代码&#xff0c;补全代码&#xff08;1&#xff09;-&#xff08;10&…

如何“0基础”备考CISSP一次通过?

2019年进入网络安全行业的PM&#xff0c;苦恼于行业与业务认知存在较大的知识差距&#xff0c;恰好公司一系列政策鼓励员工学习网安等专业知识&#xff0c;和报考相关专业认证&#xff0c;通过了解&#xff0c;最终决定「挑战」CISSP&#xff01; 选择CISSP的原因 CISSP八个域…

第2-3-6章 打包批量下载附件的接口开发-文件存储服务系统-nginx/fastDFS/minio/阿里云oss/七牛云oss

文章目录5.6 接口开发-根据文件id打包下载附件5.6.1 接口文档5.6.2 代码实现5.6.3 接口测试5.7 接口开发-根据业务类型/业务id打包下载5.7.1 接口文档5.7.2 代码实现5.7.3 接口测试5.6 接口开发-根据文件id打包下载附件 第2-1-2章 传统方式安装FastDFS-附FastDFS常用命令 第2-…

LeetCode——Weekly Contest 319

LeetCode周赛第319场记录 这场周赛的质量也很高&#xff0c;有很多值得学习的地方。 2469. 温度转换 这道题很简单&#xff0c;直接根据已有的信息转换即可&#xff0c;一行代码搞定&#xff0c;注意公式不要敲错。 class Solution { public:vector<double> convertTem…

【案例 5-2】 模拟默认密码自动生成

【案例介绍】 1.任务描述 本例要求编写一个程序&#xff0c;模拟默认密码的自动生成策略&#xff0c;手动输入用户名&#xff0c;根据用户名 自动生成默认密码。在生成密码时&#xff0c;将用户名反转即为默认的密码。 2.运行结果 运行结果如图 5-2 所示。 图 5-2 模拟密码自…

[附源码]java毕业设计企业公开招聘系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…