代码随想录:dayo4
1. 【24】两两交换链表中的节点
【题目描述】:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
【示例1】:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
【示例2】:
输入:head = []
输出:[]
【示例3】:
输入:head = [1]
输出:[1]
【提示】:
- 链表中节点的数目在范围
[0, 100]
内 0 <= Node.val <= 100
虚拟头结点实现
使用虚拟头结点完成,cur指针(用于遍历)要指向要操作的两个相邻节点的前一个节点,这样才方便进行二者之间的相互交换。
具体的两两交换流程可以通过自己画图演示来加强理解,尤其是对与交换顺序边的理解会有很大的帮助。
【Java代码实现】
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
//虚拟头结点方式实现
class Solution {
public ListNode swapPairs(ListNode head) {
//创建一个虚拟头结点并指向真正的头结点head
ListNode dummyHead = new ListNode(-1, head);
//当前指针cur指向虚拟头结点,用于遍历
ListNode cur = dummyHead;
/*终止条件:
当节点个数为偶数时,最后cur指向最后一个节点,cur.next = null
当节点个数为奇数时,最后cur指向倒数第二个节点,cur.next.next = null
*/
//注意两个条件的位置不能写反,否则可能报空指针异常或进入死循环
while (cur.next != null && cur.next.next != null) {
//以链表:dummyHead--1--2--3--4--5为例
//如:准备将1--2交换为2--1,cur指向1的前一位dummyHead
ListNode tmp1 = cur.next; //记录1节点
ListNode temp2 = cur.next.next.next; //记录3节点
//开始交换
cur.next = cur.next.next; // 变为:dummyHead--2
cur.next.next = tmp1; //变为:2--1
cur.next.next.next = temp2; //变为:1--3
//新链表为:dummyHead--2--1--3--4
//下次循环准备将:3--4换为4--3,此时cur要指向3的前一位1
cur = cur.next.next;
}
return dummyHead.next;
}
}
递归实现
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
//递归方法:建议通过画图演示加深理解
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
//获取当前节点的下一个节点
ListNode next = head.next;
//开始递归
ListNode newNode = swapPairs(next.next);
//进行交换
next.next = head;
head.next = newNode;
return next;
}
}
2.【19】删除链表的倒数第N个节点
【题目描述】:给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
【示例1】:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
【示例2】:
输入:head = [1], n = 1
输出:[]
【示例3】:
输入:head = [1,2], n = 1
输出:[1]
【提示】:提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
双指针算法
在链表中,若要删除一个节点,则一定要找到其前一个节点。
使用虚拟头结点可以方便具体操作,不用判断是是头结点还是普通节点。
寻找倒数第n个节点【思路】:
- 定义一个快指针和一个慢指针,先让快指针向后移动n步,再快慢指针一起向后移动,当快指针指向null时结束。此时慢指针所指的位置即为倒数第n个节点。
- 只需要保证快慢指针相差n步即可,再一起向后移动。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
//双指针算法(快慢指针)
ListNode dummyHead = new ListNode(-1, head); //创建虚拟头结点
ListNode fast = dummyHead; //创建快指针
ListNode slow = dummyHead; //创建慢指针
//快指针先移动n步
while (n-- > 0) {
fast = fast.next;
}
//快慢指针同时移动
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
//此时slow指向倒数第n个节点的前一个节点
slow.next = slow.next.next; //删除倒数第n个节点
return dummyHead.next;
}
}
3. 面试题【02.07】:链表相交
【题目描述】:
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
【示例1】:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
【示例2】:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
【示例3】:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
【提示】:
- listA 中节点数目为 m
- listB 中节点数目为 n
- 0 <= m, n <= 3 * 104
- 1 <= Node.val <= 105
- 0 <= skipA <= m
- 0 <= skipB <= n
- 如果 listA 和 listB 没有交点,intersectVal 为 0
- 如果 listA 和 listB 有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]
解题思路:
此题简单来说,就是求两个链表交点节点的指针。 这里大家要注意,交点不是数值相等,而是指针相等。
代码实现:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode cur1 = headA;
ListNode cur2 = headB;
int len1 = 0, len2 = 0;
while (cur1 != null) { //求A链表的长度
len1++;
cur1 = cur1.next;
}
while (cur2 != null) { //求B链表的长度
len2++;
cur2 = cur2.next;
}
//cur1、cur2重新指向各自链表头结点
cur1 = headA;
cur2 = headB;
//令cur1指向长链表的头结点、len1为其长度
if(len1 < len2) {
//交换长度,len1为大者
int tmpLen = len1;
len1 = len2;
len2 = tmpLen;
//交换头结点指向,len1指向长者
ListNode tmpNode = cur1;
cur1 = cur2;
cur2 = tmpNode;
}
int subLen = len1 - len2; //长度差
//令A、B链表尾部对齐,即令cur1向后移动subLen步
while (subLen-- > 0) {
cur1 = cur1.next;
}
//开始遍历,指针相同时则返回
while (cur1 != null) {
if (cur1 == cur2) {
return cur1;
}
//指针不相同,向后移动继续比较
cur1 = cur1.next;
cur2 = cur2.next;
}
return null;
}
}
4.【142】环形链表II
题目描述:
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
【示例1】:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
【示例2】:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
【示例3】:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
【提示】:
提示:
- 链表中节点的数目范围在范围
[0, 104]
内 -105 <= Node.val <= 105
pos
的值为-1
或者链表中的一个有效索引
快慢指针算法
做题思路:
可以使用快慢指针进行求解。【题目稍难】
-
判断链表是否有环:
若链表无环为一条直线,则快指针走得更快的情况下(每次移动2步),二者是不会相遇的。
若链表有环,快指针会先到达环里,并且在环里转圈。随后,慢指针**(每次移动1步)**也会到达环里。因为二者移动速度有差别,二者最后终将相遇于环。
相遇原因:当二者均走在环里的时候,快指针相对于慢指针是走1步去追赶慢指针,并且环是有一定长度的。所以二者必将相遇。
-
找环的入口:★
(以下涉及到部分的数学公式推导,推导后能更方便理解,便于具体代码的实现)
以下内容来自代码随想录网站,有兴趣的同学可以自行查看,卡哥有更详细的讲解,非常详细!
假设从头结点到环形入口节点的节点数为x,环形入口节点到 fast指针与slow指针相遇节点的节点数为y,从相遇节点再到环形入口节的点节点数为 z。
则相遇时: slow指针走过的节点数为:
x + y
; fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈后才遇到slow指针, (y+z)为 一环内节点的个数A。因为fast指针是一步走2个节点,slow指针一步走1个节点, 因此:fast指针走过的节点数 = slow指针走过的节点数 * 2,
即:
(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y):
x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:
x = n (y + z) - y
,再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:
x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。这个公式说明什么呢?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
-
当 n为1的时候,公式就化解为
x = z
,这就意味着,==从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。==也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。 -
当n大于1时,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
-
代码实现:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
//注意while里的判断顺序,因为fast走2步,要判断两个节点
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) { //链表有环
ListNode index1 = fast; //相遇的位置
ListNode index2 = head; //头结点位置
//等价于:
//两指针,从头结点走向相遇节点,各走1步,直到相遇,相遇处为环入口
while (index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
}
}
5. 总结
今天对双指针算法,尤其是快慢指针算法有了更深的理解。最后一题【环形链表2】中确实没想到利用数学公式推导节点之间的距离关系,只有把这个过程理解了才能用合适的代码实现。
万丈高楼平地起,加油!day day up !