数据结构学习笔记 1-1 链表(List)与 LeetCode真题图解(Java)

喜欢该类型文章可以给博主点个关注,博主会持续输出此类型的文章,知识点很全面,再加上LeetCode的真题练习,每一个LeetCode题解我都写了注释,比较适合新手入门数据结构与算法,后续也会更新进阶的文章。
课件参考—开课吧《门徒计划》

1-1 链表(List)及经典问题

链表基础知识

1

链表的典型应用场景

image-20211219114319942

现在我们有这样一个需求:现在这个4GB的内存是一个连续的存储空间,接下来我们是要在4GB的内存空间划分出来1GB的存储空间,如图:

image-20211219114523917

malloc函数是c语言里的内容,就好比java里的new;作用相同的,就是开辟存储空间。

可以想象成我找了一个1GB大小的数组,虽然有点夸张,只是举例。

经过我这样的申请空间之后,我们把我们的整个数据切分了,原本是好好的一整块数组,变成两块了,一个是2GB的碎片,一个是1GB的碎片,如图:

image-20211219114749982

那我们接下来在程序里申请空间的话,我们如何去管理这个内存碎片?

操作系统底层里面,把两个内存碎片用链表给他窜起来,我们可以通过2GB找到1GB的碎片,这样就不至于说我们内存里面出现碎片丢失,有一片存储空间找不到的情况;这是简单的一个应用。

image-20211219115107735

现在只是简单拿出来了解一下,后续会对这个LRU缓存淘汰算法进行详细的讲解。

什么是LRU?你可以理解为冰箱里有好多菜,我周一买了些菜,周二买了些菜,周日也买了些菜,但我们最后发现冰箱里放不下了,这时候我们新买的菜也不能不放进冰箱里,所以这时我们只能在冰箱里挑出一些菜给它扔掉,这时候我们会扔哪些菜呢?答案肯定是放的最久的菜,因为是最容易坏的。

image-20211219115224414

那我们如何区分哪个最久呢?我们可以用链表给它链起来,这样我们就知道,哪个菜是最开始就放进冰箱里的,我们可以优先将它淘汰掉,并放入我们新的菜,就好比将图片中的数据1删掉,存入新的数据5,再想填入新的数据,删掉数据2,以此类推。

image-20211219125113979

假设数据3是我们非常想吃的菜,但按照顺序即将被淘汰掉,我们可以把数据3拿出来,放在最后,更新一下它的优先级,这样的话数据3的优先级就高于数据4,我们删除数据2之后,会删除数据4;这是链表常用的一个实现方式。

image-20211219125354833

链表使用的场景还是非常多的。

链表与数组的结构性能对比

数组的存储是连续、聚合

链表的存储是离散的,我们通过抽象的指针域概念连接了起来

数组是一段连续的存储空间,支持数据随机访问,我们想找到哪个数据就可以立刻找到;但我们假设有一个100内存的数组,我想对这个数组进行扩容的话,我可能需要申请一个200内存的数组,再把100内存数组的所有数据原封不动的搬过去,这样我们才能得到一个200内存的数组。

链表是非连续的,扩容非常简单,找一个结点,就可以实现插入或者删除;但我们如果访问数据的话,就只能随机的访问,它不能按顺序进行访问;

这是我们从数据结构层面进行对比。

那我们从硬件层面考虑呢?

CPU读取优化

image-20211219130642401

读取数组时,假设我想读取数组的第100个数,我可以直接找到数据并将它拿出来,但真正计算机实现里面,我们真的是只把100这一个数据从内存拿到cpu里面吗?其实不是的,内存的内容导到cpu里,这叫做上下文切换。这种数据之间的读取是非常耗时的,所以说cpu不会大材小用的,你想读一个东西,我就只老老实实的读一个吗?cpu不是这样的,cpu很不老实,在我们当前节点前后,分别多读取一段内存空间,具体数据各个硬件不一样,当我们读取100的时候,cpu很多余的将100前后的x个空间都拿出来,它具体读取的内容是x+1+x,虽然你只想读取一个,但是cpu会读取2x+1个数。

image-20211219131515110

cpu为什么会这么做?正是因为数组的连续的,如果我想读取一个数的话,那么我读取前面或者后面这个数的概率会大一点,所以它会进行一个多余的操作,但这个操作也不算是多余,它读取一个数据,和再读取这个数据前后的数据,如果时间消耗几乎是一样的话,cpu为什么不多读一点呢?这就是我们cpu层面上的缓存优化

那对于链表来说,可以用这种优化吗?答案肯定是不能的。因为链表的存储是非常随机的,在内存里一个在东面,一个在西面,当你cpu想读当前这一个链表结点时,它不知道你前面的结点和后面的结点都在哪里,所以说链表有很大的一个缺点,它没有办法运用在我们的cpu缓存优化;对应到我们的工程实现里面,这两个的差距是非常非常大的。

