赞
踩
前言:小伙伴们好久不见啦,上篇文章我们一起学习了数据结构线性表其一的单链表,了解了单链表的不少好处,但是不可能有完美的数据结构,就算是单链表,也会有很多缺点。
那么今天这篇文章,我们就来学习单链表的promax版本——带头双向循环链表。
关于带头双向循环链表,我们将它拆分为带头、双向、循环、链表四个部分,其中链表我们已经知道是怎么回事了,那我们就来一起结合下图分析前三个概念。
1.带头
所谓带头,也就是在链表的开头处,有一个不存放任何数据的头节点,我们通常称其为“哨兵位”。
那么哨兵位存在的意义是什么呢???
它可以帮助我们更方便的进行对链表的各种操作。具体好在哪里,我们结合后边实现链表的各种操作来进行展示。
2.双向
我们前边学习过的单链表,它的每个节点之间只有一条链子相连,并且只能由前一个节点去找到后一个节点。
而双向链表,也就是两个节点之间有两条链子相连,不仅能从前一个找到后一个,也能从后一个去找到前一个。
3.循环
循环,顾名思义,就是将链表的头尾也进行连接,形成一个逻辑意义上的环形链表。
那么理解完带头双向循环链表的含义之后,我就就一起来看看到底来如何实现它吧。
此后我们将该链表的名字简化为双链表。
- typedef int DLLDataType;
- //定义双链表
- typedef struct DLinkList
- {
- DLLDataType data;
- struct DLinkList* prev;//指向前一个节点
- struct DLinkList* next;//指向后一个节点
- }DLLNode;
双链表是在单链表的基础上,比它多出一个prev指针去指向前一个节点,还是比较容易理解的。
- //初始化双链表
- DLLNode* DLinkListInit()
- {
- DLLNode* phead = (DLLNode*)malloc(sizeof(DLLNode));
- if (phead == NULL)
- {
- perror("DLinkListInit->malloc");
- }
- phead->next = phead;
- phead->prev = phead;
- return phead;
- }
双链表的初始化需要先造出哨兵位,考虑到链表为空,并且链表还要循环,所以我们将哨兵位的prev和next都指向自己。
DLLNode* dll = DLinkListInit();
创建一个双链表,我们习惯于运用上述方式。
因为如果用单链表的初始化方式,我们需要用到二级指针,但是我们后续双链表各种功能的操作,完全不和二级指针沾边。
所以为了让我们的双链表全部由一级指针完成,选择采用接收函数返回值的方式来创建双链表。
- DLLNode* CreateNewNode(DLLDataType x)
- {
- DLLNode* newnode = (DLLNode*)malloc(sizeof(DLLNode));
- if (newnode == NULL)
- {
- perror("CreateNewNode->malloc");
- }
- newnode->data = x;
- newnode->next = NULL;
- newnode->prev = NULL;
- return newnode;
- }
双链表创建新节点就和单链表差不多啦,要注意的就是不要忘记两个指针置空,防止出现野指针。
这样,我们就实现了一个基本的双链表框架,下面来实现双链表的各种基础操作。
那么为了方便其他功能的测试,我们还是先来实现双链表的打印功能:
- void DLinkListPrint(DLLNode* phead)
- {
- assert(phead);
- DLLNode* cur = phead->next;
- printf("phead<=>");
- while (cur != phead)
- {
- printf("%d<=>", cur->data);
- cur = cur->next;
- }
- printf("phead\n");
- }
我们还是严格的进行一下assert断言,如果phead为空,就说明双链表不存在。
这里要注意两点:
1.cur为什么是phead->next???
不难理解,我们在双链表初始化的时候,给到dll的返回值是哨兵位,但是哨兵位不存储数据,所以要从哨兵位的下一个节点开始。
2.while循环的判断条件
因为我们是一个可循环的链表,所以并不存在cur为空的情况,但是cur最后会重新指向哨兵位,所以当cur == phead时,说明我们已经将双链表遍历一遍了。
至于printf函数的内容,只是为了好看哈哈,展示一下:
这样能够让大家更形象的认识双链表。
双链表的尾插相较于单链表有什么优势呢???
单链表想尾插,首先要进行循环找尾,时间复杂度就高了,但是双链表就好办,因为哨兵位的前一个节点就是尾,也就是phead->prev,尾找到之后,就好办了:
- //尾插
- void DLinkListPushBack(DLLNode* phead, DLLDataType x)
- {
- assert(phead);
- DLLNode* newnode = CreateNewNode(x);
- DLLNode* tail = phead->prev;
- tail->next = newnode;
- newnode->next = phead;
- newnode->prev = tail;
- phead->prev = newnode;
- }
用tail代替尾,接下来的一顿操作,就是:
旧尾的next指向新尾
新尾的next指向哨兵位
新尾的prev指向旧尾
哨兵位的prev指向新尾
看起来很简单,但是我们知道,单链表必须得考虑一下链表是否为空的特例,但是双链表不需要。
因为双链表如果为空,那就只有哨兵位,哨兵位自己的头尾相连,带入上述代码操作之后,不会有任何错误。
尾删就更简单了,只需要找到尾,再通过尾找到尾的前一个节点,再让此节点和哨兵位互连,再将尾free即可:
- //尾删
- void DLinkListPopBack(DLLNode* phead)
- {
- assert(phead);
- assert(phead->next != phead);
- DLLNode* tail = phead->prev;
- DLLNode* tailprev = tail->prev;
- phead->prev = tailprev;
- tailprev->next = phead;
- free(tail);
- tail = NULL;
- }
尾删要考虑只有一个节点的特例吗,依然不用,因为进行一顿操作之后,还是让哨兵位自己头尾相连。
但是尾删要考虑空链表的情况,因为如果链表为空,free的就是哨兵位了,哨兵位一旦不存在了,我们就无法进行后续的操作了。所以要多进行一次assert断言。
到这里,小伙伴们是否已经感受到了哨兵位,以及双链表的强势之处啦。
头插就和尾插差不多了,这里我直接给出代码,希望小伙伴们可以自己理解掌握哦。
- //头插
- void DLinkListPushFront(DLLNode* phead, DLLDataType x)
- {
- assert(phead);
- DLLNode* head = phead->next;
- DLLNode* newnode = CreateNewNode(x);
- phead->next = newnode;
- newnode->next = head;
- head->prev = newnode;
- newnode->prev = phead;
- }
头删也和尾删类似,要考虑空链表的情况:
- //头删
- void DLinkListPopFront(DLLNode* phead)
- {
- assert(phead);
- assert(phead->next != phead);
- DLLNode* head = phead->next;
- DLLNode* headnext = head->next;
- phead->next = headnext;
- headnext->prev = phead;
- free(head);
- head = NULL;
- }
如果是查找的话,那我们还得老老实实的从头遍历:
- //查找
- DLLNode* DLinkListFind(DLLNode* phead,DLLDataType x)
- {
- assert(phead);
- DLLNode* cur = phead->next;
- while(cur)
- {
- if (cur->data == x)
- return cur;
- else
- cur = cur->next;
- }
- return NULL;
- }
还是要注意这里while循环的条件,和双链表的打印一样。
双链表的任意位置的插入依然要和查找连用,因为只有查找才能得到pos位置的地址。
但是我们这里规定一下,任意插就是pos位置前插。
比如说我想在表的第四个位置插入新数据,那我就要把第四个位置空出来,让原来的第四位以及他后边的都老老实实往后退。
这样一来,我们就需要找到pos节点的前一个节点,这样方便我们进行操作:
- //pos位置插
- void DLinkListInsert(DLLNode* pos, DLLDataType x)
- {
- assert(pos);
- DLLNode* newnode = CreateNewNode(x);
- DLLNode* posprev = pos->prev;
- posprev->next = newnode;
- newnode->prev = posprev;
- pos->prev = newnode;
- newnode->next = pos;
- }
任意删的形式就和任意插差不多,只是还需要另外记录pos的下一个节点:
- //pos位置删
- void DLinkListEease(DLLNode* pos)
- {
- assert(pos);
- DLLNode* posprev = pos->prev;
- DLLNode* posnext = pos->next;
- posprev->next = posnext;
- posnext->prev = posprev;
- free(pos);
- pos = NULL;
- }
想要修改数据,还是要用查找操作来找到要修改pos位置的地址,而后就简单了:
- //pos位置改
- void DLinkListAmend(DLLNode* pos, DLLDataType x)
- {
- assert(pos);
- pos->data = x;
- }
直接修改data即可。
双链表的销毁,同样是需要遍历对个个空间进行free,值得注意的是,哨兵位也需要销毁:
- //销毁
- void DLinkListDestroy(DLLNode* phead)
- {
- assert(phead);
- DLLNode* cur = phead->next;
- while (cur != phead)
- {
- DLLNode* next = cur->next;
- free(cur);
- cur = next;
- }
- free(phead);
- phead = NULL;
- }
- #include <stdio.h>
- #include <assert.h>
- #include <stdlib.h>
-
- typedef int DLLDataType;
- //定义双链表
- typedef struct DLinkList
- {
- DLLDataType data;
- struct DLinkList* prev;
- struct DLinkList* next;
- }DLLNode;
-
- //初始化双链表
- DLLNode* DLinkListInit();
- //打印双链表
- void DLinkListPrint(DLLNode* phead);
- //创造新节点
- DLLNode* CreateNewNode(DLLDataType x);
- //尾插
- void DLinkListPushBack(DLLNode* phead, DLLDataType x);
- //尾删
- void DLinkListPopBack(DLLNode* phead);
- //头插
- void DLinkListPushFront(DLLNode* phead, DLLDataType x);
- //头删
- void DLinkListPopFront(DLLNode* phead);
- //查找
- DLLNode* DLinkListFind(DLLNode* phead,DLLDataType x);
- //pos位置插
- void DLinkListInsert(DLLNode* pos, DLLDataType x);
- //pos位置删
- void DLinkListEease(DLLNode* pos);
- //pos位置改
- void DLinkListAmend(DLLNode* pos,DLLDataType x);
- //销毁
- void DLinkListDestroy(DLLNode* phead);
- #include "DLinkList.h"
- //初始化双链表
- DLLNode* DLinkListInit()
- {
- DLLNode* phead = (DLLNode*)malloc(sizeof(DLLNode));
- if (phead == NULL)
- {
- perror("DLinkListInit->malloc");
- }
- phead->next = phead;
- phead->prev = phead;
- return phead;
- }
- //打印双链表
- void DLinkListPrint(DLLNode* phead)
- {
- assert(phead);
- DLLNode* cur = phead->next;
- printf("phead<=>");
- while (cur != phead)
- {
- printf("%d<=>", cur->data);
- cur = cur->next;
- }
- printf("phead\n");
- }
- //创造新节点
- DLLNode* CreateNewNode(DLLDataType x)
- {
- DLLNode* newnode = (DLLNode*)malloc(sizeof(DLLNode));
- if (newnode == NULL)
- {
- perror("CreateNewNode->malloc");
- }
- newnode->data = x;
- newnode->next = NULL;
- newnode->prev = NULL;
- return newnode;
- }
- //尾插
- void DLinkListPushBack(DLLNode* phead, DLLDataType x)
- {
- assert(phead);
- DLLNode* newnode = CreateNewNode(x);
- DLLNode* tail = phead->prev;
- tail->next = newnode;
- newnode->next = phead;
- newnode->prev = tail;
- phead->prev = newnode;
- }
- //尾删
- void DLinkListPopBack(DLLNode* phead)
- {
- assert(phead);
- DLLNode* tail = phead->prev;
- DLLNode* tailprev = tail->prev;
- phead->prev = tailprev;
- tailprev->next = phead;
- free(tail);
- tail = NULL;
- }
- //头插
- void DLinkListPushFront(DLLNode* phead, DLLDataType x)
- {
- assert(phead);
- DLLNode* head = phead->next;
- DLLNode* newnode = CreateNewNode(x);
- phead->next = newnode;
- newnode->next = head;
- head->prev = newnode;
- newnode->prev = phead;
- }
- //头删
- void DLinkListPopFront(DLLNode* phead)
- {
- assert(phead);
- DLLNode* head = phead->next;
- DLLNode* headnext = head->next;
- phead->next = headnext;
- headnext->prev = phead;
- free(head);
- head = NULL;
- }
- //查找
- DLLNode* DLinkListFind(DLLNode* phead,DLLDataType x)
- {
- assert(phead);
- DLLNode* cur = phead->next;
- while(cur)
- {
- if (cur->data == x)
- return cur;
- else
- cur = cur->next;
- }
- return NULL;
- }
- //pos位置插
- void DLinkListInsert(DLLNode* pos, DLLDataType x)
- {
- assert(pos);
- DLLNode* newnode = CreateNewNode(x);
- DLLNode* posprev = pos->prev;
- posprev->next = newnode;
- newnode->prev = posprev;
- pos->prev = newnode;
- newnode->next = pos;
- }
- //pos位置删
- void DLinkListEease(DLLNode* pos)
- {
- assert(pos);
- DLLNode* posprev = pos->prev;
- DLLNode* posnext = pos->next;
- posprev->next = posnext;
- posnext->prev = posprev;
- free(pos);
- pos = NULL;
- }
- //pos位置改
- void DLinkListAmend(DLLNode* pos, DLLDataType x)
- {
- assert(pos);
- pos->data = x;
- }
- //销毁
- void DLinkListDestroy(DLLNode* phead)
- {
- assert(phead);
- DLLNode* cur = phead->next;
- while (cur != phead)
- {
- DLLNode* next = cur->next;
- free(cur);
- cur = next;
- }
- free(phead);
- phead = NULL;
- }
测试代码大家自行进行测试,这里就不在进行展示啦。
双链表相比于单链表还是有很大优势的,建议大家在学习过单链表的基础上完全靠自己的写一写双链表,这将会让你对链表知识的掌握更上一层楼!
最后还是提醒大家不要忘记一键三连哦!!!
我们下期再见啦!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。