【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表

这篇具有很好参考价值的文章主要介绍了【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++

君兮_的个人主页

勤时当勉励 岁月不待人

C/C++ 游戏开发

Hello,米娜桑们,这里是君兮_,我们今天接着上回的单链表来讲讲带头双向循环链表,这种链表也是我们在实际应用中最常用的几种链表之一,学好这种链表是是非常重要的,我会尽量用通俗易懂的文字配合逻辑图来帮助更好的理解的
好了,废话不多说,开始今天的学习吧!

带头双向循环链表

  • 下面的是带头双向的循环链表逻辑图
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++

1.不同于单链表的特点

  • 1.双向:双向是指在带头双向循环链表的结构中,存在两个指针来链接链表,其中一个指针是指向前一个结点,另一个指针指向后一个结点。
  • 2.循环:单链表的尾部结点指向的是NULL,而双向循环链表的尾部结点指向头部的结点head,而head中指向前一个结点的指针则指向尾部结点(没理解了结合图看看哦)
  • 3.带头:带头是指在双向循环链表中存在一个哨兵位作为链表的头部(这里别急,先知道有这个东西就行,下面会重点解释的)

2.一个结点中存储的元素

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;//链表中存的值
	struct ListNode* next;//指向下一个结点的指针
	struct ListNode* prev;//指向上一个结点的指针
}ListNode;

3.初始化链表的函数 BuyListNode

//初始化双链表,创建节点
ListNode* BuyListNode(LTDataType x)
{
    ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));//malooc开辟一个结点的空间
    if (newnode == NULL)//判断一下是否开辟空间成功
    {
        perror("malloc failed\n");
        exit(-1);
    }
    //初始化时把两个指针都置空
    newnode->next = NULL;
    newnode->prev = NULL;
    newnode->data = x;
    return newnode;//返回创建好的链表节点
}
  • 这一步比较简单,看看注释相信代码非常容易理解就不过多解释了。

4.创建链表的头节点 ListCreate

// 创建返回链表的头结点.
ListNode* ListCreate()
{
    ListNode* phead = BuyListNode(0);//开辟结点所需动态空间
    phead->next = phead;
    phead->prev = phead;
    return phead;
    
}
  • 我们刚才介绍了链表的头是有一个哨兵位的,这个哨兵位在链表没有节点时,它中的头指针和尾指针都是指向自己的,那么这个哨兵位存在的意义是什么呢?

什么是哨兵位以及哨兵位存在的意义

  • 哨兵位是一种在数据结构中使用的特殊值或节点,它的作用是简化算法的实现并提高效率。在查找操作中,可以将哨兵位预置在适当的位置上,以避免边界越界和找不到的情况。比如在链表中,哨兵位用来定位链表的位置,即使是空链表也会有哨兵位。在插入排序中,哨兵位用于辅助移动数据的循环处理,它可以存储需要移动的数据,并且不会受到后续操作的覆盖。通过使用哨兵位,可以简化算法的实现,减少边界判断的次数,提高程序的执行效率
  • 在我们这个链表中具体来讲有什么意义呢?
  • 在我们使用链表时,如果没有这个哨兵位,为了防止出现越界访问和空链表的情况,我们每一次使用链表时都得进行判空操作,这样会很影响程序运行的效率。而当存在这个哨兵位,当这是个空链表时,此时的phead就是一个指向自己的结点,不会导致出错,而当链表中存在结点时,它就是一个指向头结点的结点,同时也方便了我们的循环,我们直接把尾结点的next指向这个phead就行。

5.打印链表的函数 ListPrint

// 双向链表打印
void ListPrint(ListNode* phead)
{
    assert(phead);//断言,判断是否有结点,
    printf("Phead");//没有其他结点,打印phead
    ListNode* cur = phead->next;     
    while (cur!= phead)
    {
        printf(" <==> %d", cur->data);
        cur = cur->next;

    }
    printf("\n");
}
  • 与之前单链表的打印基本相同,但是由于我们是循环链表,链表的尾部不再指向NULL,刚才我们说了此时链表的尾部的next指向phead,因此当我们遍历链表时,当链表再次等于phead时说明已经遍历过一次链表了,这就是我们遍历时循环停止的条件

6.销毁链表的函数 ListDestory

// 双向链表销毁
void ListDestory(ListNode* phead)
{
    assert(phead);

    ListNode* cur = phead->next;
    while (cur!=phead) 
    {
        ListNode* next = cur->next;
        free(cur);
        cur = next;
    }
    free(phead);
       
    
}
  • 我们遍历整个链表,把每个结点都释放掉即可,遍历链表的循环条件与上面打印链表的相同。
  • 好了,讲完上面的几个基础的接口,接下来才是重头戏,我们怎样往我们的链表中插入结点呢?

7.链表的头插与头删

  • 其实关于插入与删除这一块,如果你能理解之前单链表的插入与删除,这一块的逻辑其实非常好理解。

头插

