单链表进阶版 -->双向链表

个人主页流年如梦

专栏《C语言》 《数据结构》

文章目录

Ladies and gentlemen,本篇文章先了解一下双向链表,其中主要学习双向链表的实现(重点);全程高能,不容错过!!!

前言

双向链表是在单链表基础上优化升级的重要链式存储结构,通过前驱指针与后继指针实现双向访问,解决了单链表无法快速找到前驱节点的缺陷

一.了解双向链表

双向链表是一种带头、双向、循环 的链式结构(单链表则为不带头单向不循环链表 ),是实际开发中最常用、最好用的链表结构

1.1什么是双向链表

  1. 物理空间不连续,逻辑连续
  2. 每个节点有三个部分:
    数据域data --> 存有效数据
    后继指针next --> 指向后一个节点
    前驱指针prev --> 指向前一个节点
  3. 带有一个哨兵位头节点,不存数据,只用来简化操作
  4. 链表首尾相连,形成循环

1.2结构特点

带头(哨兵位)

不用处理空指针,插入删除不用判断边界

双向

既能向后走,也能向前走,能直接找到前驱节点

循环🔁
尾节点的next指向哨兵位,哨兵位的prev指向尾节点

1.3与单链表的区别

区别
单链表 只能往后走,找前驱必须遍历,效率低
双向链表 前后都能走 ,找前驱时间复杂度为O(1),任意位置插入删除也都是O(1)

1.4优缺点

优点

  1. 头尾插删效率O(1)
  2. 任意位置插入删除O(1)
  3. 不用找前驱,代码简单
  4. 没有扩容、没有空间浪费
  5. 不会出现单链表的边界错误

缺点:

  1. 多存一个指针,占用略微多一点内存
  2. 不支持随机访问(链表通病)

二.双向链表的实现

2.1头文件声明 --> List.h

参考代码如下:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
}LTNode;

//创建哨兵位
LTNode* LTInit();

//销毁、打印
void LTDestroy(LTNode* phead);
void LTPrint(LTNode* phead);

//判断链表是否为空
bool LTEmpty(LTNode* phead);

//尾插尾删
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);

//头插头删
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);

//在pos之前之后插入
void LTInsertBefore(LTNode* pos, LTDataType x);
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErase(LTNode* pos);

//查找
LTNode* LTFind(LTNode* phead, LTDataType x);

2.2源文件实现 --> List.c

2.2.1销毁

c 复制代码
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
}

🧐分析 :先逐个释放有效节点,避免内存泄漏;遍历到回到哨兵位即停止即cur != phead,最后释放哨兵位

2.2.2打印

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

🧐分析:从第一个有效节点开始遍历,到哨兵位结束

2.2.3创建哨兵位

c 复制代码
LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

🧐分析 :带头双向循环链表必须有哨兵位并且哨兵位不存有效数据(自己填认为是无效数据)初始自环 --> 前后都指向自己

2.2.4创建新节点

c 复制代码
LTNode* BuyListNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}

这个不多说,跟之前的的大差不差

2.2.5判断空链表

c 复制代码
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

🧐分析 :如果为空,哨兵位的next指向自己

2.2.6尾插尾删

尾插

c 复制代码
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyListNode(x);
	LTNode* tail = phead->prev;

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

🧐分析 :双向链表可直接找到尾节点,不用遍历,四步指针链接,保证不断链

尾删

c 复制代码
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	tailPrev->next = phead;
	phead->prev = tailPrev;
	free(tail);
}

🧐分析直接找到尾节点及其前驱,然后修改指针后释放尾节点

2.2.7头插头删

头插

c 复制代码
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyListNode(x);
	LTNode* first = phead->next;

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

🧐分析 :在哨兵与第一个节点之间插入;指针修改顺序要正确,不断链

头删

c 复制代码
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* first = phead->next;
	LTNode* second = first->next;

	phead->next = second;
	second->prev = phead;
	free(first);
}

🧐分析:要删除第一个有效节点,需保存第二个节点,修改指针

2.2.8在pos之前与之后插入

在pos之前插入:

