【面试高频数据结构(四)】--《从单链到双链的进阶,读懂“双向奔赴”的算法之美与效率权衡》

🔥@晨非辰Tong:个人主页

👀专栏:《C语言》《数据结构与算法》《数据结构与算法刷题集》

💪学习阶段:C语言、数据结构与算法初学者

⏳"人理解迭代,神理解递归。"


引言:征服了单链表,却总感束缚?无法回溯的遍历、繁琐的节点删除...是时候解锁双向链表,体验指针自由穿梭的真正力量。


目录

一、链表的分类、说明

[1.1 带头、不带头](#1.1 带头、不带头)

[1.2 单向、双向](#1.2 单向、双向)

[1.3 循环、不循环](#1.3 循环、不循环)

二、双向链表介绍及基本功能

[2.1 基本定义、结构](#2.1 基本定义、结构)

[2.2 "哨兵位"的初始化](#2.2 “哨兵位”的初始化)

[2.3 双链表尾插](#2.3 双链表尾插)

[2.4 双链表头插](#2.4 双链表头插)

[2.5 双链表尾删](#2.5 双链表尾删)


一、链表的分类、说明

--根据表格知道:链表的组合共有8种。那么具体是什么?

1.1 带头、不带头

--"头节点":一般是指不存储任何有效数据的节点。

--在前面的学习中,也说过"头节点"或者"首节点",但所谓的节点是存储着有效数据 的节点,是为了好说明

--这里的头指的是前面刷题提到过的**"哨兵位"------>不存储任何有效数据 ,只是用来指向的第一个节点**(存储有效数据)。

1.2 单向、双向

--对比可以看到,双向链表比单向链表多一个指针(称为前驱指针)------>存储上一个节点地址,都有的指针在双链表称为后继指针。

--对于这样三个指针的节点结构:1指针-->前驱指针,2指针-->存储数据值,3指针-->后继指针。

1.3 循环、不循环

--对于前面所学单链表,到了尾节点就会指向NULL------>这就是不循环。

--但是对于循环的链表------>尾节点的后继指针(next)会指向第一个节点。(这样循环往复)

虽然说了有8种链表,但是常用 的组合为:不带头单向不循环链表 和 带头双向循环链表。

--下面见识一下双向链表。


二、双向链表

2.1 基本信息介绍

2.1 定义、结构

所谓双向链表:就如上面图示一样------>首先有头节点,然后存在着前驱指针指向前一个节点-->这就是双向,最后尾节点的后继指针指向头节点形成循环。

cpp 复制代码
DList.h文件
#include <stdio.h>
#include <stdlib.h>

//定义双链表基本结构(节点)
typedef int DLTDataType;
typedef struct DListNode
{
	DLTDataType data;
	struct DListNode* next;//指向下一个节点
	struct DListNode* prev;//指向前一个节点
}DLTNode;

2.2 "哨兵位"的初始化(准备工作)

--在实现链表的功能时,首先实现空链表:先将"哨兵位"的结构整出来。

双链表最开始为空 ,意味着"哨兵节点"始终指向自己(不是连"哨兵节点"都没有)------>前驱指针、后继指针都指向头节点本身。

cpp 复制代码
DList.c文件
#include "DList.h"

//定义_初始化
void DLTInit(DLTNode** pphead)//二级指针接收"哨兵位"地址-->改变结构(test.c传的头节点)
{
	//申请空间
	*pphead = (DLTNode*)malloc(sizeof(DLTNode));//*pphead 是 phead
	//判断非空
	if (*pphead == NULL)
	{
		perror("malloc");
		return 1;//申请失败
	}

	(*pphead)->data = -1;//随便一个值,后续不用
	(*pphead)->next = (*pphead)->prev = *pphead;//指向自己
}

--"哨兵节点"初始化时的数据指针存储什么数据?

:随便放什么值,以后也不会用到 head->data。

cpp 复制代码
test.c文件
#include "DList.h"

//测试
void test01()
{
	DLTNode* plist = NULL;//不是双向链表,初始化
	//初始化
	DLTInit(&plist);//传址
}

int main()
{
	test01();
	return 0;
}

--可以看到:因为是传址调用,实参(plist)与形参(*pphead)节点结构一致,初始化成功。

2.2 链表基本功能实现

2.2.1 双链表尾插

--要尾插新节点,就要改变多处指针的指向。

--进行改变,先对newnode部分进行(若先变head、d3,导致指向变化,newnode无从下手),两图对照写。

--先将创建新节点定义出来:封装函数

cpp 复制代码
DList.c文件
#include "DList.h"

//定义_由数值创建新节点
DLTNode* DLTBuyNode(DLTDataType x)
{
	//申请空间
	DLTNode* newnode = (DLTNode*)malloc(sizeof(DLTNode));
	//判断非空
	if (newnode == NULL)
	{
		perror("malloc");
		return 1;//申请失败
	}

	newnode->data = x;
	newnode->next = newnode->prev = newnode;//新节点先指向自己
	return newnode;
}

--新申请的节点,刚开始前驱指针、后继指针都要指向自己,在后续的指向改变中完成变化。

cpp 复制代码
DList.c文件
#include "DList.h"

//定义_尾插
void DLTPushBack(DLTNode* phead, DLTDataType x)//一级指针,只需要知道是哪个链表
{
	//断言
	assert(phead);
	//创建新节点
	DLTNode* newnode = DLTBuyNode(x);
	//先对newnode进行改变,防止phead、尾节点指向改变
	newnode->prev = phead->prev;
	newnode->next = phead;

	phead->prev->next = newnode;//->prev->next代表尾节点next
	phead->prev = newnode;
}
cpp 复制代码
test.c文件
#include "DList.h"


void test01()
{
	DLTNode* plist = NULL;//不是双向链表,初始化

	//初始化
	DLTInit(&plist);//传址

	//尾插
	DLTPushBack(plist, 1);
	DLTPushBack(plist, 2);
	DLTPushBack(plist, 3);
	DLTPushBack(plist, 4);
}

int main()
{
	test01();
	return 0;
}

2.2.2 双链表头插

--在头节点后,第一个节点前进行插入。

对于关于d1节点相关指针 的改变,通过 head 头节点的相关指针进行改变。

cpp 复制代码
DList.c文件
#include "DList.h"

//定义_头插
void DLTPushFront(DLTNode* phead, DLTDataType x)
{
	//断言
	assert(phead);
	//申请新节点
	DLTNode* newnode = DLTBuyNode(x);
	newnode->next = phead->next;
	newnode->prev = phead;

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

}
cpp 复制代码
test.c文件
#include "DList.h"

void test01()
{
	DLTNode* plist = NULL;//不是双向链表,初始化

	//初始化
	DLTInit(&plist);//传址

	//头插
	DLTPushFront(plist, 1);
	DLTPushFront(plist, 2);
	DLTPushFront(plist, 3);
	DLTPushFront(plist, 4);
}

int main()
{
	test01();
	return 0;
}

2.2.3 双链表尾删

--在实现尾删操作前,先来定义判空的函数

cpp 复制代码
//定义-判空
bool DLTEmpty(DLTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

--对于打印链表:为了体现尾删、头删的效果,将链表进行打印

cpp 复制代码
//定义_打印
void DLTPrint(DLTNode* phead)
{
	DLTNode* pcur = phead->next;//指向第一个节点

	//循环遍历
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

--对于尾删:

尾删操作:将头节点及尾节点的上一个节点的前驱指针、后继指针改变指向,都不再指向尾节点。

cpp 复制代码
DList.c文件
#include "DList.h"

//定义_尾删
void DLTPopBack(DLTNode* phead)
{
	assert(!DLTEmpty(phead));

	//首先定义指针指向尾节点
	DLTNode* del = phead->prev;

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

	//对del节点空间进行释放
	free(del);
	del = NULL;
}
cpp 复制代码
test.c文件
#include "DList.h"

void test01()
{
	DLTNode* plist = NULL;//不是双向链表,初始化

	//初始化
	DLTInit(&plist);//传址

	//尾插
	DLTPushBack(plist, 1);
	DLTPushBack(plist, 2);
	DLTPushBack(plist, 3);
	DLTPushBack(plist, 4);
	DLTPrint(plist);

	//尾删
	DLTPopBack(plist);
	DLTPrint(plist);
	DLTPopBack(plist);
	DLTPrint(plist); 
	DLTPopBack(plist);
	DLTPrint(plist); 
	DLTPopBack(plist);
	DLTPrint(plist);
}

int main()
{
	test01();
	return 0;
}

2.2.4 双链表头删

--对于头删的操作,画图弄清楚节点的前驱指针、后继指针的指向关系,即将头节点、第二个节点的前驱指针、后继指针不在指向第一个节点。

cpp 复制代码
DList.c文件
#include "DList.h"

//定义_头删
void DLTPopFront(DLTNode* phead)
{
	assert(!DLTEmpty(phead));

	//定义指针指向第1个节点
	DLTNode* del = phead->next;

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

	//释放del
	free(del);
	del = NULL;
}

先定义指针指向第一个节点,再根据图示,改变头节点后继指针指向第二个节点,改变第二个节点的前驱指针指向头节点。

cpp 复制代码
test.c文件
#include "DList.h"

void test01()
{
	DLTNode* plist = NULL;//不是双向链表,初始化

	//初始化
	DLTInit(&plist);//传址

	//尾插
	DLTPushBack(plist, 1);
	DLTPushBack(plist, 2);
	DLTPushBack(plist, 3);
	DLTPushBack(plist, 4);
	DLTPrint(plist);

	//头删
	DLTPopFront(plist);
	DLTPrint(plist); 
	DLTPopFront(plist);
	DLTPrint(plist);
	DLTPopFront(plist);
	DLTPrint(plist);
	DLTPopFront(plist);
	DLTPrint(plist);
}

int main()
{
	test01();
	return 0;
}

回顾:

《面试高频数据结构:单链表与顺序表之争,读懂"不连续"之美背后的算法思想》

《链表面试基础看点:这里不止"快慢指针"的完美实现,更懂"哨兵节点"的巧妙运用》
结语:从单链表的"单向枷锁"中解脱出来,我们初步领略了双向链表在头尾操作上的简洁与高效。但它的威力远不止于此。如何利用双指针的便利,实现比单链表更优雅、更高效的任意位置操作?这一切的背后,又隐藏着哪些不容错过的细节陷阱?下一篇,我们将拨开迷雾,一探究竟。

相关推荐
im_AMBER3 小时前
数据结构 03 栈和队列
数据结构·学习·算法
阿里云大数据AI技术3 小时前
云栖实录 | 通义实验室基于MaxCompute进行大模型数据管理及处理
大数据·人工智能
沐雨橙风ιε3 小时前
Spring Boot整合Apache Shiro权限认证框架(实战篇)
java·spring boot·后端·apache shiro
玉树临风江流儿3 小时前
关于pkg-config的使用示例--g++编译过程引入第三方库(如Opencv、Qt)
人工智能·opencv
struggle20253 小时前
AxonHub 开源程序是一个现代 AI 网关系统,提供统一的 OpenAI、Anthropic 和 AI SDK 兼容 API
css·人工智能·typescript·go·shell·powershell
左师佑图3 小时前
Apache POI SXSSFWorkbook 报错“没有那个文件或目录”问题排查与解决方案
java·apache·excel
后端小肥肠3 小时前
公众号对标账号文章总错过?用 WeWe-RSS+ n8n,对标文章定时到你的邮箱(下篇教程)
人工智能·agent
凸头4 小时前
以AtomicInteger为例的Atomic 类的底层CAS细节理解
java·jvm·算法
艾派森4 小时前
基于 Rokid CXR-M SDK 构建 AR 远程专家协作系统:从零实现眼镜端自定义 UI 与实时交互
java