目录
1. LeetCode24 链表中节点两两交换
题目
代码
/**
* 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 dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy;
while (cur.next != null && cur.next.next != null){
ListNode temp3 = cur.next.next.next;
ListNode temp1 = cur.next;
cur.next = cur.next.next;
cur.next.next = temp1;
temp1.next = temp3;
cur = temp1;
}
return dummy.next;
}
}
思路
cur指针的定义及遍历:
- 知道定义一个指向dummy head的cur指针来遍历链表后:首先要确定的事情,每次遍历,cur指针指向哪里?经过分析和画图,节点两两为一组,cur指向每组的上一个节点。
- 还要确定的一件事情就是遍历什么时候结束?根据前面分析,节点是两两交换位置的,所以可分奇偶讨论。如果节点个数为奇,则cur.next.next为空时停止遍历;如果节点个数为偶,则cur.next为空时停止遍历。所以可以一直遍历下去的条件即为
cur.next != null && cur.next.next != null
。为什么是&& 而不是 || ?奇数节点就不需要交换了,所以只有满足后面有偶数个节点的时候才会进入循环。 - 这个while里的条件还要注意一下,
cur.next != null
和cur.next.next != null
顺序不能反,因为一旦反过来,如果cur.next为空的话,先执行cur.next.next,就会报空指针异常了。
遍历时的操作:
- 什么样的节点需要提前定义一个temp指针指向它?当进行改变某个节点的next节点操作后,这个节点会断线,没有指针可以指向它了,则需要在进行这个操作前先定义一个temp指针指向它。
- 画图画图画图!把原来的指针走向,和操作后的指针走向都画出来,拆分一个或几个节点为一个单位进行操作,再定义针对这个单位的操作,循环遍历即可。
- 最后return,return头节点,但是函数接收到的实参head有可能已经不再是头节点了,所以不能直接
return head
,而是return dummy.next
。
画图
2. LeetCode19 删除倒数第n个节点
题目
思路
整体思路为,找到倒数第n个节点的前一个节点,改变这个节点的next指向,即可。
如何找到倒数第n个节点的前一个节点?
- 第一种方法:先for循环遍历一下,获取整个链表的size。然后再for int i = 0…i < …重新遍历到倒数第n个节点的上一个节点处(用size-n来定位),改变其next指向。即完成要求。
- 第二种方法:不需要获取整个链表的size。用快慢指针法。需要保证,当快指针指向最后一个节点的next的null节点时,慢指针指向倒数第n个节点的前一个节点,也即:快慢指针要相差n+1个节点。因此,首先,快指针先走n+1步,随后快慢指针一起向后遍历,直至快指针指向null为止。
- 第二种方法不太好理解,总感觉是已知题解倒退思路能理解,但正推就想不到这样的思路。今天想到了一种解释,觉得还挺贴合:快指针的作用就是一个指示,当它指向null的时候,就指示着慢指针到了倒数第n个节点。是为了让慢指针在不知道size的情况下,知道现在到了倒数第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) {
if (head == null){
return head;
}
//cur第一遍遍历,用来查找size大小
ListNode cur = head;
int size = 0;
while (cur != null){
size++;
cur = cur.next;
}
//cur第二遍遍历,用来找倒数第n个节点的前一个节点
ListNode dummy = new ListNode(-1, head);
cur = dummy;
int index = 0;
//用while的写法
while (cur.next != null){
if (index == size-n){
cur.next = cur.next.next;
break;
}else {
cur = cur.next;
index++;
}
}
return dummy.next;
//用for的写法
for (int i = 0; i < size; i++){
if (i == size-n){
cur.next = cur.next.next;
i++;
}
cur = cur.next;
}
return dummy.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 removeNthFromEnd(ListNode head, int n) {
if (head == null){
return head;
}
ListNode dummy = new ListNode(-1, head);
ListNode fast = dummy;
ListNode slow = dummy;
for (int i = 0; i <= n; i++){
fast = fast.next;
}
while (fast != null){
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return dummy.next;
}
}
3. LeetCode02.07 求两链表相交值
题目
思路
提醒:
交点不是数值相等,而是指针相等
刚开始的思路:
curA遍历A链表,curB遍历B链表;while (curA != null && curB != null)
,如果curA == curB
,则为交点,如果 !=,则继续遍历curA = curA.next;curB = curB.next;
结果当然报错了。。因为这相当于比较A(1)和B(1),A(2)和B(2)…没办法找出来交点在比如A(3)和B(5)的地方
于是借鉴代码随想录里的思路:
审题后可得,两链表相交之后不会再分开,所以从相交开始的节点到最后一个节点都是相同的,值也相同,数量也相同。如果链表A长度为5,链表B长度为3,那么最多最多这两个链表从链表A的第三个节点开始相交,即链表A的第三个节点与链表B的第一个节点相交。所以可得如下思路:求出sizeA和sizeB,作差,假设A链表比B链表多n个节点,即差值为n,则可跳过A链表的前n个节点,从A链表的第n+1个节点开始,与B链表的第一个节点相比较,之后二者可以同时往后遍历。
思路图:
代码
/**
* 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) {
if (headA == null || headB == null){return null;}
ListNode curA = headA;
ListNode curB = headB;
int sizeA = 0;
int sizeB = 0;
int diff = 0;
while (curA != null){
sizeA++;
curA = curA.next;
}
while (curB != null){
sizeB++;
curB = curB.next;
}
curA = headA;
curB = headB;
if (sizeA >= sizeB){
diff = sizeA - sizeB;
for (int i = 0; i < diff; i++){
curA = curA.next;
}
while (curA != null){
if (curA == curB){
return curA;
}else {
curA = curA.next;
curB = curB.next;
}
}
}else {
diff = sizeB - sizeA;
for (int i = 0; i < diff; i++){
curB = curB.next;
}
while (curB != null){
if (curA == curB){
return curA;
}else {
curA = curA.next;
curB = curB.next;
}
}
}
return null;
}
}
上述代码如果想更简短的话,可以优化一下。现在是两种情况分开讨论:sizeA >= sizeB和 sizeB > sizeA。可优化为,统一让 sizeA >= sizeB,如果 sizeB > sizeA 的话,将sizeA与sizeB交换一下。交换二者的size以及cur指针。
力扣上另一种高赞题解,很妙
力扣上有详细的算法执行过程示意图。二刷的时候温习。。
4. LeetCode142 环形链表
题目
思路
此题两个关键点:
其一是如何确定该链表有环?
其二是如何确定环的起始位置?
总体思路,快慢指针法。快指针一次走两步,慢指针一次走一步。
针对第一个问题,如果快慢指针相遇了,即某个节点处,快指针等于慢指针,则一定有环。如果没有相遇,则一定没环。
那么问题来了,为什么快指针一次走两步,慢指针一次走一步?能不能快指针一次走三步,慢指针一次走一步。答案是不行的。因为快指针一次走两步,慢指针一次走一步这个规则,保证了每一步中,快指针是多走一步的,所以如果有环,在某个节点,当快指针多走一步之后,快慢指针一定能保证相遇。但如果多走了两步,有可能就正好多走的这两步跳过了慢指针,错过了相遇。
针对第二个问题,如何确定环的起始位置,上推导——
本人第一遍推的时候犯了两个错误:
- fast指针走过的节点数写成了
x+y+z+n(y+z)
,实际上应为x+n(y+z)+y
。即fast指针在环里绕了n圈后又走了y,遇上了slow节点。 - 因为求的变量是x,所以把x单独放一边,得
x = n (y + z) - y
。再进一步优化时,优化成了x = (n-1)y + nz
。实际上y+z表示一圈,应该以整体形式出现,所以应该优化成上图所示的式子,x = (n - 1) (y + z) + z
。
针对上图还有一个问题:为什么第一次在环中相遇,slow的步数是 x+y 而不是和fast一样, x + 若干环的长度 + y 呢?
答案是想像环为追及问题,快的顺时针跑去追慢的。如果环里有m个节点的话,快追慢最坏的情况是,快指针走到环里第二个节点的时候,慢指针进入环的第一个节点,则此时快要追慢m-1个节点。每一步快比慢多1个节点,所以只需m-1步,快就能追上慢。那么慢经过m-1步,也就刚走完一圈回到环的第一个节点。所以当fast和slow在环里相遇的时候,这一定是slow在环里呆的第一圈。
那为什么fast有可能在环里呆了很多圈了呢?因为如果x很大很大,环很小很小的话,fast比slow在走x的时候多走了很多步,那么在slow入环前,fast就要不停地在环里转啊转啊转了。
解决完上述疑惑,再来看最后得到的式子:x = (n - 1) (y + z) + z
。这里n一定大于等于1的,也就是fast至少在环里转了一圈了。那么不管fast在环里转了多少圈,定义两个指针,temp1和temp2,一个指针从fast和slow相遇的位置出发,一个指针从head出发,每次都往后移一步,则这两个指针一定会在环的入口处相遇。这是上面一大段推导后得到的终极结论,也是这一段中唯一能指导下面代码如何写的结论。代码如下:
ListNode temp1 = fast;
ListNode temp2 = head;
while (temp1 != temp2){
temp1 = temp1.next;
temp2 = temp2.next;
}
return temp1;
所以最终的题解代码如下。第一个while里的判断条件,还是遵循了不能报空指针异常的原则来设计的。
最终代码都写完之后,再想一下下面两种边界情况,都可通过。
代码
/**
* 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) {
if (head == null){return null;}
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if (fast == slow){
ListNode temp1 = fast;
ListNode temp2 = head;
while (temp1 != temp2){
temp1 = temp1.next;
temp2 = temp2.next;
}
return temp1;
}
}
return null;
}
}
最后,有一个小tips:
关于链表的虚拟头节点的使用
在遇到无法定位头节点,头节点可能被移动删除导致定位失效的时候,可以考虑用虚拟头节点,并不是每道题都要用的!!!!