// 双向链表头插
void ListPushFront(ListNode* phead, LTDataType x)
{
    assert(phead);//断言,
    
    ListNode* frist = BuyListNode(x);//创建节点
    ListNode* cur = phead->next;
    frist->next = cur;
    frist->prev = phead;
    cur->prev = frist;
    phead->next = frist;

}
  • 我们先通过图来看看这里想要达到的目标
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++
  • 首先,我们需要保存一下第一个位置的地址,也就是图中的cur,然后我们让first的next指向此时的cur,first的prev指向头head,这样就建立起了结点间的联系
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++
  • 但是由于我们需要链表是一个双头链表,我们此时还需要让head的next指向插入的first,cur的prev也指向first这样才算真正完成我们的头插
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++

头删

// 双向链表头删
void ListPopFront(ListNode* phead)
{
    assert(phead);
    ListNode* frist = phead->next;
   
    phead->next = frist->next;
    frist->next->prev = phead;
    free(frist);
}
  • 让phead的next指向first下一个结点,让first的下一个结点的prev指向phead,释放此时的free,头删完成
  • 与头插类似,大家把上面的逻辑图反着看即可,就不再浪费篇幅画图了。

8.链表的尾插和尾删

尾插

// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
    assert(phead);
    assert(phead->prev);
    ListNode* newnode = BuyListNode(x);
    //ListInsert(phead, x);
    ListNode* tail = phead->prev;//起初的尾
    newnode->next = phead;//插入的结点指向头
    tail->next = newnode;//之前的尾指向现在的尾
    newnode->prev = tail;
    tail = newnode;
   
}
  • 不同于单链表的是,由于我们的链表是循环链表,其实尾部的结点就保存在phead的prev中,因此不再需要遍历就能轻易的找到我们的尾部
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++
  • 此时,就比较简单啦,我们让现在的尾(d3)的next指向插入的newnode,让newnode的prev指向d3,next指向phead,再让phead的prev指向插入的newnode,即可完成尾插

【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++

尾删

// 双向链表尾删
void ListPopBack(ListNode* phead)
{
    assert(phead);
    assert(phead->prev);
    ListNode* tail = phead->prev;
    ListNode* tailprev = phead->prev->prev;
    tailprev->next = phead;
    tail = tailprev;
    free(phead->prev);  
   // ListErase(phead->prev);

}
  • 尾删需要我们找到现在尾的前一个结点,把它变成现在的尾,最后free掉现在的尾即可。
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++

9.查找某个值是否在链表中的函数 ListFind

// 双向链表查找
ListNode* ListFind(ListNode* phead, LTDataType x)
{
    assert(phead);
   
    ListNode* cur = phead;
    while (cur->next != phead)
    {
        if (cur->data == x)
        	return cur;
        
        else
         cur = cur->next;
    }
    
    return NULL;
}
  • 这个也非常简单,我们通过遍历链表的方式,通过比较结点中存储的值于我们需要查找的值来比较,找到了就返回当前结点位置的地址,没找到就返回NULL

10.修改pos前一个位置的值的函数 ListInsert

  • 在这里先简单提一嘴修改pos位置的值的函数,非常简单所以就不浪费篇幅了,我们通过上面的ListFind找到后,直接把此位置的data修改成我们需要的x即可
cur->data=x;
  • 进入正题
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
    
    
    ListNode* newnode = BuyListNode(x);//创建结点
    ListNode* posPrev = pos->prev;
    newnode->next = posPrev->next;
    posPrev->next = newnode;
    pos->prev = newnode;
    newnode->prev = posPrev;
    

}
  • 这里的pos位置是通过我们上面的查找函数找到的。
  • 找到后就很简单啦,我们把pos位置之前的结点posprev的next指向newnode,让newnode的prev指向posprev,再让newnode的next指向pos,最后pos的prev指向插入的newnode即可。
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++

11.删除pos位置的结点 ListErase

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
    assert(pos);
    ListNode* posPrev = pos->prev;
    ListNode* posNext = pos->next;
    posPrev->next = posNext;
    posNext->prev = posPrev;
    
    free(pos);
    //pos = NULL;
}

  • 这个也是非常容易理解的,我们让pos位置的前一位置的结点posPrev指向pos后一位置的结点posNext即可。
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++
    【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++
  • 最后一个小小的细节优化,当你在面试或者笔试等限定时间的时候,要求你完成一个带头双向循环链表,对于里面的头插头删,尾插尾删你也可以这样写,只需要引用上面这两个函数即可
//头插与头删
ListInsert(phead->next, x);
ListErase(phead->next);
//尾插与尾删
ListInsert(phead, x);
ListErase(phead->prev);
  • 想想看为啥
  • 其实我们的头插头删与尾插尾删无非是对特殊pos位置的操作嘛,因此自然可以引用上面的这两个函数啦

总结

  • 今天的内容到这里就结束了,带头双向循环链表可能不是那么好理解,如果你真的想要深入看懂的话一定要配合逻辑图自己尝试去敲一敲,光看不练是不可能学好的哦!!
  • 好了,如果你有任何疑问欢迎在评论区或者私信我提出,大家下次再见啦!

新人博主创作不易,如果感觉文章内容对你有所帮助的话不妨三连一下这个新人博主再走呗。你们的支持就是我更新的动力!!!

