所属专栏:初始数据结构
博主首页:初阳785
代码托管:chuyang785
感谢大家的支持,您的点赞和关注是对我最大的支持!!!
博主也会更加的努力,创作出更优质的博文!!
关注我,关注我,关注我,重要的事情说三遍!!!!!!!!
1.前言
- 我们之前也已经学完了单链表,并且也做了不少的单链表的OJ题了,想必看到这里的小伙伴对我们的单链表也是比较熟悉了,也可以比较流畅的写出我们的单链表了。所以本章节我们进一步学习我们的链表知识——双向带头循环链表。
- 可能有点小伙伴一听到双向带头循环链表就开始慌张了,不仅双向带头,还循环,这不得难上加难。但是这里我想对大家说的是”眼见为虚,上手为实“。你不要看他 复杂其实上起手来比我们的单链表简单得多了,而且还很方便。
- 这里注意一点就是我们这个双向链表是带头的(哨兵位)这一点在我们在写单链表OJ题的时候也有用到,也是比较方便的一种写法的。我们可以对比以下我们之前写单链表的时候是没有用到我们的哨兵位的,这样写的不好的地方的就是我们要传二级指针才能通过形参改变实参。但是如果我们用到了哨兵位的话就不需要传二级指针了,原因很简单——我们不用改变头节点了,我们的头节点就是我我们创建的哨兵位指向的next,所以有了哨兵位我们会方便很多。
- 话不多说我们直接开始我们的本章节——双向带头循环链表。
2.带头双向循环链表的初始化
- 我们现在来分析以下什么是双向链表。我们之前的单链表之所以叫做单链表,原因就是单链表是单向的他只能通过next找到下一个节点,找不到他前面一个节点这也是单链表的一个缺陷。那我们的双链表就可以弥补这一缺陷即:它既可以通过next找到下一个节点,还可以通过prev找到前一个节点。这一就说明了我们的双链表在定义结构体的时候不仅要存放一个数据data和next记录下一个节点,还要存放一给指针prev来记录我们前一个节点。
- 而循环的意思就有点像我们之前做单链表OJ题的带环结构,也就是说我们可以通过尾找到我们的头,再通过头依次重复遍历,也就是说我们的尾节点的next指向phead(头节点) 而我们的phead的prev指向我们的尾节点。
- 同样的我们写一个有许多接口函数链接在一起的程序,我们把它分别写在三个文件中分别是:text.c,DList.c和DList.h这三个文件中。
- 所包含的头文件:
//防止头文件重复定义
#pragma once
#include <stdio.h>
//断言
#include <assert.h>
//动态内存malloc头文件
#include <stdlib.h>
- 数据结构体创建
//存储数据类型类型
typedef int LTDateType;
typedef struct ListNode
{
//存储数据
LTDateType data;
//记录下一个节点
struct ListNode* next;
//记录前一个节点
struct ListNode* prev;
}LTNode;
- 各给接口函数的定义,这个都是一些函数的声明,而我们函数的声明是放在后缀名为.h的头文件中的。
//初始化
LTNode* ListInit();
//链表打印
void ListPrint(LTNode* phead);
//尾插
void ListPushBack(LTNode* phead, LTDateType x);
//尾删
void ListPopBack(LTNode* phead);
//头插
void ListPushFront(LTNode* phead, LTDateType x);
//头删
void ListPopFront(LTNode* phead);
//查找
LTNode* ListFind(LTNode* phead,LTDateType x);
//给定位置插入
LTNode* ListTnsert(LTNode* pos, LTDateType x);
//给定位置删除
LTNode* ListErase(LTNode* pos);
//释放空间
void ListDestroy(LTNode* phead);
3.创建一个哨兵位头节点
- 我们常见以一个头节点间要用到malloc函数。我们分析一下,如果链表里一个数据都没有插入,也就是说我们只有头节点也就是我们的哨兵位,这个是时候哨兵位的== next== 和prev都是指向自己的。
LTNode* ListInit()
{
//设置哨兵为
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
if (phead != NULL)
{
phead->next = phead;
phead->prev = phead;
return phead;
}
}
4.链表的打印
- 我们设计链表的时候最好的检查我们功能函数有没有写对方法就是将数据打印出来,可以直观的观察数据。
- 既然我们设计了哨兵位,但是我们要要知道的是哨兵位是存储无效数据的,我们是从哨兵位的next开始存储有效数据的,也就是说我们打印也是从哨兵位的next开始打印的。
- 而我们遍历的停止条件就是到我们遍历到我们的哨兵位的时候停止。
void ListPrint(LTNode* phead)
{
assert(phead);
//从头节点的下一个开始遍历
LTNode* cur = phead->next;
//找到头节点后停止遍历
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
5.malloc函数创建节点
- 我们在进行尾插或者头插的时候都需要向内存申请一块空间来存放我们的数据,然后再将数据插入。所以既然都需要malloc一块空间,而且都是一样的创建方式,不妨创建出一个开辟空间的函数,需要创建的时候直接调用就行了,这样就可以防止代码重复累赘了。
LTNode* newnode(LTDateType x)
{
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (newNode != NULL)
{
\\存入数据
newNode->data = x;
newNode->prev = newNode->next = NULL;
return newNode;
}
}
5.链表的尾插
- 尾插的第一步就是先创建我们要插入的节点,这里就需要用到malloc。
- 我们的尾插其实和单链表的尾插是有点像,但是不一样的就是我们需要将把我们插入的节点的prev指向前面一个节点而我们的前一个节点的next指向我们插入的节点形成双向的链表。
- 而且插入的节点的next要指向我们的头节点,我们的头节点的prev指向我们的插入的节点,形成循环结构。
- 这里我们不需要像我们之前写的单链表那样判断链表是空的情况,原因就是我们设计了哨兵位。
void ListPushBack(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* newNode = newnode(x);
LTNode* tail = phead->prev;
if (newNode != NULL)
{
\\链接前后两个节点,形成双向的链表。
tail->next = newNode;
newNode->prev = tail;
\\链接为节点和头节点,形成循环结构。
newNode->next = phead;
phead->prev = newNode;
}
}
6.链表的尾删
- 说到尾删如果我们是单链表的话是相当的麻烦的,因为单链表只能找到后一个节点找不到前一个节点,所以链接的时候比较麻烦。
- 但是我们的双向链表就不一样了,他是双向的而且还是循环的,也就是说我们要找的尾节点就是我们的head->prev上面的图也是比较清楚的。
- 而且我们要找的尾节点的前一个也是很好找的,就是我们的尾节点->prev,这样我们为节点找到了,也找到了尾节点的前一个,链接起来就很方便了。
- 同时我们也需要判断我们的链表是否删完了,这个也是比较好判断的,只要我们的phead->next != phead就说明还有数据可以删除。
void ListPopBack(LTNode* phead)
{
\\判断是否还有数据可以删除
assert(phead->next != phead);
\\找到尾节点
LTNode* cur = phead->prev;
\\将我们的尾节点指向的prev指向我们的头节点。
cur->prev->next = phead;
\\phead又之指向回去,形成循环。
phead->prev = cur->prev;
\\最后释放空间
free(cur);
cur = NULL;
}
7.链表的头插
- 头插也是比较简单的,只需要将phead->next指向我们malloc出来节点,用我们malloc来的节点的prev指向phead
- 我们还得用一个Next指针记录下phead->next,这样子我们才能将malloc出来的节点与其后面对链表链接起来。
void ListPushFront(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* newNode = newnode(x);
\\记录phead指向的下一个节点,为后面链接做好准备。
LTNode* Next = phead->next;
\\链接头节点和我们malloc出来的就节点形成双向
phead->next = newNode;
newNode->prev = phead;
\\链接malloc出来的节点和Next将链表整体链接起来。
newNode->next = Next;
Next->prev = newNode;
}
8.链表的头删
- 删除链表的话同样的头删的节点是可以直接通过头节点phead->next找到的,而且我们还得用nextNext找到我们要删除节点的下一个节点,以便删除后再次链表链接起来。
- 这里我们也需要判断我们的链表是否删完了,这个也是比较好判断的,只要我们的phead->next != phead就说明还有数据可以删除。
void ListPopFront(LTNode* phead)
{
assert(phead);
\\判断是否还有数据
assert(phead->next != phead);
LTNode* next = phead->next;
LTNode* nextNext = next->next;
\\释放空间
free(next);
\\链接phead和nextNext
phead->next = nextNext;
nextNext->prev = phead;
}
9.链表的查找
- 查找数据就会比较简单了,但是由于这是一个循环链表,所以我们遍历结束条件就是当再次遍历到phead的时候停止。
- 而且我们开始遍历的节点是phead->next
LTNode* ListFind(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
\\没找到返回NULL
return NULL;
}
10.链表任意位置插入(在给点定位置的前面插入)
- 可以说双向链表的精华之处就值之这里了,为什么这么说呢,这里我们可以对比一下我们的单链表就知道双向链表的好处了。
- 既然要插入一个数据同样要先malloc一个节点。
- 而我们要插入数据在给定节点前面的话,只需要将其链接在pos前面的节点和pos之间就行了,而pos前一个节点就是pos->prev这个时候链接起来就很方便了。
LTNode* ListTnsert(LTNode* pos, LTDateType x)
{
assert(pos);
\\pos前一个节点
LTNode* posPrev = pos->prev;
\\malloc一块节点
LTNode* newNode = newnode(x);
\\链接
posPrev->next = newNode;
newNode->prev = posPrev;
newNode->next = pos;
pos->prev = newNode;
}
11.链表任意位置删除
- 可以说双向链表的精华之处就值之这里了,为什么这么说呢,这里我们可以对比一下我们的单链表就知道双向链表的好处了。
- 因为给点的位置的节点,我们可以通过这个节点找到要删除节点的前一个和后一个,那要删除不是轻而易举吗。
LTNode* ListErase(LTNode* pos)
{
assert(pos);
\\找到pos前一个节点
LTNode* posPrev = pos->prev;
\\找到pos后一个节点
LTNode* posNext = pos->next;
\\链接
posPrev->next = posNext;
posNext->prev = posPrev;
\\释放空间
free(pos);
pos = NULL;
}
12.空间释放
- 因为我们的节点都是malloc出来的,一旦我们的程序结束了这些空间都需要还给操作系统,不然就会导致内存泄漏。
void ListDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
}
- 这里我们唯一要注意的是既然我们把出来phead以外的节点都free了,那这里为什么没有free(phead)的呢?原因是这里我们传的是一级指针,这里的phead是形参,我们在函数里面改变形参是不会影响实参的,如果要通过形参改变实参的话必须传二级指针,那为什么不传二级指针呢,这个理由可能比较牵强,就是保持接口函数的一致性,毕竟前面的接口函数都是传的一级指针,所以这里我们已处一级指针。那怎么free实参呢,我们可以在创建他的最后面free掉,与就是说在他创建的作用域free。
13.代码优化
-
如果我们仔细点观察的话,我们会发现我们的尾插和头插都是可以用 (给定任意位置插入)来代替的。
-
而我的尾删和头删都是可以用 (给定任意位置删除)来代替的。
-
我们的尾插其实就是从phead开始插入的
-
你这里看似我们的newnode是子phead的后面,但是如果我们遍历的话还是从phead->next开始的,到phead停止的,而我们遍历的时候newnode还是最后一个遍历的,这里其实就是画的像这样,但是如果我们换个角度看(把newnode放在最后面)就很明显了。
-
而我们的头插其实就是在phead-next地方插入。
-
这个就相对来计较好理解,直接头插。
-
所以我们的尾插和头插就可改成这个样子:
尾插
void ListPushBack(LTNode* phead, LTDateType x)
{
assert(phead);
ListTnsert(phead, x);
}
头插
void ListPushFront(LTNode* phead, LTDateType x)
{
assert(phead);
ListTnsert(phead->next, x);
}
- 而我们的尾删其实就是phead-prev开始删除的。
- 我们的头删就是从phead->next开始删除的。
尾删文章来源:https://www.toymoban.com/news/detail-448432.html
void ListPopBack(LTNode* phead)
{
assert(phead->next != phead);
ListErase(phead->prev);
}
头删文章来源地址https://www.toymoban.com/news/detail-448432.html
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
ListErase(phead->next);
}
14.整体代码展示
14.1 DList.h展示
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int LTDateType;
typedef struct ListNode
{
LTDateType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
//初始化
LTNode* ListInit();
//链表打印
void ListPrint(LTNode* phead);
//尾插
void ListPushBack(LTNode* phead, LTDateType x);
//尾删
void ListPopBack(LTNode* phead);
//头插
void ListPushFront(LTNode* phead, LTDateType x);
//头删
void ListPopFront(LTNode* phead);
//查找
LTNode* ListFind(LTNode* phead,LTDateType x);
//给定位置插入
LTNode* ListTnsert(LTNode* pos, LTDateType x);
//给定位置删除
LTNode* ListErase(LTNode* pos);
//释放空间
void ListDestroy(LTNode* phead);
14.2 DList.c展示
#define _CRT_SECURE_NO_WARNINGS 1
#include "DList.h"
LTNode* ListInit()
{
//设置哨兵为
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
if (phead != NULL)
{
phead->next = phead;
phead->prev = phead;
return phead;
}
}
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
LTNode* newnode(LTDateType x)
{
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (newNode != NULL)
{
newNode->data = x;
newNode->prev = newNode->next = NULL;
return newNode;
}
}
void ListPushBack(LTNode* phead, LTDateType x)
{
assert(phead);
//LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
//LTNode* tail = phead->prev;
//if (newNode != NULL)
//{
// newNode->data = x;
// tail->next = newNode;
// newNode->prev = tail;
//
// newNode->next = phead;
// phead->prev = newNode;
//}
ListTnsert(phead, x);
}
void ListPopBack(LTNode* phead)
{
assert(phead->next != phead);
//LTNode* cur = phead->prev;
//cur->prev->next = phead;
//phead->prev = cur->prev;
//free(cur);
//cur = NULL;
ListErase(phead->prev);
}
void ListPushFront(LTNode* phead, LTDateType x)
{
//assert(phead);
//LTNode* newNode = newnode(x);
//LTNode* Next = phead->next;
//phead->next = newNode;
//newNode->prev = phead;
//newNode->next = Next;
//Next->prev = newNode;
ListTnsert(phead->next, x);
}
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
//LTNode* next = phead->next;
//LTNode* nextNext = next->next;
//free(next);
//phead->next = nextNext;
//nextNext->prev = phead;
ListErase(phead->next);
}
LTNode* ListFind(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//在pos位置之前插入
LTNode* ListTnsert(LTNode* pos, LTDateType x)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* newNode = newnode(x);
posPrev->next = newNode;
newNode->prev = posPrev;
newNode->next = pos;
pos->prev = newNode;
}
LTNode* ListErase(LTNode* pos)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
void ListDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
}
14.3 Text.c展示
- 这个文件主要是用来测试接口函数的,按自己需要添加
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include "DList.h"
void textList1()
{
//初始化
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPopBack(plist);
ListPopBack(plist);
//ListPopBack(plist);
//ListPopBack(plist);
//ListPopBack(plist);
ListPrint(plist);
}
void textList2()
{
LTNode* plist = ListInit();
ListPushFront(plist, 6);
ListPushFront(plist, 7);
ListPushFront(plist, 8);
ListPushFront(plist, 9);
ListPopFront(plist);
ListPopFront(plist);
//ListPopFront(plist);
//ListPopFront(plist);
ListPrint(plist);
}
void textList3()
{
LTNode* plist = ListInit();
ListPushFront(plist, 6);
ListPushFront(plist, 7);
ListPushFront(plist, 8);
ListPushFront(plist, 9);
LTNode* pos=ListFind(plist, 9);
ListTnsert(pos, 60);
ListPrint(plist);
ListDestroy(plist);
plist = NULL;
}
int main()
{
textList1();
textList2();
//textList3();
return 0;
}
到了这里,关于双向带头循环链表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!