LeetCode真题

经典面试题—链表的访问

LeetCode141.环形链表

难度:easy

判断链表是否有环

经典的快慢指针编程思想 但是我们从初学者的角度来看 是不会想到用快慢指针的

哈希表

最朴素的思想肯定是这种哈希的形式,我们只需要依次遍历整个链表,并创建一个哈希表来存储遍历过的结点。

遍历每一个结点,然后存入哈希表;在存入哈希表之前,先判断哈希表中是否存在该结点。

如果不存在,则存入哈希表;如果已存在,说明遍历到了重复结点,链表有环。

先来看无环的情况:

image-20220103182806626

有环情况:

image-20220104133803480

这个也是大部分人第一次遇到这种题的思路;大家在面试的时候,可以先写出这种最暴力的方式,然后再去优化,不要一直想着怎么去优化然后连最简单的方式都不写,要不然面试官会以为你连最简单的方式都不会。

总结

我们只需要遍历这个链表,在遍历过程中记录我们遍历的每个结点。

如果遇到next结点为null的结点,说明没有环。

如果遇到我们以前遍历过的结点,说明有环。

但是这种方式是不是有点复杂,我需要开辟一个额外的存储空间来存储每一个链表结点的地址,我们有更优化的方法吗?

题中有一段话:

进阶:你能用 O(1)(即,常量)内存解决此问题吗?

快慢指针

也就是说你不要动态的去申请内存空间,那我们如何不借助哈希表来实现这个题呢?那就是我们刚才提到的快慢指针

我们定义两个指针,p是慢指针(用红色标记),q是快指针(用黄色标记),每次让p结点走一步,q结点走两步。

image-20220104133820649

无环:

141-3

当快指针的next结点为null,或者快指针本身结点为null时,说明该链表没有环,遍历结束。

我们再来看一下有环的情况:

141-4

如果链表有环,那么快慢指针一定会相遇,指向同一个结点,当指向同一个结点时,遍历结束。

LeetCode题解:两种方法代码实现


LeetCode142.环形链表II

难度:mid

141是判断是否有环,142是求环的起点。

最简单的方法肯定还是利用哈希表来判断,这里不过多赘述,我们直接上快慢指针

image-20220104164834142

设p走了a的距离,同时q走了2a的距离,此时p处在环的起点,q在环里,设q距离p的距离为x。

image-20220104173255162

由于q与p之间的距离为x,所以当p再走x步时,q走了2x步,此时p,q两点相遇;所以相遇点离我们环起点的位置肯定是x。

链表环的总长为a+x,当我们拿到x的长度时,我们也得到了a的长度,此a的长度跟p第一次走到环的起点a的长度是一样的。

此时我们可以将p重新指向头结点,p,q同时走a步,即再次相遇,此相遇位置即为环的起点。

image-20220104173615658

重新分析一下整体过程:先看浅蓝色的线,p先走了a步,q走了2a步,此时p在环的起点,q在环里,设环的总长为a+x,看红色的线,先看下面,此时q距离p的距离即为x,再看上面,当p再走x步时,q走2x步,p,q两点相遇,此时相遇点距离起点的距离为x,看绿色的线,从而剩下的距离即为环的总长减去x,(a+x)-x=a,所以此时,我们可以将p指向头结点,p和q同时走a步,即p和q再次相遇,此时即为环的起点。

image-20220104173949342

LeetCode题解:两种方法代码实现


经典面试题—链表的反转

LeetCode206.反转链表

难度:easy

image-20220104191502046

迭代实现

双指针扫描

这个链表反转其实很像如何交换两个变量的值,我们需要借助另一个变量来实现。

定义指针prepre指向null

定义指针curcur指向我们的头结点。

定义指针nextnext指向cur所指向结点的下一个结点。

image-20220104191524763

首先,我们先将cur指针所指向的结点指向pre指针所指向的结点

1

然后移动指针pre到指针cur所在的位置

2

移动curnext所在的位置

3

将我们的next指针指向cur指针所指向结点的下一个结点

4

此时我们就完成了第一个结点的反转,1指向了null

我们继续刚才的操作,将2反转。

5

将剩下的反转。

6

cur指针指向null的时候,我们就完成了整个链表的反转。

7

递归实现

我们可以借助系统栈来实现,入栈的顺序是1->2->3->4,但我们弹出栈的顺序为4->3->2->1。

image-20220104211915361

递归实现对于新手比较难理解,后续到栈的部分会对这里有着更好的理解。

可以根据这张图看代码。

image-20220105214530641

LeetCode题解:两种方法代码实现

递归方法也可以看一下这个人的题解,清晰易懂。


