【超详细】一文学会链表解题

} else if (curIndex == toIndex+1) {

toNext = tmp;

}

tmp = tmp.next;

curIndex++;

}

if (from == null || to == null) {

// from 或 to 都超过尾结点不翻转

throw new Exception(“不符合条件”);

}

// 步骤2:以下使用循环迭代法翻转从 from 到 to 的结点

Node pre = from;

Node cur = pre.next;

while (cur != toNext) {

Node next = cur.next;

cur.next = pre;

pre = cur;

cur = next;

}

// 步骤3:将 from-1 节点指向 to 结点(如果从 head 的后继结点开始翻转,则需要重新设置 head 的后继结点),将 from 结点指向 to + 1 结点

if (fromPre != null) {

fromPre.next = to;

} else {

head.next = to;

}

from.next = toNext;

}

变形题 2: 给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么将最后剩余节点保持原有顺序。

示例 :

给定这个链表:head–>1->2->3->4->5

当 k = 2 时,应当返回: head–>2->1->4->3->5

当 k = 3 时,应当返回: head–>3->2->1->4->5

说明 :

  • 你的算法只能使用常数的额外空间。

  • 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

这道题是 LeetCode 的原题,属于 hard 级别,如果这一题你懂了,那对链表的翻转应该基本没问题了,有了之前的翻转链表基础,相信这题不难。

只要我们能找到翻一组 k 个结点的方法,问题就解决了(之后只要重复对 k 个结点一组的链表进行翻转即可)。

接下来,我们以以下链表为例

来看看怎么翻转 3 个一组的链表(此例中 k = 3)

  • 首先,我们要记录 3 个一组这一段链表的前继结点,定义为 startKPre,然后再定义一个 step, 从这一段的头结点 (1)开始遍历 2 次,找出这段链表的起始和终止结点,如下图示

  • 找到 startK 和 endK 之后,根据之前的迭代翻转法对 startK 和 endK 的这段链表进行翻转

  • 然后将 startKPre 指向 endK,将 startK 指向 endKNext,即完成了对 k 个一组结点的翻转。

知道了一组 k 个怎么翻转,之后只要重复对 k 个结点一组的链表进行翻转即可,对照图示看如下代码应该还是比较容易理解的

/**

  • 每 k 个一组翻转链表

  • @param k

*/

public void iterationInvertLinkedListEveryK(int k) {

Node tmp = head.next;

int step = 0; // 计数,用来找出首结点和尾结点

Node startK = null; // k个一组链表中的头结点

Node startKPre = head; // k个一组链表头结点的前置结点

Node endK; // k个一组链表中的尾结点

while (tmp != null) {

// tmp 的下一个节点,因为由于翻转,tmp 的后继结点会变,要提前保存

Node tmpNext = tmp.next;

if (step == 0) {

// k 个一组链表区间的头结点

startK = tmp;

step++;

} else if (step == k-1) {

// 此时找到了 k 个一组链表区间的尾结点(endK),对这段链表用迭代进行翻转

endK = tmp;

Node pre = startK;

Node cur = startK.next;

if (cur == null) {

break;

}

Node endKNext = endK.next;

while (cur != endKNext) {

Node next = cur.next;

cur.next = pre;

pre = cur;

cur = next;

}

// 翻转后此时 endK 和 startK 分别是是 k 个一组链表中的首尾结点

startKPre.next = endK;

startK.next = endKNext;

// 当前的 k 个一组翻转完了,开始下一个 k 个一组的翻转

startKPre = startK;

step = 0;

} else {

step++;

}

tmp = tmpNext;

}

}

时间复杂度是多少呢,对链表从头到尾循环了 n 次,同时每 k 个结点翻转一次,可以认为总共翻转了 n 次,所以时间复杂度是O(2n),去掉常数项,即为 O(n)。

注:这题时间复杂度比较误认为是O(k * n),实际上并不是每一次链表的循环都会翻转链表,只是在循环链表元素每 k 个结点的时候才会翻转

变形3: 变形 2 针对的是顺序的 k 个一组翻转,那如何逆序 k 个一组进行翻转呢

例如:给定如下链表,

head–>1–>2–>3–>4–>5

逆序 k 个一组翻转后,链表变成(k = 2 时)

head–>1—>3–>2–>5–>4

这道题是字节跳动的面试题,确实够变态的,顺序 k 个一组翻转都已经属于 hard 级别了,逆序 k 个一组翻转更是属于 super hard 级别了,不过其实有了之前知识的铺垫,应该不难,只是稍微变形了一下,只要对链表做如下变形即可

