问题总结
- 链表遍历时使用 node != null 还是 node.next != null。
- 链表“滑动窗口”思想,关于窗口初始大小使用:n-- 还是 --n
- 删除元素(调整next指针后)是否要立刻移动节点。
- 链表反转与java对象引用理解节点的变化。比如: ListNode cur = head; reverse(cur), head == cur ?
练习重点
对于链表而言,基础的 曾、删节点、反转链表必须要熟练,不然同一类型Medium题目,写个小20分钟,改来改去,也不是不可能。
无论是链表删除还是链表反转,主要的点都在于去找对应的节点。而且这个节点并非是单一的一个,总体来说可能会有4个节点。
以反转指定区间链表为例:
[1,2,3,4,5,6] 反转 2 ~ 4 位区间,也就是 [2,3,4], 反转结果为:[1,4,3,2,6],该题就需要记录4个节点,而且中间涉及交换赋值,还需要考虑临时节点。
left:反转区间的 左侧节点,也就是 1
right:反转区间的 右侧节点,也就是 5
end:反转区间的结束节点,也就是 4
start:反转区间的开始节点,也就是 2
删除链表指定元素
leetCode 203
错误代码
if (head == null) {
return head;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy;
while (cur.next != null) {
if (cur.next.val == val) {
cur.next = cur.next.next;
}
cur = cur.next;
}
return dummy.next;
问题分析:
首先,逻辑不严谨,出现了NPE,严重不应该。
其次,对于是否要移动当前节点判断错误。不是每次都要移动当前节点,因为无法保证 cur.next.next != val;第二次修改后发现依然不能满足原因就是如此。
注意:
while (cur.next != null) 和 while (cur != null) 使用场景如何区分?
首先这里使用cur != null 判断也没问题。不过从适用性来说,肯定是 cur.next != null 更合适。
原因是因为该题是删除节点,使用 prev.next = cur.next; 等价于:prev.next = prev.next.next; 所以判断 next != null 更方便。
cur.next != null 也有一个问题,就是无法遍历到最后一个元素。因此,代码实现中直接使用 cur.next.val 进行判断。
正确实现
if (head == null) {
return head;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy;
while (cur.next != null) {
if (cur.next.val == val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return dummy.next;
删除链表倒数第N个元素
leetCode 19
给你一个链表,删除链表的倒数第 n **个结点,并且返回链表的头结点。
示例 1:
输入: head = [1,2,3,4,5], n = 2
输出: [1,2,3,5]
示例 2:
输入: head = [1], n = 1
输出: []
示例 3:
输入: head = [1,2], n = 1
输出: [1]
提示:
- 链表中结点的数目为
sz 1 <= sz <= 300 <= Node.val <= 1001 <= n <= sz
错误代码:
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy;
ListNode fast = dummy;
ListNode slow = dummy;
while (n > 0) {
fast = fast.next;
n--;
}
// 判断是否为尾节点 一定是 fast.next != null
while (fast == null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
问题分析:
对链表理解不到位,写的时候完全凭感觉。判断链表是否为尾节点一定是使用 fast.next != null
注意:
此处使用滑动窗口思想,关于初始化窗口大小数量存在不同考虑。有时候用 while (n-- > 0) 有时候可以用while (--n > 0)
区别就在于 slow 指针的位置,使用 n--情况下,slow 指针位置为倒数 n 个节点的前一个节点。也就是 n 的 prev节点,很明显更适合删除。 --n 更适合查找倒数第n个节点然后返回。
正确代码
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy;
ListNode fast = dummy;
ListNode slow = dummy;
while (n-- > 0) {
fast = fast.next;
}
// 判断是否为尾节点 一定是 next != null
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
删除链表重复元素(不保留)
leetCode 82
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
示例 1:
输入: head = [1,2,3,3,4,4,5]
输出: [1,2,5]
示例 2:
输入: head = [1,1,1,2,3]
输出: [2,3]
提示:
- 链表中节点数目在范围
[0, 300]内 -100 <= Node.val <= 100- 题目数据保证链表已经按升序 排列
错误代码
看下是否能找到错误位置。
public ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy.next;
// 外层循环
while (fast.next != null) {
// 内层判重
while (fast.next != null && fast.next.val == fast.val) {
fast = fast.next;
}
slow.next = fast.next;
slow = slow.next;
fast = fast.next;
}
return dummy.next;
}
错误分析:
- 由于返回头结点,且考虑到原头结点也可能存在重复。所以需要一个dummy节点。
- 使用快慢指针思想,快指针用于判断重复,慢指针用于移除重复。slow.next = 不重复节点。
问题就出在第二步:以 [0,0,1,2] 为例,第一次循环,走完内层判重逻辑后,fast = 0, slow = -1,此时将slow.next = fast.next -> (1) 没有毛病,然后 slow = 1, fast = 1继续执行,最后输出结果[1,2],似乎完成了,但是关于slow移动缺乏考虑。
按照[0,0,1,1,2] 尝试就出现问题了,结果为:[1,2],多出来的这个[1] 就是因为提前移动了slow节点。
正确实现
public ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy.next;
// 删除元素 使用 fast.next 判断更合适
while (fast.next != null) {
boolean repeat = false;
while (fast.next != null && fast.next.val == fast.val) {
fast = fast.next;
repeat = true;
}
// 不是所有的情况 都需要移动slow
if (repeat) {
slow.next = fast.next;
} else {
slow = fast;
}
// fast 节点一定要记得移动
fast = fast.next;
}
return dummy.next;
}
两两交换链表节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入: head = [1,2,3,4]
输出: [2,1,4,3]
示例 2:
输入: head = []
输出: []
示例 3:
输入: head = [1]
输出: [1]
提示:
- 链表中节点的数目在范围
[0, 100]内 0 <= Node.val <= 100
“错误”代码
public ListNode swapPairs(ListNode head) {
if (head == null) {
return head;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode left = dummy;
while (left.next != null) {
// 获取第二个节点
ListNode end = left.next.next;
// 第二个节点为 null 表示到末尾了,不用交换了
if (end == null) {
break;
}
// 利用节点定位法 交换位置
// 先存储 原头节点
ListNode start = left.next;
// 再记录 原尾节点的 下一个节点(原头节点交换后要指向该节点)
ListNode right = end.next;
// 原头节点交换后要指向 right 节点, 当然这里用 start.next 也是可以的
left.next.next = right;
// 两两交换 原尾节点 要指向 原头节点了
end.next = start;
// 原头节点的左侧节点 也要指向 原尾节点了
left.next = end;
// 继续往下交换, 原头节点作为 left 节点
left = start;
}
return dummy.next;
}
emm, 这个代码是没问题的,但是...看了别人的实现后,就陷入了自我怀疑...
“正确”代码
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode next = head.next;
head.next = swapPairs(next.next);
next.next = head;
return next;
}
递归...感觉智商被碾压。