LeetCode92.反转链表II

难度:mid

反转给定区间的链表。

首先我们定义一个虚拟头结点,起名叫做hair,它指向我们的真实头结点,它在链表里有一个专有名词:哨兵结点

为什么要定义一个这样的结点?其实这个东西很简单,它相当于在头结点上面加了一个边界条件,来防止一些边界特判,

假如我们要反转的是1->2->3,那我们反转完之后就是3->2->1->4->5->null,按照我们之前的编程经验来看,我们首先定义一个int p = head,然后再对p进行操作,最后返回的就是head,通常不对head进行操作,但对于这种情况,我们还可以直接返回head吗?head指向的是1,我们最终的链表就只剩1->4->5->null,前面的数被丢掉了;所以这时我们在最前面加入一个哨兵结点,就可以防止这种边缘特判,否则我们就要反转一次判断一下谁是头结点,把头结点记录一下,然后再反转再记录,会很麻烦,而且还加了很多特判。

QQ截图20220104231121

哨兵结点没有什么实际的意义,它不参与我们链表中的活动,你可以把它想象为空,或者里面存的值是-1

但它的任务只有一个,快速找到头结点

先将hair指向头结点

定义一个指针pre指向hair

定义一个cur指向pre指针所指向结点的下一个结点

让我们的pre指针和cur指针同时向后移动,直到我们找到了第left个结点,也就是题中的2。

1

双指针-头插法

我们可以先将中间的3删除 然后插入到1和2之间 此时的链表是这样的

image-20220105154426666

那么同理,cur指向的还是2,pre指向的还是1,所以我们也可以很轻易的将4删除,插入到1和3之间

此时链表已经达到了题中的反转要求

image-20220105154704850

但是单向链表我们想要删除一个结点,我们必须通过它的上一个结点,在插入的过程中,我们应该注意什么?

一个链表1->2,我们想将3插入到1和2之间,我只可以通过1才可以找到2,因为1的下一个结点就是2,我们把1叫做A,把3叫做B,首先让B结点的下一个结点指向A结点的下一个结点,也就是B.next = A.next,如图

image-20220105160023765

这么做的目的就是防止1结点和2结点之间断开,而找不到2结点了,这样我们就可以放心大胆的让A的下一个结点指向B,也就是A.next = B,如图

image-20220105160400050

这样3就插入进去了,这就是一个头插法的过程,所以本题我们就可以用这个方法来解决。

我们的核心思路就是将cur后面的结点插入到pre后面。

递归

递归还是比较难理解,由于博主也还是初学者,怕我讲的不到位,所以直接给大家看代码吧。

LeetCode题解:两种方法代码实现


经典面试题—链表的结点删除

LeetCode19.删除链表的倒数第N个结点

难度:mid

我们想删除一个结点,那就一定需要这个结点前面的结点。

假如我们想删除b,我们可以使a不指向b而指向c,这样就完成了删除,之后要将b释放掉,代码就是a.next = a.next.next

那我们该如何找到a呢?

image-20220106145822897

双重遍历 计算链表长度

删除链表倒数第N个结点,也就是删除链表第Length - N 结点的下一个结点。

image-20220106150037687

我们需要先遍历链表,求出Length的长度,然后将4删除。

1

题中有一段话:

进阶:你能尝试使用一趟扫描实现吗?

我们按上述方法是扫描了两遍,才可以删除结点,我们如何只扫描一遍就完成删除操作呢?

双指针

我们还是需要我们的老朋友,哨兵结点

同样也是双指针扫描,快慢指针,p指向hair,q指向head,先让q走n步,此时p和q同时走,当q走到null时,p必定指向的是待删除结点的前一个结点。

2

图是力扣程序员吴师兄的。

LeetCode题解:两种方法代码实现


LeetCode83.删除排序链表中的重复元素

难度:easy

其实还是挺简单的,毕竟是升序排列,也就是说重复元素必定是挨在一起的,我们可以用指针指向的val值跟下一个结点的val值进行对比,如果相同,则直接指向相同结点的下一结点,如果不同,指针往下移。

image-20220106174239919
LeetCode题解:代码实现

总结

链表这个数据结构看似简单,没有过多的条件,但它的题都非常有意思,它不涉及一些数据结构算法的思维,但是它每一题的解法都特别巧妙,可以把链表题当成智力测试题,脑筋急转弯,第一次上手那种环形链表,谁能想到用快慢指针呢?这个在面试当中其实是一样的,你不会这个东西你肯定就一直不会,我们如果在面试如果遇到了这种题,在面试的时候也很难再想出来,如果你面试之前做过了这些东西,那么你面试的时候也能大概率的想出来。

  • 54
    点赞
  • 91
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 33
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 33
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小成同学_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值