代码的每一步其实都是用了我们之前实现好的函数,所以我们之前做的每一步都是有伏笔的哦!就是为了解决字节跳动这道终极面试题!

/**

  • 逆序每 k 个一组翻转链表

  • @param k

*/

public void reverseIterationInvertLinkedListEveryK(int k) {

// 先翻转链表

iterationInvertLinkedList();

// k 个一组翻转链表

iterationInvertLinkedListEveryK(k);

// 再次翻转链表

iterationInvertLinkedList();

}

由此可见,掌握基本的链表翻转非常重要!难题多是在此基础了做了相应的变形而已

链表解题利器—快慢指针

快慢指针在面试中出现的概率也很大,也是务必要掌握的一个要点,下文总结了市面上常见的快慢指针解题技巧,相信看完后此类问题能手到擒来。下文详细讲述如何用快慢指针解决以下两大类问题

  1. 寻找/删除第 K 个结点

  2. 有关链表环问题的相关解法

寻找/删除第 K 个结点

小试牛刀之一

LeetCode 876:给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

解法一

要知道链表的中间结点,首先我们需要知道链表的长度,说到链表长度大家想到了啥,还记得我们在上文中说过哨兵结点可以保存链表的长度吗,这样直接 从 head 的后继结点 开始遍历 链表长度 / 2 次即可找到中间结点。为啥中间结点是 链表长度/2,我们仔细分析一下

  1. 假如链表长度是奇数: head—>1—>2—>3—>4—>5, 从 1 开始遍历 5/2 = 2 (取整)次,到达 3,3确实是中间结点

  2. 假如链表长度是偶数: head—>1—>2—>3—>4—>5—>6, 从 1 开始遍历 6/2 = 3次,到达 4,4 确实是中间结点的第二个结点

画外音:多画画图,举举例,能看清事情的本质!

哨后结点的长度派上用场了,这种方式最简单,直接上代码

public Node findMiddleNode() {

Node tmp = head.next;

int middleLength = length / 2;

while (middleLength > 0) {

tmp = tmp.next;

middleLength–;

}

return tmp;

}

解法二

如果哨兵结点里没有定义长度呢,那就要遍历一遍链表拿到链表长度(定义为 length)了,然后再从头结点开始遍历 length / 2 次即为中间结点

public Node findMiddleNodeWithoutHead() {

Node tmp = head.next;

int length = 1;

// 选遍历一遍拿到链表长度

while (tmp.next != null) {

tmp = tmp.next;

length++;

}

// 再遍历一遍拿到链表中间结点

tmp = head.next;

int middleLength = length / 2;

while (middleLength > 0) {

tmp = tmp.next;

middleLength–;

}

return tmp;

}

解法三

解法二由于要遍历两次链表,显得不是那么高效,那能否只遍历一次链表就能拿到中间结点呢。

这里就引入我们的快慢指针了,主要有三步

1、 快慢指针同时指向 head 的后继结点

2、 慢指针走一步,快指针走两步

3、 不断地重复步骤2,什么时候停下来呢,这取决于链表的长度是奇数还是偶数

  • 如果链表长度为奇数,当 fast.next = null 时,slow 为中间结点

  • 如果链表长度为偶数,当 fast = null 时,slow 为中间结点

由以上分析可知:当 fast = null 或者 fast.next = null 时,此时的 slow 结点即为我们要求的中间结点,否则不断地重复步骤 2, 知道了思路,代码实现就简单了

/**

  • 使用快慢指针查找找到中间结点

  • @return

*/

public Node findMiddleNodeWithSlowFastPointer() {

Node slow = head.next;

Node fast = head.next;

while (fast != null && fast.next != null) {

// 快指针走两步

fast = fast.next.next;

// 慢指针走一步

slow = slow.next;

}

// 此时的 slow 结点即为哨兵结点

return slow;

}

有了上面的基础,我们现在再大一下难度,看下下面这道题

输入一个链表,输出该链表中的倒数第 k 个结点。比如链表为 head–>1–>2–>3–>4–>5。求倒数第三个结点(即值为 3 的节点)

分析:我们知道如果要求顺序的第 k 个结点还是比较简单的,从 head 开始遍历 k 次即可,如果要求逆序的第 k 个结点,常规的做法是先顺序遍历一遍链表,拿到链表长度,然后再遍历 链表长度-k 次即可,这样要遍历两次链表,不是那么高效,如何只遍历一次呢,还是用我们的说的快慢指针解法

  1. 首先让快慢指针同时指向 head 的后继结点

  2. 快指针往前走 k- 1 步,先走到第 k 个结点

  3. 快慢指针同时往后走一步,不断重复此步骤,直到快指针走到尾结点,此时的 slow 结点即为我们要找的倒序第 k 个结点

