数据结构入门指南:带头双向循环链表

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

目录

文章目录

前言

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种结构,但最常用的就是无头单向链表、和带头双向循环链表。单链表的结构存在着很多的缺陷,但它是许多数据结构的子结构,在刷题中经常见到,而带头双向循环链表弥补了单链表所有的缺陷,可以说是一个完美结构,虽然相对于单链表来说结构更复杂,但它的特性使它的实现逻辑较为简单,今天我就向大家一一介绍。


1.结构与优势

结构

数据结构入门指南:带头双向循环链表,链表,数据结构,经验分享,c语言

优势

  1. 可以实现快速的插入和删除操作:由于链表的特性,插入和删除节点的时间复杂度为O(1),不需要像数组一样需要移动其他元素。
  2. 可以实现快速的遍历操作:双向链表可以通过前向或后向的指针进行遍历,而不需要像单向链表那样只能从头节点开始遍历。
  3. 可以实现双向遍历:双向链表可以通过前向和后向的指针进行双向遍历,可以方便地从任意一个节点开始向前或向后遍历。
  4. 可以实现循环遍历:由于链表的循环性质,可以通过不断移动指针进行循环遍历,不需要额外的循环条件判断。
  5. 可以实现更多高级功能:带头双向循环链表可以实现更多高级功能,如反转链表、查找倒数第k个节点等。

总结,带头双向循环链表具有灵活性和高效性,适用于需要频繁插入、删除和遍历操作的场景。

2.链表实现      

 2.1 定义链表

        步入正题,带头双向循环链表,首先它是双向的,什么是双向呢?在单链表定义时会有指向下一个节点的指针,而双向链表有两个指针,一个指向下一个节点,一个指向上一个节点,可以实现前后双向遍历。

typedef struct ListNode
{
	int data;
	struct ListNode* next;//指向下一个节点的指针
	struct ListNode* prev;//指向上一个节点的指针
}LN;

 2.2 创建头节点

         和无头单向链表不同,带头双向循环链表它有头节点,所以在开始执行各操作之前,我们先创建一个头节点并初始化。

        创建头节点需要新建一个节点,在其他插入接口中也需要新建节点,那我们就封装一个创建新节点的函数,最后返回新建节点的地址。

