一、题目描述
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
复杂链表的定义:
public class RandomListNode {
int label;
RandomListNode next = null;
RandomListNode random = null;
RandomListNode(int label) {
this.label = label;
}
}
下图是一个含有5个结点的复杂链表。图中实线箭头表示Next指针,虚线箭头表示random指针。为简单起见,指向NULL的指针没有画出。
二、解题思路
2.1 O(n2)的普通解法
第一步是复制原始链表上的每一个结点,并用Next节点链接起来;
第二步是设置每个结点的random节点指针。
对于一个含有n个结点的链表,由于定位每个结点的random都需要从链表头结点开始经过O(n)步才能找到,因此这种方法的总时间复杂度是O(n2)。
2.2 借助辅助空间的O(n)解法
第一步仍然是复制原始链表上的每个结点N创建N',然后把这些创建出来的结点用Next链接起来。同时我们把<N,N'>的配对信息放到一个哈希表中。
第二步还是设置复制链表上每个结点的random。由于有了哈希表,我们可以用O(1)的时间根据S找到S'。
此方法使用空间换时间。对于有n个结点的链表我们需要一个大小为O(n)的哈希表,也就是说我们以O(n)的空间消耗把时间复杂度由O(n2)降低到O(n)。
2.3 不借助辅助空间的O(n)解法
第一步仍然是根据原始链表的每个结点N创建对应的N'。(把N'链接在N的后面)
第一步 在原链表每个结点后面分别复制创建新的结点
//第一步 在原链表每个结点后面分别复制创建新的结点
//下面注释仅表示如何使A->B变成 A->A'->B->B',并且pNode 指向A变成pNode指向B
public static void cloneNodes(RandomListNode pHead) {
RandomListNode pNode = pHead;
while (pNode != null) {
RandomListNode pCloned = new RandomListNode(0);
pCloned.label = pNode.label;
pCloned.next = pNode.next;//A'->B
pCloned.random = null;
pNode.next = pCloned;//A->A'
pNode = pCloned.next;// 引用(指针)pNode指向节点B
}
说明:1)pNode = pCloned.next, 下面说明
/**
* 此处可以把pNode 理解成指向头节点的引用(指针)变量
* pNode = pCloned.next 表示把引用变量pNode指向pCloned指向节点的下一个节点
* 而pNode 原来节点的位置不发生变化
* 另外,这个链表依然有一个头指针pHead,它并没没有消失,RandomListNode pNode = pHead;
* 所以虽然引用变量pNode 往后面移动,但最开始引用变量指向的节点并没有变化。
* 所以,这个链表最终可以用pHead 进行表示。
*/ 可以参考:https://blog.csdn.net/nsjlive/article/details/88032748 (链表的节点与指针)pNode = pCloned.next;// pNode和pCloned都是节点的引用,指向相应的节点。
2)pCloned.next = pNode.next; 下面解释
//若pNode指向A,,pNode.next表示A的下一个节点,假设该节点是B。
若pCloned指向的节点为A', 现在使pCloned.next==B,表示节点A'下一个节点是B
//第一步 在原链表每个结点后面分别复制创建新的结点
//下面注释仅表示如何使A->B变成 A->A'->B->B',并且pNode 指向A变成pNode指向B
public static void cloneNodes(RandomListNode pHead) {
RandomListNode pNode = pHead;
while (pNode != null) {
RandomListNode pCloned = new RandomListNode(0);
pCloned.label = pNode.label;
pCloned.next = pNode.next;//A'->B
pCloned.random = null;
pNode.next = pCloned;//A->A'
pNode = pCloned.next;// 引用(指针)pNode指向节点B
}
第二步设置复制出来的结点的random。
(把N'的random指向N的rabdom.next)
//第二步 根据旧链表的兄弟结点 初始化新链表的兄弟结点
//下面 假设初始pNode指向节点A 仅说明节点A 与B及其随机指针指向节点的关系
public static void connectRandom(RandomListNode pHead) {
RandomListNode pNode = pHead;
while (pNode != null) {
RandomListNode pCloned = pNode.next;//pCloned 指向A’
/**
* 若A.random存在
* 则A'.random也一定存在。则A'的随机(random)节点为A的随机节点的下一个节点
*/
if (pNode.random != null) {
pCloned.random = pNode.random.next;
}
pNode = pCloned.next;//引用pNode移动到B节点 ,然后从B节点开始一直往后面......
}
}
第三步把这个长链表拆分成两个链表
把奇数位置的结点用Next链接起来就是原始链表,偶数数值的则是复制链表。
//第三步 从旧链表拆分得到新的结点
public static RandomListNode reconnectNode(RandomListNode pHead) {
RandomListNode pNode = pHead;
RandomListNode pClonedHead = null;
RandomListNode pClonedNode = null;
if (pNode != null) {//此处并非循环,仅执行一次
pClonedHead = pClonedNode = pNode.next;//引用 pClonedHead 和 pClonedNode指向A'
pNode.next = pClonedNode.next;//A的下一个节点是B
pNode = pNode.next;// 引用pNode指向B
}
while (pNode != null) {
// 复制链表的连接
pClonedNode.next = pNode.next;//A'的下一个节点是B'(也可以说A'指向B’),指向的同时断开A'与B的联系
pClonedNode = pClonedNode.next;// pClonedNode指向B'
// 原始链表的连接
pNode.next = pClonedNode.next;//节点B的下一个节点是C
pNode = pNode.next;//引用pNode指向C
}
return pClonedHead;
}
最后,将三个步骤衔接起来形成Clone方法:
//分为三个步骤
//第一步 在原链表每个结点后面分别复制创建新的结点
//第二步 根据旧链表的兄弟结点 初始化新链表的兄弟结点
//第三步 从旧链表拆分得到新的结点
public static RandomListNode Clone(RandomListNode pHead) {
if(pHead==null) return null;//最好加上这一条语句,不然有可能case通过率为80%
cloneNodes(pHead);
connectRandom(pHead);
return reconnectNode(pHead);
}
完整代码汇总:
public class Test {
//第一步 在原链表每个结点后面分别复制创建新的结点
public static void cloneNodes(RandomListNode pHead) {
RandomListNode pNode = pHead;
while (pNode != null) {
RandomListNode pCloned = new RandomListNode(0);
pCloned.label = pNode.label;
pCloned.next = pNode.next;
pCloned.random = null;
pNode.next = pCloned;//pNode 的下一个节点是PCloned
pNode = pCloned.next;// pNode和pCloned都是节点的引用,指向相应的节点。
}
}
//第二步 根据旧链表的兄弟结点 初始化新链表的兄弟结点
//下面 假设初始pNode指向节点A 仅说明节点A 与B及其随机指针指向节点的关系
public static void connectRandom(RandomListNode pHead) {
RandomListNode pNode = pHead;
while (pNode != null) {
RandomListNode pCloned = pNode.next;//pCloned 指向A’
/**
* 若A.random存在
* 则A'.random也一定存在。则A'的随机(random)节点为A的随机节点的下一个节点
*/
if (pNode.random != null) {
pCloned.random = pNode.random.next;
}
pNode = pCloned.next;//引用pNode移动到B节点 ,然后从B节点开始一直往后面......
}
}
//第三步 从旧链表拆分得到新的结点
public static RandomListNode reconnectNode(RandomListNode pHead) {
RandomListNode pNode = pHead;
RandomListNode pClonedHead = null;
RandomListNode pClonedNode = null;
if (pNode != null) {//此处并非循环,仅执行一次
pClonedHead = pClonedNode = pNode.next;//引用 pClonedHead 和 pClonedNode指向A'
pNode.next = pClonedNode.next;//A的下一个节点是B
pNode = pNode.next;// 引用pNode指向B
}
while (pNode != null) {
// 复制链表的连接
pClonedNode.next = pNode.next;//A'的下一个节点是B'(也可以说A'指向B’),指向的同时断开A'与B的联系
pClonedNode = pClonedNode.next;// pClonedNode指向B'
// 原始链表的连接
pNode.next = pClonedNode.next;//节点B的下一个节点是C
pNode = pNode.next;//引用pNode指向C
}
return pClonedHead;
}
//分为三个步骤
//第一步 在原链表每个结点后面分别复制创建新的结点
//第二步 根据旧链表的兄弟结点 初始化新链表的兄弟结点
//第三步 从旧链表拆分得到新的结点
public static RandomListNode Clone(RandomListNode pHead) {
if(pHead==null) return null;//考虑为空的情形
cloneNodes(pHead);
connectRandom(pHead);
return reconnectNode(pHead);
}
}
其中,分拆链表也可以写为:
但是,前面Clone方法(如果没有 if(pHead==null) return null; 会不通过,显示仅有80%通过。
public static RandomListNode reconnectNode(RandomListNode pHead) {
RandomListNode pNode = pHead;
RandomListNode pClonedHead = pHead.next;
RandomListNode tmp=null;
while ( pNode.next!=null) {
tmp = pNode.next;
pNode.next =tmp.next;
pNode = tmp;
}
return pClonedHead;
}
参考:https://www.cnblogs.com/edisonchou/p/4790090.html
这个题目比较复杂,但有助于加深对链表的理解。