数据结构——双向链表 双向链表的定义和构造方法
toqiye 2024-12-16 16:18 49 浏览 0 评论
双向链表的概念
前面章节讨论的链式存储结构的结点中只有一个指示直接后继的指针域。当访问链表中结点时,需要从某个结点出发顺着next指针域寻查其他结点。若要寻查结点的直接前驱,则必须从表头指针出发。在单链表中,这种方式在查找直接后继结点的时间复杂度为,而查找直接前驱的时间复杂度为;然而对于双向链表(Double Linked List),查找直接后继结点和直接前驱结点的时间复杂度都为。
双向结点可以简称为双链表,顾名思义每个数据结点都有两个指针,分别指向直接后继和直接前驱。如图所示:
双向链表的存储结构
从图1中可以看到,双向链表中各节点包含以下3部分信息(如图2所示):
- 指针域:用于指向当前节点的直接前驱节点。
- 数据域:用于存储数据元素。
- 指针域:用于指向当前节点的直接后继节点。
【算法代码】
#define ElemType int
typedef struct _LinkedListNode
{
ElemType data; //数据类型
struct _LinkedListNode* pre; //指向直接前继元素的指针
struct _LinkedListNode* next; //指向直接后继元素的指针
}LinkedListNode, *LinkedList;//ListNode表示结点的类型,LinkedList表示指向ListNode结点类型的指针类型
其中,data表示数据,其可以是简单的类型(如int, double等等),也可以是复杂的结构体(struct类型);pre代表的是前驱指针,它永远指向当前结点的前一个结点;注意,如果当前结点是头结点,则pre指针为空;next代表的是后继指针,它永远指向当前结点的下一个结点,注意,如果当前结点是尾结点,则next指针为空。
双向链表的创建
双向链表的创建过程和单链表创建过程相同,它也是一种动态结构。整个可用存储空间可为多个链表共同享用,每个链表占用的空间不需预先分配划定,而是由系统按需即时生成。因此,建立线性表的链式存储结构的过程就是一个动态生成链表的过程。从空表的初始状态开始,依次建立各元素的结点,并逐个插入链表。与单链表不同的是,pre指针域指向前驱结点,next指针域指向后继结点。双向链表根据结点插入位置的不同,链表的创建方法也可以分为头插法和尾插法。
【头插法】
头插法是指每次申请一个新结点,读入相应的数据元素值,然后将新结点从链表的头部之后逐个插入。
【算法代码】
//O(1)
LinkedList LinkedListCreateByHeader(LinkedList slist, ElemType elem)
{
LinkedListNode* header = nullptr;
if (nullptr == slist)
{
header = (LinkedListNode*)malloc(sizeof(LinkedListNode));
if (nullptr == header)
{
cout << "创建链表头失败" << endl;
return nullptr;
}
else
{
header->data = elem;
header->pre = nullptr;
header->next = nullptr;
return header;
}
}
else
{
LinkedListNode* node = (LinkedListNode*)malloc(sizeof(LinkedListNode));
if (nullptr == node)
{
cout << "创建结点失败" << endl;
return nullptr;
}
else
{
LinkedListNode* clist = slist;
node->data = elem;
node->next = clist;
clist->pre = node;
header = node;
return header;
}
}
}
头插法建立双向链表的时间复杂度为。
【尾插法】
尾插法是通过将新结点逐个插入到链表的尾部来创建链表。同头插法一样,每次申请一个新结点,读入相应的数据元素值。不同的是,为了使新结点能插入到链表尾部,必须遍历整个线性链表找到尾指针,然后从尾指针后插入新结点。
【算法代码】
//O(n)
LinkedList LinkedListCreateByTail(LinkedList slist, ElemType elem)
{
if (nullptr == slist)
{
LinkedListNode* header = (LinkedListNode*)malloc(sizeof(LinkedListNode));
if (nullptr == header)
{
cout << "创建链表头失败" << endl;
return nullptr;
}
else
{
header->data = elem;
header->pre = nullptr;
header->next = nullptr;
return header;
}
}
else
{
LinkedListNode* node = (LinkedListNode*)malloc(sizeof(LinkedListNode));
if (nullptr == node)
{
cout << "创建结点失败" << endl;
return nullptr;
}
else
{
LinkedListNode* header = slist;
LinkedListNode* clist = slist;
for (clist = slist; clist->next != nullptr; clist = clist->next);//寻找尾结点
node->data = elem;
node->pre = clist;
node->next = nullptr;
clist->next = node;
return header;
}
}
}
尾插法建立双向链表的时间复杂度为。
双向链表的取值
双向链表的取值与单链表取值相同,只能从链表的首元结点出发,顺着链域next逐个结点向下访问。
【算法代码】
//O(n)
LinkedList GetLinkedList(LinkedList slist, int pos)
{
if (nullptr == slist)
{
cout << "空链表" << endl;
return nullptr;
}
else if (pos <= 0)
{
cout << "输入位置错误" << endl;
return slist;
}
else
{
int index = 1;
LinkedListNode* clist = slist;
for (clist = slist; index < pos && clist->next != nullptr; clist = clist->next, ++index);
if (index < pos)
{
cout << "寻找位置 " << pos << " 超出链表长度" << index << " ,默认输出尾结点" << endl;
}
return clist;
}
}
双向链表取值的时间复杂度为。
双向链表的插入
双向链表插入过程如图所示:
双向链表插入新结点时,根据插入的位置不同,分为以下3种情况:
- 插入到链表的头部(头节点之后),作为首元节点。
- 插入到链表中间的某个位置。
- 插入到链表的尾部,作为链表中最后一个数据元素。
对于每一次的双向链表的插入操作,首先需要创建一个独立的结点并通过malloc操作开辟相应的空间;其次选中这个新创建的独立结点,将其的pre指针指向所需插入位置的前一个结点;同时,其所需插入的前一个结点的next指针修改指向为该新的结点,同理,该新的结点的next指针将会指向一个原本的下一个结点,而修改下一个结点的pre指针为指向新结点自身。
【算法代码】
//O(n)
LinkedList LinkedListInsert(LinkedList slist, ElemType elem, int pos)
{
if (nullptr == slist)
{
cout << "空链表" << endl;
return nullptr;
}
else if (pos <= 0)
{
cout << "输入位置错误" << endl;
return slist;
}
else
{
LinkedListNode* nlist = (LinkedListNode*)malloc(sizeof(LinkedListNode));
if (nullptr == nlist)
{
cout << "创建结点失败" << endl;
}
else
{
if (1 == pos)
{
nlist->data = elem;
nlist->pre = nullptr;
nlist->next = slist; //指向下一个结点
slist->pre = nlist;
slist = nlist;//插入的结点作为新的头结点
}
else
{
int index = 1;
LinkedListNode* clist = slist;
for (clist = slist; index < pos && clist->next != nullptr; clist = clist->next, ++index);
if (index == pos)
{
nlist->data = elem;
nlist->next = clist; //指向下一个结点
nlist->pre = clist->pre;
clist->pre->next = nlist; //指向插入结点
clist->pre = nlist;
}
else
{
cout << "插入位置 " << pos << " 超出链表长度" << index << " ,默认插入到尾结点"<< endl;
nlist->data = elem;
nlist->next = nullptr;
nlist->pre = clist;
clist->next = nlist;
}
}
}
return slist;
}
}
双向链表插入的时间复杂度为。
双向链表的删除
双向链表删除过程如图所示:
从图中可以看出,删除操作过程是选择需要删除的结点,将前一个结点的next指针指向自己的下一个结点;同时,将一个结点的pre指针修改指向自己的上一个结点;最后释放需要删除的结点。
【算法代码】
//O(n)
LinkedList LinkedListDelete(LinkedList slist, int pos)
{
if (nullptr == slist)
{
cout << "空链表" << endl;
return nullptr;
}
else if (pos <= 0)
{
cout << "输入位置错误" << endl;
return slist;
}
else
{
LinkedListNode* nlist = nullptr;
if (1 == pos) //删除头结点
{
nlist = slist;
slist = slist->next;
free(nlist);
}
else
{
int index = 1; //头结点位置
LinkedListNode* clist = slist;//当前结点
for (clist = slist; index < pos && clist->next != nullptr; clist = clist->next, ++index);
if (index == pos)
{
nlist = clist;
clist->next != nullptr ? clist->pre->next = clist->next : clist->pre->next = nullptr;
clist->next != nullptr ? clist->next->pre = clist->pre : nullptr;
free(nlist);
}
else
{
cout << "删除位置 " << pos << " 超出链表最大长度 " << index << endl;
}
return slist;
}
return slist;
}
}
双向链表删除的时间复杂度为O(n)。
获取链表长度
遍历链表中每一个结点,直到指针域next为空结束,则遍历的次数就是链表的长度。
【算法代码】
//O(n)
int GetLinkedListLength(LinkedList slist)
{
if (nullptr == slist)
{
cout << "空链表" << endl;
return 0;
}
else
{
int index = 1;
LinkedListNode* clist = slist;
for (clist = slist; clist->next != nullptr; clist = clist->next, ++index);
return index;
}
}
完整代码
GitHub: https://github.com/MrYuxiangZhu/DataStructure/tree/master/01.%20LinkedList
相关推荐
- Star 17.3k!给它一张屏幕截图,即可一键克隆网页!
-
本文为大家分享一款本周爆火的GPT开源项目。前言你敢信,只凭借着一张屏幕截图即可转换生成HTML/TailwindCSS代码。可以算得上是前端工程师的福音。它就是screenshot-to-...
- AI从截图直接生成代码、前端程序员的福利!
-
简介项目可以将任何屏幕截图或设计转换为干净的代码(支持大多数框架)。来自领先公司的开发人员和设计师使用的排名第一的工具。完全开源,在GitHub上拥有超过35,000颗星。非常受欢迎。各位小伙...
- 一款高颜值、跨平台、自托管的免费开源CRM项目——Twenty
-
前言大家好,这里是可爱的Cherry。作为一个“甲方”,Cherry其实挺知道客户管理的重要的。但是对于客户管理怎么做,以及CRM的作用,我却是一无所知。之前有朋友在评论区留言,说有没有开源的CRM系...
- 解放双手,前端界面再也不用自己写了?
-
随着AI技术的发展,现在有越来越多的尝试将AI应用于UI设计和开发中,以期提高效率和降低成本。今天就给大家介绍一个开源的AI网页生成工具:OpenUIOpenUIOpenUI是一个创...
- 代码调试,教给你(代码调试是什么意思)
-
昨天我和一些朋友一起调试代码,他们做程序员这一行都不太久,我向他们展示了一些代码调试技巧。今天早上我在想,我应该如何教授他们学习代码调试?我在Twitter上发了一条推文说,我从来没有见过任何好的调试...
- Screenshot-to-code:用屏幕截图生成代码
-
Screenshot-to-code是一个简单的工具,可使用AI将屏幕截图、模型和Figma设计转换为干净、实用的代码。现在支持ClaudeSonnet3.5和GPT-4o!Scre...
- next实现原理(next method)
-
Next.js是一个基于React的服务器端渲染(SSR)和静态生成(SSG)框架,它的实现原理涉及多个关键技术点,包括服务端渲染(SSR)、静态生成(SSG)、客户端渲染(CSR)、...
- 可逐步操作的具体流程(可逐步操作的具体流程包括)
-
假设你是一个单人开发者,使用主流技术栈(React+Node.js+MySQL),以下是详细步骤:---###**一、需求分析与原型设计**1.**核心功能清单**-用户能添加、删除、...
- 截图转代码只需1步!你离高效开发只差这款神器
-
引言在现代前端开发中,将设计稿转换为代码是一个既重要又耗时的环节。手动编写HTML结构、调试CSS样式、调整布局对齐,不仅耗费时间,还容易出错。而Screenshot-to-Code这款革...
- web开发 前端 后端(web开发前端后端)
-
区别:1、前端是指用户可见的界面,而后端是指用户看不到的东西,考虑底层业务逻辑的实现,平台的稳定性、性能等。2、前端开发用到的技术有HTML5、CSS3、JS、jQuery、Bootstrap、Nod...
- 手把手教你Dify私有化部署,打造专属AI平台
-
一、Dify是什么?Dify是一款极具创新性的开源LLM应用开发平台,它就像是一把万能钥匙,为开发者们打开了通往生成式AI应用开发新世界的大门。其融合了后端即服务(BackendasS...
- 前后端分离架构设计:提升开发效率与业务支撑力的密钥
-
前后端分离架构设计解析一、定义与核心思想前后端分离是一种将用户界面(前端)与业务逻辑(后端)解耦的架构模式,通过RESTfulAPI或GraphQL实现数据交互。前端专注于视图渲染与交互逻辑...
- Kubernetes最小部署单元Pod(kubernetes最小部署单元)
-
一、Kubernetes与Pod简介在当今云计算和容器化技术盛行的时代,Kubernetes已然成为容器编排领域的中流砥柱。它是一个开源的容器编排平台,由Google基于其内部使用的Bo...
- 【程序员必藏!零基础本地部署DeepSeek大模型保姆级教程】
-
为什么选择本地部署?数据安全:敏感代码/业务数据永不外传闪电响应:局域网推理延迟<100ms,告别云端排队深度定制:自由修改模型代码,打造专属AI助手准备工具(5分钟搞定)1核心工具下载...
- 【Python程序开发系列】使用Flask实现前后端分离(案例)
-
这是我的第398篇原创文章。一、引言随着web开发的不断发展,前后端分离已成为越来越流行的架构设计。Flask是一个轻量级的Pythonweb框架,非常适合用于构建API,然后配合前端框...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- minorgc (62)
- systemproperties (77)
- vue3-template-admin (63)
- electronsqlite3 (65)
- npm版本管理 (61)
- materialtheme (86)
- node-ssh (68)
- 图床搭建 (62)
- vue3addeventlistener (60)
- mybatisselectone (78)
- css圆形进度条 (69)
- androidble蓝牙开发 (62)
- android-gif-drawable (60)
- appender-ref (64)
- springbootmockito (68)
- 依赖注入的方式 (62)
- cookie跨域共享 (63)
- easyexcel导出图片 (77)
- dp数组 (61)
- js获取兄弟节点 (68)
- sysctl-a (60)
- window.target (62)
- apimodel注解的作用 (60)
- window.onerror (66)
- springmvc教程 (65)