数据结构——实现双向链表

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

🆒前言

  • 怎么说呢?光乍一听名字好像很难的样子是吧,那如果你这样认为的话,可就要让你大跌眼镜了哦,其实双向带头循环链表从操作和理解上来说都是要易于单项不带头不循环链表(俗称单链表)的。

  • 咱们就来见识见识吧!希望真的能让你们“大跌眼镜”哈!

  • 双向带头循环链表是一种最为常见的数据结构之一,非常适合于需要常操作插入、删除等操作的情况。其基本结构和单向链表相似,不同的是除了对前驱结点的引用,还有对后继结点的引用。

  • 在循环链表中,表头和表尾是相连的,形成了一个闭环。而带头指针则是为了方便操作而加入的,同时便于对空链表的判断。由于双向循环链表可以双向循环遍历,因此它的操作非常灵活,可以很容易地修改链表,使得其更加高效、稳定。

😄带头双向循环链表的结构体搭建和初始化的操作

  • 先简单看看这个逻辑图,整体的节点之间的联系在图中就能表现的非常清楚了。
    数据结构——实现双向链表
  • 这一步还是一如既往的相同,所需头文件的包含和结构体的定义。
    具体代码:
//带头双向循环链表
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

//数据的类型
typedef int LTDataType;
// 结构体的定义
typedef struct ListNode
{
	// 指向下一个节点的指针
	struct ListNode* next;
	// 指向前一个节点的指针
	struct ListNode* prev;
	// 储存数据
	LTDataType data;
}LTNode;
  • 各个函数的的声明
    具体代码:
//创建返回链表的头结点
LTNode* ListCreate();

// 双向链表的节点申请
LTNode* BuyLTNode(LTDataType x);

// 双向链表的初始化
LTNode* LTInit();

// 双向链表的打印
void LTPrint(LTNode* phead);

// 双向链表的判空
bool LTEmpty(LTNode* phead);

// 双向链表的销毁
void LTDestroy(LTNode* phead);

// 双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x);

// 双向链表的头插
void LTPushFront(LTNode* phead, LTDataType x);

// 双向链表的尾删
void LTPopBack(LTNode* phead);

// 双向链表的头删
void LTPopFront(LTNode* phead);

// 双向链表的查找
LTNode* LTFind(LTNode* phead, LTDataType x);

// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x);

// 删除pos位置的节点
void LTErase(LTNode* pos);

🐻创造一个哨兵位头结点

  • 哨兵位头节点(sentinel node)是一种特殊的节点,通常用于链表等数据结构中。这个节点不代表任何真实的值,而是在表头部提供了一个便利的占位符,来简化一些操作的实现,从而减少代码的复杂性。
  • 创建哨兵位头节点:
    这个节点的值可以是任意值,但是它的 next 指针应该指向链表的头节点。这个函数会返回一个指向哨兵节点的指针,你可以像使用普通链表一样使用它。但是,注意到哨兵节点不存储任何有用的值,因此,当你插入或者删除元素时,要特别小心喔。
    具体代码:
// 创建返回链表的头结点
LTNode* ListCreate()
{
	// BuyListNode() 该函数是申请一个节点,下面会讲解
	LTNode* head = BuyLTNode(-1);
	return head;
}
  • 这里需要注意一个小细节:上述代码中头结点与哨兵位头结点是等价的。

🐒申请一个节点

  • 因为后面频繁需要用到申请一个节点,所以我们不妨就奖这个功能单独实现一个函数,在后面需要这个功能的时候直接调用就OK啦。
  • 这个功能实际上就是在内存上申请一块空间,这块空间就用作newnode这块空间。
  • 申请这个节点我们可以让每个节点首先指向空,然后再将自己给定的值存入数据中,最后直接返回这个节点的地址即可。(因为这段空间的是在堆上的,所以销毁栈帧不会影响这段空间)
    具体代码:
//申请一个节点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

🐶初始化

  • 这个操作相信大家已经在熟悉不过了,而双向链表也不过如此,就是将链表的头节点的nextprev指向自己。
  • 最后记得返回头结点。
    具体代码:
//初始化
LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

