【数据结构】双向链表的实现

深入理解双向链表:从创建到核心操作的完整指南

双向链表是数据结构中极具代表性的链式结构,它解决了单链表 "单向遍历" 的局限,让数据操作更灵活。本文将带你从概念理解代码实现 ,彻底掌握双向链表的创建、初始化以及头插、尾插、头删、尾删等核心操作。

双向链表

  • 深入理解双向链表:从创建到核心操作的完整指南
    • 一、双向链表是什么?和单链表有何不同?
      • 结构对比:单链表 vs 双向链表
    • 二、双向链表的节点结构定义
    • 三、双向链表的初始化与创建
    • 四、核心操作 1:尾插(在链表尾部插入节点)
    • 五、核心操作 1:头插(在链表头部插入节点)
    • 六、核心操作 4:尾删(删除链表尾部节点)
    • 七、核心操作 3:头删(删除链表头部节点)
  • 八、查找节点(搭配指定位置添加和删除)
  • 九、指定位置尾插
  • 十、指定位置头插
  • 十一、指定位置删除

一、双向链表是什么?和单链表有何不同?

如果把单链表比作 "单向行驶的火车"(只能从车头到车尾),那双向链表就是 "双向行驶的高铁"------ 它的每个节点不仅能指向 "下一个节点",还能指向 "前一个节点"。

结构对比:单链表 vs 双向链表

  • 单链表节点:仅包含 "数据域" 和 "指向下一节点的指针域"。
  • 双向链表节点 :包含 "数据域"、"指向下一节点的指针域(next)"、"指向前一节点的指针域(prev)"。

这种结构让双向链表具备两大核心优势:

  • 双向遍历:既能从前往后找,也能从后往前找。
  • 插入 / 删除效率更高 :无需像单链表那样遍历找前驱节点,通过prev指针可直接定位。

二、双向链表的节点结构定义

首先定义双向链表的节点结构,这是实现所有操作的基础:

头文件:

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;    //后驱指针:指向后一个节点
}ListNode;

三、双向链表的初始化与创建

初始化的目标是创建一个空链表 ,让头、尾指针都指向NULL,长度置为 0。

形式:

C 复制代码
//初始化
void ListInit(ListNode** PPhead);

实现函数:

C 复制代码
//申请节点
ListNode* LTBuyNode(LTDataType x)
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (node == NULL)
	{
		perror("maloc fail!");
		exit(1);
	}
	node->data = x;
	//双向链表的节点要自己指向自己
	node->next = node->prev = node;

	return node;
}

//初始化
void ListInit(ListNode** PPhead)
{
	//给链表创建一个哨兵位
	*PPhead = LTBuyNode(-1);

}

四、核心操作 1:尾插(在链表尾部插入节点)

尾插的逻辑是:新节点成为 "新尾",原尾节点成为新节点的prev,新节点成为原尾节点的next

形式:

C 复制代码
//尾删
void ListPopBack(ListNode* Phead);
//由于不能改变哨兵位,使用不用传入节点的地址,预防改变哨兵位

函数实现:

C 复制代码
//尾删
void ListPopBack(ListNode* Phead)
{
	assert(Phead && Phead->next);

#if 0
	//方案1:
	
	//让被删除的上一个节点,指向头节点
	Phead->prev->prev->next = Phead;

	//指向好了,就把尾节点释放掉
	ListNode* scr = Phead->prev;
	free(scr);
	scr = NULL;

	//让头节点的尾指向被删除的上一个节点
	Phead->prev = Phead->prev->prev;
#endif

#if 1
	//方案2:

	//创建一个被删除节点的变量
	ListNode* del = Phead->prev;

	//Phead del->prev del
	del->prev = Phead;
	Phead->next = del->prev;

	//释放掉删除的节点
	free(del);
	del = NULL;
#endif
}

五、核心操作 1:头插(在链表头部插入节点)

头插的逻辑是:新节点成为 "新头",原头节点成为新节点的next,新节点成为原头节点的prev

形式:

C 复制代码
//头插
void ListPushFront(ListNode* Phead, LTDataType x);

函数实现:

C 复制代码
//头插
void ListPushFront(ListNode* Phead, LTDataType x)
{
	assert(Phead);

	ListNode* newnode = LTBuyNode(x);

	//与尾插的思维相同,画图分析
	newnode->next = Phead->next;
	newnode->prev = Phead;

	//需改变的节点:Phead newnode Phead->next;
	//两行代码不能完全交换
	Phead->next->prev = newnode;
	Phead->next = newnode;
}

六、核心操作 4:尾删(删除链表尾部节点)

尾删的逻辑是:将尾指针前移一位,同时断开原尾节点的prevnext,并释放内存。

C 复制代码
//尾删
void ListPopBack(ListNode* Phead);

函数实现:

