双向链表-----------(c语言)

双向链表-----------(c语言)

前言:

​ 也是有一段时间没有更新博客了,如今来填填坑,今天要讲的内容是双向循环链表。顾名思义,就是有两个方向的链表,并且双向循环,具体是怎么一回事呢。请看下方
在这里插入图片描述

在这里插入图片描述
有两个指针,一个是prev,一个是next。下面介绍它的食用方法

双向循环链表的创建
1.创建

参照上图,在结构体ListNode中创建两个指针。

typedef struct ListNode {
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LNode;
2.初始化

此处用malloc一个新节点,此处形参的改变不会影响实参,因此后面插入链表时会造成链表指针错误,可以用二级指针或者返回值的方式解决,这里我选择用返回值的方式,返回新节点的地址。

//创建节点
LNode* BuyListNode(LTDataType x) {
	LNode* node = (LNode*)malloc(sizeof(LNode));

	node->data = x;
	node->next = NULL;
	node->prev = NULL;

	return node;
}

为了简化代码,并且方便对双向链表的操作,这里我选择使用带哨兵位的结构,当然如果你已经对链表掌握到如火纯情的地步了,可以使用不带哨兵位的结构(要避免空指针还有边界问题)

// 创建返回链表的头结点.
LNode* ListCreate() {
	//创建哨兵节点
	//哨兵节点不存储有效数据
	LNode* phead = BuyListNode(-1);

	phead->next = phead;
	phead->prev = phead;
	//返回phead的地址
	return phead;
}

ok,到这里双向链表的带头哨兵结构也是创建起来了。
在这里插入图片描述

双向循环链表的增删查改
1.插入方式:
1.头插

1.观察双向链表的结构,插入时要修改四个指针,因此我们要格外小心,不要牛头不对马嘴

2.由于双向链表优秀的结构,修改这个结构体时,可以不需要二级指针来操作。借助哨兵节点完成即可

3.四个指针的足以给一些对指针操作不熟悉的人来说是噩梦,因此这里建议跟博主一样,创建三个指针来表达清楚,这样子无论先是那一步,都清晰明了。

4.插入前要检查头节点是否为空

// 双向链表头插
void ListPushFront(LNode* pHead, LTDataType x) {
	assert(pHead);
	LNode* newnode = BuyListNode(x);
	LNode* first = pHead->next;

	//phead newnode first
	pHead->next = newnode;
	newnode->prev = pHead;
	//如果没有节点,下面两条语句相当于第一个插入的节点的地址不变
	newnode->next = first;
	first->prev = newnode;

	//代码复用,在哨兵节点的下一个节点之前插入,即是头插
	ListInsert(pHead->next, x);
}

图解
在这里插入图片描述

只有一个哨兵位节点时插入非常easy,最后把他们整理一下就变成下面这样

在这里插入图片描述

当继续插入节点时,也用上面一样反复循环,这里不做过多探讨,给个高清大图,让你们自己试试看(bushi:是博主懒得画)

在这里插入图片描述
在这里插入图片描述

2.尾插

1.按照链表的经典插入,尾插首先要找尾巴,那么如何找到尾巴呢,观察双向链表的结构可以得出,prev指针指向的地方就是尾巴

2.同样思考只有一个哨兵结点跟有多个尾插是如何操作的

3.尾插前也要检查头结点是否为空

// 双向链表尾插
void ListPushBack(LNode* pHead, LTDataType x) {
	//断言
	assert(pHead);

	LNode* tali = pHead->prev;
	//新结点
	LNode* newnode = BuyListNode(x);
	//修改四个指针,newnode先链接
	newnode->prev = tali;//此处不是pHead,因为是尾插,要从尾结点插入
	tali->next = newnode;

	newnode->next = pHead;
	pHead->prev = newnode;
	//pHead的perv就是尾结点,也是在尾结点后面插入
	//ListInsert(pHead, x);
}

图解

在这里插入图片描述

依旧跟头插一样,继续插入节点时也是类似的效果,自己逝逝看吧(也是懒得画)
在这里插入图片描述

2.删除方式

1.同样,删除前也要检查是否为空(都是空了还删什么)

2.与单链表的删除很类似,头删也要考虑到链表丢失的问题,因此这里推荐用一个指针保存删除节点的下一个节点位置来操作(注:对链表结构了解到炉火纯青的,可以按照自己的想法写)

1.头删
// 双向链表头删
void ListPopFront(LNode* pHead) {
	assert(pHead);
	LNode* cur = pHead->next;//从哨兵节点后面开始删除
	LNode* cur_next = pHead->next->next;//保存要删除节点的下一个节点的位置,防止链表丢失

	pHead->next = cur_next;
	cur_next->prev = pHead;
	free(cur);
}

图解

1.这里可以把cur的next跟prev置空,但是没必要

2.当只剩下一个有效节点时,这个代码也能完美解决,不信你自己逝逝看(因为是双向循环链表,也是懒得画)
在这里插入图片描述

2.尾删

1.同样,尾删之前进行检查,这里还要在判断哨兵节点后面是否还有节点

2.跟尾插一样,设置一个尾指针,这里建议在设置一个尾结点前一个节点的指针,让操作变得更简单,否则操作不当也会导致链表的指针发生不可描述的错误

3.前面已经提到过,尾结点就是哨兵节点的前指针指向的位置

// 双向链表尾删
void ListPopBack(LNode* pHead) {
	assert(pHead);
	assert(pHead->next != pHead);//判断哨兵结点后面是否有结点
	LNode* tail = pHead->prev;
	LNode* tailprev = tail->prev;
	free(tail);
	tailprev->next = pHead;
	pHead->prev = tailprev;
}

图解
在这里插入图片描述

同样,这段代码对于双向循环链表的结构来说,还是很好解决了只剩下一个有效节点如何删除

3.其他操作
1.打印双向链表

1.与单链表不同,双向循环链表遍历时的停止条件略有差异,单链表一般是以NULL作为结束条件,而双向循环链表是以pHead作为停止条件,至于为什么,动动你的脑袋瓜想一想

2.同样也要检查链表是否为空

3.从有效节点开始打印

// 双向链表打印
void ListPrint(LNode* pHead) {
	//断言
	assert(pHead);

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

写完这个后,我们来打印一下头插、尾插、头删、尾删的结果

当然并不是写到这里才开始打印插入跟删除的结果,每写一个函数就要测试

运行结果
头插:

在这里插入图片描述

尾插:

在这里插入图片描述

头删:

void test4() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);

	//头删
	ListPopFront(head);
	ListPrint(head);

	ListPopFront(head);
	ListPrint(head);

	ListPopFront(head);
	ListPrint(head);

	ListPopFront(head);
	ListPrint(head);

	ListPopFront(head);
	ListPrint(head);
}

