24. 两两交换链表中的节点
题目链接:https://leetcode.cn/problems/swap-nodes-in-pairs/description/
解题思路
类似双指针法,两两交换,就是每两个一组,进行内部交换,交换就可以使用双指针法。
注意点:需要有个指针来保存上一组的尾结点,用于执行下一组被交换后的头结点。
代码如下:
/**
* 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 cur = head.next;
ListNode prev = head;
ListNode lastGroup = null; // 上一组的尾节点
head = head.next; // 反转后,head的下一个节点成为了头节点
while(true) {
// 保存下一组节点的第一个节点
ListNode next = cur.next;
// 反转当前组的节点
cur.next = prev;
prev.next = next;
// 将上一组的尾节点指向当前新的头节点
if (lastGroup != null) {
lastGroup.next = cur;
}
// 跳转到下一组节点
if (next == null || next.next == null) {
break;
}
lastGroup = prev;
cur = next.next;
prev = next;
}
return head;
}
}
19. 删除链表的倒数第 N 个结点
题目链接:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/
解题思路
数组存储法,内存占用较多
使用一个数组来存储整个链表的,遍历整个链表后,就可以使用O(1)来删除指定节点。
代码如下:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode[] indexNode = new ListNode[30];
int index = 0;
while(head != null) {
indexNode[index++] = head;
head = head.next;
}
// 如果删除的是头节点,则返回头结点执行的下一个节点
if (n == index) {
return indexNode[1];
}
indexNode[index-n-1].next = indexNode[index-n].next;
return indexNode[0];
}
}
双指针法
该题是双指针的经典案例,使用一个first节点从头往后先移动n个节点,当first到达第n个节点后,second节点和first开始同步向后移动,当first到达结尾后,second节点即是待删除的节点。
代码如下:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode mkHead = new ListNode(0, head); // 虚拟头结点
ListNode deletePrevNode = mkHead;
while(head != null) {
// 当first移动到n个节点后,待删除的上一个节点一起移动
if (n-- <= 0) {
deletePrevNode = deletePrevNode.next;
}
head = head.next;
}
// 删除节点
deletePrevNode.next = deletePrevNode.next.next;
return mkHead.next;
}
}
02.07. 链表相交
题目链接:https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/description/
解题思路
链表相交后的节点,都是一样的,所以只需要判断出两个链表相差多少个节点,然后使用双指针法,同时向后移动,同时判断两个链表的节点是否相同
复杂代码如下:
public class Solution {
public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode mkHeadA = new ListNode(0, headA); // 设置虚拟头节点
ListNode frontA = mkHeadA;
ListNode afterA = mkHeadA;
ListNode mkHeadB = new ListNode(0, headB);// 设置虚拟头节点
ListNode frontB = mkHeadB;
ListNode afterB = mkHeadB;
// 遍历链表A/B,frontA、frontA跳转到最后一个节点
ListNode rtn = null;
while(true) {
// 如果两个链表都到尾结点了
if (frontA.next == null && frontB.next == null) {
// 如果两个链表的最后一个节点都不是相交的,则表示没有相交节点
if (frontA != frontB) {
break;
}
// 如果两个链表的最后一个节点是相交的,则afterA 和 afterB一起向后移动,直到afterA == afterB
while(afterA != afterB) {
afterA = afterA.next;
afterB = afterB.next;
}
rtn = afterA;
break;
}
// 如果frontA到尾节点了,则afterB需要和frontB一起往后移动
if (frontA.next == null && frontB.next != null) {
afterB = afterB.next;
}
// 如果frontB到尾节点了,则afterA需要和frontA一起往后移动
if (frontB.next == null && frontA.next != null) {
afterA = afterA.next;
}
// 遍历链表A
if (frontA.next != null) {
frontA = frontA.next;
}
// 遍历链表B
if (frontB.next != null) {
frontB = frontB.next;
}
}
return rtn;
}
}
合并链表实现同步移动
使用两个指针同时移动,如果某个链表到尾部了,则跳转到另一个链表的头节点,同步移动。这样如果两个链表都到达尾部,并且都跳转到另一个节点后,先跳转的指针移动的节点数,就是两个链表相差的节点数。
代码如下:
public class Solution {
public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1 = headA, p2 = headB;
while(p1 != p2) {
if (p1 != null) {
p1 = p1.next;
} else {
p1 = headB;
}
if (p2 != null) {
p2 = p2.next;
} else {
p2 = headA;
}
}
return p1;
}
}
142. 环形链表 II
题目链接:https://leetcode.cn/problems/linked-list-cycle-ii/description/
暴力解法
设置一个列表,存储已走过的节点地址,判断列表中是否存在相同的地址,如果存在,则是环的入口节点地址。
优点:代码简洁,思路清晰。
缺点:空间复杂度为O(n)
时间复杂度:O(n)
空间复杂度:O(n)
代码如下:
public class Solution {
public ListNode detectCycle0(ListNode head) {
Set<ListNode> visited = new HashSet<>();
ListNode cur = head;
while(cur != null) {
if (!visited.add(cur)) {
return cur;
}
cur = cur.next;
}
return null;
}
}
快慢指针
分别定义一个 fast 指针和一个 slow 指针,fast指针每次移动两个节点,slow每次移动一个节点。如果fast指针和slow指针重合了,就表示该链表有环。
动画参考:
这里确认链表有环后,就是找出链表环的入口节点,这里需要用到数学知识:
假设头结点到环形入口节点的节点数为x,slow指针和fast指针相遇时,slow指针在环内移动的节点数为y,环链表剩下的节点数为z。
如图所示:
因为fast指针的移动速度是slow指针的两倍,且fast节点可能在环内已经移动了n圈,所以有以下公式:
(x + y) * 2 = x + y + n*(y + z)
我们如果要找出环的入口节点,即需要算出x,公式化简:
x + y = n * (y + z)
x = n * (y + z) - y
也就是说,如果设置index1、index2两个指针,分别从头结点和入口节点同时开始移动,且每次移动一个节点。那么,当index1到达入口节点时,index2已经跑了n圈,且还差 y 个节点就到达入口节点了。
再假设 index1 从头结点开始移动,index2从入口节点后面的 y 个节点开始移动,那当index1到达入口节点时,index2是不是也刚好到达入口节点。
而入口节点后面的 y 个节点,不就是 slow 指针和 fast 指针相遇时的节点吗?
总结:
分别定义一个 fast 指针和一个 slow 指针,fast指针每次移动两个节点,slow每次移动一个节点,
如果fast 和 slow没有相遇,表示没有环。
如果fast 和 slow指针相遇,表示有环。此时再设置index1指针从头结点开始,index2指针从相遇节点开始,同时以一个节点的速度开始移动,当index1和index2相遇时,就是入口节点。
时间复杂度:O(n)
空间复杂度:O(1)
代码如下:
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
// 快慢指针相遇,表示有环
if (fast == slow) {
ListNode ringStart = head;
// 右环后,再用一个指针从头结点出发,slow指针继续走,如果相遇,则该节点就是环的开始节点
while (ringStart != slow) {
ringStart = ringStart.next;
slow = slow.next;
}
return ringStart;
}
}
return null;
}
}