Leetcode 24. 两两交换链表中的节点
思路
完成链表中的两两交换,不是仅仅将数值交换,而是完成结点的交换。这里建议使用虚拟头结点,会方便很多,要不然没有前一个指针指向头结点,还要单独处理。对于虚拟头结点,可以参考昨日Leetcode203. 移除链表元素的讲解。
在两两交换元素时,建议画图来理解。由于链表结构的特殊性,我们需要拿到被交换元素的前后两个元素来操作。
初始时,我们进行如下三个步骤的操作,即可完成头两个结点交换。
完成头两个结点的交换后,链表如下:
难点
在两两交换链表中结点时,由于每组有两个结点。为了方便表述,这里分别用一号结点和二号结点来表示原始链表中被交换的左、右两个结点。而被交换两结点后一位结点叫作三号结点。
- 当完成步骤1操作时(pre.next指向二号结点),此时一号结点发生断链,无法获取到一号结点。所以在步骤1之前,我们需要额外定义一个指针cur来存储一号结点。
- 当完成步骤2操作时,原本二号结点的next指向三号结点,此时三号结点发生断链,无法获取到三号结点。所以在步骤3之前,我们需要额外定义一个指针temp来存储三号结点。
- 完成三个步骤后,就完成了两两结点的交换。需要移动pre指针,由思路里的分析得到,pre指针需要在被交换两结点的前一个位置,所以每次完成交换后,pre需向后移动两个位置。
- 如何确定遍历链表时的终止条件?
当链表元素是偶数时,终止条件为pre.next==null;当链表元素是奇数时,终止条件未pre.next.next==null。所以while()中填入pre.next!=null && pre.next.next!=null,两个条件不可以交换顺序,否则会出现空指针异常。
代码(Java)
写法1:虚拟头结点
时间复杂度:O(n)
空间复杂度:O(1)
/**
* 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) {
ListNode dummyhead = new ListNode(0);
dummyhead.next=head;
ListNode pre = dummyhead;
while (pre.next!=null && pre.next.next!=null){
//当链表为偶数个结点时,pre.next为空则终;
//当链表为奇数个结点时,pre.next.next为空则终止,即仅剩下一个未交换的结点;
//两个条件不能交换位置,以免发生空指针异常
ListNode cur = pre.next; //被交换的左结点(一号结点)
ListNode temp = cur.next.next; //被交换两结点后的下一个结点(三号结点)
pre.next=cur.next;///前结点的next指向二号结点,此时一号结点断开
pre.next.next=cur;//二号结点的next指向一号结点,此时三号结点断开
cur.next=temp;
pre=pre.next.next;//移动pre结点
}
return dummyhead.next;
}
}
写法2:递归
时间复杂度:O(n)
空间复杂度:O(1)
/**
* 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) {
// base case 退出提交
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;
}
}
代码随想录链接
下面是代码随想录的文章链接与视频链接,帮助大家更好地理解这道题。
文章
视频
Leetcode 19. 删除链表的倒数第N个节点
思路
这道题我们仍然采用虚拟头结点,此时无论删除哪个结点,都可以统一处理。
由于需要删除的是倒数第N个结点,我们需要用到双指针法,定义两个快慢指针,初始时都指向dummyhead,快指针先走N步,然后快慢指针一起移动,直到快指针为null,此时慢指针落后快指针N步,也就拿到了要删除的结点。
难点
从前面链表的学习中,我们知道要想删除某个结点,需要拿到该结点的前一个结点,从而进行删除操作。所以让快指针先走N+1步,随后快慢指针一起向后移动,从而拿到删除结点的前一个结点。
- 快慢指针初始化
- 快指针先移动N+1个单位
- 快慢指针一起移动,直到快指针为null,此时慢指针指向删除结点的前一个结点。
- 通过慢指针,完成对结点的删除操作。
代码(Java)
写法:双指针法
时间复杂度:O(n)
空间复杂度:O(1)
/**
* 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(0);
dummyhead.next=head;
ListNode fast = dummyhead;
ListNode slow = dummyhead;
for (int i=0;i<n+1;i++){
fast=fast.next;
}
while (fast!=null){
fast=fast.next;
slow=slow.next;
}
slow.next=slow.next.next;
return dummyhead.next;
}
}
代码随想录链接
下面是代码随想录的文章链接与视频链接,帮助大家更好地理解这道题。
文章
视频
Leetcode 面试题 02.07. 链表相交
思路
这道题需要求两个链表的共同结点,注意并不能理解为找到共同的结点值,而是求共同结点的指针。根据题意,我们可知一旦有共同结点的两链表,则从该结点开始到尾部都是共同结点。
所以,在判断是否是共同结点前,我们可以先将两个链表的长度变为一致,再来判断是否为共同结点。
代码(Java)
时间复杂度:O(n)
空间复杂度:O(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) {
int lenA=0,lenB=0;
ListNode curA=headA;
ListNode curB=headB;
while (curA!=null){//求得链表A的长度
lenA++;
curA=curA.next;
}
while (curB!=null){//求得链表B的长度
lenB++;
curB=curB.next;
}
//重置cur指针,使其指向链表头
curA=headA;
curB=headB;
//让链表A为长度最长的链表,如果链表B长度大于链表A,则交换
if (lenB>lenA){
//交换长度
int maxLen = lenB;
lenB=lenA;
lenA=maxLen;
//交换链表
ListNode temp = curB;
curB=curA;
curA=temp;
}
int margin = lenA-lenB;
//舍去链表A多于链表B的部分,使两链表尾部对齐
for (int i = margin; i>0 ; i--) {
curA=curA.next;
}
while(curA!=null){
if (curA==curB){
return curA;
}else {
curA=curA.next;
curB=curB.next;
}
}
return null;
}
}
代码随想录链接
下面是代码随想录的文章链接,帮助大家更好地理解这道题。
文章
Leetcode 142.环形链表II
思路
这道题可以看作两个问题:
- 判断链表是否有环
- 如果有环,如何确定环的入口
难点
- 判断链表是否有环
采用快慢指针的方法,分别定义快慢指针从头结点出发,快指针每次移动两个结点,慢指针每次移动一个结点,则若有环,快慢指针则会相遇。
为什么有环时,快慢指针一定会相遇呢?是否存在快慢指针一直在环里移动,从而错个彼此? 因为快指针相对于慢指针每次多移动一个结点,可以看作慢指针静止,快指针每次以一个结点的速度在环内追逐慢指针,所以一定会相遇。
- 如果有环,如何确定环的入口
因为快指针的移动速度是慢指针的两倍,
所以 2(x+y)=x+y+n(y+z),这里n代表快指针在环内转的圈数。
化简可得x=(n-1)(y+z)+z,当n>=1时,y+z都是环的长度(结点数),代表快指针在环内转的圈数,而落脚点都是在环入口结点,当n=1时,x=z代表的也就是相遇结点到环入口结点的距离🟰头结点到环入口结点的距离。
注意:在慢指针走第一圈时,就和快指针相遇了。
首先慢指针进环时,快指针已经进环了。
下面考虑边界情况,假设慢指针在进环入口,快指针也在环入口,那么把这个环展开成直线,如下图所示:
那么快慢指针一定会在环入口3相遇,此时慢指针走了一圈,快指针走了两圈。
所以在慢指针走第一圈时,就和快指针相遇了。
代码(Java)
/**
* 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!=null && fast.next!=null){//由于快指针每次移动两个结点
fast=fast.next.next;
slow=slow.next;
if (fast==slow){//有环
ListNode index1 = fast;//相遇点
ListNode index2 = head;//头结点
while(index1!=index2){
index1=index1.next;
index2=index2.next;
}
return index1;
}
}
return null;
}
}
代码随想录链接
下面是代码随想录的文章链接与视频链接,帮助大家更好地理解这道题。
文章
视频
总结
今天的内容较昨天来说,更进阶了一些,不仅考察对链表数据结构的基础操作,也需要对题目进行分析。特别是142.环形链表II,第一次写很困难,没有一点头绪,建议大家先看视频,最后写出来也要多花点时间理解,确定环和找环入口。另外使用了双指针法,多画图多分析,就很容易搞清楚两个指针分别的任务,循环的终止条件等。