数据结构与算法:双向链表

news2024/11/24 22:27:53

朋友们大家好啊,在上节完成单链表的讲解后,我们本篇文章来对带头循环双向链表进行讲解

双向链表

  • 双向链表、头节点和循环的介绍
  • 构建双向链表
    • 节点的构建
    • 初始化双向循环链表(空链表)
    • 销毁双向链表
  • 链表的打印
  • 双向链表头尾的插与删
    • 尾插
    • 尾删
    • 头插
    • 头删
  • 查找特定节点
    • 在指定位置前插入数据
    • 删除pos节点
  • 总结

双向链表、头节点和循环的介绍

在这里插入图片描述
单链表中,一个节点存储数据和指向下一个节点的指针,而双向链表除了上述两个内容,还包括了指向上一个节点的指针
在这里插入图片描述

带头的双向链表,是指在双向链表的最前端添加了一个额外的节点,这个节点被称为头节点(哨兵节点),但它一般不用于存储实际的数据(或者可以说存储的数据不被使用)。头节点的主要目的是为了简化链表操作的逻辑,避免在处理链表的开始和结束位置时需要进行特殊的条件判断

在没有头节点的普通双向链表中,如果链表为空,则链表的第一个节点(head pointer)直接为NULL,这使得插入和删除操作时,需要分别检查特定情况,如链表是否为空、是否在链表开始或结束位置进行操作等。

循环链表,即最后一个节点指向下一个节点的指针并不指向空,而是指向头结点,且头结点的指向上一个节点的指针也并不指向空,而是指向最后一个节点

简单介绍之后,我们就来讲解双向循环链表的各个细节吧

构建双向链表

typedef int LTDatatype;

typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDatatype val;
}LTNode;

这里typedef int LTDatatype;我们多次提到,为类型抽象

构建的节点中,每个节点包括两个指针:

  • struct ListNode* next;
    这是一个指针,指向下一个ListNode节点。在链表中,每个节点通过这样的next指针连接到下一个节点。对于链表的最后一个节点,这个指针通常设为NULL,表示没有后续节点。但在循环链表的情况下,最后一个节点的next指针会指向链表的第一个节点,形成一个闭环。

  • struct ListNode* prev;
    这是另一个指针,指向前一个ListNode节点。在双向链表中,除了能够向前遍历,我们还可以通过这个prev指针向后遍历链表。对于链表的第一个节点,这个指针在非循环链表中通常设为NULL,表示没有前驱节点**。而在循环链表中,第一个节点的prev指针会指向链表的最后一个节点。**

节点的构建

我们首先定义一个函数

LTNode* CreatNode(LTDatatype x)

与单链表不同的是,这个函数多了一个指向前一个节点的指针,其他内容均相同

