数据结构的线性表 之 链表
前言:本章节内容、例题较多,可能朋友们需要多花一些时间去看才能理解。事不宜迟,我们赶紧来进入今天的内容 ——— 链表。
知识与概念
链表,它是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。接下来所看到的链接其实不是内存地址,而是引用。
它的引用链接次序十分像一辆小火车,把链表想象成一节一节的火车会更好理解。

以下是链表的举例:链表的每个节点可以看作成两个格子,上方的格子表示 val (在链表中表示基本数据类型),上方的格子的上方的引用是表示这个节点的引用,下方的格子(用next 表示)会写上下个节点的引用,作用是连接了下个节点。

图的上方的链表的具体定义是单向不带头非循环链表。拆分来说,单向就是指只能通过单向路径去遍历、查找、删除等操作,且只能遍历一次,遍历多次则会编译报错。
不带头就是在这个链表中没有“头”,如果你在有头的链表中进行插入节点的操作,那么你是只能插入到“头”的后面那些节点,头本身是不能动的;不带头的话则可以在最开始进行插入。
主要的方法及如何实现
下来我们来了解一下链表的所有的方法以及它们怎么去用,这里面有些方法与顺序表相同,但是实现方式完全不同,且链表的实现会更简单、快速。
先来看看IDEA中如何创建一个链表出来。我们得先创建出一个类(这里是MySingleList)。然后自定义一个方法(这里是createList),然后通过图一个个写出来。最后再实例化这个类出来一个对象,再通过对象.引用去调用这个方法即可。

接下来先是 display ,打印链表;这里的 head 并不是指它是带头的,这里的head是指从哪个节点开始遍历,可以进行修改,下同。这里通过 cur = cur.next;就可以不断遍历下一个节点。这里循坏条件是 cur != null 的原因是:最后一个节点的 .next 是null,如果你的条件写成 cur.next != null 的话,最后一个节点就不会遍历,运行的结果就会不同。如下图所示:


也可以直接这么去写,同样可以遍历成功。

下来是 addLast ,这种我们称为尾插法;就如字面意思,是在链表的末尾插入。由于链表是一个个的引用连接起来的,因此链表没有空间会占满的概念,所以就不需要去判满。看下图,应该就知道怎么写 addLast 了


还有一个叫做头插法,这里本文不写,大家可以自己尝试一下,与尾插法是相类似的。
所以在链表的相关操作中,基本上都是通过 val、head、next 来执行的。相当的方便,只需进行相对应修改就可以完成操作。在IDEA中看看实际效果:

与addLast 属于一类的还有 addIndex ,是指在某个位置插入节点。那么大家想了,如果你是在2下标作为插入的节点(假设head 为0下标),那是不是应该修改1下标节点的 next 和要加入节点的next 才能插入;3下标后面的也不用后移,因为链表没有内存占满的概念。具体可看下图:

不知道大家有没有想到,插入的时候可能会有一些特殊情况:
1.插入的位置可能是head,那么这时其实就可以看做为头插法,所以我们可以直接调用头插法即可
2.插入的位置可能是最后一个,那么这时其实就可以看做为尾插法,所以我们直接调用尾插法即可
3.给定的位置(下标)错误,当 index < 0 || index > len 时,可以抛出异常,也可以通过其他方式表示出异常。
具体实现如下:

所以我们可以看到,链表的逻辑是比较严密的;我们在编译的时候要考虑到各种情况。
下来是 remove(int key) ,它用来删除第一个关键字与key相同的节点。具体实现图大家可以先画出来,学习数据结构是一定要画图的,结合图形才能更好的理解。实现图如下:

既然是要找出节点,那么我们可以定义一个 findNodeOfKey 方法来找。


然后是 removeAllKey ,不用说,就是删除所有与key 系统的节点。那么,它也会有两三种情况:
1.出现要删除的节点连续
2.出现要删除的节点不连续
3.head 为空时
4.暂时先不说,留给大家猜一下
大家先照着这样画图、写代码看看。具体实现如下:


其实还有一种情况没有考虑,是什么情况呢?
答:当head.val = key 的时候。
所以我们应该加上这几行代码:

那么,这样就对了吗?
答:还是不对;不是return 的问题,去掉了return 也有问题。因为你放在前面,代码执行后,head其实被删除了,但是你有没有想过,如果head的下一个仍然是 head.val = key 呢?那是不是还是没删完。所以我们要么就写成循环;要么就这样

把判断放在最后去执行,这样才是没有问题。最后去判断不会影响我 prev 和 cur 。
下来是 clear ,是清除所有的节点。即将所有的节点都置为空;大家再来画图写一下。下面是实现:


这里需要注意的是,当所有的next 置为空时,若直接进行打印,打印出的结果会是1,为什么呢?
答:因为虽然所有的next 置为空了,但是还有 head 仍在被引用,head 它本身也是一个引用,所以我们在最后也要把它手动置空。
下来是 contains(int key) ,作用是查找key是不是在单链表当中。注意返回值是true 和 false 。
同样我们也可以定义一个指针来完成。