c 复制代码
void LTInsertBefore(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* newnode = BuyListNode(x);

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

🧐分析可以直接找到pos的前驱 (双向链表最大优势),在前驱与pos之间插入

在pos之后插入

c 复制代码
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = BuyListNode(x);
	LTNode* posNext = pos->next;

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

🧐分析:通用后插函数,可实现头插或尾插,只需改4个指针

2.2.9删除pos节点

c 复制代码
void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* next = pos->next;

	prev->next = next;
	next->prev = prev;
	free(pos);
}

🧐分析 :直接通过pos找到前后节点,然后改指针后释放pos

2.2.10查找

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

🧐分析 :先遍历找值为x的节点,然后找到返回节点地址,否则返回NULL;查找用于定位插入或删除位置

2.3主函数 --> test.c

参考代码如下:

c 复制代码
#include "List.h"

void TestList()
{
	//创建带头双向循环链表
	LTNode* plist = LTInit();

	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	printf("尾插后:");
	LTPrint(plist);

	//头插
	LTPushFront(plist, 0);
	printf("头插后:");
	LTPrint(plist);

	//尾删
	LTPopBack(plist);
	printf("尾删后:");
	LTPrint(plist);

	//头删
	LTPopFront(plist);
	printf("头删后:");
	LTPrint(plist);

	//查找、在pos之前插入
	LTNode* pos = LTFind(plist, 2);
	if (pos != NULL)
	{
		LTInsertBefore(pos, 99);
		printf("在 2 之前插入 99:");
		LTPrint(plist);
	}

	//查找、在pos之后插入
	pos = LTFind(plist, 1);
	if (pos != NULL)
	{
		LTInsert(pos, 66);
		printf("在 1 之后插入 66:");
		LTPrint(plist);
	}

	//删除指定节点pos
	pos = LTFind(plist, 2);
	if (pos != NULL)
	{
		LTErase(pos);
		printf("删除节点 2 后:");
		LTPrint(plist);
	}

	//不要用的时候销毁,有借有还
	LTDestroy(plist);
	plist = NULL;
}

int main()
{
	TestList();

	return 0;
}

最终运行结果

🎯总结

  1. 双向链表是带头、双向、循环结构,含哨兵位
  2. 每个节点包含prevnextdata三部分
  3. 头尾插删、任意位置插入删除均为O(1)
  4. 可直接找前驱,解决单链表最大缺陷
  5. 无扩容、无空间浪费,是最常用链表

⚠️易错点

  1. 混淆哨兵位与有效节点
  2. 插入或删除时指针顺序错误导致断链
  3. 空链表执行删除,程序崩溃
  4. 销毁不彻底,造成内存泄漏
  5. 遍历条件错误,出现死循环
  6. 不判断pos合法性,导致空指针崩溃

👀 关注 我们一路同行,从入门到大师,慢慢沉淀、稳步成长
❤️ 点赞 鼓励原创,让优质内容被更多人看见
⭐ 收藏 收好核心知识点与实战技巧,需要时随时查阅
💬 评论 分享你的疑问或踩坑经历,一起交流避坑、共同进步

相关推荐
流年如夢3 小时前
单链表 -->增、删、查、改等详细操作
c语言·数据结构
handler015 小时前
【算法模板】最小生成树:稠密图选 Prim,稀疏图选 Kruskal
c语言·数据结构·c++·算法
此生决int6 小时前
快速复习之数据结构篇——栈和队列
数据结构·c++
昵称小白6 小时前
子串专题部分
数据结构·算法·哈希算法
ShoreKiten7 小时前
cpp考前急救
数据结构·c++·算法
诙_8 小时前
C++数据结构--AVL树
数据结构
Cando学算法9 小时前
欧拉回路(一笔画)
数据结构·c++·图论
图码9 小时前
一文搞懂如何判断字符串是否为Pangram(全字母句)
数据结构·算法·网络安全·数字雕刻·ping++
khalil10209 小时前
代码随想录算法训练营Day-43 动态规划10 | 300.最长递增子序列、674. 最长连续递增序列、718. 最长重复子数组
数据结构·c++·算法·leetcode·动态规划·子序列问题