数据结构|双向链表的类型和实现

1.前言

之前的博客介绍并实现了单链表(<-戳它查看详细),本篇博客将介绍双向链表。同样,双向链表也有许多类型,但是本篇博客只挑最实用的一种———双向带头循环链表来实现,其他类型仅做介绍。

2.双向链表的概念

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

简单来说,双向链表就是一个结构体中,有一个变量用来存储数据,另外还有两个结构体指针变量分别指向前一个节点和后一个节点。双向链表相对于单链表,有双向遍历、删除更高效、插入更灵活、逆序等操作更方便的特点(详细原因在最后优缺点中给出)。

3.双向链表的类型

双向链表可以分为普通双向链表、双向带头链表、双向带头循环链表等类型(其实就是叠buff),而本篇博客要实现的双向带头循环链表,虽然名字听起来很长很复杂,但这样的链表在代码实现上更简洁,在实际运用中更方便,这也是本篇博客挑双向带头循环链表来实现的主要原因。

4.双向带头循环链表的实现

4.1.头文件

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

//初始化双链表
ListNode* ListInit();
// 创建返回链表的头结点.
ListNode* ListCreate(LTDataType x);
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

这里我们创建一个叫做SListNode的结构体,并typedef它,这样在后续使用这个结构体时就不用打struct了。同样,在上面我们把int类型typedefSLDataType,这样如果我们要存储其他类型的数据时,只需要把这里的int的改成需要存储的类型,就可以一键更改存储的数据类型了。

在这个结构体中,data用来存储数据,next是指向下一个结构体变量的指针,prev是指向上一个结构体的指针。

4.2.具体功能的实现

这里功能实现的顺序按照上面头文件从上到下的顺序,方便查找。

4.2.1初始化链表

ListNode* ListInit()
{
	ListNode* pHead = ListCreate(-1);
	pHead->next = pHead;
	pHead->prev = pHead;
	return pHead;
}

在初始化链表时,由于我们要实现的是带不存储有效数据的头节点的双向链表,所以这里我们要给头节点(pHead)申请一块内存空间,这里给它赋什么值都可以,毕竟我们不会用它存储有效数据。接着让头节点的next指针和prev指针都指向自己。这样,一个带头双向循环列表就初始化完成了。
图示:
头节点初始化

4.2.2. 创建节点

ListNode* ListCreate(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}

这里我们通过malloc函数向内存申请一个空间,并把这个空间给到newnode变量。注意下方判断newnode是否创建成功的语句是必要的,它能在malloc申请空间失败后(即newnode指向空),向屏幕输出精确的错误原因呢并终止程序。
接着把要传入的x赋给newnodedata,再把两个指向前、后节点的结构体指针制空,一个新节点就创建完成了。这个函数一般是在嵌套在添加数据的函数中的,所以要把newnode作为返回值赋予增添数据时创建的变量。

4.2.3.双向链表的销毁

void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while (cur!=pHead)
	{
		ListNode* nex = cur->next;
		free(cur);
		cur = nex;
	}
	free(pHead);
}

当我们使用完链表且不再需要时,需要释放整个链表开辟的空间。当然如果你的程序已经走向结束,其实不主动销毁链表,内存也会自行回收这块空间。但如果我们的链表是在24小时不间断运行的服务器上使用,销毁链表的操作就尤为重要了。
这里我们创建一个cur变量从头节点的下一个节点开始遍历,将循环的条件设置成cur指向的节点不是头节点为止,这是因为我们创建的链表是循环链表,当cur走到了pHead,就正好说明cur已经走完了一圈。在循环语句中,创建了一个结构体指针变量nex来记录cur的下一个节点,这么做是为了防止释放掉cur所指向的节点的空间后,后续的空间丢失的问题。毕竟链表每个节点的地址都是随机的,如果先释放了节点而不是先连接,那么后续的节点也就无法被找到了。
图示:
释放

4.2.4.打印双向链表

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	printf("哨兵位<=>");
	while (cur != pHead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

既然上面双链表的销毁已经解释了cur的作用,这里不再赘述。
这里的printf("哨兵位<=>")是用来提示这是一个带头节点的链表的,实际使用可以自定义成其他的样式。
效果演示:

效果

4.2.5. 尾插数据

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* tail = pHead->prev;
	ListNode* newnode = ListCreate(x);
	newnode->prev = tail;
	newnode->next = pHead;
	tail->next = newnode;
	pHead->prev = newnode;
}

在进行尾插时,我们首先创建一个结构体指针变量tail指向双链表的最后一个节点,在这里我们就可以看出循环链表的遍历性——因为头节点的prev指向尾节点,所以我们只需要进行一次操作tail=pHead->prev就能直接找到尾节点,时间复杂度为O(1),十分高效。
在单链表中,在前插入和在后插入代码逻辑不同,但双链表的在前面插入和后面插入的逻辑是相同的,并且在进行插入节点时也不用像单链表那样考虑顺序问题,这里的指针指向设置的顺序可以随意排列组合,这也双链表的代码便利性体现之一。
图示:
插入

4.2.6.尾删数据

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->next != pHead);
	ListNode* tail = pHead->prev;
	ListNode* tailPrev = tail->prev;
	tailPrev->next = pHead;
	pHead->prev = tailPrev;
	free(tail);
}

