目录
24.两两交换链表的节点
【文章讲解】代码随想录 (programmercarl.com)
【24. 两两交换链表中的节点|leetcode题目】24. 两两交换链表中的节点
【Carl视频讲解】帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点_哔哩哔哩_bilibili
24. 两两交换链表中的节点 题目
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路
方法一 正常画图
代码
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode virtualNode = new ListNode(-1);
virtualNode.next = head;
ListNode cur = virtualNode; //操作第一个节点的 前一个节点
ListNode temp ; //第二个节点
ListNode fistNode; //第一个节点
ListNode thirdNode; //第三个节点
while(cur.next != null && cur.next.next != null){//不能head为空、不能一组节点中只有一个
//画图
temp = cur.next.next; //保存第二个节点
fistNode = cur.next; //保存第一个节点
thirdNode = cur.next.next.next; //保存第三个节点
cur.next = temp; //virtualNode 连 2号节点
temp.next = fistNode; //2号节点 连 1号节点
fistNode.next = thirdNode; //1号节点 连 3号节点
cur = cur.next.next; //cur 后移两位 cur只在0,2,4..
}
return virtualNode.next;//head已变
}
}
方法二 递归
【递归详解.24.两两交换链表中的节点】LeetCode每日打卡.24.两两交换链表中的节点_哔哩哔哩_bilibili
使用递归来解决该题,主要就是递归的三部曲:
- 找终止条件:当递归到链表为空或者链表只剩一个元素的时候,没得交换了,自然就终止了。
- 找返回值:返回给上一层递归的值应该是已经交换完成后的子链表。
- 单次的过程:因为递归是重复做一样的事情,所以从宏观上考虑,只用考虑某一步是怎么完成的。我们假设待交换的俩节点分别为head和next,next的应该接受上一级返回的子链表(参考第2步)。就相当于是一个含三个节点的链表交换前两个节点,就很简单了,想不明白的画画图就ok
代码
class Solution {
public ListNode swapPairs(ListNode head) {
//递归
if(head == null || head.next == null){ //找终止条件
return head;
}
//单次的过程:因为递归是重复做一样的事情,所以从宏观上考虑,只用考虑某一步是怎么完成的单次的过程
ListNode next = head.next;//2号节点
//swapPairs(next.next)迟早等于null,等于null后,return next(第二个节点) 赋给前值swapPairs(next.next)
head.next = swapPairs(next.next);//1号节点 连 下一组的4号节点 后写!
next.next = head;//2 连 1
return next; //找返回值 第二个节点 先写!
}
}
总结
正常写法 是1号节点连接下一组的3号节点,递归 是1号节点连接下一组的4号节点。
19.删除链表的倒数第N个节点
【文章讲解】代码随想录 (programmercarl.com)
【LeetCode:19.删除链表倒数第N个节点|leetcode题目】19. 删除链表的倒数第 N 个结点
【Carl视频讲解】链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点_哔哩哔哩_bilibili
【5分钟学会:快慢指针】【LeetCode 每日一题】19. 删除链表的倒数第 N 个结点 | 手写图解版思路 + 代码讲解_哔哩哔哩_bilibili
19.删除链表倒数第N个节点 题目
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
- 遇到无法定位头节点,头节点可能被移动删除,导致定位失效的时候可以考虑用虚拟头节点。
- 使用虚拟头节点:主要是方便,在于我们不需要对操作的节点,是不是头节点进行特殊判断,而可以采用统一的一个方式进行删除操作
方法一:暴力解法
遍历整个链表,倒数第 n 个结点,其实删除节点是 size - n + 1,其前一个节点是size - n。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode virtualNode = new ListNode(-1);
virtualNode.next = head;
//计算链表长度
int size = 0;
ListNode cur = head; //临时变量
while(cur != null){
size++;
cur = cur.next;
}
//找倒数第n个的前一个节点
cur = virtualNode;
for(int i = 0; i < size - n; i++){ //倒数第n个节点的前一个节点 = size - n处的节点
cur = cur.next;
}
cur.next = cur.next.next;
return virtualNode.next; //可能head已被删
}
}
方法二:快慢指针 1.1
思路
双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。最后删掉slow所指向的节点就可以了
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode virtualNode = new ListNode(-1);
virtualNode.next = head;
//快慢指针
ListNode fast = virtualNode, slow = virtualNode;
//快指针先走n步 到达第 n 个节点
for(int i = 0; i < n; i++){
fast = fast.next;
}
//然后快慢指针 同时走 size - n 步 使快指针到第size个节点(尾部) 慢指针刚好到 倒数第n个节点的前一个节点
while(fast.next != null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return virtualNode.next;
}
}
方法三 快慢指针1.0
方法二和方法三的主要区别是 fast 最后的指向不同。
方法二是最终指向链表最后一个节点,而方法三是最终指向链表最后一个节点的next域(null)
其实,也可以这样理解方法二操作的是链表长度数量的节点,方法三操作的是链表长度数量的节点 + 最后一个空域
代码
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode virtualNode = new ListNode(-1);
virtualNode.next = head;
//快慢指针
ListNode fast = virtualNode, slow = virtualNode;
n++; //防止n 超过链表长度,在for循环后 再添一步fast = fast.next,fast已为空,就操作空指针了
//快指针先走 n + 1 步 到达第 n + 1 个节点
for(int i = 0; i < n; i++){
fast = fast.next;
}
//画图
//然后快指针走 size - (n+1) + 1 步 使快指针指向第size个节点(尾部) 的next (null处)
// 慢指针也走 size - (n+1) + 1 步, 使慢指针刚好到 倒数第n个节点的前一个节点
while(fast != null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return virtualNode.next;
}
}
160.链表相交
【文章讲解】代码随想录 (programmercarl.com)
【LeetCode:160.链表相交|leetcode题目】面试题 02.07. 链表相交
【视频讲解】Leetcode刷题 160.相交链表 Intersection of Two Linked Lists_哔哩哔哩_bilibili
160.链表相交 题目
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
方法一 先统计两个链表的长度
可以先统计两个链表的长度,如果两个链表的长度不一样,就让链表长的先走,直到两个链表长度一样,这个时候两个链表再同时每次往后移一步,看节点是否一样,如果有相等的,说明这个相等的节点就是两链表的交点,否则如果走完了还没有找到相等的节点,说明他们没有交点,直接返回null即可,来画个图看一下。
链表的长度情况
如果2个链表的长度相等的话,进行遍历时,将同时指针指向头节点。且当2个指针地址相同时,就是相交结点,
但是当链表长度不相等,循环中,a b两指针的运动步数又相同,所以a b指针肯定是不能同时到达相交结点。
然后解决这个问题的方法就是让a b在同一个循环中。让两个链表有相同的循环次数。
如果两个链表如果相交的话,两个指针一定某时刻同时指向同一个节点,即地址相同。所以有两种思想,一种思想是对齐2个链表的尾部。因为从链表尾部进行遍历判断比较难,所以我们使循环中,2个链表尾部对齐,且长度相等,为链表长度小的。
还有一种思想就是a指针先遍历完, a链表再遍历 b链表, b指针遍历完 b列表再遍历 a链表,这样,他们在循环中 循环次数就是相同的,如果两个链表相交,一定会在遍历后一部分的时候2个指针同时指向同一个节点。
使循环中 链表a、b长度相等 有两种方法
一种方法是2个链表谁长谁减小长度,走到 两链表长度相等的位置
另一种方法是谁长谁就是a链表(始终操作长链表),然后a链表遍历到相等的位置
//统计链表A和链表B的长度
int lenA = length(headA), lenB = length(headB);
//统计链表的长度
private int length(ListNode node) {
int length = 0;
while (node != null) {
node = node.next;
length++;
}
return length;
}
对了,统计链表A和链表B的长度,用这种解法,就可以在计算链表长度的时候,少写一步(改变临时指针cur的指向)
整体代码
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 使同长 计算链表长度法
int sizeA = 0, sizeB = 0;
ListNode curA = headA;
ListNode curB = headB;
while(curA != null){// 求链表A的长度
sizeA++;
curA = curA.next;
}
while(curB != null){// 求链表B的长度
sizeB++;
curB = curB.next;
}
curA = headA;
curB = headB;
//方法一 使同长
//如果节点长度不一样,节点多的先走,直到他们的长度一样为止
// while(sizeA != sizeB){
// if(sizeA > sizeB){
// sizeA--;
// //如果链表A长,那么链表A先走
// curA = curA.next;
// }else{
// sizeB--;
// //如果链表B长,那么链表B先走
// curB = curB.next;
// }
// }
//方法二 使同长
//始终最长的链表是A
// 让curA为最长链表的头,lenA为其长度
if (sizeB > sizeA) {
//1. swap (sizeA, sizeB);
int tmpLen = sizeA;
sizeA = sizeB;
sizeB = tmpLen;
//2. swap (curA, curB);
ListNode tmpNode = curA;
curA = curB;
curB = tmpNode;
}
// 求长度差
int gap = sizeA - sizeB;
// 让curA和curB在同一起点上(末尾位置对齐)
while (gap-- > 0) {
curA = curA.next;
}
//然后开始比较,如果他俩不相等就一直往下走
while(curA != curB){
curA = curA.next;
curB = curB.next;
}
//走到最后,最终会有两种可能,一种是headA为空,
//也就是说他们俩不相交。还有一种可能就是headA
//不为空,也就是说headA就是他们的交点
return curA;
}
}
方法二,双指针解决
要是能实现一种算法让两个指针分别从A和B点往C点走,两个指针分别走到C后,又各自从另外一个指针的起点,也就是A指针第二次走从B点开始走,B指针同理,这样,A指针走的路径长度 AO + OC + BO 必定等于B指针走的路径长度 BO + OC + AO,这也就意味着这两个指针第二轮走必定会在O点相遇,相遇后也即到达了退出循环的条件
推理过程如下
我们还可以使用两个指针,最开始的时候一个指向链表A,一个指向链表B,然后他们每次都要往后移动一位,顺便查看节点是否相等。如果链表A和链表B不相交,基本上没啥可说的,我们这里假设链表A和链表B相交。那么就会有两种情况,
一种是链表A的长度和链表B的长度相等,他们每次都走一步,最终在相交点肯定会相遇。
一种是链表A的长度和链表B的长度不相等,如下图所示
虽然他们有交点,但他们的长度不一样,所以他们完美的错开了,即使把链表都走完了也找不到相交点。
我们仔细看下上面的图,如果A指针把链表A走完了,然后再从链表B开始走到相遇点就相当于把这两个链表的所有节点都走了一遍,同理如果B指针把链表B走完了,然后再从链表A开始一直走到相遇点也相当于把这两个链表的所有节点都走完了
所以如果A指针走到链表末尾,下一步就让他从链表B开始。同理如果B指针走到链表末尾,下一步就让他从链表A开始。只要这两个链表相交最终肯定会在相交点相遇,如果不相交,最终他们都会同时走到两个链表的末尾,我们来画个图看一下
如上图所示,A指针和B指针如果一直走下去,那么他们最终会在相交点相遇
代码
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//走 A + B 链表法
ListNode curA = headA;
ListNode curB = headB;
while(curA != curB){ // 相等时 退出
//如果指针curA不为空,curA就往后移一步。
//如果指针curA为空,就让指针curA指向curB(注意这里是headB不是curB,curB目前可能不处于头节点位置。)
curA = curA == null ? headB : curA.next;
//指针curB同上
curB = curB == null ? headA : curB.next;
}
//curA要么是空,要么是两链表的交点
return curA;
}
}
142.环形链表II
【文章讲解】代码随想录 (programmercarl.com)
【LeetCode:142.环形链表II|leetcode题目】142. 环形链表 II
【Carl视频讲解】把环形链表讲清楚! 如何判断环形链表?如何找到环形链表的入口? LeetCode:142.环形链表II_哔哩哔哩_bilibili
【环内相遇|动画】141.环形链表.gif (568×402) (bcebos.com)
142.环形链表II 题目
- 给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
- 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
- 不允许修改 链表
主要考察两知识点:
- 判断链表是否环
- 如果有环,如何找到这个环的入口
判断链表是否有环
使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么一定会相遇
快指针每次移动两个节点,慢指针每次移动一个节点。快指针先进入环,慢指针后进入。快指针相当于追慢指针的过程,快指针相对于慢指针来说,每一次以一个节点的移动速度靠近慢指针,所以一定会相遇,不会直接跳过慢指针。如果快指针每次移动3步,则每一次以两个节点的移动速度靠近慢指针,可能会跳过。
判断是否有环的时间复杂度
如果链表不存在环,快指针移动到链表末尾空域结束,需要O(n)的时间,如果链表存在环,快指针和慢指针的差距每一轮缩小一步,所以需要n轮
如果有环,如何找到这个环的入口
- 当fast == slow时, 两指针在环中 第一次相遇 。下面分析此时fast 与 slow走过的 步数关系 :
- 设链表共有 a+b 个节点,其中 链表头部到链表入口 有 a 个节点(不计链表入口节点),链表环 有 b 个节点;设两指针分别走了 f,s 步,则有:
- fast 走的步数是slow步数的 2 倍,即 f=2s;
- fast 比 slow多走了 n 个环的长度,即f=s+nb;( 解析: 双指针都走过 a 步,然后在环内绕圈直到重合,重合时 fast 比 slow 多走 环的长度整数倍 )
- 以上两式相减得:f=2nb,s=nb,即fast和slow 指针分别走了 2n,n 个 环的周长
目前情况分析:
- 如果让指针从链表头部一直向前走并统计步数k,那么所有 走到链表入口节点时的步数 是:k=a+nb(先走 a 步到入口节点,之后每绕1 圈环( b 步)都会再次到入口节点)。
- 而目前,slow 指针走过的步数为 nb 步。因此,我们只要想办法让 slow 再走 a 步停下来,就可以到环的入口。
- 但是我们不知道 a 的值,该怎么办?依然是使用双指针法。我们构建一个指针,此指针需要有以下性质:此指针和slow 一起向前走 a 步后,两者在入口节点重合。那么从哪里走到入口节点需要 a 步?答案是链表头部head。
- 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
为什么一圈内会相遇
因为慢指针走一圈,快指针走了两圈,而快指针和慢指针的距离肯定在一圈之内
公式中有n,快指针多转n圈,且圈数n 最少为1圈,因为y、z 的分界点是相遇点,快指针最少多走(z + y)个节点
为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?
证明一圈内会相遇
slow进环的时候,fast一定是在环的任意一个位置
那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。
因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。
也就是说slow一定没有走到环入口3,而fast已经到环入口3了。
这说明什么呢?
在fast没到环入口3之前,slow已经与fast相遇了。
那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去。
代码
public ListNode detectCycle(ListNode head) {
//快慢指针
ListNode fast = head;
ListNode slow = head;
//先判断有无环,后找环入口节点
while(fast != null && fast.next != null){//fast走在前面,一次走两步,若有环,fast不可能为空
fast = fast.next.next;
slow = slow.next;
if(fast == slow){ //若有环 , fast 和 slow 相遇
ListNode index1 = slow; //从相遇结点
ListNode index2 = head; //从头结点
while(index1 != index2){ //找环入口节点,相等退出
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
}
}