一文了解双向链表

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


在这里插入图片描述

前言

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。今天我们就先来学习一下链表中的双向链表。


一、链表分类

在这里插入图片描述
有头(哨兵位):在链表的最前端有一个节点,这个节点的数据没有意义,这个节点充当链表的头部进行维护。
循环:链表的第一个节点和最后一个节点是否相连。
双向,单向:从一个节点如果可以找到前一个节点和后一个节点就是双向,如果只能找到后一个节点,就是单向。
这三个特征进行组合就可以组合出8种链表。而今天的双链表就是有头循环双向的链表。博主上一篇的单链表就是无头不循环单向链表。

二、双向链表是什么?

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
其结构如图
在这里插入图片描述
如下是一个节点的定义,也是下文代码实现中的节点定义

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType* data;//储存数据
	struct ListNode* prev;//指向上一个节点
	struct ListNode* next;//指向下一个节点
}LTNode;

三、功能函数实现

1.申请一个节点

代码如下(示例):

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc");
		exit(1);
	}
	node->data = x;
	node->next = node->prev = node;
	return node;
}

在一个节点中需要两个数据,一个是数值,一个是下一节点的地址。所以我们申请新节点时就传入该节点中的数值(参数),之后使用malloc申请空间并判断是否成功,将data赋值,next指针和prev指针都指向自己(因为这是一个循环结构),最后返回节点地址。
在这里插入图片描述

2.初始化

代码如下(示例):

LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

初始化其实就是设置哨兵位,我们暂时赋-1,这里写成多少都可以。最后返回哨兵位地址,以后的增删改查都与他有关。

3.尾插

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->next=phead;
	newnode->prev = phead->prev;
	phead->prev->next = newnode;
	phead->prev = newnode;
}

在这里插入图片描述
尾插就是将新节点插在最后,但因为链表循环,所以尾插也就是插在头节点前面,首先创建新节点,再将新节点的next指针指向头节点,其prev指针指向原本头节点前面的节点。完成后将原本头节点前一个节点的节点的next指向新节点,将头节点的prev指向新节点。

4.头插

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = phead->next;
	newnode->prev = phead;
	phead->next->prev = newnode;
	phead->next = newnode;
}

在这里插入图片描述
头插的位置如图是head和d1节点中间的位置,其插入逻辑和尾插一样就不再赘述,主要理解头插的位置。

5.尾删

void LTPopBack(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}

在这里插入图片描述

尾删实际上就是删除头节点的上一个节点,这里我们定义一个del指针记录删除节点的地址方便使用。首先先将删除节点的前一个节点的next指针指向头节点,再将头节点的prev指针指向新尾节点,这样就摘除了原来的尾节点,最后通过del释放尾节点。

6.头删

void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;
	free(del);
	del = NULL;
}

在这里插入图片描述
头删的位置如图是d1节点的位置,其插入逻辑和尾删一样就不再赘述,主要理解头删的位置。

7.在指定位置后插入

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next->prev = newnode;
	pos->next = newnode;
}

在这里插入图片描述
在指定位置处插入,其逻辑如图所示,因为这一操作需要调整d1,d2,newnode三个节点,所以我们先调整新节点的指针指向,因为对新指针的更改不影响原链表。接着将pos下一个节点prev指向新节点,pos前一个节点next指向新节点。

8.删除指定位置数据

void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* del = pos;
	del->next->prev = del->prev;
	del->prev->next = del->next;
	free(del);
	del = NULL;
}

在这里插入图片描述
删除指定位置的逻辑和插入指定数据与尾删相似,可以类比理解。

9.查找

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;
}

查找是比较容易的函数,只要遍历整个链表并进行比较即可,但注意循环停止的条件,当pcur遍历一遍链表回到头结点时,循环结束。

10.销毁

void LTDesTroy(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}

销毁和整体逻辑就是一边遍历,一边一个一个的释放节点,还是注意循环停止条件和避免出现空指针。

四、整体代码

1.头文件

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>


typedef int LTDataType;
typedef struct ListNode
{
	LTDataType* data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

LTNode* LTInit();
void LTDesTroy(LTNode* phead);
//插入数据之前,链表必须初始化到只有一个头结点的情况
//不改变哨兵位的地址,因此传一级即可
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);


//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErase(LTNode* pos);
LTNode* LTFind(LTNode* phead, LTDataType x);

总结

以上就是作者对单链表的一些理解和介绍,希望看到这篇文章的朋友们可以积极评价,还请一键三连。
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值