数据结构【双链表】

前言

我们前面学习了单链表(点击这里跳转到单链表博客),那么应该发现了一个问题,就是我每次尾插和尾删都需要先把链表遍历一遍,这样是不是过于麻烦了,这时候我们就可以使用双向链表。

1. 链表的分类

  • 带头和不带头

首先带头就是在链表的头节点前面再添加一个节点,这个节点的next指针指向的是头节点并且不存储任何的数据,就像一个哨兵一样(所以我们也把这个节点成为哨兵位)。
不带头就是我的链表的第一个节点就会存储数据。
在这里插入图片描述

  • 单向和双向

单向就是我只有一个指针变量,只用来存放我下一个节点的地址。
双向就是我在单向的基础上,再加一个指针变量,这个指针变量是用来存放前一个节点的地址。
在这里插入图片描述

  • 循环和不循环

是否循环是看你最后一个节点的next的指向;如果指向NULL,就为不循环;如果指向的是我的头节点或者链表的其他节点,就为循环链表。
在这里插入图片描述

一个链表都是包含这三个元素的,这样下来,前前后后能组成八种链表,其中单链表和双链表是两个极端。
单链表的全名是不带头单向不循环链表
双链表的全名是带头双向循环链表
在这里插入图片描述

2. 双链表的结构

typedef int LTDATATYPE;

typedef struct ListNode
{
	LTDATATYPE data;
	struct ListNode* next; //指向下一个节点
	struct ListNode* prev; //指向前一个节点 prev(previous的缩写)
}LTNode;

3. 双链表接口的实现

头文件(List.h)

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

typedef int LTDATATYPE;

typedef struct ListNode
{
	LTDATATYPE data;
	struct ListNode* next; //指向下一个
	struct ListNode* prev; //指向前一个 prev(previous的缩写)
}LTNode;

//初始化
LTNode* LTInit();
void LTDesTroy(LTNode* phead);

void LTPrint(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);

3.1 创建新节点

LTNode* BuyNode(LTDATATYPE 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;
}

由于这段函数只用来服务该文件内的函数,所以不需要再头节点声明;如果后面需要再其他文件内使用,则再声明到头文件即可。

3.2 双链表的初始化

由于我们这个双链表是带头的,所以我们可以使用函数(创建新节点)来解决

//初始化
LTNode* LTInit()
{
	LTNode* phead = BuyNode(-1);//这里可以传任意的值,我们要使用双链表的时候并不会使用哨兵位的数据
	return phead;
}

创建哨兵位节点时,我们可以给随机的值,我们在使用双链表的时候并不会使用哨兵位的数据。

3.3 尾插

由于我们并不改变哨兵位的地址,因此传一级即可!!!

void LTPushBack(LTNode* phead, LTDATATYPE x)
{
	assert(phead);
	LTNode* NewNode = BuyNode(x);
	
	//用来存放当前链表的尾节点
	LTNode* pcur = phead->prev;

	//改变新节点的指向
	NewNode->prev = pcur;
	NewNode->next = phead;

	pcur->next = NewNode;
	phead->prev = NewNode;
}

具体如下图
在这里插入图片描述

3.4 头插

//头插
void LTPushFront(LTNode* phead, LTDATATYPE x)
{
	assert(phead);
	//头插是在插入在哨兵位的后面,成为哨兵位后的第一个节点
	//不是插入到哨兵位的前面

	LTNode* NewNode = BuyNode(x);
	
	LTNode* ptmp = phead->next;

	NewNode->prev = phead;
	NewNode->next = ptmp;

	ptmp->prev = NewNode;
	phead->next = NewNode;

}

原理和尾插大致相同,但要注意的是头插是在插入在哨兵位的后面,成为哨兵位后的第一个节点,而不是在链表的前面插入节点。
具体如下图:
在这里插入图片描述

3.5 尾删

我们除了判断phead是否为空以外,我们还要判断phead是否只有哨兵位。

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead && phead->prev != phead);

	LTNode* del = phead->prev;

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

	free(del);
	del = NULL;
}

我们先创建一个del来储存链表的尾节点,将del的前节点的next指针指向phead,再将phead的prev的指针指向del的前一个节点。在这里插入图片描述

3.6 头删

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

	LTNode* del = phead->next;

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

	free(del);
	del = NULL;
}