**(可莉请求你们三连支持一下博主!!!点击下方评论点赞收藏帮帮可莉吧)**

【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表,初阶数据结构,数据结构,链表,c语言,程序人生,c++文章来源地址https://www.toymoban.com/news/detail-633323.html

到了这里,关于【数据结构】庖丁解牛,图文结合带你轻松上手带头循环链表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • C生万物 | 操作符汇总大全【庖丁解牛,精细讲解】

    C生万物 | 操作符汇总大全【庖丁解牛,精细讲解】

    本篇博客全站热榜最高排名:2 因为MarkDown的语法,所以用图片的形式显示 对于算术操作符而言有上面这五种,对于前面的【+】、【-】、【*】来说操作数可以是整数或者浮点数 对于【/】来说,叫做 整除 ,结果就是我们在数学中说到的 商 。若是两边都是整数,则执行执行

    2023年04月08日
    浏览(13)
  • 【C++庖丁解牛】自平衡二叉搜索树--AVL树

    【C++庖丁解牛】自平衡二叉搜索树--AVL树

    🍁你好,我是 RO-BERRY 📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识 🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油 前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个共同点是:其底层都

    2024年04月09日
    浏览(17)
  • 【C++庖丁解牛】二叉搜索树(Binary Search Tree,BST)

    【C++庖丁解牛】二叉搜索树(Binary Search Tree,BST)

    🍁你好,我是 RO-BERRY 📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识 🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油 二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树: 若它的左子树不为空,

    2024年03月28日
    浏览(13)
  • 【C++庖丁解牛】vector容器的简易模拟实现(C++实现)(最后附源码)

    【C++庖丁解牛】vector容器的简易模拟实现(C++实现)(最后附源码)

    🍁你好,我是 RO-BERRY 📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识 🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油 我们前面介绍了vector容器的概念以及对其基本使用进行了介绍,如果你在这里不知道vector是什么以及不知

    2024年03月14日
    浏览(14)
  • 【C++庖丁解牛】C++内存管理 | new和delete的使用以及使用原理

    【C++庖丁解牛】C++内存管理 | new和delete的使用以及使用原理

    📙 作者简介 :RO-BERRY 📗 学习方向:致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识 📒 日后方向 : 偏向于CPP开发以及大数据方向,欢迎各位关注,谢谢各位的支持 我们先来看下面的一段代码和相关问题 选择题: 选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

    2024年03月09日
    浏览(11)
  • 【C++庖丁解牛】实现string容器的增删查改 | string容器的基本接口使用

    【C++庖丁解牛】实现string容器的增删查改 | string容器的基本接口使用

    🍁你好,我是 RO-BERRY 📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识 🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油 函数名称 功能说明 push_back 在字符串后尾插字符c append 在字符串后追加一个字符串 operator+= (重点) 在字符

    2024年03月14日
    浏览(19)
  • 【庖丁解牛】vue-element-admin前端CRUD通用操作组件详解,对,核心就是crud.js文件

    【庖丁解牛】vue-element-admin前端CRUD通用操作组件详解,对,核心就是crud.js文件

    vue-element-admin 框架之所以能够快速定制应用,得益于其通配的CRUD操作,属性配置多样化且个性化,能够满足绝大部分开发需求,也方便了代码生成。 可以深入学习重点源文件是: src/components/Crud/crud.js ,一共 863 行代码,以及下图中其它四个vue组件,形成了对通用CRUD操作的高

    2024年01月18日
    浏览(18)
  • 【C++庖丁解牛】STL之vector容器的介绍及使用 | vector迭代器的使用 | vector空间增长问题

    【C++庖丁解牛】STL之vector容器的介绍及使用 | vector迭代器的使用 | vector空间增长问题

    🍁你好,我是 RO-BERRY 📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识 🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油 vector的文档介绍 vector是表示可变大小数组的序列容器。 就像数组一样,vector也采用的连续存储空间来存

    2024年03月14日
    浏览(22)
  • 【C++庖丁解牛】面向对象的三大特性之一多态 | 抽象类 | 多态的原理 | 单继承和多继承关系中的虚函数表

    【C++庖丁解牛】面向对象的三大特性之一多态 | 抽象类 | 多态的原理 | 单继承和多继承关系中的虚函数表

    🍁你好,我是 RO-BERRY 📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识 🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油 需要声明的,本节课件中的代码及解释都是在vs2013下的x86程序中,涉及的指针都是4bytes。如果要其他平台

    2024年04月10日
    浏览(14)
  • 图文结合带你搞懂GreatSQL体系架构

    图文结合带你搞懂GreatSQL体系架构

    往期系列回顾 图文结合系列之带你搞懂MySQL日志系列 很多小伙伴使用了GreatSQL,但是对GreatSQL的底层原理还不是很了解,今天就带大家一起揭开GreatSQL体系架构的神秘面纱! 首先来回顾一张经典的体系架构图: 图1_GreatSQL5.7 版本体系架构图 由此可以发现,GreatSQL5.7 由以下几部

    2024年02月11日
    浏览(9)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包