复杂链表的复制
今天重刷剑指offer,又遇到这道题了,今天绝对的要把它搞的明明白白。
奋战一天,完成任务。
不太写博客,都在有道上面,以后坚持下,多多输出,文中有写的不好的地方希望大佬们轻喷。
题目描述
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。
思路分析
这道题可能刚开始做会懵逼,但是没关系从暴力开始,然后逐步优化
1.暴力-------时间复杂度O(n²),空间复杂度O(1)
复制的过程分为两步
- 复制原始链表的每一个结点,并用 next 指针连接
- 复制原始链表的每一个结点 random 指针所指向的结点
第一步很容易,创建个头节点,然后移动指针就可以了
第二步稍稍有点复杂(其实也不是特复杂😂),什么样的思路呢
- 拿到原始链表的每个结点的 random指针指向的节点
- 每次从 新链表中根据 label 值找到这个节点,然后将 新链表的节点的 random 指针指向这个节点
- 链表指针后移,重新定位下一个节点 random指针指向的位置,这个时候就需要回到新链表的第一个节点
但是存在问题
就是 random 指针可能为空,为空的话那么直接就将 新链表当前节点的 random 指针置空,退出循环
对于一个含有 n 个节点的链表,由于定位每个节点的 random 都需要从链表头节点开始经过 O(n) 步才能找到,因为这种方法的总时间复杂度是 O(n²)
public RandomListNode Clone(RandomListNode pHead){
if(pHead==null){
return null;
}
//1.创建头节点,方便复制
RandomListNode newList = new RandomListNode(-1);
RandomListNode res = newList;
RandomListNode head = pHead;
//2.复制原始链表中next
while(head!=null){
RandomListNode cur = new RandomListNode(head.label);
res.next = cur;
res = cur;
head = head.next;
}
//3.复制原始链表中的random
RandomListNode node = newList.next;
RandomListNode temp = null;
head = pHead;
while(node!=null){
RandomListNode randomNode = head.random;
temp = newList.next;
while(temp!=null){
if(randomNode!=null){
if(temp.label==randomNode.label){
node.random = temp;
}
temp=temp.next;
}else{
node.random=null;
temp = null;
}
}
head = head.next;
node = node.next;
}
return newList.next;
}
2.HashMap-----时间复杂度O(n),空间复杂度O(n)
暴力法 n² 的原因就在于每次需要从链表头找到 random 指向的那个节点,但是想把时间复杂度降下来,可以从空间这方面考虑
使用 HashMap 也是分为两步
- 把原始链表的每个节点存到 哈希表中 ,key 是原始链表的结点,value 是新复制的结点
- 复制原始链表中对应结点的 next 和 random 指向
这里想一想,为什么要放到 哈希表 中了呢?
放入之后,那么就可以根据 key.next 或者 key.random 很快的找到节点的下一个 或者 随机指向的了,那么就避免了暴力解法中每次从链表头去找,降低了时间复杂度
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
这两行代码是整个过程中最妙的两行代码
慢慢体会~~~
public RandomListNode Clone(RandomListNode pHead){
//1.判断链表是否合法
if(pHead==null){
return null;
}
//2.创建 HashMap 来存储
HashMap<RandomListNode,RandomListNode> map = new HashMap<>();
RandomListNode cur = pHead;
//3.把原始链表中的每个节点存储 HashMap
//其中 Key 是原始节点,value 是复制节点
while(cur!=null){
map.put(cur,new RandomListNode(cur.label));
cur = cur.next;
}
//4.复制原始链表中的 next 和 random 指向
cur = pHead;
while(cur!=null){
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
//5.返回新链表的头节点
return map.get(pHead);
}
3.更巧妙的方法,复制next、复制random、拆分-----时间复杂度O(n),空间复杂度O(1)
分为三步,但是这种方法不具有普遍性,只是针对这个题最妙的解法了
- 在原始链表的每个节点后面插入复制的节点
- 对复制的节点的 random 进行连接
①这一步原始链表上的节点 N 指向的 random 节点 其实就在 复制节点 N1 指向的 random 节点的前面
②所以就是 N1.random = N.random.next
- 接下来就是拆分的过程了,将链表分为 原始链表 和 复制链表 两部分
//4.拆分
head = pHead;
RandomListNode newHead = head.next;
while(head.next != null){
RandomListNode node = head.next;
head.next = node.next;
head = node;
}
拆分是最后一步
自己写一直陷入了newHead、head 的关系中
但是看了大佬的代码明白了
拆分的过程可以对原始节点、复制节点一个一个的来拆分
先拆分原始链表,然后通过 head = node 这行代码继续拆分 复制链表
public RandomListNode Clone(RandomListNode pHead){
//1.链表是否合法
if(pHead==null){
return null;
}
//2.将复制节点插入到原始节点的后面
RandomListNode head = pHead;
while(head!=null){
RandomListNode cur = new RandomListNode(head.label);
RandomListNode temp = head.next;
head.next = cur;
cur.next = temp;
head = cur.next;
}
//3.复制节点的random指向
head = pHead;
while(head!=null){
RandomListNode copyNode = head.next;
if(head.random!=null){
copyNode.random = head.random.next;
}
head = copyNode.next;
}
//4.拆分
head = pHead;
RandomListNode newHead = head.next;
while(head.next != null){
RandomListNode node = head.next;
head.next = node.next;
head = node;
}
return newHead;
}
总结
从这个题中学到了很多,解决链表这块的问题先是暴力,然后考虑优化时间复杂度,这个时候就可以去考虑用空间来降低时间复杂度,一般的话就是 HashMap 或 HashSet 。
然后呢
再去考虑降低空间复杂度,达到O(1)的空间复杂度,一般的做法是采用双指针,可能是快慢指针、也可能是前后指针
但是这个题第三种复制+拆分的思路自己还是第一次见(可能是我刷题还不够多),针对这个题来说它就很妙、很妙
自己还得继续学习呀!!!
以上的图来自:链表的图以及借鉴大佬的代码