数据结构(Linked List链表)

前言:

嘿,亲爱的编程小伙伴

如果你正在为学习链表感到一丝紧张或者有点小茫然,别担心!链表就像一个神秘的魔法盒子,打开它,你会发现它其实并没有那么可怕,相反是一个充满无限可能和乐趣的世界。就像哈利·波特最初接触魔法那样

这里,你将与节点搜索下一步 "神秘添加 、**删除删除 和**查首次操作,您将逐步解锁链

另外,别看链表看上去有点儿抽象,我们将以最简单易懂的方式,带你一步一步攻克每一个难关。从一个个分段练习开始,阐明你可以自如地操作链表,就像和它打成一片,玩得不亦乐乎

那么,准备好了吗?放下手中的忧虑,带着满满的好奇心和大象的心态,一起进入有趣的编程大冒险吧!谁知道,学会链表之后,你可能会发现,它不仅仅是编程世界里的宝藏,还能激发你更多的编程创意和思路!🌈

开始吧,未来的链表大师,让我们一起愉快地探险,逐步征服这个神秘的编程大陆!🚀

1.链表的介绍

1.1什么是 Java 链表?

在Java中,链表(Linked List)是一种线性数据结构,由一系列节点(Node)组成。每个节点包含两个部分:

  1. 数据部分:存储实际的数据。
  2. 指针部分:指向链表中的下一个节点。该指针帮助连接链表中的所有节点。

链表的一个特点是,它的要素不需要在内存中是连续存储的,每个节点通过指针与下一个节点连接起来。

1.2链表的概念及结构

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。

2.链表的作用

链表的主要作用是提供一种灵活的方式来动态存储数据,特别是对于那些不需要随机访问数据,而是主要进行插入和删除操作的场景。相比于数据库,链表具有以下优势:

  1. 动态大小:链表的大小不是固定的,可以动态增长或缩小,不像队列那样需要提前定义大小。
  2. 插入和删除效率高:链表的插入和删除操作通常比阵列更高效(尤其是在中间插入或删除元素时),因为不需要移动元素,只需要调整指针。
  3. 内存使用灵活性:链表在内存中的分配不需要连续,因此阵列更适合存储大量动态数据,尤其是在内存分配不连续的情况下。

3.链表应用场景

链表在很多场景下都有应用,以下是一些常见的应用场景:

  1. 实现动态数据结构:

    • 链表非常适合实现需要间隙插入、删除元素的场景。例如:队列、栈等数据结构。
    • Java 提供了LinkedList类来实现链表结构,该类不仅可以作为队列(Queue)或栈(Stack)使用,还可以作为队列链表使用。
  2. 内存管理:

    • 操作系统中的内存管理,特别是对于内存碎片的管理,可以使用链表来实现。链表中的每个内存块表示一个节点,指向下一个可用内存块。
  3. 多层存储系统(LRU Cache):

    • LRU(最近最少使用,最近最少使用)存储算法可以通过链表来实现。每次访问一个元素时,元素就会被移到链表的头部,最少使用的元素则在链表的尾部,可以方便地在O(1) 期限终止。
  4. 圖表表示:

    • 在图中的邻接表表示中,链表用于存储每个节点的邻接节点。 每个节点存储一条链表,链表中的元素代表与该节点相邻的节点。
  5. 保险:

    • 在一些文件系统中,用链表来表示文件和目录之间的结构。特别是目录和文件有系统关系时,链表能够很方便地进行表示。
  6. 编辑器的实现:

    • 许多文本编辑器(如 Sublime Text、Vim 等)实现撤销功能时,采用了链表的数据结构,通过链表来存储操作历史,使得撤销操作更加高效。
  7. 实现复杂数据结构:

    • 链表也常用于实现其他复杂的数据结构,如跳表(Skip List)、LRU缓存、缓存表的碰撞链表等。

4.链表的优缺点

4.1优点:

  1. 插入和删除操作效率高,尤其是在链表的头部或中间。
  2. 动态分配内存,大小可以根据需求变化。