在这里插入图片描述

尾删:

void test5() {
	//尾插
	LNode* head = ListCreate();

	ListPushBack(head, 1);
	ListPushBack(head, 2);
	ListPushBack(head, 3);
	ListPushBack(head, 4);
	ListPushBack(head, 5);
	ListPrint(head);
	//尾删
	ListPopBack(head);
	ListPrint(head);

	ListPopBack(head);
	ListPrint(head);

	ListPopBack(head);
	ListPrint(head);

	ListPopBack(head);
	ListPrint(head);

	ListPopBack(head);
	ListPrint(head);
}

在这里插入图片描述

2.查找

顾名思义,就是查单链表的数据,并且返回这个链表所在的地址

// 双向链表查找
LNode* ListFind(LNode* pHead, LTDataType x) {
	assert(pHead);
	LNode* pos = pHead->next;

	while (pos != pHead) {
		if (pos->data == x) {
			return pos;
		}
		else {
			pos = pos->next;
		}
	}
}

运行结果

void test7() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);

	//在pos位置插入
	ListPrint(head);
	printf("%p\n", ListFind(head, 3));//输出3的地址
}

这里查找一下3所在节点的地址

在这里插入图片描述

用调试窗口找到3的地址,与输出的一致,说明代码写对了

在这里插入图片描述

3.在pos位置前插入

1.从头插跟尾插可以看出来,在pos位置插入简直易如反掌

// 双向链表在pos的前面进行插入
void ListInsert(LNode* pos, LTDataType x) {
	LNode* posprev = pos->prev;
	LNode* newnode = BuyListNode(x);

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

图解

在这里插入图片描述

运行结果

例如在4的前面插入20,首先要用ListFind找到4的地址,在上面已经介绍了Listfind的写法,这里直接用就行了

void test6() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);
	ListPrint(head);

	//在pos位置插入

	ListInsert(ListFind(head, 4), 20);
	ListPrint(head);
}

在这里插入图片描述

可以看见成功在4的前面插入20

4.删除pos位置的节点

1.删除前也要检查pos位置是否为空

