普通链表的复制
链表节点的定义:
// Definition for a Node.
class Node {
int val;
Node next, random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
给定链表的头节点head,复制普通链表很简单,只需遍历链表,每轮建立新节点 + 构建前驱节点pre和当前节点node的引用指向即可。这里用到了伪头部节点!!!
class Solution {
public Node copyRandomList(Node head) {
Node cur = head;
// 伪造头部节点dum
Node dum = new Node(0), pre = dum;
while(cur != null) {
Node node = new Node(cur.val); // 复制节点 cur
pre.next = node; // 新链表的 前驱节点 -> 当前节点
cur = cur.next; // 遍历下一节点
pre = node; // 保存当前新节点
}
// 返回的伪节点的下一个节点
return dum.next;
}
}
复杂链表的复制
// Definition for a Node.
class Node {
int val;
Node next, random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
本题难点: 在复制链表的过程中构建新链表各节点的 random 引用指向。
在普通链表的复制中,是可以直接通过前驱节点连接新建立的节点。而复杂节点中的random引用指向采用上面的思路是不能构建的,因为random所指向的节点此时还没有建立。
此时能想到的一个方法就是哈希大法,哈希表具有查询的功能,那么将原链表和新链表之间通过哈希表k-v方式建立联系,就可以自然寻找到random和next引用指向了
class Solution {
public Node copyRandomList(Node head) {
// 使用哈希表:构建原链表和新链表之间的联系
// 特判
if(head == null){
return null;
}
HashMap<Node,Node> map = new HashMap<>();
// 建立遍历指针
Node cur = head;
while(cur != null){
Node node = new Node(cur.val);
// 储存原节点 和 新节点
map.put(cur,node);
// 遍历指针下移
cur = cur.next;
}
// 已经构造好新链表节点,开始建立next和random引用指向
cur = head;
while(cur != null){
// next引用指向:新节点指向新节点
map.get(cur).next = map.get(cur.next);
// random引用指向
map.get(cur).random = map.get(cur.random);
// cur指针下移
cur = cur.next;
}
// 返回新链表的head节点
return map.get(head);
}
}
LRU缓存设计中涉及链表操作
// 其中head为伪头部,tail为伪尾部,这是双向链表的常规操作!
// 插入到头部(这里只能利用head)
public void addToHead(DLinkedList node){
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next= node;
}
// 移动到头部
public void moveToHead(DLinkedList node){
// 先删除该节点
node.prev.next = node.next;
node.next.prev = node.prev;
//插入到头部
addToHead(node);
}
// 删除最后一个节点(只能用tail)
public DLinkedList deleteLastNode(){
DLinkedList res = tail.prev;
res.prev.next = tail;
tail.prev = res.prev;
return res;
}
在链表操作这里尤其要警惕指针丢失和内存泄露
上图中,要先将x的next指针指向p的next指针,再将p的next指针指向x。一定要注意顺序!!!
利用伪节点(哨兵节点)进行增删改
边界条件的处理
这里不单单指链表,在写代码的时候都要考虑边界条件,比如空指针、空值问题。
反映到链表就是链表为空,代码是否能工作,链表只有头结点,代码能否正常工作…
画图
要在图形中去思考顺序问题,特殊情况问题。比如LRU设计中,移动到头部节点时,只能用head指针进行操作(因为head和tail中间还有未知的节点,所以只能选择head),单单看伪头节点和伪尾结点很容易被迷惑,直接操作tail节点。