C 复制代码
//尾删
void ListPopBack(ListNode* Phead)
{
	assert(Phead && Phead->next != Phead);

#if 0
	//方案1:
	
	//让被删除的上一个节点,指向头节点
	Phead->prev->prev->next = Phead;

	//指向好了,就把尾节点释放掉
	ListNode* scr = Phead->prev;
	free(scr);
	scr = NULL;

	//让头节点的尾指向被删除的上一个节点
	Phead->prev = Phead->prev->prev;
#endif

#if 1
	//方案2:

	//创建一个被删除节点的变量
	ListNode* del = Phead->prev;

	//Phead del->prev del
	Phead->prev = del->prev;
	del->prev->next = Phead;

	//释放掉删除的节点
	free(del);
	del = NULL;
#endif
}

七、核心操作 3:头删(删除链表头部节点)

头删的逻辑是:将头指针后移一位,同时断开原头节点的prevnext,并释放内存。

形式:

C 复制代码
//头删
void ListPopFront(ListNode* Phead);

函数实现:

C 复制代码
//头删
void ListPopFront(ListNode* Phead)
{
	assert(Phead && Phead->next != Phead);

#if 0
	//让头节点指向被删除的下一个节点
	//1.必须先把被删除的下一个节点用指针保存起来
	//2.因为在释放内存时,空指针不能解引用
	ListNode* PheadNext = Phead->next->next;

	//手动释放被删除的空间
	//1.将第一个节点释放时,需要一个指针接收
	//2.因为在释放时,不用指针接收的地址释放,就会产生未初始化的指针解引用
	ListNode* scr = Phead->next;
	free(scr);
	scr = NULL;

	//让头节点指向被删除的下一个节点
	Phead->next = PheadNext;


	//让被删除的下一个节点,指向头节点
	Phead->next->next->prev = Phead;
#endif

#if 1
	ListNode* del = Phead->next;

	//Phead del->next del
	//指向第二个节点
	Phead->next = del->next;
	//指向哨兵位
	del->next->prev = Phead;

	//手动释放删除的节点
	free(del);
	del = NULL;
#endif
}

八、查找节点(搭配指定位置添加和删除)

查找节点的逻辑是:循环遍历双向链表,如果节点中的数据等于要找的数据,就返回当前地址,否则返回NULL

形式:

C 复制代码
//查找节点
ListNode* ListFind(ListNode* Phead, LTDataType x);

函数实现:

C 复制代码
//查找节点
ListNode* ListFind(ListNode* Phead, LTDataType x)
{
	ListNode* pcur = Phead->next;

	while (pcur != Phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

九、指定位置尾插

指定位置尾插,无论插入的位置在哪都不会影响该结果,包括尾插也一样 。函数实现可以查考尾插

形式:

C 复制代码
//指定位置之后插入数据
void ListInsert(ListNode* pos, LTDataType x);

函数实现:

C 复制代码
//指定位置之后插入数据
void ListInsert(ListNode* pos, LTDataType x)
{
   assert(pos);

   //接收新节点
   ListNode* newnode = LTBuyNode(x);

   //让新节点指向pos节点
   newnode->prev = pos;
   //让新节点指向pos前一个节点
   newnode->next = pos->next;

   //让pos节点前一个节点的后面指向新节点
   pos->next->prev = newnode;
   //让pos节点指向新节点
   pos->next = newnode;
}

十、指定位置头插

指定位置头插其实和指定位置尾插很类型,将条件改成相反即可

形式:

C 复制代码
//指定位置之前插入数据
void ListInsertend(ListNode* pos, LTDataType x);

函数实现:

C 复制代码
//指定位置之前插入数据
void ListInsertend(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* newnode = LTBuyNode(x);

	//让新节点前面指向pos节点
	newnode->next = pos;
	//让新节点后面指向pos后一个节点
	newnode->prev = pos->prev;

	//让pos后一个节点的前面指向新节点
	pos->prev->next = newnode;
	//让pos后一个指向新节点
	pos->prev = newnode;
}

十一、指定位置删除

形式:

C 复制代码
//删除指定节点
void ListPop(ListNode* pos);

函数实现:

C 复制代码
//删除指定节点
void ListPop(ListNode* pos)
{
	assert(pos);

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

	free(pos);
	pos = NULL;
}
相关推荐
Apifox3 小时前
如何在 Apifox 中使用 OpenAPI 的 discriminator?
前端·后端·测试
ol木子李lo3 小时前
Doxygen入门指南:从注释到自动文档
c语言·c++·windows·编辑器·visual studio code·visual studio·doxygen
朝新_3 小时前
【SpringBoot】玩转 Spring Boot 日志:级别划分、持久化、格式配置及 Lombok 简化使用
java·spring boot·笔记·后端·spring·javaee
一 乐3 小时前
二手车销售|汽车销售|基于SprinBoot+vue的二手车交易系统(源码+数据库+文档)
java·前端·数据库·vue.js·后端·汽车
我不是彭于晏丶3 小时前
238. 除自身以外数组的乘积
数据结构·算法
用户5965906181344 小时前
在asp.net 控制器传入json对象的格式验证的几种方法
后端
代码雕刻家4 小时前
1.6.课设实验-数据结构-栈、队列-银行叫号系统2.0
c语言·数据结构
国服第二切图仔4 小时前
Rust入门开发之Rust中如何实现面向对象编程
开发语言·后端·rust
yq14682860904 小时前
C (统计二进制中“1“的个数)
c语言·开发语言·算法