4.2缺点:

  1. 随机访问效率低,查找特定元素需要从头遍历链表。
  2. 每个节点都需要额外的空间仓储,增加了空间开销。
  3. 操作复杂度尤为突出,尤其是在实际情况下需要管理指针。

5.链表基本类型

链表可以根据不同的组织方式划分不同的类型:

1.单向链表(Singly Linked List)

单向链表是追踪的链表类型,每个节点有两个部分:

  • 数据部分:存储实际的数据。
  • 指针部分 :指向链表中的下一个节点。如果是最后一个节点,则指针指向null

特点

  • 只能从头节点开始遍历链表。
  • 每个节点只包含一个指向下一个节点的指针。
  • 操作简单,但只能单向遍历。

Head -> [data | next] -> [data | next] -> [data | null]

2.结构链表(双向链表)

链表与单向链表类似,但每个节点除了有一个指向下一个节点的指针(next)外,还包含一个指向前一个节点的指针(prev)。因此,每个节点允许在两个方向上进行遍历。

特点

  • 可以从头到尾和从尾到头进行遍历。
  • 每个节点有两个指针(nextprev),需要更多的内存空间。
  • 插入和删除操作更加灵活,尤其是在中间操作时。

null <- [prev | data | next] <-> [prev | data | next] <-> [prev | data | next] -> null

3.循环链表(Circular Linked List)

循环链表是一种特殊类型的链表,在这种链表中,最后一个节点的指针不指向null,而是指向链表的头节点。根据节点的指向方向,循环链表可以分为:

  • 单向循环链表:每个节点指向下一个节点,最后一个节点指向头节点。
  • 同时循环链表 :每个节点指向相邻两个节点,最后一个节点的next指向头,头节点的prev指向最后一个节点。

特点

  • 无头尾之分,可以从任意一个节点开始遍历。
  • 不会遇到null值,因此需要额外的控制条件来结束循环。

Head -> [data | next] -> [data | next] -> [data | next] -> (head)

4.循环双向链表(Circular Double Linked List)

循环节点链表结合了节点链表和循环链表的特点:每个节点都有两个指针(指向同类节点),并且链表的尾节点和头节点通过指针形成一个循环。

特点

  • 可以在两个方向上循环遍历链表。
  • 每个节点有两个指针(nextprev),比单向链表和节点链表更灵活。
  • 在尾部和头部插入、删除节点时更加高效。

(head) <-> [prev | data | next] <-> [prev | data | next] <-> [prev | data | next] <-> (head)

总结

  • 单向链表:每个节点指向下一个节点,适合顺序遍历,但不能逆向遍历。
  • 节点链表:每个节点都有指向前后节点的指针,支持节点遍历,操作更灵活。
  • 循环链表:最后一个节点指向头节点,形成环形结构,适合某些循环操作的场景。
  • 循环节点链表:结合了节点链表和循环链表的特点,适合需要进行节点循环交换的场景。

2.单向链表的实现

1.单向链表实现的方法:

//头插法

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.2不同方法的实现讲解

在实现链表前,我们需要构造出实现链表的基础:

复制代码
复制代码
static  class ListNode {
        public int val;
//val表示链表中的值
        public ListNode next;
//此val之下所对应的地址下表
        public ListNode(int val) {
            this.val = val;
        }
    }
    public ListNode head;
 //第一个val值的头结点  

|----------|------|------|------|------|------|
| val | 12 | 23 | 34 | 45 | 56 |
| val.next | 0x12 | 0x56 | 0x89 | 0x23 | 0x75 |
| | head | | | | |

1.链表的打印:

  • cur指标指向链表的头节点(head)。
  • 通过while循环,当curnull为时,将当前打印节点的值cur移动到下一个节点。
  • 直到遍历完整个链表,cur即为null,结束。

注意事项

  • 链表为空时 :如果链表为空(即考虑head == null),这个方法将不会打印任何东西。是否需要添加一个提示信息,比如"链表为空"
  • 换行符 :在链表输出完成后,你可能要添加一个行符System.out.println(),保证输出格式正确换行。