注:需要注意临界情况:k 大于链表的长度,这种异常情况应该抛异常

public Node findKthToTail(int k) throws Exception {

Node slow = head.next;

Node fast = head.next;

// 快指针先移到第k个结点

int tmpK = k - 1;

while (tmpK > 0 && fast != null) {

fast = fast.next;

tmpK–;

}

// 临界条件:k大于链表长度

if (fast == null) {

throw new Exception(“K结点不存在异常”);

}

// slow 和 fast 同时往后移,直到 fast 走到尾结点

while (fast.next != null) {

slow = slow.next;

fast = fast.next;

}

return slow;

}

知道了如何求倒序第 k 个结点,再来看看下面这道题

给定一个单链表,设计一个算法实现链表向右旋转 K 个位置。举例:

给定 head->1->2->3->4->5->NULL, K=3,右旋后即为 head->3->4->5–>1->2->NULL

分析:这道题其实是对求倒序第 K 个位置的的一个变形,主要思路如下

  • 先找到倒数第 K+1 个结点, 此结点的后继结点即为倒数第 K 个结点

  • 将倒数第 K+1 结点的的后继结点设置为 null

  • 将 head 的后继结点设置为以上所得的倒数第 K 个结点,将原尾结点的后继结点设置为原 head 的后继结点

public void reversedKthToTail(int k) throws Exception {

// 直接调已实现的 寻找倒序k个结点的方法,这里是 k+1

Node KPreNode = findKthToTail(k+1);

// 倒数第 K 个结点

Node kNode = KPreNode.next;

Node headNext = head.next;

KPreNode.next = null;

head.next = kNode;

// 寻找尾结点

Node tmp = kNode;

while (tmp.next != null) {

tmp = tmp.next;

}

// 尾结点的后继结点设置为原 head 的后继结点

tmp.next = headNext;

}

有了上面两道题的铺垫,相信下面这道题不是什么难事,限于篇幅关系,这里不展开,大家可以自己试试

输入一个链表,删除该链表中的倒数第 k 个结点

小试牛刀之二

判断两个单链表是否相交及找到第一个交点,要求空间复杂度 O(1)。

如图示:如果两个链表相交,5为这两个链表相交的第一个交点

画外音:如果没有空间复杂度O(1)的限制,其实有多种解法,一种是遍历链表 1,将链表 1 的所有的结点都放到一个 set 中,再次遍历链表 2,每遍历一个结点,就判断这个结点是否在 set,如果发现结点在这个 set 中,则这个结点就是链表第一个相交的结点

分析:首先我们要明白,由于链表本身的性质,如果有一个结点相交,那么相交结点之后的所有结点都是这两个链表共用的,也就是说两个链表的长度主要相差在相交结点之前的结点长度,于是我们有以下思路

1、如果链表没有定义长度,则我们先遍历这两个链表拿到两个链表长度,假设分别为 L1, L2 (L1 >= L2), 定义 p1, p2 指针分别指向各自链表 head 结点,然后 p1 先往前走 L1 - L2 步。这一步保证了 p1,p2 指向的指针与相交结点(如果有的话)一样近。

2、 然后 p1,p2 不断往后遍历,每次走一步,边遍历边判断相应结点是否相等,如果相等即为这两个链表的相交结点

public static Node detectCommonNode(LinkedList list1, LinkedList list2) {

int length1 = 0; // 链表 list1 的长度

int length2 = 0; // 链表 list2 的长度

Node p1 = list1.head;

Node p2 = list2.head;

while (p1.next != null) {

length1++;

p1 = p1.next;

}

while (p2.next != null) {

length2++;

p2 = p2.next;

}

p1 = list1.head;

p2 = list2.head;

// p1 或 p2 前进 |length1-length2| 步

if (length1 >= length2) {

int diffLength = length1-length2;

while (diffLength > 0) {

p1 = p1.next;

diffLength–;

}

} else {

int diffLength = length2-length1;

while (diffLength > 0) {

p2 = p2.next;

diffLength–;

}

}

// p1,p2分别往后遍历,边遍历边比较,如果相等,即为第一个相交结点

while (p1 != null && p2.next != null) {

p1 = p1.next;

p2 = p2.next;

if (p1 == p2) {

// p1,p2 都为相交结点,返回 p1 或 p2

return p1;

}

}

// 没有相交结点,返回空指针

return null;

}

