题目描述(中等难度)
给一个链表,返回复制后的链表。链表节点相对于普通的多了一个 random
指针,会随机指向链表内的任意节点或者指向 null
。
思路分析
这道题其实和 133 题 复制一个图很类似,这里的话就是要解决的问题就是,当更新当前节点的 random
指针的时候,如果 random
指向的是很后边的节点,但此时后边的节点还没有生成,那么我们该如何处理。
和 133 题 一样,我们可以利用 HashMap
将节点提前生成并且保存起来,第二次遍历到他的时候直接从 HashMap
里边拿即可。
这里的话就有两种思路,一种需要遍历两边链表,一种只需要遍历一遍。
2020.3.3 更新,leetcode 增加了样例,之前没有重复的数字所以
key
存的val
,现在有了重复数字,将key
修改为Node
。此外Node
的无参的构造函数也被去掉了,也需要修改。
解法一
首先利用 HashMap
来一个不用思考的代码。
遍历第一遍链表,我们不考虑链表之间的相互关系,仅仅生成所有节点,然后把它存到 HashMap
中,val
作为 key
,Node
作为 value
。
遍历第二遍链表,将之前生成的节点取出来,更新它们的 next
和 random
指针。
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
HashMap<Node, Node> map = new HashMap<>();
Node h = head;
while (h != null) {
Node t = new Node(h.val);
map.put(h, t);
h = h.next;
}
h = head;
while (h != null) {
if (h.next != null) {
map.get(h).next = map.get(h.next);
}
if (h.random != null) {
map.get(h).random = map.get(h.random);
}
h = h.next;
}
return map.get(head);
}
解法二
解法一虽然简单易懂,但还是有可以优化的地方的。我们可以只遍历一次链表。
核心思想就是延迟更新它的 next
。
1 -> 2 -> 3
用 cur 指向已经生成的节点的末尾
1 -> 2
^
c
然后将 3 构造完成
最后将 2 的 next 指向 3
1 -> 2 -> 3
^
c
期间已经生成的节点存到 HashMap 中,第二次遇到的时候直接从 HashMap 中拿
看下代码理解一下含义吧
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
HashMap<Node, Node> map = new HashMap<>();
Node h = head;
Node cur = new Node(-1); //空结点,dummy 节点,为了方便头结点计算
while (h != null) {
//判断当前节点是否已经产生过
if (!map.containsKey(h)) {
Node t = new Node(h.val);
map.put(h, t);
}
//得到当前节点去更新它的 random 指针
Node next = map.get(h);
if (h.random != null) {
//判断当前节点是否已经产生过
if (!map.containsKey(h.random)) {
next.random = new Node(h.random.val);
map.put(h.random, next.random);
} else {
next.random = map.get(h.random);
}
}
//将当前生成的节点接到 cur 的后边
cur.next = next;
cur = cur.next;
h = h.next;
}
return map.get(head);
}
解法三
上边的两种解法都用到了 HashMap
,所以额外需要 O(n)
的空间复杂度。现在考虑不需要额外空间的方法。
主要参考了这里-and-linear-time-complexity-O(N))。主要解决的问题就是我们生成节点以后,当更新它的 random
的时候,怎么找到之前生成的节点,前两种解法用了 HashMap
全部存起来,这里的话可以利用原来的链表的指针域。
主要需要三步。
- 生成所有的节点,并且分别插入到原有节点的后边
- 更新插入节点的
random
- 将新旧节点分离开来
一图胜千言,大家看一下下边的图吧。
代码对应如下。
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
Node l1 = head;
Node l2 = null;
//生成所有的节点,并且分别插入到原有节点的后边
while (l1 != null) {
l2 = new Node(l1.val);
l2.next = l1.next;
l1.next = l2;
l1 = l1.next.next;
}
//更新插入节点的 random
l1 = head;
while (l1 != null) {
if (l1.random != null) {
l1.next.random = l1.random.next;
}
l1 = l1.next.next;
}
l1 = head;
Node l2_head = l1.next;
//将新旧节点分离开来
while (l1 != null) {
l2 = l1.next;
l1.next = l2.next;
if (l2.next != null) {
l2.next = l2.next.next;
}
l1 = l1.next;
}
return l2_head;
}
解法四
不利用额外的空间复杂度还有一种思路,参考 这里。
解法三利用原链表的 next
域把新生成的节点保存了起来。类似的,我们还可以利用原链表的 random
域把新生成的节点保存起来。
主要还是三个步骤。
- 生成所有的节点,将它们保存到原链表的
random
域,同时利用新生成的节点的next
域保存原链表的random
。 - 更新新生成节点的
random
指针。 - 恢复原链表的
random
指针,同时更新新生成节点的next
指针。
一图胜千言。
相应的代码如下。
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
Node l1 = head;
Node l2 = null;
//生成所有的节点,讲它们保存到原链表的 random 域,
//同时利用新生成的节点的 next 域保存原链表的 random。
while (l1 != null) {
l2 = new Node(l1.val);
l2.next = l1.random;
l1.random = l2;
l1 = l1.next;
}
l1 = head;
//更新新生成节点的 random 指针。
while (l1 != null) {
l2 = l1.random;
l2.random = l2.next != null ? l2.next.random : null;
l1 = l1.next;
}
l1 = head;
Node l2_head = l1.random;
//恢复原链表的 random 指针,同时更新新生成节点的 next 指针。
while (l1 != null) {
l2 = l1.random;
l1.random = l2.next;
l2.next = l1.next != null ? l1.next.random : null;
l1 = l1.next;
}
return l2_head;
}
总
解法一、解法二是比较直接的想法,直接利用 HashMap
存储之前的节点。解法三、解法四利用原有链表的指针,通过指来指去完成了赋值。链表操作的核心思想就是,在改变某一个节点的指针域的时候,一定要把该节点的指针指向的节点用另一个指针保存起来,以免造成丢失。