当前位置: 首页 > news >正文

【数据结构】双向链表

Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
💥💥个人主页:奋斗的小羊
💥💥所属专栏:C语言

🚀本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。


目录

  • 前言
  • 1、链表的分类
  • 2、双向链表的实现
    • 2.1双向链表节点
    • 2.2申请节点
    • 2.3数据的打印和查找
    • 2.4头插和尾插
    • 2.5头删和尾删
    • 2.6在指定位置插入、删除数据
    • 2.7销毁双向链表
  • 3、双向链表完整代码
  • 4、顺序表和链表比较
  • 总结

前言

单链表只能通过某个节点的地址单向的访问数据,而双向链表可以通过某个节点的地址双向的访问数据,增删查改的效率更高,但是代码的实现却比单链表简单,总的来说,双向链表优势更明显一些。


1、链表的分类

链表分为带头的、不带头的,单向的、双向的,循环的、不循环的组合起来一共八种,而我们只学习常见的不带头单向不循环链表带头双向循环链表,也就是单链表双向链表
双向链表:
在这里插入图片描述
其实在上篇文章中我们将单链表的第一个节点称作头节点是不准确的,之所以这么称呼是为了好理解,因为单链表是不带头的,双向链表才有头结点,也称作哨兵位

  • 双向链表头节点内不存有效数据,存的是无效的数据。
  • 哨兵位是不能改变的,只能改变其指向

2、双向链表的实现

2.1双向链表节点

双向链表的节点需要存数据,还要存前一个和后一个节点的地址,因此双向链表的节点为:

typedef int dlist_data_type;//双向链表节点
typedef struct dlist
{dlist_data_type data;struct dlist* nextstruct dlist* prev;
}dlist;

2.2申请节点

不像单链表,双向链表最少得有一个节点,就是头节点,也叫哨兵位。
在增删查改之前,双向链表必须初始化一个哨兵位,哨兵位内存一个无效数据。

申请的节点初始时两个指针指向自己

//申请节点
dlist* dlist_buy(dlist_data_type x)
{dlist* newdlist = (dlist*)malloc(sizeof(dlist));if (newdlist == NULL){perror("malloc fail!");return 1;}newdlist->data = x;newdlist->next = newdlist;newdlist->prev = newdlist;return newdlist;
}

初始化哨兵位有以下两种方法:
以参数的形式返回:

void dlist_init(dlist** pphead)
{assert(pphead);*pphead = dlist_buy(-1);
}

这个方法要求使用二级指针。
以返回值的形式返回:

dlist* dlist_init()
{dlist* phead = dlist_buy(-1);return phead;
}

这个方法更加简单易理解。


2.3数据的打印和查找

数据的打印和查找跟单链表区别不大,就不再赘述了。
唯一需要注意的是结束循环的条件,当指针指向哨兵位时结束循环,而不是判NULL

