您现在的位置是:首页 >其他 >数据结构——实现双向链表网站首页其他

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

荣智码路 2024-07-01 11:59:58
简介数据结构——实现双向链表

?前言

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

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

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

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

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

  • 先简单看看这个逻辑图,整体的节点之间的联系在图中就能表现的非常清楚了。
    在这里插入图片描述
  • 这一步还是一如既往的相同,所需头文件的包含和结构体的定义。
    具体代码:
//带头双向循环链表
#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("
");
}

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

?判空

  • 这里的判空不是判断有没有数据,而是判断有没有节点,若有节点,说明不为空,就返回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("
");
}

//判空
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;
}

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

?写在最后

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

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

最后感谢各位阅读本小白的博客,希望能帮助到大家!也请大家严厉指出并纠正我在文章中的错误。?

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。