🐱打印

  • 相较于单向链表,它可以通过指向前驱节点的指针实现反向遍历,提高了遍历的效率。
  • 我们定义了一个指针变量 cur,将其初始化为链表头节点的指针 phead。然后,使用 while 循环遍历链表,同时打印节点的值。最后,在每次打印完所有节点后,我们使用 printf 函数输出一个换行符,以便美化输出结果。
    具体代码:
//打印
void LTPrint(LTNode* phead)
{
	assert(phead);

	printf("guard<==>");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

总的来说,打印双向链表并非复杂的操作,只需遍历链表,并打印每个节点的值即可。

🥔判空

  • 这里的判空不是判断有没有数据,而是判断有没有节点,若有节点,说明不为空,就返回false;若无节点,说明为空,就返回true
  • LTEmpty函数接收一个头指针 phead 作为参数,如果 phead 指向空,那么我们认为该双向链表为空,返回头节点。否则链表不为空。
    具体代码:
//判空
bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

🍅销毁

  • 在使用完双向链表之后需要将它销毁以释放占用的内存空间。
  • 由于双向链表中每个节点都有两个指针,销毁该链表时需要遍历整个链表来把每个节点都销毁。
  • 可以定义一个名为LTDestroy的函数来销毁双向链表。该函数接收一个头指针 phead 作为参数,并将每个节点都销毁,最后将头节点也销毁并将头指针置free掉。
    具体代码:
//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}


	free(phead);
}

🐮尾插

  • 因为是尾插,就是在链表的最后一节点后面链接一个节点,我们首先要申请一个节点,然后再找到尾部的节点,在进行链接。
  • 又因为这是循环链表,所以找到尾部节点不就是找到newnodeprev吗,那就变得so easy了。
  • 过程就是可以简单理解为这样:
    数据结构——实现双向链表
    具体代码:
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	//申请一个节点
	LTNode* newnode = BuyLTNode(x);
	//存放最后一个节点的指针
	LTNode* tail = phead->prev;
	//将整个链表链接起来
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

需要注意的是:
在执行这些指针操作时,要确保所有指针都指向正确的节点,否则可能导致程序崩溃或出现其他错误。

🍓头插

  • 首先将新节点的 prev 指针置为NULL,即表示该节点是双向链表的头节点。然后将新节点的 next 指针指向头节点的后继节点。
  • 如果头节点的后继节点不为空,则将其 prev 指针指向新节点;否则,新节点就是双向链表的尾节点。
  • 最后,将头节点的 next 指针指向新节点,完成头插操作。
  • 过程简单的可视化:
    数据结构——实现双向链表
    具体代码:
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	//申请一个节点
	LTNode* newnode = BuyLTNode(x);
	//存放下一个节点
	LTNode* first = phead->next;
	//将链表连接起来
	phead->next = newnode;
	newnode->prev = phead;

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

需要注意的是:
如果不存放当前头节点的下一个节点的地址,那么需要先将新的节点与头节点的下一个节点连接,然后才与头节点连接,因为先与头节点连接的话,就找不到下一个节点了,此时就会连接中断。

🍌尾删

  • 使用指针遍历整个链表,直到找到最后一个节点,然后将其删除。(删除就记得要判空)
  • 删除操作需要改变两个指针值,即前驱节点的 next 指针和待删除节点的 prev指针,同时需要使用 free 函数释放该节点占用的内存空间。
  • 至于如果是空指针咋办,交给assert就好了。其他的情况都是这套操作。
  • 静态过程就放在下面了:
    数据结构——实现双向链表
    具体代码:
//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;

}

🍊头删

  • 首先找到第一个实际节点,并将其存储在指针 phead 中。(记得删除就要判空)
  • 然后,将头节点的 next 指针设置为 pheadnext 指针,以绕过要删除的节点,并修改待删除节点的后继节点的 prev 指针。
  • 最后,使用 free 函数释放该节点占用的内存空间。
    数据结构——实现双向链表
    具体代码:
//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	// 存放当前链表头节点的下一个节点的地址
	LTNode* prev = phead->next;
	// 存放当前链表头节点的下一个节点的下一个节点的地址
	LTNode* next = prev->next;
	//链接
	phead->next = next;
	next->prev = phead;
	//释放
	free(prev);
}

