前言
Leetcode链表题组
一、24题(两两交换链表中的节点)
题目描述:
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
class Solution {
public ListNode swapPairs(ListNode head) {
if(head==null) return head;
// 虚拟头节点统一操作
ListNode dummyhead = new ListNode(0);
dummyhead.next = head;
// 四个节点为一组
ListNode pre,curleft,curright,tmp;
pre = dummyhead;
curleft = head;
curright = head.next;
// 判断当前处理的两个节点是否存在
while(curleft!=null&&curright!=null){
tmp = curright.next;
pre.next = curright;
curright.next = curleft;
curleft.next = tmp;
pre = curleft;
curleft = tmp;
if(curleft==null){
break;
}
curright = tmp.next;
}
return dummyhead.next;
}
}
算法解析:
- 每个当前操作的一组节点取名为[curleft,curright],除此以外还涉及curleft左边的节点left,curright右边的节点tmp。
- 加入一个dummyhead以统一操作,每次操作涉及四个节点(详见易错点的第一点分析)。
易错点:
- 首先要分析出来进行一次互换节点操作涉及四个节点(区别于反转链表的三个节点),假设操作的是curleft和curright的互换:
pre->curleft->curright->tmp
则一次完整的互换操作为:
pre.next = curright;
curright.next = currleft;
currleft = tmp;
所以对于一组当前current需要互换的节点[curleft,curright],要记录curleft左边的节点pre,以及curright右边的节点tmp。
- 还是老问题,对于Xxx.next,一定要判断Xxx有没有可能为null指针。
- 交换过后指针的移动:注意交换之后四个元素的顺序是pre->curright->curleft->tmp,所以pre移到curleft处(而不是curright),curleft移到tmp处,curright移到tmp处。
二、19题(删除链表的倒数第N个节点)
题目描述:
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
解法1
mine:时长击败100%用户😀
/**
* 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 cur=head;
int size = 0;
while(cur.next!=null){
size++;
cur = cur.next;
}
size = size+1;
//一共size个节点
//删除倒数第n个,即删除正数第(size-x+1)个
if(n==size){
return head.next;
}
//删除第m个元素,要找到第m-1个元素
int m = size-n+1;
cur = head;
int i = 1;
while(i<m-1){
cur=cur.next;
i++;
}
cur.next = cur.next.next;
return head;
}
}
算法思路:
- 所谓倒数第n个,其实是正数第(size-n+1)个,所以找到第size-n个元素,执行cur.next = cue.next.next即可。
- 遍历一次数组找到size,再从头找到第size-n个元素。
解法2
虽然但是,mine的时间复杂度也是O(2n)=O(n),这个是O(n),感觉也没太必要
public ListNode removeNthFromEnd(ListNode head, int n){
ListNode dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode fastIndex = dummyNode;
ListNode slowIndex = dummyNode;
// 只要快慢指针相差 n 个结点即可
for (int i = 0; i <= n ; i++){
fastIndex = fastIndex.next;
}
while (fastIndex != null){
fastIndex = fastIndex.next;
slowIndex = slowIndex.next;
}
//此时 slowIndex 的位置就是待删除元素的前一个位置。
//具体情况可自己画一个链表长度为 3 的图来模拟代码来理解
slowIndex.next = slowIndex.next.next;
return dummyNode.next;
}
算法思路:快慢指针,初始化让快指针比慢指针多走n步。然后遍历节点,当快指针走到末尾时,慢指针正好指向需要能执行删除操作元素的位置。(大意是这样,具体代码实现的+1-1还要再分析)
三、02.07(链表相交)
题目描述:
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
/**
* 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 lastA=headA,lastB=headB;
if(headA==null||headB==null) return null;
//计算长度
int sizeA=1,sizeB=1;
while(lastA.next!=null){
lastA = lastA.next;
sizeA++;
}
while(lastB.next!=null){
lastB = lastB.next;
sizeB++;
}
if(lastA.val!=lastB.val) return null;
lastA = headA;
lastB = headB;
//把指针移到链表长度相等的位置
while(sizeA>sizeB){
lastA=lastA.next;
sizeA--;
}
while(sizeB>sizeA){
lastB=lastB.next;
sizeB--;
}
while(lastA!=lastB){
lastA = lastA.next;
lastB = lastB.next;
if(lastA == null){
break;
}
}
return lastA;
}
}
题目分析:得从空间内存角度理解,即两个链表从某个节点开始共用链表了,而不是两个链表某一段值相等。所以如果这个节点在A链表中是lastA,在B链表中是lastB,则有:lastA = lastB
。所以关键是比较节点的地址,而不是节点的值。
算法思路:某一段是公用的,必然长度相等,所以首先要把两个链表变成同一长度。然后逐个节点进行地址比较即可。
易错点:
①一开始思考错了,想成是找到两个节点:lastA.next=lastB.next,这样会很容易出错,因为有长度为1这个特殊情况,导致lastA.next == lastB.next == null
。其实是找到两个地址相同的节点。
②为什么会这么想呢,因为题目的那个图是图1这样的,如果是图2这样我就可能不会错了/(ㄒoㄒ)/~~。
图1.
图2
四、142(环形链表)
题目描述:
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
我的评价是...很有创意的一道题..抽象的仿佛回到了高中
大佬解法
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {// 有环
ListNode index1 = fast;
ListNode index2 = head;
// 两个指针,从头结点和相遇结点,各走一步,直到相遇,相遇点即为环入口
while (index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
}
}
算法分析:
- 快慢指针思路。让快指针以2的速度前进,慢指针以1的速度前进,如果他们最终相遇,则表示有环。
- 如果有环,假设环点距离head为x,环点距离相遇点位y,相遇点距离环点为z(顺时针)。则必然有公式:
①2(x+y) = x+y+n(y+z);//其中,n>=1,因为只有在快指针在绕了多圈过程中才可能被慢指针追上
变形:
②x = (n-1)(y+z)+z;//该公式表明,当一个指针从head出发,一个指针从相遇点顺时针运动,
相遇时的位置就是环点。【怎么看出来的呢?答:举一个极端情况n=1,则x=z】
- 公式1的解释:相遇时,慢指针走了x+y,快指针走了x+y+n(y+z),又快指针的速度是慢指针的两倍,所以存在公式①。
问题来了,为什么慢指针没有走完一圈就被快指针追上了呢?不是应该是x+y+k(y+z),k>=0吗?答案如下
①理论解释:关于找环的入口,为什么慢指针不会出现走好几圈才被快指针追上。可以先假设当慢指针第一次到环入口处的时候,和快指针的距离为m,此时快指针已经在环里面走了,而慢指针接下来也会在环里面走。再假设环长为s,所以快指针和慢指针的距离是(s-m),(注:都是按顺时针看距离),而快指针每次都会比慢指针多走一步,相当于每次都以一步的距离再缩进距离,所以当慢指针走(s-m)步的时候,快指针就能把距离缩为0了,也就是两点相遇了。而s-m是肯定小于s的,也就是小于一圈,所以慢指针肯定在没有走完一圈的时候就会被快指针追上。(来自b友)
②画图解释
2024/8/25新总结:①小学奥数追赶问题不多解释 ②为什么慢指针只走一圈就会被追上:详见上述分析 ③环点寻找:首先求得相遇点,经过推导知道:head距离环入口等于相遇点距离环入口+n圈环的长度。所以此时,如果让两个速度均为1的指针,1个从head出发,1个从相遇点出发,它们的相遇处一定就是环入口。
20240825错误写法
while(slow!=null&&fast.next!=null){
slow = slow.next;
fast = fast.next.next;
if(slow==fast){
ListNode index1=fast;
ListNode index2=head;
while(index1!=index2){
index1=index1.next;
index2=index2.next;
}
return index1;
}
}
错误原因:①因为会有[1,2]这种情况,此时head==fast。所以内部循环体应该是while(index1!=index2){…}return index1;②大循环体判断条件应该是while(slow!=null&&fast!=null&&fast.next!=null),且fast!=null要在fast.next!=null前面。
总结
24题思考了一会儿。19题思路比较简单,就是有点绕,可能是因为植树问题从小到大我都很差的原因,每次都要靠画图来分析(;′⌒`)。相交这道题想到思路,理解对题意就比较容易。142难。