面试题35. 复杂链表的复制
请实现 copyRandomList 函数,复制一个复杂链表。
在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
解题思路:这个复制过程可以分成两步:第一步是复制原始链表上的每个节点,并用next指针相连; 第二步是设置每个节点的random指针。
第一步的实现较为简单,第二步,对于一个n个节点的链表,定位每个节点的random 指针都需要从链表头节点开始经过O(n)步才能找到,因此这种方法总的时间复杂度是O(n^2)
为了降低第二步的时间复杂度,下面有两种解决方案:
解决方案一:对应推荐解法一
还是分为两步:第一步仍然是将原始链表上的每个节点 N复制为N’,然后把这些创建出来的节点N’连接起来。
同时我们把<N,N’>的配对信息放到一个HashMap<Node,Node> map中;
第二步设置每个节点的random指针:如果在原始链表中节点 N的random指针指向节点 S,那么在复制链表中,对应的 N’节点应该指向 S’。
由于有了map,我们可以用O(1)的时间根据 S找到 S’;
这种解决方案相当于用空间换时间。对于有n个节点的链表,我们需要一个大小为O(n)的HashMap,
也就是说我们以O(n)的空间消耗把时间复杂度由O(n^2)降低至O(n).
Java实现上述思路代码如下:
//推荐双百方法一:指针版 需要一个辅助的map,下面还有一个调用方法的版本,不需要额外创建指针
Map<Node,Node> map1 = new HashMap<>();//map前面保存原链表节点,后面保存复制节点,则可通过前面的key直接找到后面的value
public Node copyRandomList(Node head) {
if(head==null) return null;
Node res = new Node(head.val);
map1.put(head,res);
Node tmpR = res;
Node tmpH = head;
while(tmpH.next!=null){
tmpR.next = new Node(tmpH.next.val);
tmpR=tmpR.next;
tmpH=tmpH.next;
map1.put(tmpH,tmpR);
}
tmpR = res;
tmpH = head;
while(tmpH!=null){
if(tmpH.random!=null) tmpR.random = map1.get(tmpH.random);
tmpH = tmpH.next;
tmpR = tmpR.next;
}
return res;
}
也可以通过调用外部方法省去两个指针,Java代码如下:
Map<Node,Node> map3= new HashMap<>();
public Node copyRandomList3(Node head) {
if(head==null) return null;
Node res = new Node(head.val);
map3.put(head,res);
copy3(head,res);
randomAdd3(head,res);
return res;
}
public void copy3(Node head,Node res){
while(head.next!=null){
res.next = new Node(head.next.val);
res = res.next;
head = head.next;
map3.put(head,res);
}
}
public void randomAdd3(Node head,Node res){
while(head!=null){
if(head.random!=null) res.random = map3.get(head.random);
head= head.next;
res= res.next;
}
}
解决方案二:在不使用辅助空间的情况下实现O(n)的时问效率。对应推荐解法二
第一步:复制节点:将原始链表的任意节点 N复制为新节点N’,再把N’连接到 N的后面。
即如果原始链表为A->B->C->D 则复制过后为A->A’->B->B’->C->C’->D->D’
第二步:确定每个新增节点N’的random指针指向
显然,如果原始链表上的节点 N 的random指针指向节点S,则它对应的复制节点N’的random指针指向节点S的复制节点S’,也就是当前节点S的下一个节点。
第三步:把这个长链表拆分成两个链表,把奇数位置的节点连接起来就是原始链表,把偶数位置的节点连接起来就是复制出来的链表。
注意,此时在得到复制链表的同时,不要忘记将原有链表进行复原。
对应思路的实现代码如下:
//推荐双百方法二:不需要辅助空间,但是需要额外将两个链表进行拆分,但理解难度较大
public Node copyRandomList2(Node head) {
if(head==null) return null;
copy2(head);
randomAdd2(head);
return build2(head);
}
public void copy2(Node head){
while(head!=null){
Node copy = new Node(head.val);
copy.next = head.next;
head.next =copy;
head = copy.next;
}
}
public void randomAdd2(Node head){
while(head!=null){
if(head.random!=null) head.next.random = head.random.next;
head=head.next.next;
}
}
public Node build2(Node head){
//将链表拆成两个,注意要恢复原有的链表
Node res = head.next;
Node tmp = res;
head.next = head.next.next;//这一步不可缺少,保证第一个复制节点对N N'时的分离操作
head = head.next;
while(head!=null){
tmp.next = head.next;
head.next = head.next.next;
tmp=tmp.next;
head = head.next;
}
return res;
}