//打印数据
void dlist_print(dlist* phead)
{assert(phead);dlist* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
//查找
dlist* dlist_find(dlist* phead, dlist_data_type x)
{assert(phead);dlist* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}

2.4头插和尾插

与单链表不同的是,双向链表的增删查改操作不会改变哨兵位,因此只需要传值调用就可。
双向链表的插入操作需要对三个节点做修改,在修改的过程中,注意先修改新节点,再修改后一个节点,最后修改前一个节点。

//插入操作不改变哨兵位,因此传一级指针即可
//尾插
void dlist_push_back(dlist* phead, dlist_data_type x)
{assert(phead);dlist* newdlist = dlist_buy(x);//新尾节点newdlist->prev = phead->prev;newdlist->next = phead;//旧尾节点phead->prev->next = newdlist;//头节点(哨兵位)phead->prev = newdlist;
}
//头插
void dlist_push_front(dlist* phead, dlist_data_type x)
{assert(phead);dlist* newdlist = dlist_buy(x);//新节点newdlist->prev = phead;newdlist->next = phead->next;//旧节点phead->next->prev = newdlist;//哨兵位phead->next = newdlist;
}

2.5头删和尾删

删除操作的前提是不能没有节点(哨兵位不算),在删除前还是先保存节点的地址,将双向链表重新拼接起来后再释放节点。

//尾删
void dlist_pop_back(dlist* phead)
{assert(phead);assert(phead->next != phead);//双向链表不能为空dlist* del = phead->prev;//新尾节点del->prev->next = phead;//哨兵位phead->prev = del->prev;free(del);del = NULL;
}//头删
void dlist_pop_front(dlist* phead)
{assert(phead);assert(phead->next != phead);dlist* del = phead->next;del->next->prev = phead;phead->next = del->next;free(del);del = NULL;
}

2.6在指定位置插入、删除数据

//在指定位置之后插入数据
void dlist_insert_back(dlist* pos, dlist_data_type x)
{assert(pos);dlist* newdlist = dlist_buy(x);newdlist->prev = pos;newdlist->next = pos->next;pos->next->prev = newdlist;pos->next = newdlist;
}
//删除指定位置的节点
void dlist_erase(dlist* pos)
{assert(pos);dlist* del = pos;pos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}

删除指定位置的数据后,因为这个函数我们用的是传值调用,在释放掉指定位置的节点后,只是把形参指针置NULL,而实参指针依旧指向这个位置,因此在函数调用结束后要给实参指针置NULL


2.7销毁双向链表

双向链表销毁的结束条件也是当遍历指针指向头节点时跳出循环,最后还要单独释放哨兵位,双向链表的销毁函数调用结束后,也要给指向哨兵位的指针置NULL

//销毁
void dlist_destroy(dlist* phead)
{assert(phead);dlist* pcur = phead->next;while (pcur != phead){dlist* next = pcur->next;free(pcur);pcur = next;}free(pcur);pcur = NULL;
}

3、双向链表完整代码

dlist.h:

#pragma once#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef int dlist_data_type;//双向链表节点
typedef struct dlist
{dlist_data_type data;struct dlist* next;struct dlist* prev;
}dlist;//初始化
//插入数据之前,双向链表必须初始化到只有一个头节点(哨兵位)
//void dlist_init(dlist** pphead);//以参数的形式返回
dlist* dlist_init();//以返回值的形式返回//打印数据
void dlist_print(dlist* phead);
//查找
dlist* dlist_find(dlist* phead, dlist_data_type x);
//尾插
void dlist_push_back(dlist* phead, dlist_data_type x);
//头插
void dlist_push_front(dlist* phead, dlist_data_type x);
//尾删
void dlist_pop_back(dlist* phead);
//头删
void dlist_pop_front(dlist* phead);
//在指定位置之后插入数据
void dlist_insert_back(dlist* pos, dlist_data_type x);
//删除指定位置的节点
void dlist_erase(dlist* pos);
//销毁
void dlist_destroy(dlist* phead);

dlist.c:

#define  _CRT_SECURE_NO_WARNINGS#include "dlist.h"//申请节点
dlist* dlist_buy(dlist_data_type x)
{dlist* newdlist = (dlist*)malloc(sizeof(dlist));if (newdlist == NULL){perror("malloc fail!");return 1;}newdlist->data = x;newdlist->next = newdlist;newdlist->prev = newdlist;return newdlist;
}//初始化
//void dlist_init(dlist** pphead)
//{
//	assert(pphead);
//	*pphead = dlist_buy(-1);
//}dlist* dlist_init()
{dlist* phead = dlist_buy(-1);return phead;
}//打印数据
void dlist_print(dlist* phead)
{assert(phead);dlist* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}//查找
dlist* dlist_find(dlist* phead, dlist_data_type x)
{assert(phead);dlist* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}//插入操作不改变哨兵位,因此传一级指针即可
//尾插
void dlist_push_back(dlist* phead, dlist_data_type x)
{assert(phead);dlist* newdlist = dlist_buy(x);//新尾节点newdlist->prev = phead->prev;newdlist->next = phead;//旧尾节点phead->prev->next = newdlist;//头节点(哨兵位)phead->prev = newdlist;
}//头插
void dlist_push_front(dlist* phead, dlist_data_type x)
{assert(phead);dlist* newdlist = dlist_buy(x);//新节点newdlist->prev = phead;newdlist->next = phead->next;//旧节点phead->next->prev = newdlist;//哨兵位phead->next = newdlist;
}//尾删
void dlist_pop_back(dlist* phead)
{assert(phead);assert(phead->next != phead);//双向链表不能为空dlist* del = phead->prev;//新尾节点del->prev->next = phead;//哨兵位phead->prev = del->prev;free(del);del = NULL;
}//头删
void dlist_pop_front(dlist* phead)
{assert(phead);assert(phead->next != phead);dlist* del = phead->next;del->next->prev = phead;phead->next = del->next;free(del);del = NULL;
}//在指定位置之后插入数据
void dlist_insert_back(dlist* pos, dlist_data_type x)
{assert(pos);dlist* newdlist = dlist_buy(x);newdlist->prev = pos;newdlist->next = pos->next;pos->next->prev = newdlist;pos->next = newdlist;
}//删除指定位置的节点
void dlist_erase(dlist* pos)
{assert(pos);dlist* del = pos;pos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}//销毁
void dlist_destroy(dlist* phead)
{assert(phead);dlist* pcur = phead->next;while (pcur != phead){dlist* next = pcur->next;free(pcur);pcur = next;}free(pcur);pcur = NULL;
}

test.c:

#define  _CRT_SECURE_NO_WARNINGS#include "dlist.h"void test()
{//dlist* plist = NULL;//dlist_init(&plist);dlist* plist = dlist_init();//尾插dlist_push_back(plist, 1);dlist_push_back(plist, 2);dlist_push_back(plist, 3);dlist_print(plist);//头插//dlist_push_front(plist, 1);//dlist_push_front(plist, 2);//dlist_push_front(plist, 3);//dlist_print(plist);//尾删//dlist_pop_back(plist);//dlist_pop_back(plist);//dlist_pop_back(plist);//dlist_print(plist);//头删//dlist_pop_front(plist);//dlist_print(plist);//dlist_pop_front(plist);//dlist_print(plist);//dlist_pop_front(plist);//dlist_print(plist);//dlist* find = dlist_find(plist, 2);//dlist_insert_back(find, 4);//dlist_print(plist);//dlist* find = dlist_find(plist, 2);//dlist_erase(find);//find = NULL;//dlist_print(plist);dlist_destroy(plist);plist = NULL;//手动置空
}int main()
{test();return 0;
}

4、顺序表和链表比较

顺序表链表(双向链表)
存储空间上逻辑、物理上都连续逻辑上连续、物理上不一定连续
随机访问复杂度O(1)复杂度O(N)
任意位置插入或删除数据需要挪动数据,复杂度O(N)只需要改变指针指向
插入动态顺序表,空间不够时扩容,扩容本身就有消耗,还容易空间浪费没有容量的概念
应用场景数据高效存储+频繁访问任意位置频繁插入、删除数据
缓存利用率

顺序表和链表优势互补,在不同的应用场景下能发挥各自的优势。


总结

  • 如果应用场景中需要频繁进行查找和删除操作,并且能够接受更多的内存消耗,双向链表可能更加合适。
  • 如果内存比较有限或者对查找和删除操作的效率要求不高,单链表可能更适合.

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • DDR3 (四)
  • WPF引入多个控件库使用
  • 机器学习——LR、‌GBDT、‌SVM、‌CNN、‌DNN、‌RNN、‌Word2Vec等模型的原理和应用
  • Kodcloud可道云安装与一键发布上线实现远程访问详细教程
  • Linux操作系统CentOS如何更换yum镜像源
  • 【智能制造-14】机器视觉软件
  • 2.5 C#视觉程序开发实例1----CamManager实现模拟相机采集图片
  • python杨辉三角的两种书写方式
  • LLM代理应用实战:构建Plotly数据可视化代理
  • 【区块链农场】:农场游戏+游戏
  • Unity之OpenXR+XR Interaction Toolkit实现 Gaze眼部追踪
  • 使用node-cmd重启electron
  • 常见的开源工具(代码托管平台)都有哪些
  • 前端预览图片的两种方式:转Base64预览或转本地blob的URL预览,并再重新转回去
  • 纹波电流与ESR:解析电容器重要参数与应用挑战
  • 【从零开始安装kubernetes-1.7.3】2.flannel、docker以及Harbor的配置以及作用
  • ES6 ...操作符
  • express + mock 让前后台并行开发
  • gf框架之分页模块(五) - 自定义分页
  • JS变量作用域
  • Js实现点击查看全文(类似今日头条、知乎日报效果)
  • KMP算法及优化
  • mysql常用命令汇总
  • 翻译--Thinking in React
  • 老板让我十分钟上手nx-admin
  • 前端每日实战:70# 视频演示如何用纯 CSS 创作一只徘徊的果冻怪兽
  • 深度解析利用ES6进行Promise封装总结
  • 使用权重正则化较少模型过拟合
  • MiKTeX could not find the script engine ‘perl.exe‘ which is required to execute ‘latexmk‘.
  • 哈罗单车融资几十亿元,蚂蚁金服与春华资本加持 ...
  • #快捷键# 大学四年我常用的软件快捷键大全,教你成为电脑高手!!
  • #免费 苹果M系芯片Macbook电脑MacOS使用Bash脚本写入(读写)NTFS硬盘教程
  • (11)MATLAB PCA+SVM 人脸识别
  • (2021|NIPS,扩散,无条件分数估计,条件分数估计)无分类器引导扩散
  • (C语言)fread与fwrite详解
  • (vue)页面文件上传获取:action地址
  • (ZT)北大教授朱青生给学生的一封信:大学,更是一个科学的保证
  • (二)丶RabbitMQ的六大核心
  • (附程序)AD采集中的10种经典软件滤波程序优缺点分析
  • (简单) HDU 2612 Find a way,BFS。
  • (七)Flink Watermark
  • (三) diretfbrc详解
  • (转)Spring4.2.5+Hibernate4.3.11+Struts1.3.8集成方案一
  • (转)我也是一只IT小小鸟
  • .NET C# 配置 Options
  • .NET Core WebAPI中使用Log4net 日志级别分类并记录到数据库
  • .NET NPOI导出Excel详解
  • .NET Standard 支持的 .NET Framework 和 .NET Core
  • .net 中viewstate的原理和使用
  • .Net各种迷惑命名解释
  • .NET构架之我见
  • .NET命令行(CLI)常用命令
  • .pyc文件是什么?
  • [ C++ ] STL---仿函数与priority_queue
  • [.NET 即时通信SignalR] 认识SignalR (一)