2.由于双向链表指针过多,我们可以在创建多个指针来方便我们识别并且操作,避免不必要的错误

3.同头删跟尾删类似

// 双向链表删除pos位置的节点
void ListErase(LNode* pos) {
	assert(pos);
	LNode* posprev = pos->prev;
	LNode* posnext = pos->next;

	posprev->next = pos->next;
	posnext->prev = posprev;
	free(pos);
}

运行结果

删除4这个节点

void test8() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);
	ListPrint(head);

	//在pos位置插入
	ListErase(ListFind(head, 4));
	ListPrint(head);
}

在这里插入图片描述

非常成功
在这里插入图片描述

5.双向链表的销毁

本质上就是遍历链表一个个删除节点,删除时要注意保存下一个节点地址,这里博主用pnext来保存下一个节点的位置防止链表丢失

// 双向链表销毁
void ListDestory(LNode* pHead) {
	assert(pHead);
	LNode* p = pHead->next;
	free(pHead);
	pHead->next = NULL;
	pHead->prev = NULL;
	while (p != pHead) {
		LNode* pnext = p->next;
		free(p);
		p->next = NULL;
		p->prev = NULL;

		p = pnext;
	}
	printf("SUCCEESFUL DELETE!\n");
}

运行结果

可以看见链表已经被销毁,再次打印时就已经没有任何数据了

void test9() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);
	ListPrint(head);
	//销毁
	ListDestory(head);
	ListPrint(head);
}

在这里插入图片描述

4.代码优化

细心的同学已经注意到,在头插跟尾插这里,有被注释掉的代码,在写出函数ListInsert跟ListErase时,头插、尾插、头删、尾删就可以复用这两个函数来实现,这里我只演示了头插跟尾插

//代码复用,在哨兵节点的下一个节点之前插入,即是头插
ListInsert(pHead->next, x);
//pHead的perv就是尾结点,也是在尾结点后面插入
ListInsert(pHead, x);

头删跟尾删可以自己尝试

在这里插入图片描述

源代码

DList.h

