数据结构 -- 双向链表

谁说我没有死过? 出生以前, 太阳已无数次起落. 悠久的时光被悠久的虚无吞没, 又以我生日的名义卷土重来. --史铁生


正文开始

1. 前言

双向链表是一种常见的数据结构,它与单向链表相比,在存储元素的同时还记录了元素的前驱节点。双向链表可以实现双向遍历,不仅可以从头到尾遍历元素,还可以从尾到头遍历。这种特性使得双向链表在某些场景下更加方便和高效。

在双向链表中,每个节点都有两个指针,一个指向前驱节点,一个指向后继节点。这样,我们可以通过前驱指针和后继指针,方便地进行插入、删除、查找等操作。

在接下来的文章中,我们将详细讨论双向链表的实现和常见操作。我们将逐步介绍双向链表的构造、插入、删除、查找等操作,并给出相应的代码示例。通过学习和理解这些操作,我们可以更好地理解和应用双向链表,提升自己的编程能力。让我们开始吧!

更多知识点击: 酷酷学!!!
更多精彩 不要忘了关注哟~~


2. 双向链表的结构

双向链表与顺序表的区别:

在这里插入图片描述

注意:

这里的“带头”跟前面我们说的“头节点”是两个概念,实际前面的在单链表阶段称呼不严谨,但是为了更好的理解就直接称为单链表的头节点。带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨的”.

“哨兵位”存在的意义:

遍历循环链表避免死循环。

在这里插入图片描述

链表的分类可以分为八种, 但最常见的无非两种:

单链表和双向带头循环链表

在这里插入图片描述

在这里插入图片描述

  1. 无头单向非循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
  2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带来很多优势,实现反⽽简单了,下面我们就来实现带头双向循环链表.

3. 双向链表的实现

第一步:
在头文件中定义和声明需要使用的双向链表结构体和函数的声明, 我们分别创建三个文件, LIst.h 文件主要用来进行类型和函数的声明, List.c文件主要进行函数的实现, test.c用来进行测试文件 :

List.h:

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

typedef int DataType;

typedef struct ListNode
{
	DataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

//初始化
void LTInit(LTNode** pphead);
//LTNode* LTInit();

//尾插
void LTpushBack(LTNode* phead,DataType x);

//打印
void LTprint(LTNode* phead);

//头插

void PushFront(LTNode* phead, DataType x);

//尾删
void PopBack(LTNode* phead);
//头删
void PopFront(LTNode* phead);

//查找
LTNode* LTFind(LTNode* phead, DataType x);

//在指定位置之后插入数据
void LTInsert(LTNode* pos, DataType x);
//删除指定位置数据
void Erase(LTNode* pos);

void LTDestory(LTNode* phead);

第二步: 进行链表的初始化, 首先一个双向带头循环链表需要有一个哨兵位, 也就是头结点, 并且需要循环 , 所以可以在test.c中创建一个指针指向这个头结点, 也可以使用函数返回一个指针指向这个函数. 如下:

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

void LTInit(LTNode** pphead)
{
	*pphead = LTBuyNode(-1);
}

//或者

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


第三步: 实现链表的其他方法:
尾插:
这里不需要传递二级指针, 因为phead指向的位置只能是哨兵位, 不可以发生改变, 申请新的结点, 为了不影响原链表, 先改变新节点的前驱指针和后继指针, 在改变受影响的结点.让新节点next指向头结点, prev指向头结点的前驱结点, 在改变d3, 最后改变头结点.

如图:

在这里插入图片描述

void LTpushBack(LTNode* phead,DataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead;
	newnode->prev = phead->prev;
	phead->prev->next = newnode;//注意这样写不能改变与下一行交换位置
	phead->prev = newnode;
}

可以封装一个打印函数来显示我们的链表, 注意结束条件是遍历到phead前停止

void LTprint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

测试一下:

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"


void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);
	LTpushBack(plist,100);
	LTprint(plist);


}

int main()
{	
	test01();
	return 0;
}

运行结果:

在这里插入图片描述

头插:
将新节点插入到头结点之前是尾插, 插入到头结点之后即尾插, 同样先改变新结点, 在改变受到影响的结点.

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

void PushFront(LTNode* phead, DataType x)
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead->next;
	newnode->prev = phead;
	phead->next->prev = newnode;//这样写也不能和下一行交换位置
	phead->next = newnode;
}