注意,再进行尾删的时候,必须要断言一下双链表是不是空链表(assert(pHead->next != pHead)),如果链表为空,那么头节点的前一个节点指向的是它自己,这时候进行尾删就会把头节点删掉,这样如果想再进行增删查改操作就会越界访问了,所以必须要首先禁止空链表尾删的情况。
图示:
尾删

4.2.7. 头插数据

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* head = pHead->next;
	ListNode* newnode = ListCreate(x);
	newnode->next = head;
	newnode->prev = pHead;
	pHead->next = newnode;
	head->prev = newnode;
}

循环双链表的头插和尾插逻辑是一致的,这里不再赘述。

4.2.8. 头删数据

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->next != pHead);
	ListNode* head = pHead->next;
	ListNode* headNext = head->next;
	pHead->next = headNext;
	headNext->prev = pHead;
	free(head);
}

循环双链表的头删和尾删逻辑也是一致的,这样也不赘述。

4.2.9. 查找数据

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while ( cur!=pHead)
	{
		cur = cur->next;
		if (cur->data == x)
		{
			return cur;
		}
	}
	return NULL;
}

查找数据的逻辑是先在链表中逐个查找各个节点的值是不是等于我们传进来的x,如果有一个节点等于x,就把这个节点作为返回值返回。如果找不到这个值,就返回空指针。(注意,这个实现方法不能找到多个值相同的节点)

4.2.10 插入数据

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* posPrev = pos->prev;

	ListNode* newnode = ListCreate(x);
	posPrev->next = newnode;
	pos->prev = newnode;
	newnode->next = pos;
	newnode->prev = posPrev;
}

这里的插入数据必须要先使用上一个SListFind函数查找到需要插入的节点位置(pos),这也是上一个函数把节点指针作为返回值的原因,并且这里会用assert断言判断pos是否有效,在SListFind函数中我们设置了找不到就返回空指针,若我们把找不到的位置传入这个函数就会报错提示。
注意,这里的插入是在pos之前插入。
图示:
插入

4.2.11删除中间数据

void ListErase(ListNode* pos)
{
	assert(pos);
	assert(pos->next != pos);
	ListNode* posNext = pos->next;
	ListNode* posPrev = pos->prev;

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

删除中间数据的代码也十分简单,这里也不多赘述了。

4.3.双链表的优缺点

4.3.1.优点:

双向遍历:
可以方便地进行双向遍历,从头到尾或者从尾到头都很容易实现。这对于某些问题,如需要反向遍历或在给定节点前后插入新节点等情况,是很有用的。

删除操作更高效:
在双向链表中,删除节点时,如果已知节点的前一个和后一个节点,删除操作更加高效。因为在单链表中,为了删除一个节点,你通常需要遍历链表找到待删除节点的前一个节点,而在双向链表中,你可以直接通过两个指针完成删除操作。

插入操作更灵活:
在双向链表中,插入一个节点变得更加灵活,因为你可以直接访问前一个节点。在单链表中,如果要在某个节点后插入一个新节点,你需要保留对前一个节点的引用,而在双向链表中,你可以直接使用前一个节点的指针。

方便逆序操作:
有时候需要对链表进行逆序操作,这在双向链表中变得更加方便。因为你可以直接通过前向指针遍历链表,而在单链表中,逆序遍历就变得相对麻烦。

4.3.2.缺点:

空间开销大: 每个节点需要存储两个指针,一个指向前一个节点,一个指向后一个节点。这导致双向链表相对于单链表有更大的空间开销。

4.4. 完整代码在这里

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

//初始化双链表
ListNode* ListInit();
// 创建返回链表的头结点.
ListNode* ListCreate(LTDataType x);
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);



ListNode* ListInit()
{
	ListNode* pHead = ListCreate(-1);
	pHead->next = pHead;
	pHead->prev = pHead;
	return pHead;
}

ListNode* ListCreate(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}

void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while (cur!=pHead)
	{
		ListNode* nex = cur->next;
		free(cur);
		cur = nex;
	}
	free(pHead);
}

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	printf("哨兵位<=>");
	while (cur != pHead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* tail = pHead->prev;
	ListNode* newnode = ListCreate(x);
	newnode->prev = tail;
	newnode->next = pHead;
	tail->next = newnode;
	pHead->prev = newnode;
}

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->next != pHead);
	ListNode* tail = pHead->prev;
	ListNode* tailPrev = tail->prev;
	tailPrev->next = pHead;
	pHead->prev = tailPrev;
	free(tail);
}

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* head = pHead->next;
	ListNode* newnode = ListCreate(x);
	newnode->next = head;
	newnode->prev = pHead;
	pHead->next = newnode;
	head->prev = newnode;
}

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->next != pHead);
	ListNode* head = pHead->next;
	ListNode* headNext = head->next;
	pHead->next = headNext;
	headNext->prev = pHead;
	free(head);
}

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while ( cur!=pHead)
	{
		cur = cur->next;
		if (cur->data == x)
		{
			return cur;
		}
	}
	return NULL;
}

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* posPrev = pos->prev;

	ListNode* newnode = ListCreate(x);
	posPrev->next = newnode;
	pos->prev = newnode;
	newnode->next = pos;
	newnode->prev = posPrev;
}

void ListErase(ListNode* pos)
{
	assert(pos);
	assert(pos->next != pos);
	ListNode* posNext = pos->next;
	ListNode* posPrev = pos->prev;

	posNext->prev = posPrev;
	posPrev->next = posNext;
	free(pos);
}
  • 35
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值