注:本文的代码实现使用的是 JS(JavaScript),为前端中想使用JS练习算法和数据结构的小伙伴提供解题思路。
描述
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)。 下图是一个含有5个结点的复杂链表。图中实线箭头表示next指针,虚线箭头表示random指针。为简单起见,指向null的指针没有画出。
示例:
输入:{1,2,3,4,5,3,5,#,2,#}
输出:{1,2,3,4,5,3,5,#,2,#}
解析: 我们将链表分为两段,前半部分{1,2,3,4,5}为ListNode,后半部分{3,5,#,2,#}是随机指针域表示。
以上示例前半部分可以表示链表为的ListNode:1->2->3->4->5
后半部分,3,5,#,2,#分别的表示为
1的位置指向3,2的位置指向5,3的位置指向null,4的位置指向2,5的位置指向null
如下图:
解题思路
直接法
首先按照正常链表的思路复制出来一个新的链表,其中pHead
是题中给出需要复制的链表的头节点。RandomListNode
是创建节点的类,其结构如下:
function RandomListNode(x){
// label 相当于普通链表的 val
this.label = x;
// 当前节点的下一个节点
this.next = null;
// 当前节点所指向的随机节点
this.random = null;
}
复制链表的代码如下,新链表的头节点为newHead
// 复制头节点指针,用于遍历链表
// 如果直接用 pHead 进行遍历,后续再想找到原始链表的头节点就找不到了
let p = pHead
// 手动创建一个头节点,也就是新的链表的头节点
let newHead = new RandomListNode(p.label)
// 新的链表的节点指针,用于遍历
let p_newHead = newHead
while(p.next){
p_newHead.next = new RandomListNode(p.next.label)
p = p.next
p_newHead = p_newHead.next
}
至此,我们的工作可以看作是完成了一半,剩下的就是把新链表中每个节点的random
指针指向对应的节点即可。那么我们该如何去做呢?
解决方案肯定不止一种,本人想到了一种思路:因为链表是有序的,因此,我们可以根据索引,去寻找这个关系。
以题中给的示例说明,原始链表中,D的random
指针指向的是B,而B在整个链表中的索引是1
(索引从0开始),因此在我们创建的新的链表中,让D’(新链表中根据原始链表中D创建的节点)指向新创建节点的第1
个节点即可。
所以,创建了一个新的函数find
去查找节点node
的random
所指向节点在其所在链表中的索引,结构如下:
function find(node, pHead){
// node 节点的 random 指针指向空, 则索引为 null
if(!node.random) return null
// 记录索引的变量
let index = 0
while(pHead){
// 如果当前节点和node 节点的 random 指向的是同一个节点,则返回 index
if(pHead === node.random) return index
pHead = pHead.next
index ++
}
// 此处无需进行其他 return 因为一定能找到索引的
}
最后本人创建了两个变量:
// 用于存放索引的数组,初始化的时候,将头节点的 random 指向节点的index放入
let list = [find(pHead, pHead)]
// 用于存放新创建链表的每个节点的指针,方便给新节点的 random 赋值
let node_pointer = [p_newHead]
完整的代码如下:
function find(node, pHead){
if(!node.random) return null
let index = 0
while(pHead){
if(pHead === node.random) return index
pHead = pHead.next
index ++
}
}
function Clone(pHead)
{
// 头节点为 null ,则直接返回即可
if(!pHead) return pHead
let p = pHead
let newHead = new RandomListNode(p.label)
let p_newHead = newHead
let list = [find(pHead, pHead)]
let node_pointer = [p_newHead]
while(p.next){
p_newHead.next = new RandomListNode(p.next.label)
// 在创新新链表的时候,顺便按顺序把原始链表每个节点的 random 所指向节点的索引记录
list.push(find(p.next, pHead))
// 也顺便存放新创建链表的每个节点的指针
node_pointer.push(p_newHead.next)
p = p.next
p_newHead = p_newHead.next
}
p_newHead = newHead
let count = 0
while(p_newHead){
// list[count] 是当前节点random 指向节点的索引
// 然后再从 node_pointer 找到对应的指针即可
p_newHead.random = node_pointer[list[count]]
// 为了清晰,把这句话单独拿出来了,其实可以合并到上一句代码中
count++
p_newHead = p_newHead.next
}
return newHead
}
这个方法可能看着乱,但这是最直观,最容易理解的,时间复杂度是 O ( n 2 ) O(n^2) O(n2)的。其实,你可以直接return pHead的,也能通过,就看面试官办不办你了,哈哈哈哈。
原地复制法
- 把原始链表中的每个节点复制,并按下图的方式串接起来,只需要给新创建的节点指定label和next即可
代码如下:
let p = pHead
while(p){
const temp = p.next
// 创建节点并指定label
p.next = new RandomListNode(p.label)
// 为新创建的节点指定next
p.next.next = temp
p = temp
}
- 把复制的结点的random指针指向被复制结点的random指针的下一个结点
p = pHead
while(p){
// 保证当前节点的 random 非空,若为空,就不用管了
if(p.random)
p.next.random = p.random.next
p = p.next.next
}
3. 拆分成两个链表,具体拆分方式可以看下面代码,跟着代码走一两个循环就能明白了
p = pHead
// 新的链表的头节点
let newHead = p.next
// 用于遍历新的链表的指针
let p_newHead = newHead
// 到倒数第二个节点结束,本题中是 E ,否则在 p = p.next 的时候会报错
while(p.next.next){
p.next = p_newHead.next
p = p.next
p_newHead.next = p.next
p_newHead = p.next
}
// 如果这句话不加上,虽然输出的 newHead 没问题,但是原始链表最后一个节点的复制节点(E')也会挂在E的后面
p.next = null
完整代码如下:
function Clone(pHead)
{
if(!pHead) return pHead
let p = pHead
while(p){
const temp = p.next
p.next = new RandomListNode(p.label)
p.next.next = temp
p = temp
}
p = pHead
while(p){
if(p.random)
p.next.random = p.random.next
p = p.next.next
}
p = pHead
let newHead = p.next
let p_newHead = newHead
while(p.next.next){
p.next = p_newHead.next
p = p.next
p_newHead.next = p.next
p_newHead = p.next
}
p.next = null
return newHead
}