尾删:
为了方便理解, 以及减少代码复杂性, 我们先将要删除的结点保存在del中, 然后将del的前一个结点指向del的下一个结点, 也即头结点, 并且改变头结点的前一个结点指向del的前一个结点

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

void PopBack(LTNode* phead)
{
	assert(phead && phead->next != phead);

	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}

头删:
头删就是删除头结点的下一个结点, 先将要删除的结点保存到del中, 改变del的下一个结点的前驱结点指向头结点, 并且改变头结点的下一个结点指向del的下一个结点.

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

void PopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);

	LTNode* del = phead->next;

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

	free(del);
	del = NULL;
}

查找:
下面将实现一个查找函数, 以便于我们找到指定位置, 用作后续的指定位置的插入和删除, 其实和打印函数的思想基本一致, 只是遍历的途中需要与x值进行对比, 找到了就返回该地址.

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

在指定位置的插入:
此时我们并不需要头结点了, 根据pos的指针域就可以找到要插入的位置, 并且申请新节点, 改变指针域就可以了,代码也非常简单, 逻辑也非常简单

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

void LTInsert(LTNode* pos, DataType x)
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);

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

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

删除指定位置:
和前面的删除思想基本一致, 改变受到影响的结点之后, 销毁此节点.

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

void Erase(LTNode* pos)
{
	assert(pos);

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

	free(pos);
	pos = NULL;
}

最后一步: 销毁结点, 既然动态开辟了内存, 我们就需要手动释放, 因为我们这传递的是一级指针, 所以形参不会影响实参, 在测试代码中需要再次将phead的值置为NULL, 但是我们为了代码的一致性, 传递了一级指针, 遍历链表, 保存下一个结点, 以此销毁, 最后销毁头结点.

在这里插入图片描述

在这里插入图片描述

void LTDestory(LTNode* phead)
{
	assert(phead);

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

4. 完整代码

List.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

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

void LTInit(LTNode** pphead)
{
	*pphead = LTBuyNode(-1);
}

void LTpushBack(LTNode* phead,DataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

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

void LTprint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

void PushFront(LTNode* phead, DataType x)
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);

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

void PopBack(LTNode* phead)
{
	assert(phead && phead->next != phead);

	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}

void PopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);

	LTNode* del = phead->next;

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

	free(del);
	del = NULL;
}

LTNode* LTFind(LTNode* phead, DataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}
void LTInsert(LTNode* pos, DataType x)
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);

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

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

void Erase(LTNode* pos)
{
	assert(pos);

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

	free(pos);
	pos = NULL;
}

void LTDestory(LTNode* phead)
{
	assert(phead);

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

List.h

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

typedef int DataType;

typedef struct ListNode
{
	DataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

//初始化
void LTInit(LTNode** pphead);

//尾插
void LTpushBack(LTNode* phead,DataType x);

//打印
void LTprint(LTNode* phead);

//头插

void PushFront(LTNode* phead, DataType x);

//尾删
void PopBack(LTNode* phead);
//头删
void PopFront(LTNode* phead);

//查找
LTNode* LTFind(LTNode* phead, DataType x);

//在指定位置之后插入数据
void LTInsert(LTNode* pos, DataType x);
//删除指定位置数据
void Erase(LTNode* pos);

void LTDestory(LTNode* phead);

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"


void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);
	LTpushBack(plist,100);
//	PushFront(plist, 200);
//	PushFront(plist, 300);
//
//	LTNode* find = LTFind(plist, 100);
	PopBack(plist);
//	LTInsert(find, 888);
//
//	Erase(find);
//	find = NULL;

	//LTDestory(plist);
	//plist = NULL;
	//plist = NULL;
	LTprint(plist);


}

int main()
{	
	test01();
	return 0;
}

5. 总结

是不是发现双向链表的实现更加容易了呢, 这是因为我们不需要像单链表一样每次都遍历结点, 总的来说,双向链表是一种功能强大的数据结构,拥有插入、删除和反向遍历等额外功能。然而,它也增加了额外的复杂性和内存开销。在选择数据结构时,需要根据具体的场景和需求来权衡使用双向链表的优缺点。


完 , 感谢铁子们的支持 别忘了 关注+点赞 ~~~

  • 53
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

酷酷学!!!

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

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

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

打赏作者

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

抵扣说明:

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

余额充值