进阶

接下来我们来看如何用快慢指针来判断链表是否有环,这是快慢指针最常见的用法

判断链表是否有环,如果有,找到环的入口位置(下图中的 2),要求空间复杂度为O(1)

首先我们要看如果链表有环有什么规律,如果从 head 结点开始遍历,则这个遍历指针一定会在以上的环中绕圈子,所以我们可以分别定义快慢指针,慢指针走一步,快指针走两步, 由于最后快慢指针在遍历过程中一直会在圈中里绕,且快慢指针每次的遍历步长不一样,所以它们在里面不断绕圈子的过程一定会相遇,就像 5000 米长跑,一人跑的快,一人快的慢,跑得快的人一定会追上跑得慢的(即套圈)。

还不明白?那我们简单证明一下

1、 假如快指针离慢指针相差一个结点,则再一次遍历,慢指针走一步,快指针走两步,相遇

2、 假如快指针离慢指针相差两个结点,则再一次遍历,慢指针走一步,快指针走两步,相差一个结点,转成上述 1 的情况

3、 假如快指针离慢指针相差 N 个结点(N大于2),则下一次遍历由于慢指针走一步,快指针走两步,所以相差 N+1-2 = N-1 个结点,发现了吗,相差的结点从 N 变成了 N-1,缩小了!不断地遍历,相差的结点会不断地缩小,当 N 缩小为 2 时,即转为上述步骤 2 的情况,由此得证,如果有环,快慢指针一定会相遇!

**画外音:如果慢指针走一步,快指针走的不是两步,而是大于两步,会有什么问题,大家可以考虑一下 **

/**

  • 判断是否有环,返回快慢指针相遇结点,否则返回空指针

*/

public Node detectCrossNode() {

Node slow = head;

Node fast = head;

while (fast != null && fast.next != null) {

fast = fast.next.next;

slow = slow.next;

if (fast == null) {

return null;

}

if (slow.data == fast.data) {

return slow;

}

}

return null;

}

判断有环为啥要返回相遇的结点,而不是返回 true 或 false 呢。

因为题目中还有一个要求,判断环的入口位置,就是为了这个做铺垫的,一起来看看怎么找环的入口,需要一些分析的技巧

假设上图中的 7 为快慢指针相遇的结点,不难看出慢指针走了 L + S 步,快指针走得比慢指针更快,它除了走了 L + S 步外,还额外在环里绕了 n 圈,所以快指针走了 L+S+nR 步(R为图中环的长度),另外我们知道每遍历一次,慢指针走了一步,快指针走了两步,所以快指针走的路程是慢指针的两倍,即

2 (L+S) = L+S+nR,即 L+S = nR

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

Ending

Tip:由于文章篇幅有限制,下面还有20个关于MySQL的问题,我都复盘整理成一份pdf文档了,后面的内容我就把剩下的问题的目录展示给大家看一下

如果觉得有帮助不妨【转发+点赞+关注】支持我,后续会为大家带来更多的技术类文章以及学习类文章!(阿里对MySQL底层实现以及索引实现问的很多)

吃透后这份pdf,你同样可以跟面试官侃侃而谈MySQL。其实像阿里p7岗位的需求也没那么难(但也不简单),扎实的Java基础+无短板知识面+对某几个开源技术有深度学习+阅读过源码+算法刷题,这一套下来p7岗差不多没什么问题,还是希望大家都能拿到高薪offer吧。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
733870249)]

[外链图片转存中…(img-Iynmb6wW-1712733870250)]

[外链图片转存中…(img-12OxVvz7-1712733870250)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

Ending

Tip:由于文章篇幅有限制,下面还有20个关于MySQL的问题,我都复盘整理成一份pdf文档了,后面的内容我就把剩下的问题的目录展示给大家看一下

如果觉得有帮助不妨【转发+点赞+关注】支持我,后续会为大家带来更多的技术类文章以及学习类文章!(阿里对MySQL底层实现以及索引实现问的很多)

[外链图片转存中…(img-Fp2CFNaX-1712733870251)]

[外链图片转存中…(img-i77rmI31-1712733870251)]

吃透后这份pdf,你同样可以跟面试官侃侃而谈MySQL。其实像阿里p7岗位的需求也没那么难(但也不简单),扎实的Java基础+无短板知识面+对某几个开源技术有深度学习+阅读过源码+算法刷题,这一套下来p7岗差不多没什么问题,还是希望大家都能拿到高薪offer吧。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值