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

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

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

双向链表

  • 深入理解双向链表:从创建到核心操作的完整指南
    • 一、双向链表是什么?和单链表有何不同?
      • 结构对比:单链表 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;
}
相关推荐
Zane19943 分钟前
ClassName() 只是一步?拆开看 __new__ 和 __init__ 各自在干什么
后端·python
geovindu4 分钟前
java: Memento Pattern
java·开发语言·后端·备忘录模式·行为模式
星哥的编程之路7 分钟前
万字深度解析 Agent 学习路线
后端
程序边界10 分钟前
SQL Server数据迁移这件事,远比你想的复杂——但也远比你想的简单(下)
后端
zephyr0512 分钟前
链表实现O(n log n)时间复杂度的排序——归并排序详解
java·数据结构·算法
Zane199412 分钟前
JRE 去哪了?一次讲清楚 JDK、JRE、JVM 到底是什么关系?
java·后端
JavaPub16 分钟前
Ontology 本体论是什么?从哲学概念到 AI、知识图谱与软件工程
后端
liuxiaocheng20 分钟前
聊聊 Vercel AI SDK 的流式协议:前后端到底是怎么"边想边说"的
前端·后端
得物技术32 分钟前
得物知识问答:复合检索 Agent 的系统设计实践
人工智能·后端·ai编程
andongni20342 分钟前
SpringBoot 入门实验报告
java·spring boot·后端