双向链表介绍

目录

1. 双向链表的结构

2. 双向链表的实现

初始化哨兵位:

打印链表:

尾插:

头插:

尾删:

头删:

查找:

在指定位置之后插入数据:

删除目标位置的数据:

销毁链表:


1. 双向链表的结构

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

带头双向循环链表: 

注意:这里的“带头”指的是“哨兵位”,哨兵位不存储任何的有效元素。

哨兵位的意义在于,遍历循环链表避免出现死循环。

当链表中只有哨兵位时,称该链表为空链表。

2. 双向链表的实现

根据上图,可以写出双向链表的结构:

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;//后继
	struct ListNode* prev;//前驱
	LTDataType data;
}LTNode;

初始化哨兵位:

老样子使用 malloc() 开辟空间,哨兵位的初始值可以是随便的数据。

双向链表在初始的时候只有哨兵位 phead 一个节点,哨兵位也有它的前驱指针和后继指针。因为双向链表是循环的,所以有:

使用一级指针,形参的改变不能影响实参。使用二级指针的目的就是对实参产生影响。

为了将哨兵位的初始值定义为-1,所以使用二级指针。 

具体代码为:

//List.h

//定义双向链表中节点的结构
typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
}LTNode;

//注意,双向链表是带有哨兵位的,使用之前必须初始化一个哨兵位
void LTInit(LTNode** pphead);
//List.c

void LTInit(LTNode** pphead)
{
	*pphead = (LTNode*)malloc(sizeof(LTNode));
	if (*pphead == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	(*pphead)->data = -1;//初始化一个随便的值
	(*pphead)->next = (*pphead)->prev = *pphead;
}

测试初始化:

 这是传参数的写法,如果不传参数呢?

环形链表的约瑟夫问题 中,通过返回值的形式将链表的头/尾返回。

同样的,在进行双向链表的初始化时,就可以不传参数,通过返回值的形式把创建好的哨兵位返回。

//List.h

LTNode* LTInit();
//List.c

//创建新节点
LTNode* LTBuyNode(LTDataType x) 
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL) {
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

//创建哨兵位
LTNode* LTInit() 
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

打印链表:

//List.c

void LTPrint(LTNode* phead) 
{
	assert(phead);
    //遍历链表
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

 注意:这里pcur 不指向哨兵位是为了避免死循环,所以选择指向哨兵位的下一位。

尾插:

首先要注意,不管插入还是删除都不能影响到哨兵位。

所以,这里将使用一级指针。

//List.h

//不需要改变哨兵位,则不需要传二级指针
//如果需要修改哨兵位的话,则传二级指针
void LTPushBack(LTNode* phead, LTDataType x);

 尾插示意图:

很显然,尾插影响到的节点有:newnode、ptail、哨兵位。

从图中可知,双向链表的尾节点 ptail 就是 head->prev 。

所以,和单链表不同的是,双向链表找尾节点不需要遍历链表!

示例代码:

//List.c

//尾插
void LTPushBack(LTNode* phead, LTDataType x) 
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//顺序为 phead   phead->prev(ptail)  newnode
	newnode->next = phead;
	newnode->prev = phead->prev;

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

 测试尾插:

头插:

注意:头插指的是在第一个有效节点之前插入新节点!

如果在哨兵位之前插入,就相当于尾插!

头插示意图:

很显然,头插影响到的节点有:哨兵位、newnode、head->next。

先将 newnode->next 指向 head->next,newnode->prev 指向 head;

然后再改变 head->next 指向 newnode,head->next->prev 指向 newnode。

让 newnode 成为新的 head->next。

//List.h

//头插
void LTPushFront(LTNode* phead, LTDataType x);
//List.c

//头插
void LTPushFront(LTNode* phead, LTDataType x) 
{
	assert(phead);

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

测试头插:

尾删:

尾删示意图:

很显然,尾删影响到的节点有:ptail(head->prev)、ptail->prev、哨兵位。

先将 head->prev->prev(图中就是d2)->next 指向 head;

然后将 head->prev 指向 head->prev->prev;

最后将原来的尾节点(d3)释放。

//List.h

//尾删
void LTPopBack(LTNode* phead);
//List.c

//尾删
void LTPopBack(LTNode* phead) 
{
	assert(phead);
	//链表为空:只有一个哨兵位节点
	assert(phead->next != phead);

	LTNode* del = phead->prev;
	LTNode* prev = del->prev;

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

	free(del);
	del = NULL;
}

 测试尾删:

头删:

一样的,头删删除的是第一个有效的节点!

头删示意图:

很显然,头删影响到的节点有:head->next->next、head->next、哨兵位。

先将 head->next->next(就是d2)->prev 指向 head;

然后将 head->next 指向 head->next->next;

最后将原来的第一个节点(d1)释放。

//List.h

//头删
void LTPopFront(LTNode* phead);
//List.c

//头删
void LTPopFront(LTNode* phead) 
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* del = phead->next;
	LTNode* next = del->next;

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

	free(del);
	del = NULL;
}

 测试头删:

查找:

很简单,遍历链表,有则返回,无则NULL。

//List.h

//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//List.c

//查找
LTNode* LTFind(LTNode* phead, LTDataType x) 
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

 测试查找:

在指定位置之后插入数据:

插入示意图:

很显然,插入影响到的节点有:pos、pos->next。

但是要注意,如果先将 pos->next 指向 newnode,然后 pos->next->prev 指向 newnode,这个顺序是错误的,会导致找不到原来的 pos->next。

示例代码:

//List.h

//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//List.c

//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x) 
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	
	newnode->next = pos->next;
	newnode->prev = pos;

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

测试插入:

删除目标位置的数据:

删除示意图:

很显然,删除影响到的节点有:pos、pos->next、pos->prev。

示例代码:

//List.h

//删除pos位置的数据
void LTErase(LTNode* pos);
//List.c

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

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

	free(pos);
	pos = NULL;
}

测试删除: 

销毁链表:

链表的销毁其实就是把链表里的数据一个个销毁。

多了要将哨兵位销毁的操作。

示例代码:

//List.h

//销毁
void LTDesTroy(LTNode* phead);
//List.c

void LTDesTroy(LTNode* phead) 
{
	//哨兵位不能为空
	assert(phead);

	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//链表中只有一个哨兵位
	free(phead);
	phead = NULL;
}

自行调试。

  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

YMLT花岗岩

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

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

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

打赏作者

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

抵扣说明:

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

余额充值