逻辑如下图
在这里插入图片描述

3.7 查找节点

根据节点的数据进行查找,如果找到了,返回被找到的节点;如果找不到,就返回NULL

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

3.8 在指定节点后插入数据

//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDATATYPE x)
{
	assert(pos);

	LTNode* NewNode = BuyNode(x);

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

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

注意:改变原链表的顺序不能变
假设我是先改变了pos的next指针,再改变原pos的下一个节点的指向就会很麻烦
在这里插入图片描述
所以我们先改变pos的下一个节点的prev,再改变pos的next
在这里插入图片描述

3.9 删除指定节点

//删除pos节点
void LTErase(LTNode* pos)
{
	assert(pos && pos->next != pos);

	LTNode* del = pos;

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

	free(del);
	del = NULL;

}

大致和头删一样,先改变前一个节点的指向和先改变后一个节点的指向都可以。

3.10 打印双链表

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

3.11 销毁双链表

void LTDesTroy(LTNode* phead)
{
	assert(phead);
	LTNode* del = phead->next;
	while (del != phead)
	{
		LTNode* pcur = del->next;
		free(del);
		del = pcur;
	}
	//跳出循环后,phead只剩下哨兵位
	free(phead);
	phead = NULL;
}

大体和单链表的销毁类似,就是要在循环结束后把哨兵位给释放掉。

4. 完整代码

头文件

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

typedef int LTDATATYPE;

typedef struct ListNode
{
	LTDATATYPE data;
	struct ListNode* next; //指向下一个
	struct ListNode* prev; //指向前一个 prev(previous的缩写)
}LTNode;


//初始化
LTNode* LTInit();
//销毁
void LTDesTroy(LTNode* phead);
//打印
void LTPrint(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);

源文件

#include"List.h"
//创建新节点
LTNode* BuyNode(LTDATATYPE x)
{
	LTNode* Node = (LTNode*)malloc(sizeof(LTNode));
	Node->data = x;
	Node->next = Node->prev = Node;
	return Node;
}
//初始化
//void LTInit(LTNode** pphead);
LTNode* LTInit()
{
	LTNode* phead = BuyNode(-1);
	return phead;
}

//销毁
void LTDesTroy(LTNode* phead)
{
	assert(phead);
	LTNode* del = phead->next;
	while (del != phead)
	{
		LTNode* pcur = del->next;
		free(del);
		del = pcur;
	}

	free(phead);
	phead = NULL;
}

//打印
void LTPrint(LTNode* phead)
{
	assert(phead != NULL);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d ->", pcur->data);
		pcur = pcur->next;
	}
}


//尾插
void LTPushBack(LTNode* phead, LTDATATYPE x)
{
	assert(phead);
	LTNode* NewNode = BuyNode(x);

	LTNode* pcur = phead->prev;

	NewNode->prev = pcur;
	NewNode->next = phead;

	pcur->next = NewNode;
	phead->prev = NewNode;

}
//头插
void LTPushFront(LTNode* phead, LTDATATYPE x)
{
	assert(phead);
	//头插是在插入在哨兵位的后面,成为哨兵位后的第一个节点
	//不是插入到哨兵位的前面

	LTNode* NewNode = BuyNode(x);
	
	LTNode* ptmp = phead->next;

	NewNode->prev = phead;
	NewNode->next = ptmp;

	ptmp->prev = NewNode;
	phead->next = NewNode;

}

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead && phead->prev != phead);

	LTNode* del = phead->prev;

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

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

	LTNode* del = phead->next;

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

	free(del);
	del = NULL;
}


//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDATATYPE x)
{
	assert(pos);

	LTNode* NewNode = BuyNode(x);

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

	pos->next->prev = NewNode;
	pos->next = NewNode;
}
//删除pos节点
void LTErase(LTNode* pos)
{
	assert(pos && pos->next != pos);

	LTNode* del = pos;

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

	free(del);
	del = NULL;

}

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

结语

最后感谢您能阅读完此片文章,如果有任何建议或纠正欢迎在评论区留言,也可以前往我的主页看更多好文哦(点击此处跳转到主页)。
如果您认为这篇文章对您有所收获,点一个小小的赞就是我创作的巨大动力,谢谢!!!

  • 26
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值