今天花了一些时间再回看链表部分。链表部分其实只要搞清楚几个重点,感觉还是比较好下手的。
个人认为要把握的重点:
1、虚拟头结点的运用
2、操作节点的次序
3、迭代前后的节点相对位置
4、操作节点本身与其属性
一、虚拟头结点的运用
虚拟头结点一般涉及到节点删除操作时,都会使用到,目的是减去处理头结点的逻辑,将链表中的所有节点统一看待,即所有节点用一套删除逻辑。
以LC_203删除链表节点来说。
删除任意节点,需要让当前节点的前一个节点指向当前节点后一个节点,那么头结点该怎么删?
此时需要虚拟头结点,让其next指向实际头结点。
之后从虚拟头结点开始遍历就可以了。
ListNode prehead = new ListNode(); // 虚拟头结点
prehead.next = head; // 连接实际头结点
ListNode node = prehead; // 遍历节点从虚拟头结点开始 可以认为node就是prehead
这里有个关键点,一开始的node是等于prehead的,如果要删除的是头结点,那么让node.next指向node.next.next,会连带着prehead.next一起指向node.next.next,那么就做到了删除头结点。
后续遍历过程中如果遇到不是要删除的节点,那么只有node会向后走,prehead和node会分离。
这个也是重点里面的第四点。
完整代码如下,要注意最后返回的是prehead.next,因为这才是实际的头结点。
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode prehead = new ListNode(); // 虚拟头结点
prehead.next = head; // 连接实际头结点
ListNode node = prehead; // 遍历节点从虚拟头结点开始 可以认为node就是prehead
while (node.next != null) { // 注意是node.next不为空 即node指向的节点为空
if (node.next.val == val) {
// 让node指向下一个节点的下一个节点
// 一开始node和prehead是一起移动 所以如果删头结点也是可以的
node.next = node.next.next;
}else {
// 开始执行这一步 意味着node和prehead分离
node = node.next;
}
}
// 虚拟头结点的下一个节点才是实际头结点
return prehead.next;
}
}
二、操作节点的次序
以LC_206反转链表为例。
翻转链表涉及的节点操作次序问题很重要,因为次序错误会导致丢失部分节点。
反转链表的操作节点顺序如下:
- 首先用temp保存curNode的next 因为接下来要改变lastNode节点的next指向了
- 再操作curNode对应的节点,让该节点指向preNode
- 接着更新指针节点的指向,让preNode等于curNode
- 最后让curNode等于temp
次序不能乱。
如果想不明白,画画图会清晰很多
完整代码如下
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode preNode = null; // 前一个节点
ListNode curNode = head; // 当前节点
while (curNode != null) {
ListNode temp = curNode.next;
curNode.next = preNode; // 操作属性 就是操作对应节点的属性
preNode = curNode; // 循环结束的节点指向的状态一定是循环未开始时的状态
curNode = temp;
}
return preNode; // preNode成为头结点
}
}
三、迭代前后的节点相对位置
节点指针在进入循环前的相对位置,和每轮循环后的相对位置要保持一致。
这一点只要是在遍历链表那么就一定是成立的。
节点指针在进入循环前的相对位置,需要通过具体的题目去分析,也可能有多种不同的初始化相对位置。
以LC_24两两交换节点为例。
进入循环前的初始状态
ListNode pre = preHead; // 两个指针,pre为cur的前一个节点
ListNode cur = head;
进入循环对节点进行操作后,这三个节点相对位置应当保持与循环前一致,所以要重新调整节点的指向,使其与循环前一致。
// 更新节点指向 回到之前的状态 pre在cur前面
pre = cur;
cur = cur.next;
完整代码如下,不太理解的话画一下操作完节点之后的各指针状态就比较清楚了。
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null) return head;
ListNode preHead = new ListNode(); // 虚拟头结点
preHead.next = head; // next指向head
ListNode pre = preHead; // 两个指针,pre为cur的前一个节点
ListNode cur = head;
while (cur != null && cur.next != null) { // cur.next != null 是到尾结点 不用交换
// 两两交换
ListNode tmp = cur.next; // tmp其实可以不用
pre.next = tmp;
cur.next = tmp.next;
tmp.next = cur; // tmp.next的更新得放最后 不然cur.next会找不到下一个
// 更新指针指向
pre = cur;
cur = cur.next;
}
return preHead.next;
}
}