2.链表的长度:

  • 使用cur遍历链表,当curnull为时,递增count
  • 遍历完成后,返回节点的总数。

注意事项

  • 链表为空时 :当链表为空时,headnull,方法会返回0
  • 效率问题size()方法的时间复杂度是O(n),它需要遍历链表来统计节点数量。对大链表来说可能会慢一些,但这是链表本身的特性。

3.判断链表中是否存在该值

  • 使用cur遍历链表,遇到与key找到的节点时,返回true
  • 如果旅程完成都没有找到,返回false

注意事项

  • 链表为空时 :如果head == null,方法会直接返回false,没有问题。
  • 值的处理:当链表中存在多个相同的值时,该方法仅返回第一个重复匹配的节点。如果需要检查所有节点是否都包含该值,需要调整方法逻辑。

4.添加头结点:

  • 创建一个新节点nodenode.next指向当前的头节点(head)。
  • 然后,将head更新为新节点node,使得新节点成为链表的头节点。

注意事项

  • 链表为空时 :如果链表为空,headnull此方法仍然能正确工作,head将指向新节点。
  • 空指针问题node.next = head;能处理head == null的情况。如果没有正确处理,可能会引发空指针异常。

5.添加尾节点

  • 如果链表为空(head == null),则新节点成为链表的唯一节点,head指向新节点。
  • 否则,遍历链表直到找到最后一个节点,将其next指向新节点,从而将新节点添加到链表的末尾

注意事项

  • 空链表时的处理 :在链表为空时,应该将node.next指向null,而不是指向head,否则会导致死循环。需要修改为:node.next = null;
  • 遍历时的边界问题while (cur.next != null)确保能走到链表的最后一个节点,但如果链表有一个节点时,curnext就是null,跳出循环,防止空指针异常。
  • 尾部插入的效率:每次在尾部插入时都需要遍历整个链表,效率较低(O(n))。可以考虑维护一个尾部卸载来优化该操作。

6.任意位置插入,第一个数据节点为0号下标

  • 如果index小于0或大于链表的当前大小,则输出错误信息并返回。
  • 如果index == 0,调用addFirst(data)在头部插入。
  • 如果index == size(),调用addLast(data)在尾部插件。
  • 否则,遍历链表到指定位置,在该位置插入新节点。
    • 插入操作:首先将新节点的next指向当前节点的下一个节点,然后将当前节点的next指向新节点。

注意事项

  • index > size()的情况 :在判断位置合法时,需要检查index == size()是否合法,因为在addLast中已经处理了index == size()的情况。所以判断条件应该是index < 0 || index > size()
  • 位置插入时的逻辑 :当index == 0index == size()时直接调用addFirstaddLast,但在其他位置插入时,遍历过程中应注意保证正确更新cur.next
  • index--的错误 :在循环中,index--是为了控制循环次数,但代码写到了while (index - 1 != 0),应该写成while (index - 1 > 0),否则会导致死循环或越界。

7.删除第一次出现关键字为key的节点

  • 如果链表为空,直接返回。
  • 如果头节点的得分key,则将head更新为头节点的下一个节点,删除头节点。
  • 否则,调用finfNodeOfKey(key)查找包含key的节点。
    • 如果缺少节点,直接返回。
    • 如果找到,删除该节点:通过调整前一个节点(cur)的next指针,跳过待删除节点。

注意事项

  • 头节点删除 :如果head.val == key,直接删除头节点,但此时head已经更新为head.next,如果链表为空,head可能为null,需要小心避免空指针异常。
  • 找不到节点时的处理 :如果curnull,说明没有找到价值key的节点,此时方法应该返回,不进行后续操作。

  • 遍历链表,找到计算出key的节点时,返回它的前一个节点。

注意事项

  • 空指针异常 :在访问cur.next.val时,应该首先检查cur.next是否为null。如果是null,将导致空指针异常。修改为:if (cur != null && cur.next != null && cur.next.val == key)
  • 返回值的处理 :是返回节点cur,而不是cur.next,否则将返回错误的节点。