LN* BuyListNode(Datatype x)
{
	LN* newnode = (LN*)malloc(sizeof(LN));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

         和无头单链表不同,这里带有头节点,就需要对头节点进行初始化一下,虽然在创建节点时就已经对节点进行了初始化,但头节点的初始化与新建节点初始化需求不同所有这里需要单独进行初始化初始化节点时可以使用双指针,也可以直接返回头节点地址。

LN* InItNode()
{
	LN* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

         头节点进行初始化时,只需将两个指针指向自己,然后返回头节点的地址即可。

 2.3 尾插

        建好头节点后要怎么链接节点呢?我们先写一个尾插来体验一下它的便捷。在单链表中想要进行尾插,还需要遍历一遍链表找到尾节点,而带头双向循环链表就不需要,可以通过头节点直接找到尾节点:tail  =  phead -> prev ;头节点的前一个节点就是尾节点。

我们新建一个节点:

数据结构入门指南:带头双向循环链表,链表,数据结构,经验分享,c语言

         要想插入就需要把尾节点的next改为新节点的地址,新节点的prev改为尾节点tail的地址

数据结构入门指南:带头双向循环链表,链表,数据结构,经验分享,c语言

        再把新节点的next改为头节点的地址,把头节点的prev改位新节点的地址。

 数据结构入门指南:带头双向循环链表,链表,数据结构,经验分享,c语言

 整体逻辑就是这样,具体代码如下:

void PushBack(LN* phead, Datatype x)
{
	assert(phead);
	LN* tail = phead->prev;
	LN* newnode = BuyListNode(x);

	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

2.4 输出链表

        为了便于后续的测试,我们先写一个函数用于输出链表数据。输出函数很简单。

void PrintList(LN* phead)
{
	assert(phead);
	LN* cur = phead->next;//有效节点不包含头节点
	printf("phead <->");
	while (cur != phead)
	{
		printf(" %d <->", cur->data);
		cur = cur->next;
	}
}

         这里需要注意的是遍历链表时的循环条件,由于它是循环链表,所有结束条件有所变化。其次是输出数据时不需要输出头节点的数据,头节点的下一个节点才是有效数据。

我们可以来测试一下:

void test1()
{
	LN* plist = InItNode();
	PushBack(plist, 1);
	PushBack(plist, 2);
	PushBack(plist, 3);
	PushBack(plist, 4);
	PushBack(plist, 5);
	PrintList(plist);
}

int main()
{
	test1();
	return 0;
}

 输出效果如下:

数据结构入门指南:带头双向循环链表,链表,数据结构,经验分享,c语言

 2.5 尾删

        基本逻辑理解后,可以先自主尝试写一下尾删。思路也是非常的简单,但要注意的是,有效节点为0的情况。把尾节点的前一个节点next改为头节点地址,头节点的prev改为尾节点的前一个节点,最后释放掉尾节点的空间。

数据结构入门指南:带头双向循环链表,链表,数据结构,经验分享,c语言

void PopBack(LN* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LN* tail = phead->prev;
	LN* tailprev = tail->prev;

	tailprev->next = phead;
	phead->prev = tailprev;
	free(tail);
}

2.6 头插

        接下来我们进行头插操作,我们使用的是带头的形式,所有这里进行头插时,不像单链表那样需要使用二级指针,我们需要改的是头节点中的内容,所有使用一级指针就可以解决。

         此外头插时一定要注意操作的次序,避免后续操作有误。例如:

数据结构入门指南:带头双向循环链表,链表,数据结构,经验分享,c语言

        如果不提前保存第一个节点的地址, 就会导致新节点链接后续节点时找节点麻烦或出现错误

 正确的顺序如下图:

数据结构入门指南:带头双向循环链表,链表,数据结构,经验分享,c语言

 代码实现:

void PushFront(LN* phead,Datatype x)
{
	assert(phead);
	LN* newnode = BuyListNode(x);
	newnode->next = phead->next;
	phead->next->prev = newnode;

	newnode->prev = phead;
	phead->next = newnode;

}

         当然新手不建议这样写,这样写很容易把人搞晕,我们可以先保存第一个节点的地址,这样就不会搞错。代码如下:

void PushFront(LN* phead,Datatype x)
{
	assert(phead);
	LN* newnode = BuyListNode(x);
	LN* first = phead->next;

	phead->next = newnode;
	newnode->prev = phead;

	newnode->next = first;
	first->prev = newnode;

}

2.7头删

        这里头删也是非常简单,为了避免出错,我们可以额外创建两个指针,记录第一个节点和第二个节点,逻辑较为简单,就不再画逻辑图,代码如下:

void PopFront(LN* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LN* first = phead->next;
	LN* second = first->next;

	free(first);

	phead->next = second;
	second->prev = phead;

}

需要额外注意链表为空的情况。

2.8 节点个数

        统计节点个数很简单,和输出链表数据一样遍历一下链表统计链表个数代码如下:

int Listsize(LN* phead)
{
	assert(phead);
	int sz = 0;
	LN* cur = phead->next;
	while (cur != phead)
	{
		sz++;
		cur = cur->next;
	}
	return sz;
}

         有人可能会想:用头节点统计不也可以吗?还不用额外的去写一个函数。初始时把头节点的data初始化为0,每次插入data++。这种方式严格来说是不可以的,我们在写链表时每个节点不一定存储的是整形,也可能是字符型,虽然也能计数,但如果节点是1000呢?数据就溢出了,所以不建议那样写。

 2.9 查找

        查找也比较简单,不再多说,代码如下:

LN* ListFind(LN* phead, Datatype x)
{
	assert(phead);
	LN* cur = phead->next;
	while (cur!=phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

 2.10 位置插入

         带头双向循环链表在位置插入时没有像单链表那样有位置前插入,位置后插入。这里的位置插入都是位置前插入,由于它是循环双向的链表,不像单链表那样不可以向前遍历,双向循环链表可以任意插入,所以位置后插入也并没有什么意义,就统一默认位置前插入。

        位置插入操作和上述的插入操作很类似,推荐额外创建一个指针保存节点信息,就可以避免操作时次序不当造成程序错误,代码如下:

void ListInsert(LN* pos, Datatype x)
{
	assert(pos);
	LN* newnode = BuyListNode(x);
	LN* posprev = pos->prev;

	posprev->next = newnode;
	newnode->prev = posprev;

	newnode->next = pos;
	pos->prev = newnode;

}

2.11 位置删除

        位置删除也一样很简单,我们多创建两个指针变量存储节点信息,就可以有效避免次序不当导致程序错误的问题。代码如下:

void PosErase(LN* pos)
{
	assert(pos);
	LN* posprev = pos->prev;
	LN* posnext = pos->next;

	free(pos);

	posprev->next = posnext;
	posnext->prev = posprev;
}

 2.12 销毁链表

        在链表使用完以后就要销毁,销毁也比较简单,代码如下:

void DestoryList(LN* phead)
{
	assert(phead);
	LN* cur = phead->next;
	while (cur != phead)
	{
		LN* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

         这样还需要注意的一点,在销毁链表的这个函数里虽然销毁了头节点,但是在头节点创建之初使用了结构体指针,在后续操作中都是通过这个指针将链表传递给函数,所以最后在调用销毁链表函数之后要将指向头节点的指针置为NULL,避免出现野指针。当然这里也可以使用二级指针在函数里将这个指针置为NULL。

 3. 源码

List.c

#include"List.h"
LN* BuyListNode(Datatype x)
{
	LN* newnode = (LN*)malloc(sizeof(LN));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
LN* InItNode()
{
	LN* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}
void PrintList(LN* phead)
{
	assert(phead);
	LN* cur = phead->next;
	printf("phead <->");
	while (cur != phead)
	{
		printf(" %d <->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
void PushBack(LN* phead, Datatype x)
{
	assert(phead);
	LN* tail = phead->prev;
	LN* newnode = BuyListNode(x);

	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}
void PopBack(LN* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LN* tail = phead->prev;
	LN* tailprev = tail->prev;

	tailprev->next = phead;
	phead->prev = tailprev;
	free(tail);
}
void PushFront(LN* phead,Datatype x)
{
	assert(phead);
	/*LN* newnode = BuyListNode(x);
	newnode->next = phead->next;
	phead->next->prev = newnode;

	newnode->prev = phead;
	phead->next = newnode;*/
	LN* newnode = BuyListNode(x);
	LN* first = phead->next;
	phead->next = newnode;
	newnode->prev = phead;

	newnode->next = first;
	first->prev = newnode;

}
void PopFront(LN* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LN* first = phead->next;
	LN* second = first->next;

	free(first);

	phead->next = second;
	second->prev = phead;

}
int Listsize(LN* phead)
{
	assert(phead);
	int sz = 0;
	LN* cur = phead->next;
	while (cur != phead)
	{
		sz++;
		cur = cur->next;
	}
	return sz;
}
LN* ListFind(LN* phead, Datatype x)
{
	assert(phead);
	LN* cur = phead->next;
	while (cur!=phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}
void ListInsert(LN* pos, Datatype x)
{
	assert(pos);
	LN* newnode = BuyListNode(x);
	LN* posprev = pos->prev;

	newnode->next = pos;
	pos->prev = newnode;

	newnode->prev = posprev;
	posprev->next = newnode;
}
void PosErase(LN* pos)
{
	assert(pos);
	LN* posprev = pos->prev;
	LN* posnext = pos->next;

	free(pos);

	posprev->next = posnext;
	posnext->prev = posprev;
}
void DestoryList(LN* phead)
{
	assert(phead);
	LN* cur = phead->next;
	while (cur != phead)
	{
		LN* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

 List.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int Datatype;
typedef struct ListNode
{
	int data;
	struct ListNode* next;
	struct ListNode* prev;
}LN;

LN* BuyListNode(Datatype x);
LN* InItNode();
void PrintList(LN* phead);
void PushBack(LN* phead,Datatype x);
void PopBack(LN* phead);
void PushFront(LN* phead, Datatype x);
void PopFront(LN* phead);
LN* ListFind(LN* phead, Datatype x);
int Listsize(LN* phead);
void ListInsert(LN* pos, Datatype x);
void PosErase(LN* pos);
void DestoryList(LN* phead);

 test.c

#include"List.h"
void test1()
{
	LN* plist = InItNode();
	PushBack(plist, 1);
	PushBack(plist, 2);
	PushBack(plist, 3);
	PushBack(plist, 4);
	PushBack(plist, 5);
	PushFront(plist, 0);
	PopBack(plist);
	ListInsert(plist, 10);
	LN* pos = ListFind(plist, 10);

	ListInsert(pos, 11);

	PosErase(pos);
	PrintList(plist);
	DestoryList(plist);
	plist = NULL;
}
void test2()
{
	LN* plist = InItNode();
	PushBack(plist, 1);
	PushBack(plist, 2);
	PushBack(plist, 3);
	PushBack(plist, 4);
	PushBack(plist, 5);
	//PushFront(plist, 0);

	PopFront(plist);
	PrintList(plist);
}

int main()
{
	test1();
	return 0;
}


 

总结

        带头双向循环链表作为一种数据结构,在解决问题时展现了其独特的优势。通过快速的插入和删除操作,以及灵活的双向遍历和循环遍历能力,它为我们提供了一种高效、灵活的数据组织方式。在实际应用中,我们可以充分发挥带头双向循环链表的特性,优化算法设计,提高程序的效率和可维护性。通过深入学习和掌握带头双向循环链表,我们可以更好地解决实际问题,提升自己的编程能力。希望本文能够帮助读者对带头双向循环链表有更深入的理解,并在实践中得到应用。最后,感谢阅读!文章来源地址https://www.toymoban.com/news/detail-628165.html

到了这里,关于数据结构入门指南:带头双向循环链表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 数据结构入门指南:二叉树

    数据结构入门指南:二叉树

    目录 文章目录 前言  1. 树的概念及结构    1.1 树的概念  1.2 树的基础概念 1.3 树的表示 1.4 树的应用  2. 二叉树 2.1 二叉树的概念  2.2 二叉树的遍历         在计算机科学中,数据结构是解决问题的关键。而二叉树作为最基本、最常用的数据结构之一,不仅在算法和数据

    2024年02月12日
    浏览(8)
  • 【数据结构入门指南】二叉树

    【数据结构入门指南】二叉树

    二叉树是一棵特殊的树。一棵二叉树是结点的一个有限集合,该节点: ①:或者为空。 ②: 由一个根节点加上两棵别称为左子树和右子树的二叉树组成。 从上图可以看出: 二叉树不存在度大于2的结点. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。 Tip

    2024年02月11日
    浏览(8)
  • 【数据结构入门指南】单链表

    【数据结构入门指南】单链表

     由于顺序表插入和删除元素需要移动大量数据,导致运行效率下降。因此引入了另一种数据结构 —— 链表 。链表又分为单链表和双链表。单链表结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在

    2024年02月14日
    浏览(13)
  • C语言笔记 | 数据结构入门指南

    文章目录 0x00 前言 0x01 百鸡百钱 0x1 题目描述 0x2 问题分析 0x3 代码设计 0x4 完整代码 0x5 运行效果 0x6 举一反三 [兔鸡百钱] 0x02 借书方案知多少 0x1 题目描述 0x2 问题分析 0x3 代码设计 0x4 完整代码 0x5 运行效果 0x6 举一反三 [领导小组方案] 0x03 打鱼还是晒网 0x1 题目描述 0x2 问题分

    2024年02月08日
    浏览(7)
  • 数据结构入门指南:单链表(附源码)

    数据结构入门指南:单链表(附源码)

    目录 前言 尾删 头删 查找 位置前插入  位置后插入  位置删除  位置后删除  链表销毁 总结         前边关于链表的基础如果已经理解透彻,那么接下来就是对链表各功能的实现,同时也希望大家能把这部分内容熟练于心,这部分内容对有关链表部分的刷题很有帮助。废话

    2024年02月14日
    浏览(13)
  • Python基础数据结构入门必读指南

    Python基础数据结构入门必读指南

    作者主页:涛哥聊Python 个人网站:涛哥聊Python 大家好,我是涛哥,今天为大家分享的是Python中常见的数据结构。 含义:数组是一种有序的数据结构,其中的元素可以按照索引来访问。数组的大小通常是固定的,一旦创建就不能更改。 基本操作: 含义:列表是Python中内置的

    2024年02月07日
    浏览(23)
  • 【数据结构入门指南】二叉树顺序结构: 堆及实现(全程配图,非常经典)

    【数据结构入门指南】二叉树顺序结构: 堆及实现(全程配图,非常经典)

    普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。 而完全二叉树更适合使用顺序结构存储。   现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储 ,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一

    2024年02月12日
    浏览(10)
  • 【数据结构入门指南】二叉树链式结构的实现(保姆级代码思路解读,非常经典)

    【数据结构入门指南】二叉树链式结构的实现(保姆级代码思路解读,非常经典)

    其他数据结构不同,二叉树的增删查改接口实现的意义不大(后续搜索树的增删查改才有意义)。普通初阶二叉树更重要的是学习控制结构,为后续的AVL树、红黑树等高级数据结构打下基础。同时大部分OJ题也出在此处。 所谓二叉树遍历(Traversal)是按照某种特定的规则,依次

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

    带头双向循环链表--数据结构

    😶‍🌫️😶‍🌫️😶‍🌫️😶‍🌫️Take your time ! 😶‍🌫️😶‍🌫️😶‍🌫️😶‍🌫️ 💥个人主页:🔥🔥🔥大魔王🔥🔥🔥 💥所属专栏:🔥魔王的修炼之路–数据结构🔥 如果你觉得这篇文章对你有帮助,请在文章结尾处留下你的 点赞 👍和 关注 💖,支持一

    2024年02月01日
    浏览(16)
  • 数据结构---带头双向循环链表

    数据结构---带头双向循环链表

    什么是双向带头循环链表? 上面简单的一个非空 带头循环双向链表逻辑图 如何定义一个双向链表? 根据图和代码可以看双向链表就是单链表的每个结点中,在设置一个指向前驱节点的指针 简单认识之后,对他进行初始化(申请一个头节点,让前驱和后驱指针都指向自己) 代码

    2024年02月07日
    浏览(7)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包