双向带头循环链表

这篇具有很好参考价值的文章主要介绍了双向带头循环链表。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

所属专栏:初始数据结构
博主首页:初阳785
代码托管:chuyang785
感谢大家的支持,您的点赞和关注是对我最大的支持!!!
博主也会更加的努力,创作出更优质的博文!!
关注我,关注我,关注我,重要的事情说三遍!!!!!!!!

1.前言

  • 我们之前也已经学完了单链表,并且也做了不少的单链表的OJ题了,想必看到这里的小伙伴对我们的单链表也是比较熟悉了,也可以比较流畅的写出我们的单链表了。所以本章节我们进一步学习我们的链表知识——双向带头循环链表。
  • 可能有点小伙伴一听到双向带头循环链表就开始慌张了,不仅双向带头,还循环,这不得难上加难。但是这里我想对大家说的是”眼见为虚,上手为实“。你不要看他 复杂其实上起手来比我们的单链表简单得多了,而且还很方便。
  • 这里注意一点就是我们这个双向链表是带头的(哨兵位)这一点在我们在写单链表OJ题的时候也有用到,也是比较方便的一种写法的。我们可以对比以下我们之前写单链表的时候是没有用到我们的哨兵位的,这样写的不好的地方的就是我们要传二级指针才能通过形参改变实参。但是如果我们用到了哨兵位的话就不需要传二级指针了,原因很简单——我们不用改变头节点了,我们的头节点就是我我们创建的哨兵位指向的next,所以有了哨兵位我们会方便很多。
  • 话不多说我们直接开始我们的本章节——双向带头循环链表。

2.带头双向循环链表的初始化

  • 我们现在来分析以下什么是双向链表。我们之前的单链表之所以叫做单链表,原因就是单链表是单向的他只能通过next找到下一个节点,找不到他前面一个节点这也是单链表的一个缺陷。那我们的双链表就可以弥补这一缺陷即:它既可以通过next找到下一个节点,还可以通过prev找到前一个节点。这一就说明了我们的双链表在定义结构体的时候不仅要存放一个数据datanext记录下一个节点,还要存放一给指针prev来记录我们前一个节点。
  • 而循环的意思就有点像我们之前做单链表OJ题的带环结构,也就是说我们可以通过尾找到我们的头,再通过头依次重复遍历,也就是说我们的尾节点的next指向phead(头节点) 而我们的pheadprev指向我们的尾节点。

双向带头循环链表

  • 同样的我们写一个有许多接口函数链接在一起的程序,我们把它分别写在三个文件中分别是: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开始删除的。

尾删

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模板网!

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

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

相关文章

  • 【数据结构】实现带头双向循环链表

    之前我们已经学习了单链表,有了单链表的基础,现在开始学习带头双向循环链表~ 结构最复杂 ,一般用在单独存储数据。 实际中使用的链表数据结构,都是带头双向循环链表 。另外这个结构虽然结构复杂,但是使用代码实现以后会发现 结构会带来很多优势 ,实现反而简单

    2024年02月10日
    浏览(45)
  • 数据结构之带头双向循环链表

    目录 链表的分类 带头双向循环链表的实现 带头双向循环链表的结构 带头双向循环链表的结构示意图 空链表结构示意图 单结点链表结构示意图  多结点链表结构示意图 链表创建结点 双向链表初始化 销毁双向链表 打印双向链表  双向链表尾插 尾插函数测试 双向链表头插

    2024年02月08日
    浏览(72)
  • 数据结构_带头双向循环链表

    相较于之前的顺序表和单向链表,双向链表的逻辑结构稍微复杂一些,但是在实现各种接口的时候是很简单的。因为不用找尾,写起来会舒服一点。(也可能是因为最近一直在写这个的原因) 在实现接口的时候,除了没有找尾,其他的操作和单向链表是差不多的,这里就不多

    2024年04月14日
    浏览(62)
  • 【数据结构】线性表——带头双向循环链表

    带头双向循环链表的优点 1.支持任意位置时间复杂度为O(1)的插入和删除。 2.按照需求申请释放空间,无需担心空间不够用,无需担心浪费。 3.带头可以省去链表为空时的判断,可以使代码更加简约 带头双向循环链表的缺点 1.不可以进行下标随机访问。 2.缓存利用率低 带头双

    2024年02月03日
    浏览(68)
  • 【数据结构】带头双向循环链表及其实现

    目录 1.带头双向循环链表 2.带头双向循环链表实现 2.1初始化 2.2销毁 2.3头插 2.4链表打印 2.5头删数据 2.6尾插数据 2.7尾删数据 2.8链表判空  2.9查找一个数据 2.10在pos位置前插入数据 2.11删除pos位置 2.12求链表的长度 2.顺序表和链表的比较 我们已经实现了无头单向循环链表 带头双

    2024年02月10日
    浏览(45)
  • 数据结构: 线性表(带头双向循环链表实现)

    之前一章学习了单链表的相关操作, 但是单链表的限制却很多, 比如不能倒序扫描链表, 解决方法是在数据结构上附加一个域, 使它包含指向前一个单元的指针即可. 那么怎么定义数据结构呢? 首先我们先了解以下链表的分类 链表的结构非常多样, 以下情况组合起来就有 8 中链表

    2024年02月14日
    浏览(40)
  • 数据结构的带头,双向,循环链表来咯

    上一篇文章给大家讲了一个很简单的单向不带头,不循环的链表,是为了让大家更好的理解链表,现在我们就来学习学习他的升级版,双向,带头,循环链表。希望多多支持哦! 目录 定义的结构体节点  开辟结构体节点的函数 头插函数 尾插函数 头删函数 尾删函数 首先我们

    2024年02月21日
    浏览(40)
  • 数据结构-带头双向循环链表的实现

    前言           带头双向循环链表是一种重要的数据结构,它的结构是很完美的,它弥补了单链表的许多不足,让我们一起来了解一下它是如何实现的吧!         它的节点中存储着数据和两个指针,一个 指针_prev 用来记录前一个节点的地址,另一个指针 _next 用来记录后一

    2024年02月13日
    浏览(46)
  • 【数据结构】双向带头循环链表的实现

    前言:在前面我们学习了顺序表、单向链表,今天我们在单链表的基础上进一步来模拟实现一个带头双向链表。 💖 博主CSDN主页:卫卫卫的个人主页 💞 👉 专栏分类:数据结构 👈 💯代码仓库:卫卫周大胖的学习日记💫 💪关注博主和博主一起学习!一起努力! 带头双向循环链

    2024年01月15日
    浏览(49)
  • 数据结构入门指南:带头双向循环链表

    目录 文章目录 前言 1.结构与优势 2.链表实现       2.1 定义链表 2.2 创建头节点 2.3 尾插 2.4 输出链表 2.5 尾删 2.6 头插 2.7头删 2.8 节点个数 2.9 查找 2.10 位置插入 2.11 位置删除 2.12 销毁链表  3. 源码 总结         链表一共有8种结构,但最常用的就是无头单向链表、和带头

    2024年02月14日
    浏览(48)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包