链表界的“扛把子”—双向带头循环链表
目录
一:链表的分类
二:链表的实现
2-1:创建链表
2-2:创建新的结点
2-3:初始化链表
2-4:打印链表
2-5:销毁链表
三:链表的核心功能
3-1:尾插
3-2:尾删
3-3:链表查找
3-4:在pos结点前插入数据
3-5:头插
3-6:删除pos位置结点
3-7:头删
四:源码
一:链表的分类
链表的分类分为以下几种:
在实际中链表的结构非常多样,以上情况组合起来就有8种链表结构。我们在前面已经讲过最简单的结构:单向无头非循环链表。并且在我们所刷的OJ题里也涉及到了有无头结点情况,今天我们讲解最复杂结构:双向带头循环链表。在我们讲解之前我们先了解一下这两种结构在链表界的重要性。
1:无头单向非循环链表:
结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2:带头双向循环链表:
结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
正文开始
二:链表的实现
2-1:创建链表
思路:
因为链表为双向链表,所以在我们创建的结构体中需要创建两个结构体指针prev和next,其中prev指向上一个结点,next指向下一个结点。
Lish.h文件
typedef int LTDataType; //重定义方便数据类型的转换,这里以int为主 typedef struct ListNode {LTDataType data; //存储数据struct ListNode* next; //存储下一个结点的地址struct ListNode* prev; //存储上一个结点的地址 }LTNode;
2-2:创建新的结点
Lish.h文件
//创建一个结点 LTNode* BuyLTNode(LTDataType x);
List.c文件
//创建一个结点 LTNode* BuyLTNode(LTDataType x) {LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL) //检查内存是否开辟成功{printf("malloc fail\n");exit(-1);}newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode; }
2-3:初始化链表
思路1:
我们所创建的链表为带头链表,所以我们在初始化链表的时候就需要把哨兵位头结点创建好,哨兵位头结点不存储数据,但却有着至关重要的作用,当链表为空链表时哨兵位头结点仍存在。同时形参的改变并不会影响实参,所以我们在传参的时候需要传二级指针,因为保存哨兵位头结点地址的地址不可能为空,所以我们需要用assert进行断言,同时因为是双向链表,所以哨兵位结点的prev和next都指向自己。
⭐:对于不可能出现空指针的情况,我们需要断言。
Lish.h文件
//初始化链表(二级指针) void ListInit(LTNode** pphead);
List.c文件
//初始化链表(二级指针) void ListInit(LTNode** pphead) {assert(pphead); //pphead不可能为空指针*pphead = BuyLTNode(0);//创建一个结点给哨兵位头结点//哨兵位头结点初始化next和prev不能为空,需都指向自己(*pphead)->next = *pphead;//注意优先级问题,需要加括号。(*pphead)->prev = *pphead; }
思路2:
如果我们想传一级指针也可以,只不过函数的返回值为结构体指针。
Lish.h文件
//初始化链表(一级指针) LTNode* ListInit();
List.c文件
//初始化链表(一级指针) LTNode* ListInit() {LTNode*phead = BuyLTNode(0);phead->prev = phead;phead->next = phead;return phead; }
2-4:打印链表
思路:
首先需要明确的是哨兵位结点不存储数据,所以我们要打印链表数据就需要创建一个指针变量cur从phead->next开始依次遍历打印链表,因为链表是循环的,所以当cur回到phead时停止。
Lish.h文件
//打印数据 void ListPrint(LTNode* phead);
List.c文件
//打印数据 void ListPrint(LTNode* phead) {assert(phead);LTNode* cur = phead->next;//cur从phead->next开始while (cur != phead)//cur回到phead时停止{printf("%d ", cur->data);cur = cur->next;}printf("\n"); }
2-5:销毁链表
思路:
销毁链表我们可以创建指针变量cur遍历链表,先把有效数据的结点free掉,最后再把哨兵位结点free掉即可。
Lish.h文件
//销毁链表 void ListDestory(LTNode* phead);
List.c文件
//销毁链表 void ListDestory(LTNode* phead) {assert(phead);LTNode* cur = phead->next;while (cur != phead)//从第一个结点开始依次销毁{LTNode* next = cur->next;free(cur);cur = next;}//销毁哨兵结点free(phead);phead = NULL; }
看到这里是不是觉得双链表也就那样,并没有想象中的那么难,前面已经理解的话我们继续往下走,不理解的话多看几遍多敲敲代码,说不定过一会就理解了~~
三:链表的核心功能
3-1:尾插
思路:
尾插就体现了双向链表的结构优势,因为哨兵位头节点的phead的prev指向的就是链表尾结点的地址,所以我们找到了尾结点后就需要再创建一个结点存储插入数据。
Lish.h文件
//尾插一个结点 void ListPushBack(LTNode* phead, LTDataType x);
List.c文件
//尾插一个结点 void ListPushBack(LTNode* phead, LTDataType x) {assert(phead);LTNode* tail = phead->prev;//保存尾结点地址LTNode* newnode = BuyLTNode(x);//创建一个新的结点//循环链表tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode; }
Test.c文件
void TestList1() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPrint(pList); }
3-2:尾删
思路:
当我们谈到尾删时,双向链表的结构优势再次体现出来。对于传统的单向链表如果想要实现尾删就必须遍历链表,找到尾结点将其free掉,遍历链表时间复杂度为O(N)。而对于双向链表来说因为链表是循环的,所以链表尾结点的地址时刻存在哨兵位结点的prev里面,时间复杂度为O(1),我们可以创建指针变量tail保存尾结点地址。同时删除尾结点的同时我们也要保证尾结点的上一个结点地址被保存,所以我们创建指针变量tailPrev指向tail的prev,然后把tailPrev的next指向哨兵位结点phead,把哨兵位phead的prev置成tailPrev。
Lish.h文件
//尾删一个结点 void ListPopBack(LTNode* phead);
List.c文件
//尾删一个结点 void ListPopBack(LTNode* phead) {assert(phead);//哨兵位不能为空assert(phead->next != phead);//链表不能为空,防止删除哨兵位结点LTNode* tail = phead->prev;//尾结点放入tailLTNode* tailPrev = tail->prev;//新的尾结点tailPrevfree(tail);tail = NULL;//循环链表tailPrev->next = phead;phead->prev = tailPrev; }
Test.c文件
void TestList2() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPrint(pList);ListPopBack(pList);ListPopBack(pList);ListPopBack(pList);ListPrint(pList); }
3-3:链表查找
思路:
创建一个指针变量cur指向哨兵位phead的next,然后开始遍历链表,判断cur->data是否为查找的数据,如果是就返回cur,不是继续遍历(cur=->cur->next),终止条件是cur指向哨兵位结点。
Lish.h文件
//链表查找 LTNode* ListFind(LTNode* phead, LTDataType x);
List.c文件
//链表查找 LTNode* ListFind(LTNode* phead, LTDataType x) {assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL; }
3-4:在pos结点前插入数据
思路:
用上一个查找数据函数查找pos,然后在pos之前插入数据:创建一个新的结点newnode,pos上一个结点的next指向新的结点newnode,同时把newnode的prev指向pos上一个结点,newnode的next指向pos,pos的prev指向newnode。
Lish.h文件
//在pos前插入数据 void ListInsert(LTNode* pos, LTDataType x);
List.c文件
//在pos前插入数据 void ListInsert(LTNode* pos, LTDataType x) {assert(pos);//创建插入结点LTNode* newnode = BuyLTNode(x);//存储pos上一个结点地址LTNode* posPrev = pos->prev;//循环链表newnode->next = pos;pos->prev = newnode;posPrev->next = newnode;newnode->prev = posPrev; }
Test.c文件
void TestList3() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPrint(pList);LTNode* pos = ListFind(pList, 3);if (pos)//判断链表中是否有要查找的元素{ListInsert(pos, 30);//在数字3前插入30}ListPrint(pList); }
⭐:有了这个函数那么尾插也就可以更改一下,直接调用这个函数。
//尾插一个结点 void ListPushBack(LTNode* phead, LTDataType x) {assert(phead);ListInsert(phead, x); }
3-5:头插
思路:
同样的,头插我们也可以引用ListInsert函数,当pos=phead->next时,我们实现的就是头插。
Lish.h文件
//头插 void ListPushFront(LTNode* phead, LTDataType x);
List.c文件
//头插 void ListPushFront(LTNode* phead, LTDataType x) {assert(phead);ListInsert(phead->next, x); }
Test.c文件
void TestList4() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPrint(pList);//头插10和20ListPushFront(pList, 10);ListPushFront(pList, 20);ListPrint(pList); }
3-6:删除pos位置结点
思路:
创建两个指针变量prev和next分别保存pos上一个结点和下一个结点的地址,然后将pos结点free掉,连接prev和next。
Lish.h文件
//删除pos处结点 void ListErase(LTNode* pos);
List.c文件
//删除pos处结点 void ListErase(LTNode* pos) {assert(pos);//保存pos前一个结点LTNode* prev = pos->prev;//保存pos下一个结点LTNode* next = pos->next;free(pos);pos = NULL;//连接链表prev->next = next;next->prev->prev; }
Test.c文件
void TestList5() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPrint(pList);LTNode* pos = ListFind(pList, 3);if (pos)//判断链表中是否有要查找的元素{ListErase(pos);//删除pos位结点pos=NULL//防止野指针}ListPrint(pList); }
⭐:当我们有了ListErase函数后,直接调用此函数就能实现尾删。
void ListPopBack(LTNode* phead) {assert(phead);assert(phead->next != phead);ListErase(phead->prev); }
3-7:头删
思路:
直接调用ListErase函数,删除head->next位置结点。
Lish.h文件
//头删 void ListPopFront(LTNode* phead);
List.c文件
//头删 void ListPopFront(LTNode* phead) {assert(phead);assert(phead->next != phead);ListErase(phead->next); }
Test.c文件
void TestList6() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPushBack(pList, 5);ListPushBack(pList, 6);ListPrint(pList);//头插三个元素ListPushFront(pList, 0);ListPushFront(pList, -1);ListPushFront(pList, -2);ListPrint(pList);//尾删三个元素ListPopBack(pList);ListPopBack(pList);ListPopBack(pList);ListPrint(pList);//头删三个元素ListPopFront(pList);ListPopFront(pList);ListPopFront(pList);ListPrint(pList); }
接口终于都实现完了~
四:源码
List.h文件
#pragma once#include <stdio.h> #include <assert.h> #include <stdlib.h>typedef int LTDataType; //重定义方便数据类型的转换,这里以int为主 typedef struct ListNode {LTDataType data; //存储数据struct ListNode* next; //存储下一个结点的地址struct ListNode* prev; //存储上一个结点的地址 }LTNode; //初始化链表(二级指针) void ListInit(LTNode** pphead); //初始化链表(一级指针) //LTNode* ListInit();//销毁链表 void ListDestory(LTNode* phead);//打印数据 void ListPrint(LTNode* phead);//创建一个结点 LTNode* BuyLTNode(LTDataType x);//尾插一个结点 void ListPushBack(LTNode* phead, LTDataType x);//尾删一个结点 void ListPopBack(LTNode* phead);//头插 void ListPushFront(LTNode* phead, LTDataType x);//头删 void ListPopFront(LTNode* phead);//链表查找 LTNode* ListFind(LTNode* phead, LTDataType x);//在pos前插入数据 void ListInsert(LTNode* pos, LTDataType x);//删除pos处结点 void ListErase(LTNode* pos);
List.c文件
#include "List.h"//初始化链表(二级指针) void ListInit(LTNode** pphead) {assert(pphead);*pphead = BuyLTNode(0);//创建一个结点给哨兵位头结点//哨兵位头结点初始化next和prev不能为空,需都指向自己(*pphead)->next = *pphead;//注意优先级问题,需要加括号。(*pphead)->prev = *pphead; }初始化链表(一级指针) //LTNode* ListInit() //{ // LTNode*phead = BuyLTNode(0); // phead->prev = phead; // phead->next = phead; // return phead; //}//销毁链表 void ListDestory(LTNode* phead) {assert(phead);LTNode* cur = phead->next;while (cur != phead)//从第一个结点开始依次销毁{LTNode* next = cur->next;free(cur);cur = next;}//销毁哨兵结点free(phead);phead = NULL; }//打印数据 void ListPrint(LTNode* phead) {assert(phead);LTNode* cur = phead->next;//cur从phead->next开始while (cur != phead)//cur回到phead时停止{printf("%d ", cur->data);cur = cur->next;}printf("\n"); }//创建一个结点 LTNode* BuyLTNode(LTDataType x) {LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){printf("malloc fail\n");exit(-1);}newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode; }//尾插 void ListPushBack(LTNode* phead, LTDataType x) {assert(phead);LTNode* tail = phead->prev;//保存尾结点地址LTNode* newnode = BuyLTNode(x);//创建一个新的结点//循环链表tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;//法二/*assert(phead);ListInsert(phead, x);*/ }//尾删 void ListPopBack(LTNode* phead) {assert(phead);//哨兵位不能为空assert(phead->next != phead);//链表不能为空,防止删除哨兵位结点LTNode* tail = phead->prev;//尾结点放入tailLTNode* tailPrev = tail->prev;//新的尾结点tailPrevfree(tail);tail = NULL;//循环链表tailPrev->next = phead;phead->prev = tailPrev;/*assert(phead);assert(phead->next != phead);ListErase(phead->prev);*/ }//链表查找 LTNode* ListFind(LTNode* phead, LTDataType x) {assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL; }//在pos前插入数据 void ListInsert(LTNode* pos, LTDataType x) {assert(pos);//创建插入结点LTNode* newnode = BuyLTNode(x);//存储pos上一个结点地址LTNode* posPrev = pos->prev;//循环链表newnode->next = pos;pos->prev = newnode;posPrev->next = newnode;newnode->prev = posPrev; }//头插 void ListPushFront(LTNode* phead, LTDataType x) {assert(phead);ListInsert(phead->next, x); }//删除pos处结点 void ListErase(LTNode* pos) {assert(pos);//保存pos前一个结点LTNode* prev = pos->prev;//保存pos下一个结点LTNode* next = pos->next;free(pos);pos = NULL;//连接链表prev->next = next;next->prev=prev; }//头删 void ListPopFront(LTNode* phead) {assert(phead);assert(phead->next != phead);ListErase(phead->next); }
Test.c文件
#include "List.h"void TestList1() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPrint(pList); }void TestList2() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPrint(pList);ListPopBack(pList);ListPopBack(pList);ListPopBack(pList);ListPrint(pList); }void TestList3() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPrint(pList);LTNode* pos = ListFind(pList, 3);if (pos)//判断链表中是否有要查找的元素{ListInsert(pos, 30);//在数字3前插入30}ListPrint(pList); }void TestList4() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPrint(pList);//头插10和20ListPushFront(pList, 10);ListPushFront(pList, 20);ListPrint(pList); }void TestList5() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPrint(pList);LTNode* pos = ListFind(pList, 3);if (pos)//判断链表中是否有要查找的元素{ListErase(pos);//删除pos位结点pos = NULL;}ListPrint(pList); }void TestList6() {LTNode* pList = NULL;ListInit(&pList);ListPushBack(pList, 1);ListPushBack(pList, 2);ListPushBack(pList, 3);ListPushBack(pList, 4);ListPushBack(pList, 5);ListPushBack(pList, 6);ListPrint(pList);//头插三个元素ListPushFront(pList, 0);ListPushFront(pList, -1);ListPushFront(pList, -2);ListPrint(pList);//尾删三个元素ListPopBack(pList);ListPopBack(pList);ListPopBack(pList);ListPrint(pList);//头删三个元素ListPopFront(pList);ListPopFront(pList);ListPopFront(pList);ListPrint(pList); }int main() {/*TestList1();*//*TestList2();*//*TestList3();*//*TestList4();*//*TestList5();*/TestList6();return 0; }
看完觉得有收获的要记得点赞噢~
链表界的“扛把子”—双向带头循环链表相关推荐
- 外强中干——双向带头循环链表
前言:众所周知,链表有八种结构,由单向或双向,有头或无头,循环或不循环构成.在本篇,将介绍8种链表结构中最复杂的--双向带头循环链表.听着名字或许挺唬人的,但实际上双向带头循环链表实现起来比结构最简单 ...
- 【数据结构--双向带头循环链表(链表中的老大哥)】
双向带头循环链表及相关OJ
- 【数据结构初阶】双向带头循环链表原来是纸老虎,结构复杂,操作简单
目录 0.结构体定义 1.初始化 2.尾插 3.打印 4.头插 5.任意位置插入前面位置 6.尾删 7.头删 8.链表长度 9.任意位置删除当前位置 10. 销毁 双向带头循环链表:结构复杂,操作简单 ...
- 双向带头循环链表的(增删查改)的实现
文章目录 一.双向带头循环链表 构成 二.双向带头循环链表的实现 1.函数的定义和结构体的创建--list.h 2.函数的调用--list.c 3. 双向带头循环链表与单链表的传递参数区别 4.双向带 ...
- 数据结构------双向带头循环链表
目录 1.什么是双向带头循环链表?模型. 2.1 创建结点函数 2.2初始化 3.尾插实现 3.1图示 3.2尾插 4.打印函数实现 4.1打印函数 5.思考为啥是一级指针 6.头插实现 6.2实现代 ...
- 双向带头循环链表的实现
双向带头循环链表 双向带头循环链表 结构讲解 期望实现功能 创建链表和头节点作用 头插和头删 头插 头删 尾插与尾删 尾插 尾删 pos 删除和插入 插入 删除 打印和查找 整体代码 这个数据结构可以 ...
- 【双向带头循环链表】
双向带头循环链表 0.链表中每个节点的基本结构 1.初始化链表(创建哨兵位头节点) 2.申请新节点 3.尾插节点 4.尾删节点 5.头插节点 6.头删节点 7.寻找pos位置的节点 8.在pos位置之 ...
- 双向带头循环链表-实现思路+图解
目录 一.何为双向循环链表? 1.何为'双向'? 2.何为'带头'? 3.何为'循环'? 二.如何实现双向带头循环链表? 1.基本结构-结点的创建 2.创建哨兵位结点 3.链表的增删查改 一.何为双向 ...
- 双向带头循环链表详解
在上一篇所讲述的 单链表 中,存在一些缺陷: 1.在进行尾插和尾删时,需要遍历链表找到尾结点 2.在进行中间插入和删除时,也需要先遍历链表找到前一个结点 对于这些缺陷,可以采用一种结构更为复杂的链表 ...
最新文章
- 面对新型肺炎疫情,AI能做什么?
- 进程和线程的区别?什么时候用进程?什么时候用线程?
- LPC43xx OTP
- js kettle 设置变量_kettle与钉钉结合的企业内部应用扩展01
- windows azure之创建虚拟机
- 线程池方式调用spring mvc的业务类的简单实例
- centos6.5安装vnc-server
- java的全栈,Java全栈工程师
- SpringBoot 如何进行对象复制,老鸟们都这么玩的!
- winform调用大华相机
- 计算机中回收站的作用,windows7回收站的功能与作用
- c/c++ notify/wait 消息机制
- NYoj21 三个水杯
- 群晖NAS套件之Hyper Backup的功能和使用方法
- 基于matlab的陷波滤波器设计
- 【Java 设计模式】UML 之类图
- nyoj 1248-海岛争霸 //floyd变形
- 一劳永逸让windows 64位操作系统 禁止强制驱动签名
- 微信开发者工具代码管理
- 制作简易的个人主页(代码笔记)
热门文章
- 如何半个小时做出一个《新型冠状病毒同程查询》
- 基于视觉反馈的步进电机X-Y平台控制
- 写了个Android聊天客户端框架,基本聊天功能、数据库、服务器都有。大家可以看一看。已经开源
- 算法总结1——贪婪算法,动态规划
- MyDocument.exe病毒免疫方法
- 确定sw1开关信号输入端口_do编码器脉冲计数器ModbusTCP开关量信号采集模块pwmRJ45网络接口...
- 设计原则—YAGNI
- 新书推荐——华为·无线局域网应用技术(微课版丨第2版)
- 微星AMD 785G主板的BIOS设置详解
- 各种计算机编程语言之父