8.删除所有匹配key的节点

  • 使用prevcur遍历链表:
    • 如果cur.val == key,则删除当前节点,prev.next指向cur.next
    • 否则,移动prevcur
  • 如果头节点的估值key,需要特别处理,直接将head更新为头节点的下一个节点。

注意事项

  • 删除操作的错误 :在删除节点时,prev.next = cur;这行可能会导致链表断开。应改为prev.next = cur.next;,即跳过当前节点(cur)而不是prev.next指向cur自身。
  • 链表头部删除 :删除所有key节点时,还需要检查头节点是否是key,如果是,需要更新头节点为head.next

9.清空链表

  • 遍历链表,依次断开每个节点的next指针,释放内存。
  • 最后将head设置为null,表示链表为空。

注意事项

  • 链表清空的效率 :遍历链表时将每个节点的next设置为null,这虽然是清理链表的方法,但在cur = Ncur;之后,实际上不会再使用这个节点了。此时Java会自动进行垃圾回收(GC),不需要手动释放内存。
  • head = null重要性 :必须将head设置为null,否则链表的引用存在,造成内存泄漏。

3.给出初学者指出一些建议

1.理解链表的基本概念

  • 链表的结构 :链表是一种动态数据结构,由一系列引用节点组成,每个存储节点包含数据和指向下一个节点的(next)。与数组不同,链表的元素在内存中不连续,访问时需要通过指针遍历链表。
  • 头节点(head :链表的操作通常从头节点开始。头节点指向链表的第一个元素,若链表为空,headnull

学习链表时,首先需要掌握这些基本概念,以及链表与集群的差异。


2.小心空指向异常

  • 链表为空时的处理 :链表操作中最常见的错误就是忘记处理空链表的情况。例如,在访问cur.nextcur.val之前,必须确保链表不为空。每次操作前,检查head == nullcur == null非常重要。

  • 空链表:在删除操作、操作插入、查找操作等常见场景中,一定要处理链表为空的情况,否则会导致空指针异常或逻辑错误。

    示例 :在游览时检查cur != null


3.边界条件的处理

  • 插入与删除边界 :插入或删除节点时,特别要注意边界条件。例如,当插入操作的index == 0时,应特别处理为插入头部;index == size()当时,应处理为插入尾部。

  • 删除头节点 :删除链表中的头节点时需要特别小心。如果头节点的值匹配,需要直接将head更新为head.next,否则可能导致链表丢失。

    示例 :删除操作中,确保更新头节点时处理headnull的情况。


4.理解链表的效率

  • 线性时间复杂度(O(n)):链表的常见操作(如查找、删除、插入)时间复杂度通常为 O(n),其中 n 是链表的长度。例如,在尾部插入时,我们需要遍历整个链表找到最后一个节点。这对于长链表来说可能会比较慢。

  • 尾部优化 :为了避免每次尾部插入时维护链表,可以考虑一个尾部卸载tail,这样可以在 O(1) 的期限进行尾部插入。

    建议 :可以在链表中增加一个tail向导来优化尾部插入操作,从而减少时间复杂度。


5.边界条件与特殊情况

  • index位置插入时的检查 :当index小于0或大于链表的当前大小时,应该直接返回或发送异常。避免非法索引带来的问题。

  • 删除所有相同元素 :在删除链表中所有某个值key的节点时,需要处理所有节点都匹配的情况(例如,链表全是同一个值)。考虑清空头节点及其后面的所有节点。

    建议 :要特别注意当要删除出现在头部或尾部的节点时,如何正确调整head或链表的指针。


6.调试技巧

  • 打印链表 :在实现链表操作时,增加display()方法,可以帮助你洞察地看到链表的当前状态。调试时,通过打印链表的每个节点可以帮助你了解每一步的变化。

  • 逐步修改调试:在实现或代码时,可以逐步调试,检查每个方法的执行过程,尤其是插入、删除和查找方法。

    建议:使用调试器或打印调试信息,检查每次操作后链表是否按预期变化。


7.内存管理员垃圾回收

  • 链表节点的释放 :在Java中,垃圾回收器(GC)会自动管理内存,但需要理解对象引用的工作原理。在删除节点时,不再引用节点后,GC会回收这些节点的内存。避免使用cur.next = null这种方式强制清除节点内容,除非你有特定的需求。

  • 清空链表:在清空链表时,遍历每个节点并指向后续节点的引用是一个好习惯,这样可以保证链表中的所有节点被正确地垃圾回收。

    建议 :在清空链表时,逐个断开每个节点的引用,并最终将head设置为null,以便垃圾回收器能够处理这些无用的节点。

8.避免不必要的重复代码

  • 例如,在addIndex方法中,对于index == 0index == size(),已经分别通过调用addFirstaddLast进行了处理,可以避免代码重复。但在某些操作中,可以进一步提炼代码,减少重复。

9.掌握链表的常见操作与应用

  • 学习链表的常见操作和应用场景(如栈、队列的实现),这样可以帮助你更好地理解链表的实际使用和效率。
  • 栈(Stack) :可以使用链表的头部操作(如addFirstremoveFirst)来实现。
  • 队列(Queue) :可以通过链表的尾部操作(如addLastremoveFirst)来实现。

4.结语:

恭喜你,成功踏上了链表的奇妙旅程!🎉

在这个充满了指针、节点和动态内存的世界里,每一步都需要小心翼翼,每个操作都在拼图中看起来很有趣,缺少了一个空间细节,链表就可能"崩溃"!但别担心,只要你牢牢的牢牢掌握了插入、删除、查找这些基本操作,链表就不再是问题,而是你手中的"神奇工具"。

你可能会遇到过链表为空时的尴尬,或者解决过程中无尽的循环成功困扰,但只需耐心调试,边界条件、空指针等常见问题即可迎刃而解! 最重要的是:当你处理好一个问题时一个小小的挑战时,那份成就感,简直不比通过一场关超级难的游戏老板还要爽!🔥

编程就像是和计算机玩的"捉迷藏",有时候它藏在那里的儿不给你答案,但如果你认真思考、静静排查,每个问题都会迎刃而解。而且,你会发现:每掌握一个新、突破一个难关,你就离成为编程高手又近一步了。💪

所以,继续加油吧!每一次的编码实践都是一次马德里的探险,链表只是你编程之旅的第一站。未来还有很多数据结构等待去挑战------堆、栈、队列、图、哈希表......它们各有各的魅力和神秘的面纱等你去揭开。

最后,记住,编程是无尽乐趣的源泉。遇到困难时,对自己微笑,拍拍肩膀说一声:"嘿,我能行!"🚀

加油,未来的编程大师!🎉🎉


下期预告:

双向链表的实现

相关推荐
极客先躯4 分钟前
高级java每日一道面试题-2024年12月03日-JVM篇-什么是Stop The World? 什么是OopMap? 什么是安全点?
java·jvm·安全·工作原理·stop the world·oopmap·safepoint
梁小憨憨18 分钟前
变分推断(Variational Inference)
人工智能·算法·机器学习
就爱学编程26 分钟前
重生之我在异世界学编程之C语言:选择结构与循环结构篇
c语言·数据结构·算法
一只大侠29 分钟前
计算S=1!+2!+3!+…+N!的值:JAVA
java·开发语言
一只大侠31 分钟前
输入一串字符,以“?”结束。统计其中字母个数,数字个数,其它符号个数。:JAVA
java·开发语言·算法
以后不吃煲仔饭32 分钟前
面试小札:线程池
java·后端·面试
Oneforlove_twoforjob32 分钟前
【Java基础面试题011】什么是Java中的自动装箱和拆箱?
java·开发语言
winstongit43 分钟前
捷联惯导原理和算法预备知识
算法·机器人
优雅的落幕1 小时前
多线程---线程安全(synchronized)
java·开发语言·jvm
Charlie__ZS1 小时前
帝可得-设备管理
java·maven·intellij-idea·idea