🍐查找

  • 这里的查找我们输入的是数据,然后再链表中寻找data与输入的数据相等的节点,最后返回这个节点的地址。若没有找到,则返回NULL
  • 查找?不就是将整个链表遍历一遍吗?
    上代码:
//查找
LTNode* LTFind(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位置之前插入

  • 这里的插入是指在任意位置的插入,就是在你输入的节点之前插入数据。
  • 竟然时任意位置,自然的这里就会再多一个变量,这时候这里需要传递一个指针(pos)来指向你输入的节点的前面。
  • 再加上咱们前面学习的头插,直接复用就好了,这里就不再多啰嗦了。
  • 然后要记得存放要插入的前一个位置的节点,插入之后记得链接。
    具体代码:
// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

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

🍎删除pos位置的节点

  • 删除任意节点的位置放在这个位置是不是显得格外渺小,与插入类似,就是需要多传递一个变量来接收任意位置节点的地址。
  • 都看到这里了,其他的操作就不必多说了。算了,还是描述一下为好。
  • 当我们想要删除节点 pos 时,首先需要修改其前驱节点的 next 指针,使其指向 pos 的后继节点;再修改其后继节点的 prev 指针,使其指向 pos 的前驱节点。最后,使用 free 函数释放待删除节点占用的内存空间。
    具体代码:
// 删除pos位置的值
void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
}

🍑完整代码

List.h

//带头双向循环链表
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

// 结构体的定义
typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
}LTNode;

//创建返回链表的头结点
LTNode* ListCreate();

// 双向链表的节点申请
LTNode* BuyLTNode(LTDataType x);

// 双向链表的初始化
LTNode* LTInit();

// 双向链表的打印
void LTPrint(LTNode* phead);

// 双向链表的判空
bool LTEmpty(LTNode* phead);

// 双向链表的销毁
void LTDestroy(LTNode* phead);

// 双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x);

// 双向链表的头插
void LTPushFront(LTNode* phead, LTDataType x);

// 双向链表的尾删
void LTPopBack(LTNode* phead);

// 双向链表的头删
void LTPopFront(LTNode* phead);

// 双向链表的查找
LTNode* LTFind(LTNode* phead, LTDataType x);

// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x);

// 删除pos位置的节点
void LTErase(LTNode* pos);

List.c

#include"List.h"

// 创建返回链表的头结点
LTNode* ListCreate()
{
	// BuyListNode() 该函数是申请一个节点,下面会讲解
	LTNode* head = BuyLTNode(-1);
	return head;
}

//申请一个节点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

//初始化
LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

//打印
void LTPrint(LTNode* phead)
{
	assert(phead);

	printf("guard<==>");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

//判空
bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}


	free(phead);
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTInsert(phead, x);

//	LTNode* tail = phead->prev;
//	LTNode* newnode = BuyLTNode(x);
//
//	tail->next = newnode;
//	newnode->prev = tail;
//	newnode->next = phead;
//	phead->prev = newnode;
}

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	/*assert(phead);
	LTNode* newnode = BuyLTNode(x);
	LTNode* first = phead->next;

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

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

	LTInsert(phead->next, x);
}

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTErase(phead->prev);

	/*LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;*/

}

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTErase(phead->next);

	//LTNode* first = phead->next;
	//LTNode* second = first->next;

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

	//free(first);
}

//查找
LTNode* LTFind(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 LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

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

// 删除pos位置的值
void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
}

Test.c

#include"List.h"

void TestList1()
{
	LTNode* plist = LTInit();
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);

	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTDestroy(plist);
	plist = NULL;
}

void TestList2()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTDestroy(plist);
	plist = NULL;
}

void TestList3()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist, 3);
	if (pos)
	{
		LTInsert(pos, 30);
	}
	LTPrint(plist);

	LTDestroy(plist);
	plist = NULL;
}

int main()
{
	//TestList1();
	//TestList2();
	TestList3();

	return 0;
}

有了任意位置的插入和删除,我们可以在其他位置插入和删除操作上调用