#pragma once
#pragma warning (disable:4996)
#include<stdio.h>
#include<malloc.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode {
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LNode;
//创建节点
LNode* BuyListNode(LTDataType x);
// 创建返回链表的头结点.
LNode* ListCreate();
// 双向链表销毁
void ListDestory(LNode* pHead);
// 双向链表打印
void ListPrint(LNode* pHead);
// 双向链表尾插
void ListPushBack(LNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(LNode* pHead);
// 双向链表头插
void ListPushFront(LNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(LNode* pHead);
// 双向链表查找
LNode* ListFind(LNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(LNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(LNode* pos);

DList.c

#include "DList.h"

//创建节点
LNode* BuyListNode(LTDataType x) {
	LNode* node = (LNode*)malloc(sizeof(LNode));

	node->data = x;
	node->next = NULL;
	node->prev = NULL;

	return node;
}

// 创建返回链表的头结点.
LNode* ListCreate() {
	//创建哨兵节点
	//哨兵节点不存储有效数据
	LNode* phead = BuyListNode(-1);

	phead->next = phead;
	phead->prev = phead;
	//返回phead的地址
	return phead;
}

// 双向链表尾插
void ListPushBack(LNode* pHead, LTDataType x) {
	//断言
	assert(pHead);

	//LNode* tali = pHead->prev;
	新结点
	//LNode* newnode = BuyListNode(x);
	修改四个指针,newnode先链接
	//newnode->prev = tali;//此处不是pHead,因为是尾插,要从尾结点插入
	//tali->next = newnode;

	//newnode->next = pHead;
	//pHead->prev = newnode;
	//pHead的perv就是尾结点,也是在尾结点后面插入
	ListInsert(pHead, x);
}

// 双向链表头插
void ListPushFront(LNode* pHead, LTDataType x) {
	assert(pHead);
	//LNode* newnode = BuyListNode(x);
	//LNode* first = pHead->next;

	phead newnode first
	//pHead->next = newnode;
	//newnode->prev = pHead;
	如果没有节点,下面两条语句相当于第一个插入的节点的地址不变
	//newnode->next = first;
	//first->prev = newnode;

	//代码复用,在哨兵节点的下一个节点之前插入,即是头插
	ListInsert(pHead->next, x);
}

// 双向链表头删
void ListPopFront(LNode* pHead) {
	assert(pHead);
	LNode* cur = pHead->next;
	LNode* cur_next = pHead->next->next;

	pHead->next = cur_next;
	cur_next->prev = pHead;
	free(cur);
}

// 双向链表尾删
void ListPopBack(LNode* pHead) {
	assert(pHead);
	assert(pHead->next != pHead);//判断哨兵结点后面是否有结点
	LNode* tail = pHead->prev;
	LNode* tailprev = tail->prev;
	free(tail);
	tailprev->next = pHead;
	pHead->prev = tailprev;
}

// 双向链表打印
void ListPrint(LNode* pHead) {
	//断言
	assert(pHead);

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

// 双向链表查找
LNode* ListFind(LNode* pHead, LTDataType x) {
	assert(pHead);
	LNode* pos = pHead->next;

	while (pos != pHead) {
		if (pos->data == x) {
			return pos;
		}
		else {
			pos = pos->next;
		}
	}
}

// 双向链表在pos的前面进行插入
void ListInsert(LNode* pos, LTDataType x) {
	LNode* posprev = pos->prev;
	LNode* newnode = BuyListNode(x);

	newnode->next = pos;
	pos->prev = newnode;
	posprev->next = newnode;
	newnode->prev = posprev;
}
// 双向链表删除pos位置的节点
void ListErase(LNode* pos) {
	assert(pos);
	LNode* posprev = pos->prev;
	LNode* posnext = pos->next;

	posprev->next = pos->next;
	posnext->prev = posprev;
	free(pos);
}
// 双向链表销毁
void ListDestory(LNode* pHead) {
	assert(pHead);
	LNode* p = pHead->next;
	free(pHead);
	pHead->next = NULL;
	pHead->prev = NULL;
	while (p != pHead) {
		LNode* pnext = p->next;
		free(p);
		p->next = NULL;
		p->prev = NULL;

		p = pnext;
	}
	printf("SUCCEESFUL DELETE!\n");
}

test.c

#include "DList.h"

void test1() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);

	//头删

	ListPrint(head);
}
void test2() {
	//尾插
	LNode* head = ListCreate();

	ListPushBack(head, 1);
	ListPushBack(head, 2);
	ListPushBack(head, 3);
	ListPushBack(head, 4);
	ListPushBack(head, 5);
	ListPrint(head);
}

void test3() {
	LNode* head = ListCreate();

	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);
	ListPrint(head);

	LNode* pos = ListFind(head, 2);
	ListInsert(pos, 10);
	ListPrint(head);
	LNode* pos1 = ListFind(head, 5);

	ListErase(pos1);
	ListPrint(head);
	ListDestory(head);
	ListPrint(head);
}

void test4() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);

	//头删
	ListPopFront(head);
	ListPrint(head);

	ListPopFront(head);
	ListPrint(head);

	ListPopFront(head);
	ListPrint(head);

	ListPopFront(head);
	ListPrint(head);

	ListPopFront(head);
	ListPrint(head);
}

void test5() {
	//尾插
	LNode* head = ListCreate();

	ListPushBack(head, 1);
	ListPushBack(head, 2);
	ListPushBack(head, 3);
	ListPushBack(head, 4);
	ListPushBack(head, 5);
	ListPrint(head);
	//尾删
	ListPopBack(head);
	ListPrint(head);

	ListPopBack(head);
	ListPrint(head);

	ListPopBack(head);
	ListPrint(head);

	ListPopBack(head);
	ListPrint(head);

	ListPopBack(head);
	ListPrint(head);
}

void test6() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);
	ListPrint(head);

	//在pos位置插入

	ListInsert(ListFind(head, 4), 20);
	ListPrint(head);
}

void test7() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);

	//在pos位置插入
	ListPrint(head);
	printf("%p\n", ListFind(head, 3));
}

void test8() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);
	ListPrint(head);

	//在pos位置插入
	ListErase(ListFind(head, 4));
	ListPrint(head);
}

void test9() {
	//头插
	LNode* head = ListCreate();
	ListPushFront(head, 1);
	ListPushFront(head, 2);
	ListPushFront(head, 3);
	ListPushFront(head, 4);
	ListPushFront(head, 5);
	ListPrint(head);
	//销毁
	ListDestory(head);
	ListPrint(head);
}
int main() {
	test9();

	return 0;
}
总结

基本实现双向循环链表的增删查改,并且还完成了部分代码的优化复用,减少代码量,测试过程出现的诸多问题可以自己解决

在这里插入图片描述

就这样,开溜,如果大佬们发现有什么bug,可以私信找我

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃椰子不吐壳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值