LTNode* CreatNode(LTDatatype x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->val = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

初始化双向循环链表(空链表)

在双向循环链表中,空链表的标志性质是其头节点的 next 和 prev指针都指向它自身。即使是空的链表,依然保持着循环的特性,但它不包含任何数据节点,只有这一个特殊的头节点

这里有两种初始化的形式

void LTInit(LTNode** phead)
{
    *phead = (LTNode*)malloc(sizeof(LTNode)); 
    if (*phead != NULL) {
        (*phead)->next = *phead;
        (*phead)->prev = *phead; 
     }
}

phead 代表指向链表的“头节点”的指针

在这个初始化函数中,新创建的链表头节点的 next 和 prev 指针都被设置为指向自身,形成一个空的双向循环链表,这里用了二级指针,是因为我们对phead进行了改变,对指针进行改变,则需要二级指针
这种方法我们初始化格式如下,首先创造一个plist结构体指针,再传参

LTNode* plist;
LTInit(&plist);
LTNode* LTInit2() {
    LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
    if (phead != NULL) {
        phead->prev = phead;
        phead->next = phead;
    }
    return phead; 
}

在这个实现中,LTInit函数不接受任何参数,而是直接创建并初始化一个新的头节点,使其prev和next指针都指向自己,从而形成一个空的双向循环链表。这样设计的好处是简化了链表的初始化过程,你只需要调用LTInit来获取一个新的链表头节点即可
这种方法我们直接用plist接收返回值即可

LTNode* plist=LITnit2();

销毁双向链表

void ListDestroy(LTNode* phead) {
    if (phead == NULL) {
        return;
    }
    // 由于是循环链表,我们需要一个指针指向第一个节点
    LTNode* current = phead->next;
    // 如果链表不只是头节点自己循环(即有实际数据节点)
    if (current != phead) {
        do {
            LTNode* temp = current;
            current = current->next; // 移动到下一个节点
            free(temp); // 释放当前节点内存
        } while (current != phead);
    }
    // 最后,释放头节点内存(如果头节点是哨兵节点并且是动态分配的)
    free(phead);
}

函数首先检查传入的链表是否为空。如果不为空,它会进入一个 do-while 循环,这个循环确保至少运行一次,即使链表中只有一个节点(头节点)
在循环内部,它会释放当前节点的内存,并移动到下一个节点,直到它循环回到头节点。最后,它释放头节点的内存

链表的打印

在单链表中,我们进行循环打印的判断条件是最后一个节点的指针是否指向NULL,而在双向循环链表中,没有空指针,我们的判断条件也有所不同

void LTPrint(LTNode* phead) {
    if (phead == NULL || phead->next == phead) {
        return;
    }
    LTNode* current = phead->next;
    while (current != phead) { 
        printf("%d ", current->val); 
        current = current->next; 
    }
    printf("\n"); 
}

首先

if (phead == NULL || phead->next == phead) {
    return;
}

这串代码是判断链表是否为空或者链表是否只有一个头结点,如果是,则没有数据可打印,直接返回

遍历链表:

LTNode* current = phead->next;
while (current != phead) { 
    printf("%d ", current->val); 
    current = current->next; 
}

这部分代码初始化一个新指针 current 指向链表的第一个节点(即 phead->next),然后进入一个 while 循环。在循环中,只要 current 不指回 phead,它就打印当前节点的值,并移动到下一个节点。这个循环确保了所有节点都被访问一次。

注意,由于它从 phead->next 开始,phead 本身不存储有效数据(或者说是一个哨兵节点)

双向链表头尾的插与删

尾插

void LTPushBack(LTNode* phead, LTDatatype x) {
    LTNode* newnode = CreatNode(x);
    if (phead == NULL) {
        return;
    }
    newnode->next = phead; 
    newnode->prev = phead->prev;
    phead->prev->next = newnode;
    phead->prev = newnode;
}

在这里插入图片描述
我们构建newnode

  • newnode的next指向头结点newnode->next = phead;
  • 原来的phead的prev指针指向倒数第二个节点,那么newnode的前一个指针则为初始时phead的prev指针newnode->prev = phead->prev;
  • 现在更新倒数第二个节点的下一个指针,原来指向头指针,现在指向newnode:phead->prev->next = newnode;
  • 最后更改phead的prev指针,指向尾部的newnodephead->prev = newnode;

测试代码如下:
在这里插入图片描述

尾删

void LTPopBack(LTNode* phead) {
     if (phead == NULL || phead->next == phead) {
        return;
    }
    LTNode* tail = phead->prev; 
    LTNode* tailprev = tail->prev; 

    // 断开当前末尾节点与链表的连接,形成新的末尾
    tailprev->next = phead;
    phead->prev = tailprev;

    // 释放原末尾节点占用的内存
    free(tail);
}
  • 首先判断是否为空链表或者只有哨兵节点,如果是则没有值可以删除,直接返回
  • 找到尾部节点tail,即头结点的前一个指针指向的节点;
  • 再找到tail前面的节点,即预期的尾节点将这个节点的下一个指针指向头结点,并将头节点的前一个指针指向这个节点
  • 将tail这个尾部节点内存释放

测试代码如下:
在这里插入图片描述

头插

void LTPushFront(LTNode* phead, LTDatatype x) {
    LTNode* newnode = CreatNode(x); 
    if (phead == NULL) {
        return;
    }
    newnode->next = phead->next; 
    newnode->prev = phead;       
    phead->next->prev = newnode; 
    phead->next = newnode;       
}
  • 首先判断链表是否为空,为空直接返回
  • 新节点的next指针指向原来头节点的下一个节点:newnode->next = phead->next;
  • 新节点的prev指针指向头结点:newnode->prev = phead;
  • 接着更新头节点之后的节点的prev指针,以及头节点的next指针
    - 原来头节点之后的节点的prev指针现在应该指向新节点:phead->next->prev = newnode;
    - 头节点的next指针现在应该指向新节点:phead->next = newnode;

我们更新了四个指针:新节点的前后指针,头结点的next指针,后一个节点的prev指针

测试代码:
在这里插入图片描述

头删

void LTPopFront(LTNode* phead) {
    if (phead == NULL || phead->next == phead) {
        return;
    }
    LTNode* first = phead->next;
    phead->next = first->next;
    first->next->prev = phead;
    free(first);
}
  • 首先检查链表是否为空或者只有哨兵节点
  • 找到要删除的节点,它是头节点的下一个节点:LTNode* first = phead->next;
  • 更新头节点的next指向被删除节点的下一个节点:phead->next = first->next;
  • 更新新的第一个有效数据节点的prev指向头节点:first->next->prev = phead;
  • 最后释放被删除节点所占用的内存

测试代码:
在这里插入图片描述

查找特定节点

LTNode* ListFind(LTNode* phead, int x) {
    if (phead == NULL || phead->next == phead) {
        return NULL;
    }
    LTNode* current = phead->next; 
    while (current != phead) { 
        if (current->val == x) {
            return current;
        }
        current = current->next; 
    }
    return NULL;
}
  • 如果链表为空或者只有哨兵节点,直接返回
  • 由于第一个节点没有有效数据,我们可以从 phead 的下一个节点开始遍历
  • 在这个实现中,我们从哨兵节点的下一个节点开始遍历,即从链表的第一个实际数据节点开始。循环继续执行,直到 current 指针再次回到哨兵节点 phead。如果找到一个节点的值与 x 相等,函数返回该节点的指针。如果遍历完所有节点都没有找到,则返回 NULL。

在指定位置前插入数据

void ListInsert(LTNode* pos, LTDatatype x)
{
    if (pos == NULL)
    {
        return;
    }
    LTNode* posprev = pos->prev;
    LTNode* newnode = CreatNode(x);

    posprev->next = newnode;
    newnode->prev = posprev;
    newnode->next = pos;
    pos->prev = newnode;
}
  1. 找到pos前面的节点posprev
  2. 构建新节点
  3. posprev的next指针指向newnode;
  4. newnode的prev指针指向posprev,next指针指向pos
  5. pos的前一个指针指向newnode;

测试代码,在1 2 3 4 5的3前面插入8,首先获得3节点的地址,在传入插入函数中

在这里插入图片描述
如果再哨兵节点位置,往前插入,则相当于尾插

删除pos节点

我们假设pos不为哨兵节点

void ListErase(LTNode* pos) {
    if (pos == NULL) {
        return;
    }
    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;
    free(pos);
}

这个代码就非常简单了,改变指针后将空间释放

测试代码,删除1 2 3 4 5中的3
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/b7333188533e4900a9d0d59b6234dd66.png
这里注意置空temp

总结

对比于顺序表,双向带头循环链表有以下优势:

  • 在任意位置添加或删除元素的时间复杂度都是O(1)
  • 按需要进行申请空间,没有浪费

不足之处

  • 下标随机访问不方便,需要遍历链表,时间复杂度为O(N);

顺序表和双向带头链表根据特定的使用场景和需求具有各自的优势和劣势。选择哪种数据结构,取决于对性能、内存使用、以及操作灵活性的具体要求。

本节内容到此结束,感谢大家的阅读!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1451781.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CSS3学习(二)

目录: 1. 字体属性 1.1 字体系列 1.2 字体大小 1.3 字体粗细 1.4 文字样式 1.5 字体复合属性 1.6 总结 2 文本属性 2.1 文本颜色 2.2 对齐文本 2.3 装饰文本 2.4 文本缩进 2.5 行间距 2.6 总结 1. 字体属性 1.1 字体系列 使用font-family属性定义文…

JavaScript设计模式与开发实战

JavaScript设计模式与开发实践 第一章、面向对象的JavaScript 1.1 多态 类似java面向对象,通过继承共有特征,来实现不同方法。JavaScript的多态就是把“做什么”和“谁去做”分离,消除类型间的耦合关系。 他的作用就是把过程化的条件分支…

【JavaEE】spring boot快速上手

SpringBoot快速上手 文章目录 SpringBoot快速上手Maven会出现的一个官方bug创建完项目之后常用的的三个功能依赖管理Maven仓库中央仓库本地仓库国内源配置私服 springboot项目创建什么是springspring boot项目的创建Hello Worldweb服务器 SpringMVC什么是SpringWebMVC什么是MVC…

Sora:AI视频生产力的颠覆性跃进,让创意瞬间成“视界”!

在AI视频技术宇宙中,RunwayGen2、Stable Video Diffusion和Pika等明星产品早已名声在外。然而,今日横空出世的Sora犹如一颗璀璨新星,以其震撼性的创新突破,在视频制作领域掀起了一场革命!相较于市面上同类AI视频神器&a…

【Java程序员面试专栏 分布式中间件】Redis 核心面试指引

关于Redis部分的核心知识进行一网打尽,包括Redis的基本概念,基本架构,工作流程,存储机制等,通过一篇文章串联面试重点,并且帮助加强日常基础知识的理解,全局思维导图如下所示 基础概念 明确redis的特性、应用场景和数据结构 什么是Redis,Redis有哪些应用场景 Redi…

互联网加竞赛 基于计算机视觉的身份证识别系统

0 前言 🔥 优质竞赛项目系列,今天要分享的是 基于机器视觉的身份证识别系统 该项目较为新颖,适合作为竞赛课题方向,学长非常推荐! 🧿 更多资料, 项目分享: https://gitee.com/dancheng-sen…

Sora:继ChatGPT之后,OpenAI的又一力作

关于Sora的报道,相信很多圈内朋友都已经看到了来自各大媒体铺天盖地的宣传了,这次,对于Sora的宣传,绝不比当初ChatGPT的宣传弱。自OpenAI发布了GPT4之后,就已经有很多视频生成模型了,不过这些模型要么生成的…

【JavaEE】_CSS选择器

目录 1. 基本语法格式 2. 引入方式 2.1 内部样式 2.2 内联样式 2.3 外部样式 3. 基础选择器 3.1 标签选择器 3.2 类选择器 3.3 ID选择器 4. 复合选择器 4.1 后代选择器 4.2 子选择器 4.3 并集选择器 4.4 伪类选择器 1. 基本语法格式 选择器若干属性声明 2. 引入…

C语言从零实现贪吃蛇小游戏

制作不易,点赞关注一下呗!!! 文章目录 前言一. 技术要点二、WIN32API介绍三、贪吃蛇游戏设计与分析 1.游戏开始前的初始化 2.游戏运行的逻辑 总结 前言 当我们掌握链表这样的数据结构之后,我们就可以用它来…

Rust 学习笔记 - Hello world

前言 本文将讲解如何完成一个 Rust 项目的开发流程,从编写 “Hello, World!” 开始,到使用 Cargo 管理和运行项目。 编写 Hello world 开始一个新项目很简单,首先,创建一个包含 main.rs 文件的 hello_world 文件夹,…

GPT翻译网站的加载与使用

Sider: ChatGPT侧边栏 GPTs, GPT-4 Turbo, 联网, 绘图 sider.ai https://chromewebstore.google.com/detail/sider-chatgpt%E4%BE%A7%E8%BE%B9%E6%A0%8F-gpts-g/difoiogjjojoaoomphldepapgpbgkhkb?hlzh-CN 加入与移除 第二个翻译网站 https://chromewebstore.google.com/…

电商小程序08调用缓存

目录 1 将信息存入缓存中2 获取登录信息3 退出登录4 发布预览总结 小程序的登录功能里,如果只是将登录信息保存到全局变量中,存在的问题是如果小程序重新打开,用户的登录状态就丢失了。为了解决这个问题,我们需要用到微搭的缓存的…

13-k8s的控制器资源-rc控制器replicationcontrollers

一、rc控制器资源的概述 replicationcontrollers控制器资源,简称:rc控制器; 简单理解,rc控制器就是控制相同的pod副本数量; 使用rc控制器资源创建pod,就可以设定创建pod的数量; 二、rc控制器资源…

C++中对象的构造与析构顺序

一、对象的构造顺序 对象的构造&#xff0c;先被创建的对象&#xff0c;先被构造&#xff0c;先调用其构造函数 class A { private:int _a 0; public://构造函数A(int a 0){_a a;cout << "A(int a 0)" << " " << _a << endl…

NVIDIA 刚刚揭秘了他们的最新大作——Eos,一台跻身全球十强的超级计算机

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

HCIA-HarmonyOS设备开发认证V2.0-内核扩展组件

目录 一、CPU 占用率1.1、CPU 占用率基本概念1.2、CPU 占用率运行机制1.3、CPU 占用率开发流程 二、动态加载2.1、 动态加载基本概念2.2、动态加载运行机制 坚持就有收获 一、CPU 占用率 1.1、CPU 占用率基本概念 CPU&#xff08;中央处理器&#xff0c;Central Processing U…

Java 学习和实践笔记(11)

三大神器&#xff1a; 官方网址: http://www.jetbrains.com/idea/ 官方网址: https://code.visualstudio.com/ 官方网址: http://www.eclipse.org 装好了idea社区版&#xff0c;并试运行以下代码&#xff0c;OK&#xff01; //TIP To <b>Run</b> code, press &l…

洛谷AT_abc034_a[ABC034A] テスト

#先看题目 题目描述 输入格式 无 输出格式 无 题意翻译 输入两个数字 x 和 y&#xff0c;如果 y>x 输出Better&#xff0c;否则输出Worse。 输入输出样例 无 题目链接https://www.luogu.com.cn/problem/AT_abc034_a #思路 没有 #最后附上完整代码 #include&l…

(02)Hive SQL编译成MapReduce任务的过程

目录 一、架构及组件介绍 1.1 Hive底层架构 1.2 Hive组件 1.3 Hive与Hadoop交互过程 二、Hive SQL 编译成MR任务的流程 2.1 HQL转换为MR源码整体流程介绍 2.2 程序入口—CliDriver 2.3 HQL编译成MR任务的详细过程—Driver 2.3.1 将HQL语句转换成AST抽象语法树 词法、语…

二叉树的锯齿形层序遍历

1.题目 这道题是2024-2-16的签到题&#xff0c;题目难度为中等。 考察知识点为BFS算法和双端队列。 题目链接&#xff1a;二叉树的锯齿形层序遍历 给你二叉树的根节点 root &#xff0c;返回其节点值的 锯齿形层序遍历 。&#xff08;即先从左往右&#xff0c;再从右往左进行…