😄写在最后

❤希望本篇博客能让大家更好地理解双向链表的基本原理及实现方法,帮助大家在实际开发中更好地使用该数据结构。

💗也特别感谢大家持续的关注和支持,我会继续努力更新更好的博客。

最后感谢各位阅读本小白的博客,希望能帮助到大家!也请大家严厉指出并纠正我在文章中的错误。😄
文章来源地址https://www.toymoban.com/news/detail-468953.html

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

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

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

相关文章

  • 双向链表--C语言实现数据结构

    本期带大家一起用C语言实现双向链表🌈🌈🌈 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的;简单来说,线性表的链式存储结构生成的表,称作“链表”。 每个元素本身由两部分组成: 1、本身的信息,称

    2024年02月04日
    浏览(58)
  • 数据结构双向链表,实现增删改查

            在单链表中,查找直接后继结点的执行时间为O(1),而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可以用 双向链表 。         在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱。 2.1 双向链表结点创建 2.2 双向链表

    2024年02月16日
    浏览(41)
  • 数据结构:双向链表的实现(C实现)

    个人主页 : 个人主页 个人专栏 : 《数据结构》 《C语言》 本篇博客,将要实现的是 带头双向循环链表 ,该结构实现尾插,尾删只需要O(1)的时间复杂度。 其结构如下所示: 既然要实现的链表是双向循环的,那么对于指针域,我们就需要 指向节点上一个的指针 和 指向节点

    2024年02月14日
    浏览(51)
  • 数据结构之List(双向链表)的实现

    方法名 参数 功能 返回 find const T val, int n, listNode * p 区间查找 从p往前数n个节点 指针或NULL const T val, listNode * p 区间查找 从p往前到首节点 const T val 查找 Size void 链表规模 size empty void 判空 bool first void 返回首节点 首节点指针 clear void 清空链表 void insertAsFirst const T val 作为首节

    2024年02月16日
    浏览(46)
  • 【数据结构和算法】使用数组的结构实现链表(单向或双向)

    上文我们通过结构体的结构实现了队列 、以及循环队列的实现,我们或许在其他老师的教学中,只学到了用结构体的形式来实现链表、队列、栈等数据结构,本文我想告诉你的是,我们 可以使用数组的结构实现链表、单调栈、单调队列 目录 前言 一、用数组结构的好处 1.数

    2024年01月20日
    浏览(74)
  • 数据结构 —— 双向链表(超详细图解 & 接口函数实现)

    数据结构 —— 顺序表 数据结构 —— 单链表 数据结构 —— 双向链表 数据结构 —— 队列 数据结构 —— 栈 数据结构 —— 堆 数据结构 —— 二叉树 数据结构 —— 八大排序 数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元

    2024年02月11日
    浏览(42)
  • 【数据结构与算法】之双向链表及其实现!

    ​                                                                                 个人主页:秋风起,再归来~                                                                                             数据结构与

    2024年04月23日
    浏览(42)
  • 【数据结构】—C语言实现双向链表(超详细!)

                                          食用指南:本文在有C基础的情况下食用更佳                                       🔥 这就不得不推荐此专栏了:C语言                                     🍀 双向链表 前 置知识 :单链表      

    2024年02月13日
    浏览(49)
  • 【数据结构与算法】 - 双向链表 - 详细实现思路及代码

    前几篇文章介绍了怎样去实现单链表、单循环链表, 这篇文章主要介绍 双向链表 以及实现双向链表的步骤,最后提供我自己根据理解实现双向链表的C语言代码 。跟着后面实现思路看下去,应该可以看懂代码,看懂代码后,就对双向链表有了比较抽象的理解了,最后自己再

    2024年02月01日
    浏览(39)
  • 【数据结构】C语言实现双向链表(带头结点、循环)

    结点定义: 接口定义: 我们将申请结点的代码封装成函数,方便后续使用 由于是带头结点的双向链表,因此在使用链表前,我们需要对链表进行初始化。 遍历链表,值得说的是,带头结点的双向链表的循环结束条件是 cur != phead 尾插时,需要先找到尾结点,然后将新结点插

    2024年02月03日
    浏览(71)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包