数据结构(Doubly Linked List双向链表)
1.前言:
在计算机科学的广袤领域中,数据结构犹如构建高楼大厦的基石,它们为高效地组织、存储和处理数据提供了坚实的框架。而双向链表作为一种重要且功能强大的数据结构,在众多算法与程序设计场景中都展现出了独特的魅力与价值。
当我们踏入数据结构的学习之旅,单向链表已经向我们展示了一种灵活的链式存储方式,它通过节点间的单向链接,能够动态地管理数据元素。然而,双向链表在此基础上更进一步,为每个节点赋予了前后两个指针,这看似简单的改进,却带来了诸多卓越的特性。
双向链表的双向指针使得数据的遍历变得更加便捷与灵活。无论是从前往后还是从后往前,我们都能轻松地在链表中穿梭,如同在一条双向的信息高速公路上自由驰骋。这种特性在许多实际应用中具有显著优势,例如在文本编辑器中处理文本行的前后移动、浏览器的历史记录浏览(既可以向前查看之前访问的页面,也可以向后返回后续页面)以及某些图形界面库中对组件列表的双向操作等场景下,双向链表都能够高效地应对数据的双向访问需求。
与数组相比,双向链表在数据的插入和删除操作上表现出了更高的灵活性。在数组中,插入或删除一个元素往往需要移动大量后续元素,时间复杂度较高。而双向链表只需调整相关节点的指针,便可轻松完成这些操作,并且不会对链表的其他部分造成大规模的数据移动干扰,从而在频繁进行数据动态更新的场景中展现出卓越的性能。
此外,双向链表的结构也为一些复杂算法的实现提供了有力支持。例如,在某些高级排序算法或图数据结构的特定实现中,双向链表能够以其独特的方式协助数据的组织与处理,使得算法的逻辑更加清晰、高效。
在本章节中,我们将深入探索双向链表的奥秘。从它的基本概念与结构定义开始,逐步剖析其各种操作的实现细节,包括节点的创建、插入、删除、查找以及链表的遍历等核心功能。我们还将通过实际的代码示例与详细的注释,帮助读者透彻理解双向链表在内存中的存储方式以及操作过程中的指针变化逻辑。同时,为了让读者更好地领略双向链表在实际应用中的强大之处,我们会引入一些具体的案例分析,展示如何运用双向链表解决实际问题,并探讨在不同场景下双向链表与其他数据结构的优劣对比与选择策略。
无论你是数据结构与算法的初学者,渴望深入了解链式存储结构的精髓;还是有一定编程经验的开发者,希望进一步拓展自己在数据处理方面的技术能力,本章节关于双向链表的学习都将为你开启一扇通向高效数据管理与处理的新大门。让我们一同踏上这段充满挑战与惊喜的双向链表学习之旅,领略数据结构世界的奇妙与深邃。
2.双向链表的实现
2.1双向链表的基本概念
双向链表是一种链式存储的数据结构,它由一系列节点组成。每个节点除了存储数据元素本身之外,还包含两个指针:一个指向前一个节点(prev 指针),另一个指向后一个节点(next 指针)。这种双向的指针结构使得双向链表在数据的遍历、插入和删除操作上具有很高的灵活性。与单向链表相比,双向链表可以从任意节点开始向前或向后遍历整个链表,而单向链表只能沿着一个方向进行遍历。
2.2、双向链表的实现步骤框架
(一)节点类的定义:
成员变量:
public int val
:用于存储节点所代表的值,这个值可以是任何你想要在链表中存放的整型数据,比如整数类型的元素值等,它是节点的关键数据部分。public ListNode prev
:是一个指向当前节点前一个节点的指针(引用),用于构建双向链表中向后的关联,使得可以从当前节点回溯到前面的节点,这也是双向链表区别于单向链表的重要特性体现。ListNode next
:同样是一个指针(引用),不过它指向当前节点的下一个节点,用于构建链表向前的连接顺序,通过这个指针可以依次访问后续节点。
- 链表头和尾指针声明:
head
和last
这两个变量都是ListNode
类型,它们分别用于指向整个双向链表的头节点和尾节点。在双向链表的操作过程中,通过head
可以从链表开头进行正向遍历(利用每个节点的next
指针),而通过last
可以从链表末尾进行反向遍历(利用每个节点的prev
指针)。这两个指针方便了对整个链表的访问和操作,比如插入新节点到头部或者尾部、删除头部或者尾部节点等常见操作都依赖于它们准确地指向相应位置。
(二)双向链表实现的方法:
//头插法
public void addFirst(int data);
//尾插法
public void addLast(int data);
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data);
//查找是否包含关键字key是否在双链表当中
public boolean contains(int key);
//删除第一次出现关键字为key的节点
public void remove(int key);
//删除所有值为key的节点
public void removeAllKey(int key);
//得到单链表的长度
public int size();
//清空链表
public void clear();
// 打印链表
public void display() ;
(三)不同的方法实现:
1.头插法
1.1分析:
- 创建新节点:
在方法内部,首先通过ListNode node = new ListNode(data);
语句创建了一个新的ListNode
类型的节点对象node
,并将传入的data
值赋给这个新节点,用于存储相应的数据。 - 链表为空的情况处理(初始化链表):
通过if(head == null)
条件判断来检查链表是否为空。如果链表为空(即head
指针为null
,意味着还没有节点存在),那么将新创建的节点node
同时赋值给链表的头指针head
和尾指针last
(虽然last
在这段代码中前面未完整定义,但从逻辑上推测是用于指向链表尾节点的变量),也就是让这个新节点成为链表唯一的节点,既是头节点也是尾节点。 - 链表非空的情况处理(插入新节点到头部):
当链表不为空时(即head
不为null
),执行else
分支中的代码:- 通过
node.next = head;
语句,将新节点node
的next
指针指向当前的头节点(原来的第一个节点),这样就把新节点和原来的链表头部连接起来了,新节点将成为新的头节点,而原来的头节点会成为新节点后面的节点。 - 接着
head.prev = node;
语句,将原来头节点的prev
(前一个节点)指针指向新插入的节点node
,建立双向链表中反向的连接关系,使得从原来的头节点也能回溯到新插入的这个头节点。 - 最后通过
head = node;
将链表的头指针head
指向新插入的节点node
,完成新节点作为链表头部节点的更新操作,此时新节点正式成为链表新的头节点。
- 通过
1.3静态演示:
顺序: 1 ---->2
3----->4
2.尾插法
2.1分析:
- 创建新节点:
代码首先通过ListNode node = new ListNode(data);
语句创建了一个新的ListNode
类型的节点对象node
,把传入的data
值赋给这个新节点,以此来承载要插入到链表尾部的数据信息。 - 链表为空的情况处理(初始化链表):
利用if(head == null)
条件判断链表是否为空。若链表为空(也就是head
指针为null
,意味着链表中还不存在任何节点),那么将新创建的节点node
同时赋值给链表的头指针head
和尾指针last
。如此一来,这个新节点就成为了链表的第一个节点,同时担当头节点和尾节点的角色,完成了链表的初始化操作。 - 链表非空的情况处理(插入新节点到尾部):
当链表不为空时(即head
不为null
),执行else
分支里的代码:- 通过
last.next = node;
语句,将当前链表尾节点(由last
指向)的next
指针指向新创建的节点node
,从而把新节点连接到链表的末尾,使其成为链表最后一个节点的后续节点。 - 接着执行
node.prev = last;
语句,将新节点node
的prev
(前一个节点)指针指向当前的尾节点(也就是last
所指向的节点),这样就在双向链表中建立了反向的连接关系,确保从新的尾节点能够回溯到原来的尾节点。 - 最后通过
last = last.next;
语句,将链表的尾指针last
更新为指向新插入的这个节点node
(因为last.next
此时就是新插入的节点),完成了将新节点设置为链表新尾节点的操作,使得链表的尾节点得到了正确的更新,新节点正式成为链表的最后一个节点。
- 通过
2.3静态演示:
顺序: 1 ---->2
3----->4
3 任意位置插入,第一个数据节点为0号下标
3.1 分析:
-
addIndex
方法逻辑分析- 参数合法性校验:
首先通过if(index <0 || index > size())
条件判断来检查传入的索引index
是否合法。如果索引小于 0 或者大于链表当前的长度(通过size()
方法获取,虽然此处size()
方法的具体实现未给出,但能推测出其作用是返回链表的节点个数),说明指定的插入位置不合理,那么直接使用return
结束方法执行,不进行后续插入操作,以此避免出现数组越界之类的错误情况。 - 边界情况处理(头部插入):
接着通过if(index == 0)
判断是否要在链表头部插入节点。如果索引为 0,意味着需要将节点插入到链表的最开头,此时直接调用已有的addFirst(data)
方法(前面应该已经定义了该方法用于向链表头部插入节点)来完成插入操作,然后使用return
结束当前addIndex
方法的执行,因为头部插入的逻辑已经在addFirst
方法中实现好了。 - 边界情况处理(尾部插入):
再通过if(index == size())
判断是否要在链表的末尾插入节点。如果索引等于链表的长度,那就相当于要在链表的最后添加节点,此时调用addLast(data)
方法(同样前面应该已定义该方法用于向链表尾部插入节点)来完成相应的插入操作,随后使用return
结束addIndex
方法,因为尾插的逻辑在addLast
方法中已经处理好了。 - 一般情况插入(中间位置插入):
当要插入的索引既不是 0 也不是链表长度时,说明是要在链表中间的某个位置插入节点。首先通过ListNode node = new ListNode(data);
创建一个新的ListNode
类型的节点node
,用来承载要插入的数据data
。然后调用findIndex(index)
方法获取到链表中指定索引位置对应的当前节点cur
。
接下来进行插入节点的指针操作:- 通过
node.next = cur;
将新节点node
的next
指针指向当前找到的节点cur
,使得新节点与要插入位置的当前节点建立了正向的连接关系,后续遍历链表时能按照正确顺序访问到节点。 - 通过
cur.prev.next = node;
将当前节点cur
的前一个节点(即cur.prev
)的next
指针指向新节点node
,这样就把新节点插入到了当前节点cur
的前面,从链表的前向顺序角度完成了连接。 - 通过
node.prev = cur.prev;
将新节点node
的prev
指针指向当前节点cur
的前一个节点,建立了反向的连接关系,保证从新节点能回溯到前面的节点,符合双向链表的结构特点。 - 最后通过
cur.prev = node;
将当前节点cur
的prev
指针指向新节点node
,进一步完善反向连接,至此新节点在链表中间位置的插入操作就完成了,新节点成功插入到了指定索引对应的位置。
- 通过
- 参数合法性校验:
-
findIndex
方法逻辑分析
这个私有方法用于在链表中查找指定索引位置对应的节点。它首先将一个临时节点指针cur
初始化为指向链表的头节点(通过ListNode cur = head;
语句实现),然后通过一个循环,只要索引index
不为 0,就不断地将cur
指针向后移动(通过cur = cur.next;
语句实现,即让cur
指向当前节点的下一个节点),同时每次循环将index
减 1。当循环结束时(也就是index
变为 0 时),此时cur
指针所指向的节点就是链表中指定索引位置对应的节点,最后将该节点返回,供addIndex
方法用于后续的插入操作。
3.2 注意事项:
-
空链表的操作:
- 在所有需要访问链表的操作中(如
addLast
、remove
、removeAllKey
等),必须考虑链表为空的情况,否则可能导致空指针异常。
- 在所有需要访问链表的操作中(如
-
链表长度不足时的处理:
- 在方法
addIndex
中,访问指定索引时确保索引合法。 - 例如:
addIndex
方法中,if(index < 0 || index > size())
检查了索引边界,但建议在边界不合法时,抛出异常(如IllegalArgumentException
),而不是仅仅返回,这样更符合实际开发中的错误处理习惯。
- 在方法
-
边界节点处理:
- 删除头节点时,需要更新
head
,并处理head
的prev
。 - 删除尾节点时,需要更新
last
,并处理last
的next
。 - 删除中间节点时,需要正确更新前后节点的引用。
- 删除头节点时,需要更新
4.查找是否包含关键字key是否在双链表当中
4.1 分析
- 初始化遍历指针:
方法首先通过ListNode cur = head;
语句,将一个临时的节点指针cur
初始化为指向链表的头节点。这个指针将用于遍历整个链表,以查找是否存在值为key
的节点。 - 链表遍历及比较判断:
接着进入一个while
循环,循环的条件是cur!= null
,只要当前指针cur
所指向的节点不为null
,就意味着还没有遍历完整个链表,需要继续进行查找操作。在循环体内部,通过if(cur.val == key)
条件判断当前节点(由cur
指向)存储的值(通过cur.val
访问,这里推测ListNode
类中有一个名为val
的成员变量用于存储节点的数据)是否与传入的key
值相等。如果相等,说明找到了目标值,此时直接使用return true;
返回true
,表示链表中包含该值,结束方法的执行。如果当前节点的值与key
不相等,那么通过cur = cur.next;
语句将指针cur
移动到下一个节点,继续下一轮的比较判断,如此循环,直至遍历完整个链表或者找到目标值为止。 - 遍历结束返回结果:
当while
循环结束时(也就是cur
指针指向null
,表示已经遍历完整个链表都没有找到值为key
的节点),使用return false;
返回false
,表明链表中不存在与key
值相等的节点。
5.删除第一次出现关键字为key的节点
5.1 分析
- 初始化遍历指针:
通过ListNode cur = head;
语句,将一个临时的节点指针cur
初始化为指向链表的头节点,后续将利用这个指针来遍历整个链表,查找值为key
的节点。 - 链表遍历及删除操作判断:
进入while
循环,只要cur
指针所指向的节点不为null
,就持续遍历链表进行查找。在循环体中,通过if(cur.val == key)
条件判断当前节点(由cur
指向)存储的值是否等于要删除的key
值。- 删除头节点情况:
若cur
指向的节点就是头节点(即cur == head
条件成立),执行以下操作:- 首先通过
head = head.next;
将链表的头指针head
指向当前头节点的下一个节点,实现把头节点从链表中移除的第一步操作。 - 接着通过
if (head!=null)
条件判断新的头节点是否为空。若不为空,说明链表中还有其他节点,此时需要通过head.prev = null;
将新头节点的prev
(前一个节点)指针置为null
,因为它现在已经成为了链表的第一个节点,之前的头节点已被删除,这样就正确更新了新头节点的前驱指针,完成头节点删除后的链表结构调整。
- 首先通过
- 删除中间或尾节点情况:
若cur
指向的节点不是头节点(即执行else
分支),执行以下操作:- 通过
cur.prev.next = cur.next;
语句,将当前节点cur
的前一个节点(由cur.prev
指向)的next
指针指向当前节点cur
的下一个节点(由cur.next
指向),从而跳过当前节点cur
,在链表的正向连接上实现了将该节点移除的操作。 - 然后通过
if (cur.next == null)
条件判断当前节点cur
是否为尾节点。如果cur.next
为null
,意味着当前节点就是尾节点,此时需要通过last = last.prev;
将链表的尾指针last
更新为指向当前尾节点的前一个节点,因为当前尾节点即将被删除,这样就完成了尾节点删除后的链表尾指针更新操作。 - 如果当前节点
cur
不是尾节点(即cur.next!= null
),则通过cur.next.prev = cur.prev;
语句,将当前节点cur
的下一个节点的prev
指针指向当前节点cur
的前一个节点,在链表的反向连接上完成了节点删除后的结构调整,保证链表双向连接的正确性。
- 通过
- 无论哪种情况,只要找到要删除的节点并完成相应操作后,就通过
return
结束remove
方法的执行,因为已经完成了一个值为key
的节点删除操作。 - 若当前节点的值不等于
key
,则通过cur = cur.next;
将指针cur
移动到下一个节点,继续下一轮的查找和判断,直至遍历完整个链表为止。
- 删除头节点情况:
6.删除所有值为key的节点
此和5.删除第一次出现关键字为key的节点的处处不大,因此我们只讲差别:
6.1分析差别:
1. 代码功能和对链表结构改变的影响不同
- <5>只删除一个值为
key
的节点:
执行完代码后,链表中最多只会少一个节点,也就是第一个被查找到的那个值为key
的节点会被移除,链表的其他部分保持不变,后续的节点顺序和连接关系依旧按照原有的结构(除了被删除节点相关的指针调整外)。例如,若链表原本是1 -> 2 -> 2 -> 3
,要删除值为2
的节点,执行完代码后可能变为1 -> 2 -> 3
(只删除了第一个2
对应的节点)。 - <6>删除所有值为
key
的节点:
执行完代码后,链表中所有值为key
的节点都会被移除,链表结构会发生相应的较大改变,节点数量以及连接关系都会根据原链表中值为key
的节点分布情况重新调整。比如,若链表原本是1 -> 2 -> 2 -> 3
,要删除值为2
的节点,执行完代码后就变为1 -> 3
,所有值为2
的节点都被成功删除了。
2. 代码在处理逻辑上的复杂程度及潜在问题关注点不同
- <5>只删除一个值为
key
的节点:
相对来说逻辑较为简单直接,重点在于正确判断要删除的节点位置(头、中、尾)并进行准确的指针调整操作,保证删除一个节点后链表结构依然正确即可。主要潜在问题多集中在指针操作的异常处理上,比如要确保链表结构本身是完整的,在修改指针时不会出现空指针异常等情况。 - <6>删除所有值为
key
的节点:
除了要保证每次删除节点时的指针操作正确外,还需要妥善处理好循环继续遍历的逻辑,避免出现遗漏查找或者重复操作等问题。而且由于可能会连续删除多个节点,对链表整体结构改变较大,更需要注意在多次删除操作过程中保证链表结构的完整性以及避免出现指针异常等错误情况,代码逻辑的复杂程度和对各种边界情况的把控要求相对更高一些。
7.得到单链表的长度
7.1分析
- 初始化遍历指针与长度计数变量:
首先通过ListNode cur = head;
语句,将一个临时的节点指针cur
初始化为指向链表的头节点,后续会利用这个指针来遍历整个链表。同时,定义了一个整型变量len
并初始化为 0,这个变量将用于记录链表中节点的数量,每遍历到一个节点,len
的值就会相应地增加 1。 - 链表遍历及长度计数:
接着进入while
循环,循环的条件是cur!= null
,只要当前指针cur
所指向的节点不为null
,就意味着还没有遍历完整个链表,需要继续进行节点计数操作。在循环体内部,每次循环都会执行len++;
语句,使得表示长度的变量len
的值加 1,代表已经遍历到了一个新的节点。然后通过cur = cur.next;
语句将指针cur
移动到下一个节点,继续下一轮的遍历和计数,如此循环,直至遍历完整个链表为止。 - 返回链表长度:
当while
循环结束时(也就是cur
指针指向null
,表示已经遍历完整个链表),使用return len;
将记录的链表长度len
返回,这样调用该方法的地方就能获取到当前链表中节点的总个数了。
8.清空链表
8.1分析
- 初始化遍历指针并准备临时指针:
首先通过ListNode cur = head;
语句,将一个临时的节点指针cur
初始化为指向链表的头节点,后续会利用这个指针来遍历整个链表,依次处理每一个节点。同时在循环内部,定义了另一个临时节点指针Ncur
,通过ListNode Ncur = cur.next;
语句将其初始化为指向当前节点cur
的下一个节点,目的是在对当前节点cur
进行相关操作断开其连接后,还能通过Ncur
指针获取到下一个需要处理的节点,保证遍历能够持续进行下去。 - 逐个节点处理及断开连接:
在while
循环中(循环条件是cur!= null
,意味着只要还有节点未处理完就继续循环),针对当前节点cur
进行如下操作:- 通过
cur.prev = null;
语句,将当前节点cur
的前一个节点指针置为null
,断开当前节点与前面节点在双向链表中的反向连接关系(如果是双向链表的话,从这里能推测出是双向链表相关操作,因为有prev
属性)。 - 通过
cur.next = null;
语句,将当前节点cur
的下一个节点指针置为null
,断开当前节点与后面节点在双向链表中的正向连接关系,经过这两步操作,当前节点cur
就完全从链表结构中脱离出来了,相当于释放了这个节点(虽然在 Java 中真正的内存回收由垃圾回收机制来处理,但这样断开连接的操作使得该节点不再参与链表的逻辑结构了)。 - 然后通过
cur = Ncur;
语句,将指针cur
更新为指向之前保存好的下一个节点(也就是Ncur
所指向的节点),准备处理下一个节点,如此循环,对链表中的每一个节点都重复上述断开连接的操作,直至遍历完整个链表。
- 通过
- 重置链表头指针和尾指针:
当while
循环结束,意味着链表中的所有节点都已经被处理并从链表结构中断开连接了,此时通过head = last = null;
语句,将链表的头指针head
和尾指针last
都置为null
,从整体上表明链表现在已经是空链表了,完成了清空链表的全部操作。
9.打印链表
9.1分析
- 初始化遍历指针:
通过ListNode cur = head;
语句,将一个临时的节点指针cur
初始化为指向链表的头节点。后续会利用这个指针来遍历整个链表,按顺序访问每一个节点。 - 链表遍历及数据输出:
接着进入while
循环,循环条件为cur!= null
,只要当前指针cur
所指向的节点不为null
,就意味着链表还未遍历完,需要继续操作。在循环体内部,使用System.out.print(cur.val + " ");
语句,将当前节点(由cur
指向)存储的数据(通过cur.val
来访问,推测ListNode
类中有名为val
的成员变量用于存放节点的数据)输出到控制台,并且每个数据后面添加一个空格作为间隔,使输出的内容格式相对规整,更便于查看和区分不同节点的数据。输出当前节点数据后,通过cur = cur.next;
语句将指针cur
移动到下一个节点,以便在下一轮循环中输出下一个节点的数据,如此循环往复,直至遍历完整个链表。 - 换行操作:
当while
循环结束,也就是已经遍历并输出了链表中所有节点的数据后,使用System.out.println();
语句进行换行操作。这样做的好处是,在多次调用display()
方法输出不同链表或者同一链表不同状态的数据时,每次的输出内容会各自独占一行,避免不同次的输出内容在控制台显示时相互混淆,使得输出结果更加清晰易读。
3.全部代码展示
1.接口:
public interface MyList {//头插法public void addFirst(int data);//尾插法public void addLast(int data);//任意位置插入,第一个数据节点为0号下标public void addIndex(int index,int data);//查找是否包含关键字key是否在单链表当中public boolean contains(int key);//删除第一次出现关键字为key的节点public void remove(int key);//删除所有值为key的节点public void removeAllKey(int key);//得到单链表的长度public int size();//清空链表public void clear();// 打印链表public void display(); }
2.java代码实现:
public class MyIList implements MyList{static class ListNode {public int val;public ListNode prev;ListNode next;public ListNode(int val) {this.val = val;}}public ListNode head;public ListNode last;public void addFirst(int data) {ListNode node = new ListNode(data);if(head == null){head = last = node;}else{node.next = head;head.prev = node;head = node;}}@Overridepublic void addLast(int data) {ListNode node = new ListNode(data);if(head == null){head = last = node;}else{last.next = node;node.prev = last;last = last.next;}}@Overridepublic void addIndex(int index, int data) {if(index <0 || index > size()){return;}if(index == 0){addFirst(data);return;}if(index == size()){addLast(data);return;}ListNode node = new ListNode(data);ListNode cur = findIndex(index);node.next = cur;cur.prev.next = node;node.prev =cur.prev ;cur.prev = node;}private ListNode findIndex(int index){ListNode cur = head;while (index !=0){cur = cur.next;index--;}return cur;}@Overridepublic void remove(int key) {ListNode cur = head;while (cur !=null){if(cur.val == key) {if (cur == head) {head = head.next;if (head!=null) {head.prev = null;}} else {cur.prev.next = cur.next;if (cur.next == null) {last = last.prev;} else {cur.next.prev = cur.prev;}}return;}else {cur = cur.next;}}}@Overridepublic void removeAllKey(int key) {ListNode cur = head;while (cur !=null){if(cur.val == key) {if (cur == head) {head = head.next;if (head!=null) {head.prev = null;}} else {cur.prev.next = cur.next;if (cur.next == null) {last = last.prev;} else {cur.next.prev = cur.prev;}}cur = cur.next;}else {cur = cur.next;}}}@Overridepublic void clear() {ListNode cur = head;while (cur != null){ListNode Ncur = cur.next;cur.prev = null;cur.next = null;cur = Ncur;}head = last = null;}@Overridepublic int size() {ListNode cur = head;int len = 0;while (cur !=null){len++;cur = cur.next;}return len;}@Overridepublic boolean contains(int key) {ListNode cur = head;while (cur !=null){if(cur.val == key){return true;}cur = cur.next;}return false;}@Overridepublic void display() {ListNode cur = head;while (cur !=null){System.out.print(cur.val + " ");cur = cur.next;}System.out.println();} }
4.结语:
在数据结构的浩瀚星空中,双向链表无疑是一颗璀璨的明星。通过对双向链表的深入学习与探索,我们犹如踏上了一段奇妙的编程之旅,领略到了它独特的魅力与强大的功能。
从最初对双向链表基本概念的理解,到亲手构建节点类、实现链表类中的各种操作方法,如插入、删除和遍历等,我们逐步揭开了双向链表神秘的面纱。每一个步骤都像是在精心雕琢一件艺术品,从定义节点的前驱与后继指针,到巧妙地处理在不同位置进行数据元素添加或移除时指针的变更,无不体现着数据结构设计的精妙与严谨。
双向链表的优势在诸多实际应用场景中得以彰显。它在浏览器历史记录管理里游刃有余,让用户能够轻松地在过往页面间穿梭回溯;于文本编辑器的撤销重做功能中发挥关键作用,精准地记录操作步骤的前后顺序,使得编辑过程更加灵活可控;在操作系统任务调度队列的构建上也大显身手,高效地协调任务的排队与执行顺序,保障系统的稳定运行。这些应用实例仅仅是双向链表广泛用途的冰山一角,它在更多复杂的软件系统和算法设计中都有着不可或缺的地位。
掌握双向链表不仅丰富了我们的数据结构知识宝库,更为我们解决实际编程问题提供了一把得力的钥匙。它教会我们在面对不同的数据处理需求时,如何权衡各种数据结构的利弊,选择最为合适的工具来打造高效、健壮的程序。无论是应对大规模数据的动态管理,还是处理具有复杂逻辑关系的数据序列,双向链表都能以其独特的双向遍历和灵活的节点操作能力,为我们开辟出一条优化与创新之路。
然而,数据结构的学习之路永无止境。双向链表只是众多数据结构中的一员,在它之外,还有诸如树、图等更为复杂和高深的数据结构等待我们去探索。但双向链表无疑为我们奠定了坚实的基础,它所培养的逻辑思维和编程技巧将伴随我们在后续的数据结构与算法学习旅程中不断前行,助力我们攻克一个又一个编程难关,攀登更高的技术巅峰。希望大家能够将在双向链表学习过程中所积累的知识与经验融会贯通,在未来的编程世界里创造出更多精彩的可能。
相关文章:
数据结构(Doubly Linked List双向链表)
1.前言: 在计算机科学的广袤领域中,数据结构犹如构建高楼大厦的基石,它们为高效地组织、存储和处理数据提供了坚实的框架。而双向链表作为一种重要且功能强大的数据结构,在众多算法与程序设计场景中都展现出了独特的魅力与价值。…...
【踩坑】修复报错libcurl.so.4、LIBFFI_BASE_7.0、libssl.so.3
转载请注明出处:小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你,欢迎[点赞、收藏、关注]哦~ libcurl.so.4: sudo apt install curl -y LIBFFI_BASE_7.0: conda install libffi3.3 -y libssl.so.3: sudo apt install -y openssl li…...
【Java实现MySQL 数据库导出 Excel 表的方法详解】
MySQL 数据库导出 Excel 表的方法详解 在日常开发中,我们经常需要将数据库中的数据导出为 Excel 文件,以便进行数据分析或分享给其他同事。本文将详细介绍如何从 MySQL 数据库导出数据并生成 Excel 文件,具体实现将基于 Java 语言和 Spring …...
CentOS 7 环境下常见的操作和配置
目录 1. CentOS 7 中的 vsftpd 配置与使用 安装与启动 vsftpd 配置 vsftpd(/etc/vsftpd/vsftpd.conf) 常见命令 2. 使用 yum 包管理器 3. 安全性与防火墙配置 开放端口 4. 使用 systemd 管理服务 5. SELinux 配置 查看 SELinux 状态 临时禁用…...
使用mtools搭建MongoDB复制集和分片集群
mtools介绍 mtools是一套基于Python实现的MongoDB工具集,其包括MongoDB日志分析、报表生成及简易的数据库安装等功能。它由MongoDB原生的工程师单独发起并做开源维护,目前已经有大量的使用者。 mtools所包含的一些常用组件如下: mlaunch支…...
基于 RNN(GRU, LSTM)+CNN 的红点位置检测(pytorch)
文章目录 1 项目背景2 数据集3 思路4 实验结果5 代码 1 项目背景 需要在图片精确识别三跟红线所在的位置,并输出这三个像素的位置。 其中,每跟红线占据不止一个像素,并且像素颜色也并不是饱和度和亮度极高的红黑配色,每个红线放大…...
35页PDF | 元数据与数据血缘落地实施(限免下载)
一、前言 这份报告详细介绍了元数据与数据血缘的概念、重要性以及在企业数据中台中的应用。报告阐述了数据中台的核心价值在于整合和管理体系内的数据,以提升数据资产化能力并支持业务决策。报告还涵盖了元数据的分类(技术元数据和业务元数据࿰…...
Hyperf jsonrpc
依赖的 composer 包 composer require hyperf/json-rpc composer require hyperf/rpc-server composer require hyperf/rpc-client composer require hyperf/service-governance composer require hyperf/service-governance-consul composer require hyperf/service-gove…...
MYSQL PARTITIONING分区操作和性能测试
PARTITION OR NOT PARTITION IN MYSQl Bill Karwin says “In most circumstances, you’re better off using indexes instead of partitioning as your main method of query optimization.” According to RICK JAMES: “It is so tempting to believe that PARTITIONing wi…...
go引入skywalking
前置条件:安装好jdk11,linux服务器(centos7.9),go版本(我的是1.18,1.21都可以) 1.下载skywalking Downloads | Apache SkyWalking 2.下载agent源码 Downloads | Apache SkyWalkin…...
如何通过实构与虚构实现动态交互的态、势、感、知的编排组合
通过 实构 与 虚构 实现 动态人机交互的态、势、感、知 的编排组合,是一个涉及多领域的复杂任务。这个问题的核心在于如何将现实和虚拟世界中的元素,特别是人的 态 (状态)、 势 (趋势)、 感 (感…...
easyexcel 导出日期格式化
1.旧版本 在新的版本中formate已经被打上废弃标记。那么不推荐使用这种方式。 2.推荐方式 推荐使用另外一种方式【 Converter 】代码如下,例如需要格式化到毫秒【yyyy-MM-dd HH:mm:ss SSS】级别 创建一个公共Converter import com.alibaba.excel.converters.Conv…...
大模型Qwen面试内容整理-模型架构与原理
Qwen(通义千问)是阿里巴巴推出的大规模语言模型,其架构和原理与当前主流的大模型(如GPT、LLaMA等)有很多相似之处,但也具备一些独特的特点。下面是Qwen模型架构和原理的详细介绍: Transformer 架构 Qwen模型基于改进的 Transformer 架构,这是一种广泛用于自然语言处理(…...
Python 类的设计(以植物大战僵尸为例)
关于类的设计——以植物大战僵尸为例 一、设计类需满足的三要素1. 类名2. 属性和方法 二、以植物大战僵尸的为例的类的设计1. 尝试分类2. 创建对象调用类的属性和方法*【代码二】*3. 僵尸的继承 三、代码实现 一、设计类需满足的三要素 1. 类名 类名:某类事物的名…...
docker学习笔记(五)--docker-compose
文章目录 常用命令docker-compose是什么yml配置指令详解versionservicesimagebuildcommandportsvolumesdepends_on docker-compose.yml文件编写 常用命令 命令说明docker-compose up启动所有docker-compose服务,通常加上-d选项,让其运行在后台docker-co…...
第一个 JSP 程序
一个简单的 JSP 程序: 使用 IDEA 开发工具新建一个 maven 项目,具体操作如图所示: 配置 Tomcat 服务器 项目结构如下图所示: 3. 修改 index.jsp 页面的代码: <% page language"java" contentType&q…...
MongoDB分片集群搭建及扩容
分片集群搭建及扩容 整体架构 环境准备 3台Linux虚拟机,准备MongoDB环境,配置环境变量。一定要版本一致(重点),当前使用 version4.4.9 配置域名解析 在3台虚拟机上执行以下命令,注意替换实际 IP 地址 e…...
Transformer简述和实现
Transformer 1、概述 (一)、诞生 自从2017年此文《Attention is All You Need》提出来Transformer后,便开启了大规模预训练的新时代,也在历史的长河中一举催生出了GPT、BERT这样的里程碑模型。 (二)、优势 相比之前占领市场的LSTM和GRU模型…...
使用Python3 连接操作 OceanBase数据库
注:使用Python3 连接 OceanBase数据库,可通过安装 PyMySQL驱动包来实现。 本次测试是在一台安装部署OBD的OceanBase 测试linux服务器上,通过python来远程操作OceanBase数据库。 一、Linux服务器通过Python3连接OceanBase数据库 1.1 安装pyth…...
vue3-hooks
hooks 把模块化 发挥到极致 命名规则: useDog.ts/useDog.js useXxx(和xxx相关的所有内容) 具体内容: export function que(){} 或者 export default function () { let dogList []; const getDog () > {} //向外…...
网络安全:构建数字世界的坚固防线
在当今数字化飞速发展的时代,网络已经渗透到我们生活的方方面面。从日常的社交娱乐、在线购物,到工作中的远程协作、数据存储与传输,网络无处不在。然而,随着网络的普及和应用的深入,网络安全问题也日益凸显࿰…...
Vision Transformer (ViT) 基本原理
Vision Transformer (ViT) 基本原理 flyfish Vision Transformer (ViT) 是一种基于 Transformer 架构的计算机视觉模型 一、ViT 的基本原理 ViT 的核心思想是将一张图像视为一组序列,将其嵌入到 Transformer 的输入中,通过自注意力机制捕获全局上下文…...
【青牛科技】拥有两个独立的、高增益、内部相位补偿的双运算放大器,可适用于单电源或双电源工作——D4558
概述: D4558内部包括有两个独立的、高增益、内部相位补偿的双运算放大器,可适用于单电源或双电源工作。该电路具有电压增益高、噪声低等特点。主要应用于音频信号放大,有源滤波器等场合。 D4558采用DIP8、SOP8的封装形式 主要特点ÿ…...
LCD与lvgl
LCD与lvgl 目录 LCD与lvgl 回顾 LCD 的驱动层讲解 1、LCD 的常见接口 2、我们的 LCD 的参数 3、LCD 的设备树说明 4、LCD 的设备树说明 5、如何移植 LCD 的驱动(重点) LCD 的应用层开发 1:LCD 应用开发->界面开发的方法 2:LVGL 模拟器安装 3:LVGL 工程创建和…...
大语言模型(2)--GPT-1
GPT-1是由OpenAI在2018年推出的第一代生成式预训练模型(《Improving Language Understanding by Generative Pre-Training》),它采用了无监督预训练和有监督微调相结合的方法,以增强模型的通用任务求解能力。在此之前,…...
openstack内部rpc消息通信源码分析
我们知道openstack内部消息队列基于AMQP协议,默认使用的rabbitmq 消息队列。谈到rabbitmq,大家或许并不陌生,但或许会对oslo message有些陌生。openstack内部并不是直接使用rabbitmq,而是使用了oslo.message 。oslo.message 后端的…...
单端和差分信号的接线法
内容来源:【单端信号 差分信号与数据采集卡的【RSE】【 NRES】【 DIFF】 模式的连接】 此篇文章仅作笔记分享。 单端输入 单端信号指的是输入信号由一个参考端和一个信号端构成,参考端一般是地端,信号就是通过计算信号端口和地端的差值所得…...
服务器被ping的风险,如何开启和禁止ping?
允许服务器被ping(即响应ICMP回显请求)有其风险和好处。允许ping的主要好处是它可以帮助网络管理员快速检查服务器的连通性。然而,这也可能带来一些安全风险,例如: 暴露信息:响应ping请求可以让攻击者知道…...
pushgateway HA高可用方案
未经本人同意不得转载,若引用请附上原文链接。 项目使用flink来处理kafka中的无界流数据,采用的是flink on yarn的模式部署flink任务。最近做flink任务的监控过程中,踩了一些坑。下面是过程,只想看最终方案的直接拉到最后。 先说…...
在 Ubuntu Server 22.04 上安装 Docker 的详细步骤
本文档详细记录了在 Ubuntu Server 22.04 上安装 Docker 的完整过程,包括解决过程中遇到的问题。希望能对读者有所帮助。 安装过程,重点需要看官方文档。https://docs.docker.com/engine/install/ubuntu/ 步骤 1:卸载冲突的软件包 在安装 D…...
锻造船用发动机动力系统,铸强船舶“心脏”
船舶是海洋、湖泊及河流中重要的水上交通工具,不仅能够促进海上经济的发展,还能够保卫国家的制海权。船舶动力装置,也就是船舶的核心动力源——船用发动机动力系统对船舶的重要作用不言自明,关系到船舶的性能质量,能够…...
string类函数的手动实现
在上一篇文章中,我们讲解了一些string类的函数,但是对于我们要熟练掌握c是远远不够的,今天,我将手动实现一下这些函数~ 注意:本篇文章中会大量应用复用,这是一种很巧妙的方法 和以往一样,还是…...
前端工程化面试题(二)
前端模块化标准 CJS、ESM 和 UMD 的区别 CJS(CommonJS)、ESM(ESModule)和UMD(Universal Module Definition)是前端模块化标准的三种主要形式,它们各自有不同的特点和使用场景: CJS&…...
优化 LabVIEW 系统内存使用
在 LabVIEW 中,内存使用管理是确保高效系统性能的关键因素,尤其是在进行复杂的数据采集、信号处理和控制任务时。LabVIEW 程序的内存消耗可能会随着项目的规模和复杂度增加,导致性能下降,甚至出现内存溢出或程序崩溃。通过合理优化…...
pyqt6事件概要
例子: 利用qtdesigner建立闹钟 python代码 # 导入所需要的文件 from PyQt6.QtGui import QIcon, QPixmap from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QListWidgetItem from PyQt6 import uic from PyQt6.QtCore import Qt, QTime imp…...
鸿蒙分享(一):添加模块,修改app名称图标
码仓库:https://gitee.com/linguanzhong/share_harmonyos 鸿蒙api:12 新建公共模块common 在entry的oh-package.json5添加dependencies,引入common模块 "dependencies": {"common": "file:../common" } 修改app名称&…...
记忆泡沫垫市场:解锁舒适睡眠的黄金钥匙与增长潜力的深度剖析
在当今快节奏、高压力的生活中,优质睡眠已成为现代人追求健康生活的重要组成部分。记忆泡沫垫,作为床垫和枕头领域的一次革命性创新,凭借其独特的材质特性和对人体工学的完美贴合,正逐步成为改善睡眠质量的首选解决方案。本文将从…...
AI+电影特效产品化:开启电影人物年轻化新时代
随着人工智能技术的不断进步,它正在改变着我们生活的方方面面,包括娱乐产业。在电影制作领域,AI技术的应用尤其引人注目,尤其是在实现演员年轻化或老化效果方面。本文将介绍一款名为MyTimeMach...
探索 Python 应用的分层依赖:解决 UOS 环境中的 libvirt-python 安装问题
探索 Python 应用的分层依赖:解决 UOS 环境中的 libvirt-python 安装问题 背景Python 版本升级 问题描述原因分析与解决方案 Python 应用的分层依赖:安装与部署的视角libvirt-python的分层依赖尝试的解决方案 使用编译好的 .whl 文件"嫁接"整个…...
【MySQL 进阶之路】表级锁、行级锁详解
1. 表级锁和行级锁的概念及区别 表级锁(Table Lock) 表锁是一种较为粗粒度的锁,锁定的是整个表。当某个事务加锁表时,其他事务对该表的任何读写操作都会被阻塞,直到锁被释放。因此,表锁具有较高的冲突概率…...
FPGA系列,文章目录
前言 FPGA(Field-Programmable Gate Array,现场可编程门阵列)是一种集成电路,其内部结构可以通过软件重新配置来实现不同的逻辑功能。与传统的ASIC(Application-Specific Integrated Circuit,专用集成电路…...
离谱的梯形滤波器——增加过渡点
增加过渡点 频率采样法(Frequency Sampling Method)是一种设计FIR滤波器的方法,通过在频域中指定希望的频率响应,然后利用逆离散傅里叶变换(IDFT)来获得滤波器的脉冲响应。然而,这种方法容易导…...
容积卡尔曼滤波(CKF)仿真抛物线运动
容积卡尔曼滤波(CKF)仿真抛物线运动 容积卡尔曼滤波(Cubature Kalman Filter, CKF)的MATLAB实现。CKF是一种用于非线性系统状态估计的算法,它通过在状态空间中采样点(容积点)来近似非线性函数的…...
FlightGear+MATLAB+飞行手柄实现实时飞控视景系统
文章目录 一、软件配置二、FlightGearMATLAB联合仿真第一步 复制文件第二步 新建文件夹第三步 打开demo第四步 demo说明第五步 打开Simulink第六步 连接FlightGear第七步 设置FlightGear第八步 生成FlightGear连接文件FlightGear的设置Network的设置File的设置生成.bat文件 第九…...
Oracle 11g Data Guard 环境中的 GAP 处理办法
概述 在Data Guard 环境中,当主库的某些日志没有成功传送到备库时,就会发生归档裂缝(Archive Gap)。目前,Oracle 提供了两种日志 GAP 的检测和处理机制:自动 GAP 处理(Automatic Gap Resolutio…...
自建服务器,数据安全有保障
在远程桌面工具的选择上,向日葵和TeamViewer功能强大,但都存在收费昂贵、依赖第三方服务器、数据隐私难以完全掌控等问题。相比之下,RustDesk 凭借开源免费、自建服务的特性脱颖而出!用户可以在自己的服务器上部署RustDesk服务端&…...
华为HarmonyOS 快速构建各种文本识别应用 -- 通用文字识别
适用场景 通用文字识别,是通过拍照、扫描等光学输入方式,将各种票据、卡证、表格、报刊、书籍等印刷品文字转化为图像信息,再利用文字识别技术将图像信息转化为计算机等设备可以使用的字符信息的技术。 可以对文档翻拍、街景翻拍等图片进行…...
shell作业
计算器 #!/bin/bash num1$1 num2$3 op$2 case $op in"")echo $((num1 num2));;"-")echo $((num1 - num2));;"*")echo $((num1 * num2));;"/")if [ $num2 -ne 0 ]; thenecho $((num1 / num2))elseecho "除数不能为0"fi;;*)…...
css部分
前面我们学习了HTML,但是HTML仅仅只是做数据的显示,页面的样式比较简陋,用户体验度不高,所以需要通过CSS来完成对页面的修饰,CSS就是页面的装饰者,给页面化妆,让它更好看。 1 层叠样式表&#…...
nginx 配置 跨域、压缩、文件下载 、静态文件、防盗链
1.跨域配置 在server模块下 访问如:http://127.0.0.1:8080/static/a.txt #跨域server {listen 8080;server_name localhost;#允许跨域请求的域, *代表所有add_header Access-Control-Allow-Origin *;#允许带上cookie请求add_header Access-Contro…...