数据结构【双链表】

前言

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

1. 链表的分类

  • 带头和不带头

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

不带头就是我的链表的第一个节点就会存储数据。

  • 单向和双向

单向就是我只有一个指针变量,只用来存放我下一个节点的地址。

双向就是我在单向的基础上,再加一个指针变量,这个指针变量是用来存放前一个节点的地址。

  • 循环和不循环

是否循环是看你最后一个节点的next的指向;如果指向NULL,就为不循环;如果指向的是我的头节点或者链表的其他节点,就为循环链表。

一个链表都是包含这三个元素的,这样下来,前前后后能组成八种链表,其中单链表和双链表是两个极端。

单链表的全名是不带头单向不循环链表

双链表的全名是带头双向循环链表

2. 双链表的结构

c 复制代码
typedef int LTDATATYPE;

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

3. 双链表接口的实现

头文件(List.h)

c 复制代码
#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 创建新节点

c 复制代码
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 双链表的初始化

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

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

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

3.3 尾插

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

c 复制代码
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 头插

c 复制代码
//头插
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是否只有哨兵位。

c 复制代码
//尾删
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 头删

c 复制代码
//头删
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

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

3.8 在指定节点后插入数据

c 复制代码
//在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 删除指定节点

c 复制代码
//删除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 打印双链表

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

3.11 销毁双链表

c 复制代码
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. 完整代码

头文件

c 复制代码
#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);

源文件

c 复制代码
#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;
}

结语

最后感谢您能阅读完此片文章,如果有任何建议或纠正欢迎在评论区留言,也可以前往我的主页看更多好文哦(点击此处跳转到主页)。

如果您认为这篇文章对您有所收获,点一个小小的赞就是我创作的巨大动力,谢谢!!!

相关推荐
非概念25 分钟前
stm32学习笔记----51单片机和stm32单片机的区别
笔记·stm32·单片机·学习·51单片机
iiiiiankor1 小时前
C/C++内存管理 | new的机制 | 重载自己的operator new
java·c语言·c++
小辛学西嘎嘎1 小时前
C/C++精品项目之图床共享云存储(3):网络缓冲区类和main
c语言·开发语言·c++
韭菜盖饭2 小时前
LeetCode每日一题3261---统计满足 K 约束的子字符串数量 II
数据结构·算法·leetcode
无敌最俊朗@2 小时前
stm32学习之路——八种GPIO口工作模式
c语言·stm32·单片机·学习
♡喜欢做梦2 小时前
【数据结构】ArrayList与LinkedList详解!!!——Java
java·开发语言·数据结构·链表
好心的小明3 小时前
【深圳大学】数据结构A+攻略(计软版)
数据结构
青椒大仙KI113 小时前
24/11/13 算法笔记<强化学习> DQN算法
笔记·算法
熬夜学编程的小王3 小时前
【初阶数据结构篇】插入、希尔、选择、堆排序
数据结构·c++·插入排序·选择排序·希尔排序
三小尛3 小时前
快速排序(C语言)
数据结构·算法·排序算法