例题
下面是关于链表的一些选择题和编程题,我相信大家在做完这些题后会有进一步的提升;题目较多,但是学数据结构就是要多画图、多写代码、多调试,这三步可以说缺一不可。只有这样做才能将数据结构这个板块学的融会贯通。
203. 移除链表元素 - 力扣(LeetCode)
与上面的 remove 的写法有些类似。如下:

206. 反转链表 - 力扣(LeetCode)
本题虽然可以用递归和迭代实现,但这里不做讨论。
思路:给定一个12345,输出54321;那很明显,链表倒置过来了,但是由于是单向链表,所以只能遍历一次并且不能倒着搞。那么该怎么办呢?很简单,先提示一下,然后在下一行给出答案:用刚才链表存在的那些方法其中就有。
答案:头插法。假设给定12345,那我们先将2进行头插,head也变成是2;那么现在就是21345。然后再头插3,就是32145;不断循环往复就能得到54321。具体实现如下:

所以对于链表来说,代码量不大,重要的是思路和解法。来接着看下一题。
876. 链表的中间结点 - 力扣(LeetCode)
思路:本题可以使用快慢指针法。具体就是利用快慢指针,快指针每次走两步,慢指针每次走一步,所以快指针走的距离为慢指针的两倍,故当快指针遍历到链表末尾时,慢指针指向则记为中间节点。那么需要注意的是,得先让慢指针先走,因为链表的节点个数可能是奇数,也可能是偶数。这有什么区别呢?你可以画画图,当 fast(快指针) 走到空的时候,slow (慢指针)不是中间的右边,而是左边;如果调换则不会有这种情况。


21. 合并两个有序链表 - 力扣(LeetCode)
思路:因为两个链表都有 head 且都是有序的,那我们可以假设出指针 headA 和 headB ,让headA 和 headB 进行大小的比较,若是 A < B ,则让 B 插入 A 中;就这样不断插入下去,直到最后有可能链表长度相等,有可能不等,不等的情况下一定会有一方 null 了,另一方还有剩余。因为最后一个节点的 next 一定是 null ,这时可以定义一个 tmp ,让 tmp = headA || tmp = headB 来连接上剩余的即可。思路图及代码如下:

代码这里用IDEA进行演示:


开始下一道题之前,先考考大家,两个链表相交后是什么状态:是 X 型还是 Y 型?
答:Y 型。不会继续分支下去,大家随便画个图就能理解。
160. 相交链表 - 力扣(LeetCode)
思路:首先相交时,要相交的链表的最后一个节点后一定是跟着另一个链表的,所以需要插入节点;那么最后相交时会有两种情况,第一种是要相交的链表需要插入的节点下标等于另一个链表,第二种情况则是不等于另一个链表。那么长链表和短链表之间是一定有长度的差值的,所以我们可以定义两个指针,先求出两链表的长度,再让 lenA 减去 lenB ,在这期间如果 lenA > 0 ,则说明A链表更长,所以我们要先让长的链表(A链表)先走差值步,然后再一起走,直到两指针所指向的节点.next 相等时,就是两链表的交点。顺带一提,也要判断 null 的情况。
下面是具体的实现:



链表的回文结构_牛客题霸_牛客网 要求:只能在链表本身进行修改
思路:同样是使用快慢指针 fast 和 slow,找到中间节点后我们接下来要试图将链表的后半部分翻转,即就是使它与原来的方向相反;然后最后在首和尾分别两个用两个指针进行遍历并判断值是否相同;若最后都相同,则返回 true,否则是 false。参考图及参考代码如下:




然后下面是一些选择题,都比较简单,坚持就是胜利:

答案:A;A选项链表中节点之间是通过next引用相互指向的,故插入或者删除元素时只需要修改几个引用的指向即可,不需要搬移元素,是正确的。B选项中链表中的元素在内存中不一定连续,因为new的时候,会从堆上分配空间,具体分配出来的空间是否每次都连续是不一定的。C中链表的空间不连续,插入时也不需要扩容,因此不需要事先预估存储空间大小;这个说法错误,C不正确。D也是错误,链表不支持随机访问,需要访问任意位置元素时只能通过查找。

答案:C;如下图。


答案:D;链表的插入和删除不是所有情况下都比顺序表快,比如尾插尾删,顺序表的时间复杂度为O(1),并且如果是单链表,如果要在中间某个节点的前面插入/删除一个节点,则需要遍历。所以时间的快慢要分情况看待。
LinkedList
也是和ArrayList 相类似的,它里面也默认提供了一些方法。如下图:
这些方法和我们刚才讲的很类似,大家可以类比来看看。以下是一些具体的实现。了解一下即可
那么,ArrayList 和 LinkedList 有什么区别呢?下图仅供参考,大家可以根据自己的理解做出一张图。

那么,本篇文章到此结束!
本篇文章的截图和课件均摘自 比特科技 。希望能对你有帮助。