题目与题解
参考资料:链表基础
注意点:
- 链表的定义
Java中,链表定义如下
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;} }
主要注意一下构造函数如何书写
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
203.移除链表元素
题目链接:203.移除链表元素
代码随想录题解:203.移除链表元素
解题思路
由于头结点也有可能值为val,所以这种情况最好设置虚拟头结点dummy,来避免头结点要额外处理的情况。
根据链表的删除原理,只要将当前指针指向下下个元素,就相当于删除了当前指针的下个元素。所以设置一个指针p先指向dummy,然后不断遍历判断p.next的值是否是val,是就让p.next = p.next.next,否则p = p.next,直到p.next == null,遍历结束,返回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 removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(0, head);
ListNode p = dummy;
while (p.next != null) {
if (p.next.val == val) p.next = p.next.next;
else p = p.next;
}
return dummy.next;
}
}
注意点
第一次写的时候没有把p = p.next放到else条件里面,就一直提醒我有空指针异常,我还以为是dummy定义错了,后来才发现漏条件了,写完务必要自己在纸上画画试试。
206.反转链表
题目链接:206.反转链表
代码随想录题解:206.反转链表
解题思路
已经反转的前一个节点为p1, 需要反转的当前指针为p2,对每一截链表的反转分为三步:先用p3记录当前指针的下一个指针,然后将p2指向p1,再将前一个节点和当前节点同时向前移动一格,直到当前指针为空,最后返回p1即可。
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode p1 = null, p2 = head;
while (p2 != null) {
ListNode p3 = p2.next;
p2.next = p1;
p1 = p2;
p2 = p3;
}
return p1;
}
}
注意点
还是要在纸上画一下,注意反转之前要记录当前节点原有的下个节点用于遍历,可以将p1直接初始化为null,p2为head,避免对头节点做额外处理。
24. 两两交换链表中的节点
题目链接:24. 两两交换链表中的节点
代码随想录题解:24. 两两交换链表中的节点
解题思路
这里涉及到对头结点进行处理,所以也要设置一下dummy节点。
交换的过程在纸上画一下就非常清晰了,假设需要交换的当前节点为cur和它的下一节点cur.next,前一个节点为pre,记录一下下一个可能需要交换的节点next = cur.next.next,操作按新链表的指向顺序,分为三步:pre.next指向cur.next,cur.next指向cur,cur指向next,然后将pre和cur分别更新为cur和next即可进入下一轮遍历,直到cur或cur.next等于空,最后返回dummy.next。
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) return head;
ListNode dummy = new ListNode(0, head);
ListNode pre = dummy, cur = head;
while (cur != null && cur.next != null) {
pre.next = cur.next;
ListNode next = cur.next.next;
cur.next.next = cur;
cur.next = next;
pre = cur;
cur = cur.next;
}
return dummy.next;
}
}
注意点
同样还是要画图更加清晰,什么时候断开节点的指向还是很重要的,不能乱。
19.删除链表的倒数第N个节点
题目链接:19.删除链表的倒数第N个节点
代码随想录题解:19.删除链表的倒数第N个节点
解题思路
常规老题,同样可能涉及头节点操作,设置dummy。
设置快慢指针fast和slow,fast先从 head开始遍历链表,直到走了N步,然后slow从dummy开始和fast同步遍历链表。这样,当fast遍历到链表末尾时,即fast为null时,slow刚好到达倒数第N个节点的的前一个节点,就可以删除slow.next了。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head);
ListNode fast = head;
while (n-- > 0 && fast != null) {
fast = fast.next;
}
ListNode slow = dummy;
while(fast != null) {
slow = slow.next;
fast = fast.next;
}
if (slow.next != null)
slow.next = slow.next.next;
return dummy.next;
}
}
注意点
这里涉及到删除操作,所以slow最后一定要停在要删除的节点的前一个节点位置,所以要么是fast先多走一步,要么是slow慢一步出发,写的时候要注意二者的遍历长度差为N+1。
160.链表相交
题目链接:160.链表相交
代码随想录题解:160.链表相交
解题思路
如果链表能够相交,且知道两个链表之间的长度差diff,那么可以让一个指针先从长一点的链表的头结点出发,直到走了diff步后,让另一个指针从短一点的链表头结点出发,二者同步前进,那么两个指针碰头的地方一定是相交点所在位置。如果不相交,最后指针会走到链表末尾,为null。
代码写起来比较麻烦,还写了一个新的函数避免重复写lenA更大还是lenB更大这种情况。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int lenA = 0, lenB = 0;
ListNode pA = headA, pB = headB;
while (pA != null) {
lenA++;
pA = pA.next;
}
while (pB != null) {
lenB++;
pB = pB.next;
}
if (lenA > lenB) return getIntersectionNode(headA, headB, lenA, lenB);
else return getIntersectionNode(headB, headA, lenB, lenA);
}
public ListNode getIntersectionNode(ListNode headA, ListNode headB, int lenA, int lenB) {
ListNode pA = headA;
for (int i = 0; i < lenA - lenB; i++) {
pA = pA.next;
}
ListNode pB = headB;
while(pA != null && pB != null && pA != pB) {
pA = pA.next;
pB = pB.next;
}
return pA;
}
}
注意点
这道题还有另一种很天才很简洁的写法,想不到,仅供参考。
分别设置两个指针,同时分别从两个链表的头节点开始遍历,如果其中一个指针遍历到当前链表末尾,就让它从另一个链表的头结点开始继续遍历,直到两个指针相等。相等有两种情况:两个指针都走到了末尾,为null;或存在相交节点,这时就直接返回当前节点即可。
证明详见官方题解的双指针法,代码如下
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
142.环形链表II
题目链接:142.环形链表II
代码随想录题解:142.环形链表II
解题思路
这题是一个数学问题,沿用环形链表I这题的思路,设置快慢指针fast和slow,fast一次走两步,slow一次走一步,二者同时前进,相遇则说明存在环,否则fast会提前到达链表末尾等于null。
如果存在环,假设进环前的路程为x,相遇的地方为x+y,环长度为y+z,那么相遇时,slow走了x+y步,fast走了x+n(y+z)+y步,又因为fast的步数为slow的两倍,可以得到2(x+y)=x+n(y+z)+y,变换后得到x = (n-1)(y+z)+z。
由于在环中无论走多少圈,最后到达的结点都是一样的,所以(n-1)(y+z)可以直接舍去,得到x=z,即此时如果有个节点从head出发,而slow继续前进,二者最终会在环的入口相遇。
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) break;
}
if (fast == null || fast.next == null) return null;
ListNode p = head;
while (slow != p) {
p = p.next;
slow = slow.next;
}
return p;
}
}
注意点
这公式记住就好,知道就是知道,不知道也很难推出来。
今日收获
除了设计链表没有做,基本把链表都过了一遍,链表题目主要就是要自己画图,注意涉及到增删时哪个节点要断开,连接的时机是什么,有哪些节点需要提前记录,用几个例子试一试。还要注意是否会出现空指针异常,在循环的时候就把=null的条件写对了,就不会出错。