【数据结构】—— 双向链表

1、双向链表的概念

双向链表(Doubly Linked List)是一种常见的链表数据结构,它与普通的单向链表不同之处在于,每个节点除了存储数据元素外,还存储着指向前一个节点和后一个节点的指针(或引用)

与单链表的区别:双向链表中可以通过节点本身直接访问其前驱节点和后继节点,而不需要像单向链表那样只能从头节点开始顺序访问。

2、双向链表的接口实现

在这里插入图片描述

2.1结构

  • 双向链表的一个节点由三部分构成
    1)当前节点的数据——date
    2)前驱指针——prev
    3)后驱指针——next
//定义双向链表中节点的结构
typedef int LDateType;
typedef struct ListNode {
	LDateType date;
	struct ListNode* prev;
	struct ListNode* next;
}LNode;

带头双向链表:

  • 双向链表带有哨兵位,插入数据之前必须要初始化哨兵位
  • 哨兵节点(头节点):本身不存储有效数据,它的存在只是为了简化边界条件的处理。

这里我用双向带头循环链表来实现

2.2初始化

申请节点

对申请节点功能进行封装,方便调用。

//申请节点
LNode* LBuyNode(LDateType x) {
	LNode* newnode = (LNode*)malloc(sizeof(LNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	newnode->date = x;
	newnode->next = newnode->prev = newnode;
	return newnode;
}

直接调用申请节点函数创建哨兵位

LNode* LInit()
{
	//创建一个哨兵位
	LNode* phead = LBuyNode(-1);
	return phead;
}

2.3插入数据

尾插

当双向链表进行尾插的时候,无需像单链表那样循环找尾节点,双向链表中的尾节点与哨兵位相连,可以直接获取:

ptail = phead->prev

  • 思路:在修改指针连接时,优先对newnode的前驱和后驱进行修改,避免影响链表原来的指向,随后对ptail的后驱和phead的前驱进行修改
void LPushBack(LNode* phead, LDateType x)
{
	assert(phead);
	LNode* newnode = LBuyNode(x);//申请节点
	//phead phead->prev(ptail) newnode 修改指针连接
	newnode->next = phead;
	newnode->prev = phead->prev;

	(phead->prev)->next = newnode;
	phead->prev = newnode;
}
头插

在这里插入图片描述

与尾插相似,修改对应指针连接即可

void LPushFront(LNode* phead, LDateType x)
{
	assert(phead);
	LNode* newnode = LBuyNode(x);
	// phead newnode phead->next 修改这三个节点的连接
	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode;
	phead->next = newnode;
}
指定位置之后插入数据

对插入数据的位置前驱后驱节点修改指针连接即可
pos-> newnode-> pos->next

void LInsert(LNode* pos, LDateType x) {
	assert(pos);
	LNode* newnode = LBuyNode(x);
	//pos newnode pos->next
	newnode->next = pos->next;
	newnode->prev = pos;

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

2.4删除数据

  • 思路:修改指针连接,free函数释放被删除节点即可
尾删
void LPopBack(LNode* phead) {
	assert(phead);
	//若哨兵位节点的next指针或prev指针指向的是自己,则链表为空
	assert(phead->next != phead);
	LNode* del = phead->prev;
	LNode* prev = del->prev;
	//实现结果一致,无需分情况讨论,
	prev->next = phead;
	phead->prev = prev;
	free(del);
	del = NULL;
}
头删
void LPopFront(LNode* phead) {
	assert(phead);
	assert(phead->next != phead);

	LNode* del = phead->next;
	LNode* next = del->next;
	//改变指针指向,删除指定节点即可
	next->prev = phead;
	phead->next = next;
	free(del);
	del = NULL;
}
指定位置删除
void LErase(LNode* pos) {
	assert(pos);

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

	free(pos);
	pos = NULL;
}

2.5查找

  • 功能 :查找指定节点并返回节点
  • 实现思路:创建节点指针pcur指向链表中第一个节点(phead->next),
    遍历链表,查找指定节点,如果找到直接返回节点,停止条件为当pcur指向哨兵位,当跳出循环则链表中不存在指定节点,返回NULL
LNode* LFind(LNode* phead, LDateType x) {
	assert(phead);
	LNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->date == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

2.6打印

  • 思路:创建节点指针pcur指向链表中第一个节点(phead->next),
    遍历链表,停止条件为当pcur指向哨兵位,当pcur指向哨兵位则遍历完成
void LPrint(LNode* phead) {
	assert(phead);
	LNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->date);
		pcur = pcur->next;
	}
	printf("\n");
}

2.7销毁

  • 思路:通过遍历删除每个有效节点,最后删除哨兵位
void LDestroy(LNode** pphead) {
	assert(pphead);
	//哨兵位不能为空
	assert(*pphead);

	LNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//最后链表只剩哨兵位
	//销毁哨兵位
	free(*pphead);
	*pphead = NULL;
}

3、链表和顺序表的区别

在这里插入图片描述

4、问题与思考

Q:二级指针和一级指针应该什么时候用,如何选择?
在单链表和双向链表中,二级指针和一级指针的选择在于是否需要修改(删除)头节点/头指针(在双向链表中叫哨兵位)
在这里插入图片描述
在实际应用中:

  • 一级指针:直接操作变量和简单数据结构
  • 二级指针:用于处理复杂的数据结构(多级链表等)、动态内存分配以及需要改变指针本身(值)指向的情况

代码文件

.h文件

#pragma once

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

//定义双向链表中节点的结构
typedef int LDateType;
typedef struct ListNode {
	LDateType date;
	struct ListNode* prev;
	struct ListNode* next;
}LNode;

//注意,双向链表带有哨兵位,插入数据之前必须要初始化哨兵位
/*void LInit(LNode** plist);*///初始化哨兵位
LNode* LInit();//无需传参初始化哨兵位(函数里面创建哨兵位)

//申请节点
LNode* LBuyNode(LDateType x);

//不需要改变哨兵位,则不需要传二级指针
void LPushBack(LNode* pphead, LDateType x);//尾插

void LPushFront(LNode* phead, LDateType x);//头插(在第一个有效节点之前插入)

//打印双向链表
void LPrint(LNode* phead);

//头删
void LPopFront(LNode* phead);

//尾删
void LPopBack(LNode* phead);

//指定位置之后插入数据
void LInsert(LNode* pos, LDateType x);

//删除pos位置的数据
void LErase(LNode* pos);

//查找
LNode* LFind(LNode* phead, LDateType x);

//链表销毁
void LDestroy(LNode** pphead);//这里是用二级指针销毁
//推荐一级 ——>(保持接口一致性)

.c文件

#include "List.h"

//void LInit(LNode** pphead)
//{
//	*pphead = (LNode*)malloc(sizeof(LNode));
//	if (*pphead == NULL)
//	{
//		perror("malloc fail!");
//		exit(1);
//	}
//	(*pphead)->date = -1;//该数据无作用
//	(*pphead)->next = (*pphead)->prev = *pphead;//或者NULL
//}

LNode* LInit()
{
	//创建一个哨兵位
	LNode* phead = LBuyNode(-1);
	return phead;
}

//申请节点
LNode* LBuyNode(LDateType x) {
	LNode* newnode = (LNode*)malloc(sizeof(LNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	newnode->date = x;
	newnode->next = newnode->prev = newnode;
	return newnode;
}

//链表的打印
void LPrint(LNode* phead) {
	assert(phead);
	LNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->date);
		pcur = pcur->next;
	}
	printf("\n");
}

//尾插和头插
void LPushBack(LNode* phead, LDateType x)
{
	assert(phead);//双向链表第一个节点不可能为空,无需assert*phead
	LNode* newnode = LBuyNode(x);
	//phead phead->prev(ptail) newnode 修改指针连接
	newnode->next = phead;
	newnode->prev = phead->prev;

	(phead->prev)->next = newnode;
	phead->prev = newnode;
}
void LPushFront(LNode* phead, LDateType x)
{
	assert(phead);
	LNode* newnode = LBuyNode(x);
	// phead newnode phead->next 修改这三个节点的连接
	newnode->next = phead->next;
	newnode->prev = phead;

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

void LPopBack(LNode* phead) {
	assert(phead);
	//若哨兵位节点的next指针或prev指针指向的是自己,则链表为空
	assert(phead->next != phead);
	LNode* del = phead->prev;
	LNode* prev = del->prev;
	//实现结果一致,无需分情况讨论,
	prev->next = phead;
	phead->prev = prev;
	free(del);
	del = NULL;
}

void LPopFront(LNode* phead) {
	assert(phead);
	assert(phead->next != phead);

	LNode* del = phead->next;
	LNode* next = del->next;
	//改变指针指向,删除指定节点即可
	next->prev = phead;
	phead->next = next;
	free(del);
	del = NULL;
}


LNode* LFind(LNode* phead, LDateType x) {
	assert(phead);
	LNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->date == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

void LInsert(LNode* pos, LDateType x) {
	assert(pos);
	LNode* newnode = LBuyNode(x);
	//pos newnode pos->next
	newnode->next = pos->next;
	newnode->prev = pos;

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

void LErase(LNode* pos) {
	assert(pos);

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

	free(pos);
	pos = NULL;
}

void LDestroy(LNode** pphead) {
	assert(pphead);
	//哨兵位不能为空
	assert(*pphead);

	LNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//最后链表只剩哨兵位
	//销毁哨兵位
	free(*pphead);
	*pphead = NULL;
}

测试文件

#include "List.h"

//void ListTest01() {
//	LNode* plist = NULL;
//	LInit(&plist);
//}

void ListTest02() {
	LNode* plist = LInit();//初始化
	//尾插
	LPushBack(plist, 1);
	LPushBack(plist, 3);
	LPushBack(plist, 5);
	LPrint(plist);
}

void ListTest03() {
	LNode* plist = LInit();//初始化
	//尾插
	LPushFront(plist, 1);
	LPushFront(plist, 3);
	LPushFront(plist, 5);
	LPrint(plist);
	LPopBack(plist);
	LPrint(plist);
	LPopBack(plist);
	LPrint(plist);
	LPopBack(plist);
	LPrint(plist);
}

void ListTest04() 
{
	LNode* plist = LInit();//初始化
	//尾插
	LPushFront(plist, 1);
	LPushFront(plist, 3);
	LPushFront(plist, 5);
	LPrint(plist);
	LPopFront(plist);
	LPrint(plist);
	LPopFront(plist);
	LPrint(plist);
	LPopFront(plist);
	LPrint(plist);
}

void ListTest05()
{
	LNode* plist = LInit();//初始化
	//尾插
	LPushFront(plist, 1);
	LPushFront(plist, 3);
	LPushFront(plist, 5);
	LPrint(plist);
	LNode* findRet1 = LFind(plist, 2);
	if (findRet1 == NULL) printf("未找到!\n");
	else printf("找到了!\n");
	LNode* findRet2 = LFind(plist, 3);
	if (findRet2 == NULL) printf("未找到!\n");
	else printf("找到了!\n");
}

void ListTest06()
{
	LNode* plist = LInit();//初始化
	//尾插
	LPushFront(plist, 1);
	LPushFront(plist, 3);
	LPushFront(plist, 5);
	LPrint(plist);
	LNode* findRet1 = LFind(plist, 3);
	LInsert(findRet1, 666);
	LPrint(plist);
	LNode* findRet2 = LFind(plist, 666);
	LErase(findRet2);
	LPrint(plist);
	LDestroy(&plist);
}

int main()
{
	//ListTest01();//由传址的哨兵位初始化
	//ListTest02();//初始化+尾插
	//ListTest03();//头插+尾删
	//ListTest04();//头删
	//ListTest05();//查找
	ListTest06();//在指定位置之后插入数据,删除数据,销毁链表
	return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值